Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tuya] Add support for gateway and sub devices #497

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ public class TuyaBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_PROJECT = new ThingTypeUID(BINDING_ID, "project");
public static final ThingTypeUID THING_TYPE_TUYA_DEVICE = new ThingTypeUID(BINDING_ID, "tuyaDevice");
public static final ThingTypeUID THING_TYPE_TUYA_SUB_DEVICE = new ThingTypeUID(BINDING_ID, "tuyaSubDevice");
public static final ThingTypeUID THING_TYPE_TUYA_GATEWAY = new ThingTypeUID(BINDING_ID, "tuyaGateway");

public static final String PROPERTY_CATEGORY = "category";
public static final String PROPERTY_MAC = "mac";

public static final String CONFIG_LOCAL_KEY = "localKey";
public static final String CONFIG_DEVICE_ID = "deviceId";
public static final String CONFIG_DEVICE_UUID = "deviceUuid";
public static final String CONFIG_PRODUCT_ID = "productId";

public static final ChannelTypeUID CHANNEL_TYPE_UID_COLOR = new ChannelTypeUID(BINDING_ID, "color");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
package org.smarthomej.binding.tuya.internal;

import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CONFIG_DEVICE_ID;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CONFIG_DEVICE_UUID;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CONFIG_LOCAL_KEY;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CONFIG_PRODUCT_ID;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.PROPERTY_CATEGORY;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.PROPERTY_MAC;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.THING_TYPE_TUYA_DEVICE;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.THING_TYPE_TUYA_GATEWAY;
import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.THING_TYPE_TUYA_SUB_DEVICE;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
Expand All @@ -34,7 +36,7 @@
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.storage.Storage;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
Expand All @@ -43,8 +45,10 @@
import org.slf4j.LoggerFactory;
import org.smarthomej.binding.tuya.internal.cloud.TuyaOpenAPI;
import org.smarthomej.binding.tuya.internal.cloud.dto.DeviceListInfo;
import org.smarthomej.binding.tuya.internal.cloud.dto.DeviceSchema;
import org.smarthomej.binding.tuya.internal.cloud.dto.SubDeviceInfo;
import org.smarthomej.binding.tuya.internal.handler.ProjectHandler;
import org.smarthomej.binding.tuya.internal.schema.DeviceSchemaMapper;
import org.smarthomej.binding.tuya.internal.schema.SchemaRegistry;
import org.smarthomej.binding.tuya.internal.util.SchemaDp;

import com.google.gson.Gson;
Expand All @@ -56,14 +60,16 @@
*/
@NonNullByDefault
public class TuyaDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_TUYA_DEVICE);

public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_TUYA_DEVICE,
THING_TYPE_TUYA_GATEWAY, THING_TYPE_TUYA_SUB_DEVICE);
private static final int SEARCH_TIME = 5;

private final Logger logger = LoggerFactory.getLogger(TuyaDiscoveryService.class);
private final Gson gson = new Gson();

private @Nullable ProjectHandler bridgeHandler;
private @NonNullByDefault({}) Storage<String> storage;
private @NonNullByDefault({}) SchemaRegistry schemaRegistry;
private @Nullable ScheduledFuture<?> discoveryJob;

public TuyaDiscoveryService() {
Expand All @@ -86,6 +92,44 @@ protected void startScan() {
}

processDeviceResponse(List.of(), api, bridgeHandler, 0);
bridgeHandler.getThingRegistry().stream().filter(t -> t.getThingTypeUID().equals(THING_TYPE_TUYA_GATEWAY))
.forEach(t -> discoverSubDevices(t, api));
}

private void discoverSubDevices(Thing gatewayThing, TuyaOpenAPI api) {
Object gatewayId = gatewayThing.getConfiguration().get(CONFIG_DEVICE_ID);
if (gatewayId == null) {
logger.debug("There is no device id in gateway {} properties", gatewayThing.getUID());
return;
}
api.getSubDevices(gatewayId.toString())
.thenAccept(subDevices -> subDevices.forEach(d -> processSubDevice(d, api, gatewayThing.getUID())));
}

