From 1442a0cc30858391b351dcd477b3a1a579f931f3 Mon Sep 17 00:00:00 2001 From: Daniel Schall Date: Sun, 13 Dec 2020 14:13:10 -0800 Subject: [PATCH] Add Tuya button support --- .../zigbee/ZigBeeBindingConstants.java | 4 + .../converter/ZigBeeConverterTuyaButton.java | 215 ++++++++++++++++++ ...ZigBeeDefaultChannelConverterProvider.java | 3 +- .../command/TuyaButtonPressCommand.java | 81 +++++++ .../main/resources/ESH-INF/thing/channels.xml | 13 ++ .../resources/ESH-INF/thing/tuya/ts0041.xml | 55 +++++ .../resources/ESH-INF/thing/tuya/ts0042.xml | 62 +++++ .../resources/ESH-INF/thing/tuya/ts0043.xml | 69 ++++++ .../resources/ESH-INF/thing/tuya/ts0044.xml | 76 +++++++ .../src/main/resources/discovery.txt | 4 + 10 files changed, 581 insertions(+), 1 deletion(-) create mode 100644 org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterTuyaButton.java create mode 100644 org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/command/TuyaButtonPressCommand.java create mode 100644 org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0041.xml create mode 100644 org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0042.xml create mode 100644 org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0043.xml create mode 100644 org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0044.xml diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java index 801c41e..0ecf694 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java @@ -210,6 +210,10 @@ public class ZigBeeBindingConstants { public static final String CHANNEL_LABEL_WINDOWCOVERING_LIFT = "Window Covering Lift"; public static final ChannelTypeUID CHANNEL_WINDOWCOVERING_LIFT = new ChannelTypeUID("zigbee:windowcovering_lift"); + public static final String CHANNEL_NAME_TUYA_BUTTON = "tuyabutton"; + public static final String CHANNEL_LABEL_TUYA_BUTTON = "Button"; + public static final ChannelTypeUID CHANNEL_TUYA_BUTTON = new ChannelTypeUID("zigbee:tuya_button"); + public static final String CHANNEL_PROPERTY_ENDPOINT = "zigbee_endpoint"; public static final String CHANNEL_PROPERTY_PROFILEID = "zigbee_profileid"; public static final String CHANNEL_PROPERTY_INPUTCLUSTERS = "zigbee_inputclusters"; diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterTuyaButton.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterTuyaButton.java new file mode 100644 index 0000000..f90dbe5 --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterTuyaButton.java @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB 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.openhab.binding.zigbee.internal.converter; + +import static java.lang.Integer.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.CommonTriggerEvents; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.zigbee.converter.ZigBeeBaseChannelConverter; +import org.openhab.binding.zigbee.internal.converter.command.TuyaButtonPressCommand; +import org.openhab.binding.zigbee.internal.converter.config.ZclReportingConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zsmartsystems.zigbee.CommandResult; +import com.zsmartsystems.zigbee.ZigBeeEndpoint; +import com.zsmartsystems.zigbee.zcl.ZclAttribute; +import com.zsmartsystems.zigbee.zcl.ZclAttributeListener; +import com.zsmartsystems.zigbee.zcl.ZclCluster; +import com.zsmartsystems.zigbee.zcl.ZclCommand; +import com.zsmartsystems.zigbee.zcl.ZclCommandListener; +import com.zsmartsystems.zigbee.zcl.ZclStatus; +import com.zsmartsystems.zigbee.zcl.clusters.ZclOnOffCluster; + +/** + * Generic converter for Tuya buttons (e.g., Zemismart). + *

+ * This converter needs to be configured with the ZigBee commands that are triggered by the button presses. This is done + * by channel properties that specify the endpoint, the cluster, the command ID, and (optionally) a command parameter. + *

+ * As the configuration is done via channel properties, this converter is usable via static thing types only. + * + * @author Henning Sudbrock - initial contribution + * @author Thomas Weißschuh - support for attribute-based buttons + */ +public class ZigBeeConverterTuyaButton extends ZigBeeBaseChannelConverter + implements ZclAttributeListener, ZclCommandListener { + + private Logger logger = LoggerFactory.getLogger(ZigBeeConverterTuyaButton.class); + + private ZclCluster clientCluster = null; + private ZclCluster serverCluster = null; + + // Tuya devices sometimes send duplicate commands with the same tx id. + // We keep track of the last received Tx id and ignore the duplicate. + private Integer lastTxId = -1; + + @Override + public Set getImplementedClientClusters() { + return Collections.singleton(ZclOnOffCluster.CLUSTER_ID); + } + + @Override + public Set getImplementedServerClusters() { + return Collections.singleton(ZclOnOffCluster.CLUSTER_ID); + } + + @Override + public boolean initializeDevice() { + ZclCluster clientCluster = endpoint.getOutputCluster(ZclOnOffCluster.CLUSTER_ID); + ZclCluster serverCluster = endpoint.getInputCluster(ZclOnOffCluster.CLUSTER_ID); + + if (clientCluster == null) { + logger.error("{}: Error opening client cluster {} on endpoint {}", endpoint.getIeeeAddress(), + ZclOnOffCluster.CLUSTER_ID, endpoint.getEndpointId()); + return false; + } + + if (serverCluster == null) { + logger.error("{}: Error opening server cluster {} on endpoint {}", endpoint.getIeeeAddress(), + ZclOnOffCluster.CLUSTER_ID, endpoint.getEndpointId()); + return false; + } + + ZclReportingConfig reporting = new ZclReportingConfig(channel); + + try { + CommandResult bindResponse = bind(serverCluster).get(); + if (bindResponse.isSuccess()) { + // Configure reporting + ZclAttribute attribute = serverCluster.getAttribute(ZclOnOffCluster.ATTR_ONOFF); + CommandResult reportingResponse = attribute + .setReporting(reporting.getReportingTimeMin(), reporting.getReportingTimeMax()).get(); + handleReportingResponse(reportingResponse, POLLING_PERIOD_HIGH, reporting.getPollingPeriod()); + } else { + logger.debug("{}: Error 0x{} setting server binding", endpoint.getIeeeAddress(), + Integer.toHexString(bindResponse.getStatusCode())); + pollingPeriod = POLLING_PERIOD_HIGH; + } + + } catch (InterruptedException | ExecutionException e) { + logger.error("{}: Exception setting reporting ", endpoint.getIeeeAddress(), e); + } + + try { + CommandResult bindResponse = bind(clientCluster).get(); + if (!bindResponse.isSuccess()) { + logger.error("{}: Error 0x{} setting client binding for cluster {}", endpoint.getIeeeAddress(), + toHexString(bindResponse.getStatusCode()), ZclOnOffCluster.CLUSTER_ID); + } + } catch (InterruptedException | ExecutionException e) { + logger.error("{}: Exception setting client binding to cluster {}: {}", endpoint.getIeeeAddress(), + ZclOnOffCluster.CLUSTER_ID, e); + } + + return true; + } + + @Override + public synchronized boolean initializeConverter() { + clientCluster = endpoint.getOutputCluster(ZclOnOffCluster.CLUSTER_ID); + serverCluster = endpoint.getInputCluster(ZclOnOffCluster.CLUSTER_ID); + + if (clientCluster == null) { + logger.error("{}: Error opening device client controls", endpoint.getIeeeAddress()); + return false; + } + + if (serverCluster == null) { + logger.error("{}: Error opening device server controls", endpoint.getIeeeAddress()); + return false; + } + + clientCluster.addCommandListener(this); + serverCluster.addAttributeListener(this); + + // Add Tuya-specific command + // + HashMap> commandMap = new HashMap<>(); + commandMap.put(TuyaButtonPressCommand.COMMAND_ID, TuyaButtonPressCommand.class); + clientCluster.addClientCommands(commandMap); + + return true; + } + + @Override + public void disposeConverter() { + if(clientCluster != null) { + clientCluster.removeCommandListener(this); + } + if (serverCluster != null) { + serverCluster.removeAttributeListener(this); + } + } + + @Override + public void handleRefresh() { + // nothing to do, as we only listen to commands + } + + @Override + public Channel getChannel(ThingUID thingUID, ZigBeeEndpoint endpoint) { + // This converter is used only for channels specified in static thing types, and cannot be used to construct + // channels based on an endpoint alone. + return null; + } + + @Override + public boolean commandReceived(ZclCommand command) { + logger.debug("{} received command {}", endpoint.getIeeeAddress(), command); + Integer thisTxId = command.getTransactionId(); + if(lastTxId == thisTxId) + { + logger.debug("{} ignoring duplicate command {}", endpoint.getIeeeAddress(), thisTxId); + } + else if (command instanceof TuyaButtonPressCommand) { + TuyaButtonPressCommand tuyaButtonPressCommand = (TuyaButtonPressCommand) command; + thing.triggerChannel(channel.getUID(), getEventType(tuyaButtonPressCommand.getPressType())); + clientCluster.sendDefaultResponse(command, ZclStatus.SUCCESS); + } + else { + logger.warn("{} received unknown command {}", endpoint.getIeeeAddress(), command); + } + + lastTxId = thisTxId; + return true; + } + + @Override + public void attributeUpdated(ZclAttribute attribute, Object val) { + logger.debug("{}: ZigBee attribute reports {}", endpoint.getIeeeAddress(), attribute); + } + + private String getEventType(Integer pressType) + { + switch(pressType) + { + case 0: + return CommonTriggerEvents.SHORT_PRESSED; + case 1: + return CommonTriggerEvents.DOUBLE_PRESSED; + case 2: + return CommonTriggerEvents.LONG_PRESSED; + default: + logger.warn("{} received unknown pressType {}", endpoint.getIeeeAddress(), pressType); + return CommonTriggerEvents.SHORT_PRESSED; + } + } +} diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java index 109362e..5bb061c 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java @@ -86,7 +86,8 @@ public final class ZigBeeDefaultChannelConverterProvider implements ZigBeeChanne channelMap.put(ZigBeeBindingConstants.CHANNEL_THERMOSTAT_SYSTEMMODE, ZigBeeConverterThermostatSystemMode.class); channelMap.put(ZigBeeBindingConstants.CHANNEL_FANCONTROL, ZigBeeConverterFanControl.class); channelMap.put(ZigBeeBindingConstants.CHANNEL_WINDOWCOVERING_LIFT, ZigBeeConverterWindowCoveringLift.class); - } + channelMap.put(ZigBeeBindingConstants.CHANNEL_TUYA_BUTTON, ZigBeeConverterTuyaButton.class); +} @Override public Map> getChannelConverters() { diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/command/TuyaButtonPressCommand.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/command/TuyaButtonPressCommand.java new file mode 100644 index 0000000..4c795c4 --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/command/TuyaButtonPressCommand.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB 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.openhab.binding.zigbee.internal.converter.command; + +import com.zsmartsystems.zigbee.zcl.ZclCommand; +import com.zsmartsystems.zigbee.zcl.ZclFieldDeserializer; +import com.zsmartsystems.zigbee.zcl.ZclFieldSerializer; +import com.zsmartsystems.zigbee.zcl.clusters.ZclOnOffCluster; +import com.zsmartsystems.zigbee.zcl.protocol.ZclCommandDirection; +import com.zsmartsystems.zigbee.zcl.protocol.ZclDataType; + +public class TuyaButtonPressCommand extends ZclCommand { + /** + * The command ID. + */ + public static int COMMAND_ID = 0xFD; + + /** + * Type of button press. + *

+ * 0 = short press + * 1 = double press + * 2 = long press + */ + private Integer pressType; + + /** + * Default constructor. + * + */ + public TuyaButtonPressCommand() { + clusterId = ZclOnOffCluster.CLUSTER_ID; + commandId = COMMAND_ID; + genericCommand = false; + commandDirection = ZclCommandDirection.CLIENT_TO_SERVER; + } + + public TuyaButtonPressCommand( + Integer pressType) + { + this(); + this.pressType = pressType; + } + + public Integer getPressType() + { + return pressType; + } + + @Override + public void serialize(ZclFieldSerializer serializer) { + serializer.serialize(pressType, ZclDataType.UNSIGNED_8_BIT_INTEGER); + } + + @Override + public void deserialize(final ZclFieldDeserializer deserializer) { + pressType = (Integer) deserializer.deserialize(ZclDataType.UNSIGNED_8_BIT_INTEGER); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(113); + builder.append(this.getClass().getSimpleName()); + builder.append(" ["); + builder.append(super.toString()); + builder.append(", pressType="); + builder.append(pressType); + builder.append(']'); + return builder.toString(); + } +} diff --git a/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/channels.xml b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/channels.xml index 8c9de6f..fbba2f4 100644 --- a/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/channels.xml +++ b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/channels.xml @@ -396,5 +396,18 @@ Blinds + + + trigger + + Emits events when button is pressed + + + + + + + + diff --git a/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0041.xml b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0041.xml new file mode 100644 index 0000000..03ba99c --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0041.xml @@ -0,0 +1,55 @@ + + + + + + Generic Tuya 1-Button Wall Switch + WallSwitch + + + + + + 1 + + + + + + 1 + + + + + + 1 + + + + + + + 1 + + + + + + + Tuya + TS0041 + END_DEVICE + + + zigbee_macaddress + + + + + + + + \ No newline at end of file diff --git a/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0042.xml b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0042.xml new file mode 100644 index 0000000..4d4be26 --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0042.xml @@ -0,0 +1,62 @@ + + + + + + Generic Tuya 2-Button Wall Switch + WallSwitch + + + + + + 1 + + + + + + 1 + + + + + + 1 + + + + + + + 1 + + + + + + + 2 + + + + + + + Tuya + TS0042 + END_DEVICE + + + zigbee_macaddress + + + + + + + + \ No newline at end of file diff --git a/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0043.xml b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0043.xml new file mode 100644 index 0000000..51166fe --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0043.xml @@ -0,0 +1,69 @@ + + + + + + Generic Tuya 3-Button Wall Switch + WallSwitch + + + + + + 1 + + + + + + 1 + + + + + + 1 + + + + + + + 1 + + + + + + + 2 + + + + + + + 3 + + + + + + + Tuya + TS0043 + END_DEVICE + + + zigbee_macaddress + + + + + + + + \ No newline at end of file diff --git a/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0044.xml b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0044.xml new file mode 100644 index 0000000..fd43b18 --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/resources/ESH-INF/thing/tuya/ts0044.xml @@ -0,0 +1,76 @@ + + + + + + Generic Tuya 4-Button Wall Switch + WallSwitch + + + + + + 1 + + + + + + 1 + + + + + + 1 + + + + + + + 1 + + + + + + + 2 + + + + + + + 3 + + + + + + + 4 + + + + + + + Tuya + TS0044 + END_DEVICE + + + zigbee_macaddress + + + + + + + + \ No newline at end of file diff --git a/org.openhab.binding.zigbee/src/main/resources/discovery.txt b/org.openhab.binding.zigbee/src/main/resources/discovery.txt index 8974975..8d9ffc5 100644 --- a/org.openhab.binding.zigbee/src/main/resources/discovery.txt +++ b/org.openhab.binding.zigbee/src/main/resources/discovery.txt @@ -11,3 +11,7 @@ xiaomi_lumisensor-switchaq2,modelId=lumi.sensor_switch.aq2 xiaomi_lumisensorwaterleak,modelId=lumi.sensor_wleak.aq1 innr-rc-110,vendor=innr,modelId=RC 110 osram-switch-4x-eu,vendor=OSRAM,modelId=Switch 4x EU-LIGHTIFY +tuya_ts0041,vendor=_TZ3000_tk3s5tyg,modelId=TS0041 +tuya_ts0042,vendor=_TZ3000_adkvzooy,modelId=TS0042 +tuya_ts0043,vendor=_TZ3000_qzjcsmar,modelId=TS0043 +tuya_ts0044,vendor=_TZ3000_vp6clf9d,modelId=TS0044 -- 2.28.0.windows.1