private void processSubDevice(SubDeviceInfo subDeviceInfo, TuyaOpenAPI api, ThingUID gatewayThingUid) {
Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_CATEGORY, subDeviceInfo.category);
properties.put(CONFIG_DEVICE_UUID, subDeviceInfo.nodeId);
properties.put(CONFIG_PRODUCT_ID, subDeviceInfo.productId);
properties.put(CONFIG_DEVICE_ID, subDeviceInfo.id);

ThingUID thingUid = new ThingUID(THING_TYPE_TUYA_SUB_DEVICE, gatewayThingUid, subDeviceInfo.id);

DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUid).withLabel(subDeviceInfo.name)
.withBridge(gatewayThingUid).withRepresentationProperty(CONFIG_DEVICE_ID).withProperties(properties)
.build();

refreshSchemaForDevice(subDeviceInfo.id, api);
thingDiscovered(discoveryResult);
}

private void refreshSchemaForDevice(String deviceId, TuyaOpenAPI api) {
api.getDeviceSchema(deviceId).thenAccept(schema -> {
DeviceSchemaMapper deviceSchemaMapper = new DeviceSchemaMapper(gson);
List<SchemaDp> schemaDps = deviceSchemaMapper.covert(schema);
SchemaRegistry schemaRegistry = this.schemaRegistry;
schemaRegistry.add(deviceId, schemaDps);
});
}

private void processDeviceResponse(List<DeviceListInfo> deviceList, TuyaOpenAPI api, ProjectHandler bridgeHandler,
Expand All @@ -100,45 +144,50 @@ private void processDeviceResponse(List<DeviceListInfo> deviceList, TuyaOpenAPI

private void processDevice(DeviceListInfo device, TuyaOpenAPI api) {
api.getFactoryInformation(List.of(device.id)).thenAccept(fiList -> {
ThingUID thingUid = new ThingUID(THING_TYPE_TUYA_DEVICE, device.id);
String deviceMac = fiList.stream().filter(fi -> fi.id.equals(device.id)).findAny().map(fi -> fi.mac)
.orElse("");
deviceMac = Objects.requireNonNull(deviceMac).replaceAll("(..)(?!$)", "$1:");
if ("wfcon".equals(device.category)) {
processGateway(device, api, deviceMac);
} else if (!device.subDevice) {
processSimpleDevice(device, api, deviceMac);
}
});
}

Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_CATEGORY, device.category);
properties.put(PROPERTY_MAC, Objects.requireNonNull(deviceMac).replaceAll("(..)(?!$)", "$1:"));
properties.put(CONFIG_LOCAL_KEY, device.localKey);
properties.put(CONFIG_DEVICE_ID, device.id);
properties.put(CONFIG_PRODUCT_ID, device.productId);
private void processGateway(DeviceListInfo device, TuyaOpenAPI api, String deviceMac) {
ThingUID thingUid = new ThingUID(THING_TYPE_TUYA_GATEWAY, device.id);

DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUid).withLabel(device.name)
.withRepresentationProperty(CONFIG_DEVICE_ID).withProperties(properties).build();
Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_CATEGORY, device.category);
properties.put(PROPERTY_MAC, deviceMac);
properties.put(CONFIG_LOCAL_KEY, device.localKey);
properties.put(CONFIG_DEVICE_ID, device.id);
properties.put(CONFIG_DEVICE_UUID, device.uuid);
properties.put(CONFIG_PRODUCT_ID, device.productId);

api.getDeviceSchema(device.id).thenAccept(schema -> {
List<SchemaDp> schemaDps = new ArrayList<>();
schema.functions.forEach(description -> addUniqueSchemaDp(description, schemaDps));
schema.status.forEach(description -> addUniqueSchemaDp(description, schemaDps));
storage.put(device.id, gson.toJson(schemaDps));
});
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUid).withLabel(device.name)
.withRepresentationProperty(CONFIG_DEVICE_ID).withProperties(properties).build();

thingDiscovered(discoveryResult);
});
thingDiscovered(discoveryResult);
}

private void addUniqueSchemaDp(DeviceSchema.Description description, List<SchemaDp> schemaDps) {
if (description.dp_id == 0 || schemaDps.stream().anyMatch(schemaDp -> schemaDp.id == description.dp_id)) {
// dp is missing or already present, skip it
return;
}
// some devices report the same function code for different dps
// we add an index only if this is the case
String originalCode = description.code;
int index = 1;
while (schemaDps.stream().anyMatch(schemaDp -> schemaDp.code.equals(description.code))) {
description.code = originalCode + "_" + index;
}
private void processSimpleDevice(DeviceListInfo device, TuyaOpenAPI api, String deviceMac) {
ThingUID thingUid = new ThingUID(THING_TYPE_TUYA_DEVICE, device.id);

Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_CATEGORY, device.category);
properties.put(PROPERTY_MAC, deviceMac);
properties.put(CONFIG_LOCAL_KEY, device.localKey);
properties.put(CONFIG_DEVICE_ID, device.id);
properties.put(CONFIG_DEVICE_UUID, device.uuid);
properties.put(CONFIG_PRODUCT_ID, device.productId);

DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUid).withLabel(device.name)
.withRepresentationProperty(CONFIG_DEVICE_ID).withProperties(properties).build();

schemaDps.add(SchemaDp.fromRemoteSchema(gson, description));
refreshSchemaForDevice(device.id, api);
thingDiscovered(discoveryResult);
}

@Override
Expand All @@ -151,7 +200,7 @@ protected synchronized void stopScan() {
public void setThingHandler(ThingHandler thingHandler) {
if (thingHandler instanceof ProjectHandler) {
this.bridgeHandler = (ProjectHandler) thingHandler;
this.storage = ((ProjectHandler) thingHandler).getStorage();
this.schemaRegistry = ((ProjectHandler) thingHandler).getSchemaRegistry();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,32 @@

import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.*;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.smarthomej.binding.tuya.internal.handler.ProjectHandler;
import org.smarthomej.binding.tuya.internal.handler.TuyaDeviceHandler;
import org.smarthomej.binding.tuya.internal.local.UdpDiscoveryListener;
import org.smarthomej.binding.tuya.internal.util.SchemaDp;
import org.smarthomej.binding.tuya.internal.handler.TuyaGatewayHandler;
import org.smarthomej.binding.tuya.internal.handler.TuyaSubDeviceHandler;
import org.smarthomej.binding.tuya.internal.local.TuyaDeviceManagerFactory;
import org.smarthomej.binding.tuya.internal.schema.SchemaRegistry;
import org.smarthomej.commons.SimpleDynamicCommandDescriptionProvider;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;

/**
* The {@link TuyaHandlerFactory} is responsible for creating things and thing
Expand All @@ -55,32 +51,27 @@
@Component(configurationPid = "binding.tuya", service = ThingHandlerFactory.class)
@SuppressWarnings("unused")
public class TuyaHandlerFactory extends BaseThingHandlerFactory {

private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PROJECT,
THING_TYPE_TUYA_DEVICE);
private static final Type STORAGE_TYPE = TypeToken.getParameterized(List.class, SchemaDp.class).getType();
THING_TYPE_TUYA_GATEWAY, THING_TYPE_TUYA_DEVICE, THING_TYPE_TUYA_SUB_DEVICE);

private final SimpleDynamicCommandDescriptionProvider dynamicCommandDescriptionProvider;
private final HttpClient httpClient;
private final Gson gson = new Gson();
private final UdpDiscoveryListener udpDiscoveryListener;
private final EventLoopGroup eventLoopGroup;
private final Storage<String> storage;
private final TuyaDeviceManagerFactory tuyaDeviceManagerFactory;
private final ThingRegistry thingRegistry;
private final SchemaRegistry schemaRegistry;

@Activate
public TuyaHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference SimpleDynamicCommandDescriptionProvider dynamicCommandDescriptionProvider,
@Reference StorageService storageService) {
@Reference StorageService storageService, @Reference TuyaDeviceManagerFactory tuyaDeviceManagerFactory,
@Reference ThingRegistry thingRegistry, @Reference SchemaRegistry schemaRegistry) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.dynamicCommandDescriptionProvider = dynamicCommandDescriptionProvider;
this.eventLoopGroup = new NioEventLoopGroup();
this.udpDiscoveryListener = new UdpDiscoveryListener(eventLoopGroup);
this.storage = storageService.getStorage("org.smarthomej.binding.tuya.Schema");
}

@Deactivate
public void deactivate() {
udpDiscoveryListener.deactivate();
eventLoopGroup.shutdownGracefully();
this.tuyaDeviceManagerFactory = tuyaDeviceManagerFactory;
this.thingRegistry = thingRegistry;
this.schemaRegistry = schemaRegistry;
}

@Override
Expand All @@ -93,12 +84,16 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (THING_TYPE_PROJECT.equals(thingTypeUID)) {
return new ProjectHandler(thing, httpClient, storage, gson);
return new ProjectHandler(thing, httpClient, schemaRegistry, gson, thingRegistry);
} else if (THING_TYPE_TUYA_GATEWAY.equals(thingTypeUID)) {
return new TuyaGatewayHandler((Bridge) thing, tuyaDeviceManagerFactory);
} else if (THING_TYPE_TUYA_SUB_DEVICE.equals(thingTypeUID)) {
return new TuyaSubDeviceHandler(thing, schemaRegistry.get(thing.getUID().getId()),
dynamicCommandDescriptionProvider);
} else if (THING_TYPE_TUYA_DEVICE.equals(thingTypeUID)) {
return new TuyaDeviceHandler(thing, gson.fromJson(storage.get(thing.getUID().getId()), STORAGE_TYPE), gson,
dynamicCommandDescriptionProvider, eventLoopGroup, udpDiscoveryListener);
return new TuyaDeviceHandler(thing, schemaRegistry.get(thing.getUID().getId()),
dynamicCommandDescriptionProvider, tuyaDeviceManagerFactory);
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.smarthomej.binding.tuya.internal.cloud.dto.FactoryInformation;
import org.smarthomej.binding.tuya.internal.cloud.dto.Login;
import org.smarthomej.binding.tuya.internal.cloud.dto.ResultResponse;
import org.smarthomej.binding.tuya.internal.cloud.dto.SubDeviceInfo;
import org.smarthomej.binding.tuya.internal.cloud.dto.Token;
import org.smarthomej.binding.tuya.internal.config.ProjectConfiguration;
import org.smarthomej.binding.tuya.internal.util.CryptoUtil;
Expand Down Expand Up @@ -163,6 +164,11 @@ public CompletableFuture<DeviceSchema> getDeviceSchema(String deviceId) {
.thenCompose(s -> processResponse(s, DeviceSchema.class));
}

public CompletableFuture<List<SubDeviceInfo>> getSubDevices(String deviceId) {
return request(HttpMethod.GET, "/v1.0/devices/" + deviceId + "/sub-devices", Map.of(), null).thenCompose(
s -> processResponse(s, TypeToken.getParameterized(List.class, SubDeviceInfo.class).getType()));
}

public CompletableFuture<Boolean> sendCommand(String deviceId, CommandRequest command) {
return request(HttpMethod.POST, "/v1.0/iot-03/devices/" + deviceId + "/commands", Map.of(), command)
.thenCompose(s -> processResponse(s, Boolean.class));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2021-2023 Contributors to the SmartHome/J project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.smarthomej.binding.tuya.internal.cloud.dto;

import org.eclipse.jdt.annotation.NonNullByDefault;

import com.google.gson.annotations.SerializedName;

/**
* Gateway sub device information
*
* @author Vitalii Herhel - Initial contribution
*/
@NonNullByDefault
public class SubDeviceInfo {
public String id = "";
public String name = "";
public String category = "";
public String icon = "";
public boolean online = false;
@SerializedName("node_id")
public String nodeId = "";
@SerializedName("product_id")
public String productId = "";
@SerializedName("owner_id")
public String ownerId = "";
@SerializedName("active_time")
public long activeTime = 0;
@SerializedName("update_time")
public long updateTime = 0;

@Override
public String toString() {
return "SubDeviceInfo{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", category='" + category + '\''
+ ", icon='" + icon + '\'' + ", online=" + online + ", nodeId='" + nodeId + '\'' + ", productId='"
+ productId + '\'' + ", ownerId='" + ownerId + '\'' + ", activeTime=" + activeTime + ", updateTime="
+ updateTime + '}';
}
}
Loading