Skip to content

Commit bc2af9c

Browse files
author
jenkins
committed
Merge PR #75: WIP: Tuya: Add thermostat support
2 parents 4953f47 + 5072b2a commit bc2af9c

7 files changed

Lines changed: 459 additions & 18 deletions

File tree

zigbeegeneric/integrationpluginzigbeegeneric.cpp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -345,20 +345,6 @@ void IntegrationPluginZigbeeGeneric::createConnections(Thing *thing)
345345

346346
if (thing->thingClassId() == doorSensorThingClassId) {
347347
connectToIasZoneInputCluster(thing, endpoint, "closed", true);
348-
ZigbeeClusterIasZone *iasZoneCluster = endpoint->inputCluster<ZigbeeClusterIasZone>(ZigbeeClusterLibrary::ClusterIdIasZone);
349-
if (!iasZoneCluster) {
350-
qCWarning(dcZigbeeGeneric()) << "Could not find IAS zone cluster on" << thing << endpoint;
351-
} else {
352-
if (iasZoneCluster->hasAttribute(ZigbeeClusterIasZone::AttributeZoneStatus)) {
353-
qCDebug(dcZigbeeGeneric()) << thing << iasZoneCluster->zoneStatus();
354-
ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus = iasZoneCluster->zoneStatus();
355-
thing->setStateValue(doorSensorClosedStateTypeId, !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) && !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
356-
}
357-
connect(iasZoneCluster, &ZigbeeClusterIasZone::zoneStatusChanged, thing, [=](ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus, quint8 extendedStatus, quint8 zoneId, quint16 delays) {
358-
qCDebug(dcZigbeeGeneric()) << "Zone status changed to:" << zoneStatus << extendedStatus << zoneId << delays;
359-
thing->setStateValue(doorSensorClosedStateTypeId, !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) && !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
360-
});
361-
}
362348
}
363349

364350
if (thing->thingClassId() == motionSensorThingClassId) {

zigbeetuya/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,12 @@ This plugin adds support for ZigBee devices by Tuya.
44

55
## Supported Things
66

7-
* Smart plugs with energy metering (TS011 plugs)
7+
* Smart plugs with energy metering
8+
* PIR motion sensors
9+
* Mm Wave presence sensors
10+
* Vibration sensors
11+
* H&T sensors
12+
* H&T Display sensors
13+
* AirHouseKeeper
14+
* Smoke sensors
15+
* Door/Window sensors
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--> ZigbeeNode(0x1577, A4:C1:38:EB:34:5E:C9:DB, _TZ3000_7d8yme6f (0x1141), TS0203, End device, RxOn:false)
2+
ZigbeeNodeEndpoint(0x01, Zigbee::ZigbeeProfileHomeAutomation, Zigbee::HomeAutomationDeviceIasZone)
3+
Manufacturer: "_TZ3000_7d8yme6f"
4+
Model "TS0203"
5+
Input clusters ( 4 )
6+
- ZigbeeCluster(0x0000, Basic, Server)
7+
- ZigbeeClusterAttribute(0x0004, ZigbeeDataType(Character string, _TZ3000_7d8yme6f))
8+
- ZigbeeClusterAttribute(0x0005, ZigbeeDataType(Character string, TS0203))
9+
- ZigbeeCluster(0x0001, PowerConfiguration, Server)
10+
- ZigbeeCluster(0x0003, Identify, Server)
11+
- ZigbeeCluster(0x0500, IasZone, Server)
12+
- ZigbeeClusterAttribute(0x0000, ZigbeeDataType(16-bit bitmap, 0x04 0x00))
13+
Output clusters ( 8 )
14+
- ZigbeeCluster(0x0008, LevelControl, Client)
15+
- ZigbeeCluster(0x000a, Time, Client)
16+
- ZigbeeCluster(0x0019, OtaUpgrade, Client)
17+
- ZigbeeCluster(0x0004, Groups, Client)
18+
- ZigbeeCluster(0x0005, Scenes, Client)
19+
- ZigbeeCluster(0x0006, OnOff, Client)
20+
- ZigbeeCluster(0x1000, TouchlinkCommissioning, Client)
21+
- ZigbeeCluster(0x0003, Identify, Client)

zigbeetuya/integrationpluginzigbeetuya.cpp

Lines changed: 202 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@
9191
#define SMOKE_SENSOR_DP_BATTERY 15
9292
#define SMOKE_SENSOR_DP_TEST 101
9393

94+
#define THERMOSTAT_DP_HEATING_SETPOINT 2
95+
#define THERMOSTAT_DP_LOCAL_TEMP 3
96+
#define THERMOSTAT_DP_MODE 4
97+
#define THERMOSTAT_DP_CHILD_LOCK 7
98+
#define THERMOSTAT_DP_WINDOW_OPEN_SITERWELL 18
99+
#define THERMOSTAT_DP_TEMP_CALIBRATION 44
100+
#define THERMOSTAT_DP_VALVE_OPEN 20
101+
#define THERMOSTAT_DP_BATTERY 21
102+
#define THERMOSTAT_DP_WINDOW_DETECTION 104
103+
#define THERMOSTAT_DP_BATTERY_LOW 110
104+
#define THERMOSTAT_DP_WINDOW_OPEN 115
105+
106+
94107
IntegrationPluginZigbeeTuya::IntegrationPluginZigbeeTuya(): ZigbeeIntegrationPlugin(ZigbeeHardwareResource::HandlerTypeVendor, dcZigbeeTuya())
95108
{
96109
}
@@ -125,7 +138,16 @@ bool IntegrationPluginZigbeeTuya::handleNode(ZigbeeNode *node, const QUuid &/*ne
125138
return true;
126139
}
127140

128-
if (node->nodeDescriptor().manufacturerCode == 0x1002 && node->modelName() == "TS0601") {
141+
if (match(node, "TS0601", {
142+
"_TZE200_auin8mzr",
143+
"_TZE200_lyetpprm",
144+
"_TZE200_jva8ink8",
145+
"_TZE200_holel4dk",
146+
"_TZE200_xpq2rzhq",
147+
"_TZE200_wukb7rhc",
148+
"_TZE204_xsm7l9xa",
149+
"_TZE204_ztc6ggyl",
150+
"_TZE200_ztc6ggyl"})) {
129151
createThing(presenceSensorThingClassId, node);
130152
return true;
131153
}
@@ -170,6 +192,31 @@ bool IntegrationPluginZigbeeTuya::handleNode(ZigbeeNode *node, const QUuid &/*ne
170192
return true;
171193
}
172194

195+
if (match(node, "TS0203", {"_TZ3000_7d8yme6f"})) {
196+
// Implements IAS Zone spec, but doesn't reply to ZoneType attribute, thus not handled properly by generic plugin
197+
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(0x01);
198+
configurePowerConfigurationInputClusterAttributeReporting(endpoint);
199+
bindCluster(endpoint, ZigbeeClusterLibrary::ClusterIdIasZone);
200+
configureIasZoneInputClusterAttributeReporting(endpoint);
201+
enrollIasZone(endpoint, 0x42);
202+
createThing(closableSensorThingClassId, node);
203+
return true;
204+
}
205+
206+
if (match(node, "TS0601", {
207+
"_TZE200_hhrtiq0x",
208+
"_TZE200_zivfvd7h",
209+
"_TZE200_kfvq6avy",
210+
"_TZE200_ps5v5jor",
211+
"_TZE200_jeaxp72v",
212+
"_TZE200_owwdxjbx",
213+
"_TZE200_2cs6g9i7",
214+
"_TZE200_04yfvweb"})) {
215+
createThing(thermostatThingClassId, node);
216+
return true;
217+
}
218+
219+
>>>>>>> 95b0cca (Tuya: Add thermostat support)
173220
return false;
174221
}
175222

@@ -667,6 +714,120 @@ void IntegrationPluginZigbeeTuya::createConnections(Thing *thing)
667714

668715
});
669716
}
717+
718+
if (thing->thingClassId() == closableSensorThingClassId) {
719+
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(1);
720+
if (!endpoint) {
721+
qCWarning(dcZigbeeTuya()) << "Unable to find endpoint 1 on node" << node;
722+
return;
723+
}
724+
connectToIasZoneInputCluster(thing, endpoint, "closed", true);
725+
}
726+
727+
if (thing->thingClassId() == thermostatThingClassId) {
728+
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(1);
729+
if (!endpoint) {
730+
qCWarning(dcZigbeeTuya()) << "Unable to find endpoint 1 on node" << node;
731+
return;
732+
}
733+
ZigbeeCluster *cluster = endpoint->getInputCluster(static_cast<ZigbeeClusterLibrary::ClusterId>(CLUSTER_ID_MANUFACTURER_SPECIFIC_TUYA));
734+
if (!cluster) {
735+
qCWarning(dcZigbeeTuya()) << "Unable to find Tuya manufacturer specific cluuster on endpoint 1 on node" << node;
736+
return;
737+
}
738+
739+
if (node->reachable()) {
740+
cluster->executeClusterCommand(COMMAND_ID_DATA_QUERY, QByteArray(), ZigbeeClusterLibrary::DirectionClientToServer, true);
741+
}
742+
connect(node, &ZigbeeNode::reachableChanged, thing, [=](bool reachable){
743+
if (reachable) {
744+
cluster->executeClusterCommand(COMMAND_ID_DATA_QUERY, QByteArray(), ZigbeeClusterLibrary::DirectionClientToServer, true);
745+
}
746+
});
747+
748+
connect(cluster, &ZigbeeCluster::dataIndication, thing, [this, thing](const ZigbeeClusterLibrary::Frame &frame){
749+
750+
if (frame.header.command == COMMAND_ID_DATA_REPORT || frame.header.command == COMMAND_ID_DATA_RESPONSE) {
751+
DpValue dpValue = DpValue::fromData(frame.payload);
752+
753+
switch (dpValue.dp()) {
754+
case THERMOSTAT_DP_HEATING_SETPOINT:
755+
qCDebug(dcZigbeeTuya()) << "Heating setpoint changed:" << dpValue;
756+
thing->setStateValue(thermostatTargetTemperatureStateTypeId, dpValue.value().toUInt() / 10.0);
757+
break;
758+
case THERMOSTAT_DP_LOCAL_TEMP:
759+
qCDebug(dcZigbeeTuya()) << "Local temp changed:" << dpValue;
760+
thing->setStateValue(thermostatTemperatureStateTypeId, dpValue.value().toUInt() / 10);
761+
break;
762+
case THERMOSTAT_DP_MODE:
763+
qCDebug(dcZigbeeTuya()) << "System mode changed:" << dpValue;
764+
thing->setStateValue(thermostatModeStateTypeId, dpValue.value().toUInt());
765+
break;
766+
case THERMOSTAT_DP_CHILD_LOCK:
767+
qCDebug(dcZigbeeTuya()) << "Child lock changed:" << dpValue;
768+
thing->setStateValue(thermostatChildLockStateTypeId, dpValue.value().toUInt() == 1);
769+
break;
770+
case THERMOSTAT_DP_WINDOW_OPEN:
771+
case THERMOSTAT_DP_WINDOW_OPEN_SITERWELL:
772+
qCDebug(dcZigbeeTuya()) << "Window open changed:" << dpValue;
773+
thing->setStateValue(thermostatWindowOpenStateTypeId, dpValue.value().toUInt() == 0);
774+
break;
775+
case THERMOSTAT_DP_WINDOW_DETECTION:
776+
qCDebug(dcZigbeeTuya()) << "Window detection enabled changed:" << dpValue;
777+
thing->setSettingValue(thermostatSettingsWindowDetectionParamTypeId, dpValue.value().toUInt());
778+
break;
779+
case THERMOSTAT_DP_TEMP_CALIBRATION:
780+
qCDebug(dcZigbeeTuya()) << "Temp calibration changed:" << dpValue;
781+
thing->setSettingValue(thermostatSettingsTemperatureCalibrationParamTypeId, dpValue.value().toUInt() / 10);
782+
break;
783+
case THERMOSTAT_DP_VALVE_OPEN:
784+
qCDebug(dcZigbeeTuya()) << "Valve open changed:" << dpValue;
785+
thing->setStateValue(thermostatHeatingOnStateTypeId, dpValue.value().toUInt() == 1);
786+
break;
787+
case THERMOSTAT_DP_BATTERY:
788+
qCDebug(dcZigbeeTuya()) << "Battery changed:" << dpValue;
789+
thing->setStateValue(thermostatBatteryLevelStateTypeId, dpValue.value().toUInt());
790+
break;
791+
case THERMOSTAT_DP_BATTERY_LOW:
792+
qCDebug(dcZigbeeTuya()) << "Battery low changed:" << dpValue;
793+
thing->setStateValue(thermostatBatteryCriticalStateTypeId, dpValue.value().toUInt() == 1);
794+
break;
795+
default:
796+
qCWarning(dcZigbeeTuya()) << "Unhandled data point" << dpValue;
797+
}
798+
799+
if (frame.header.command == COMMAND_ID_DATA_RESPONSE) {
800+
qCDebug(dcZigbeeTuya()) << "Command response:" << dpValue;;
801+
foreach (ThingActionInfo *info, m_actionQueue.keys()) {
802+
if (info->thing() == thing && m_actionQueue.value(info).dp() == dpValue.dp()) {
803+
qCDebug(dcZigbeeTuya()) << "Finishing action";
804+
info->finish(Thing::ThingErrorNoError);
805+
return;
806+
}
807+
}
808+
qCWarning(dcZigbeeTuya) << "No pending action for command response found!";
809+
}
810+
} else {
811+
qCWarning(dcZigbeeTuya()) << "Unhandled thermostat command:" << frame.header.command;
812+
}
813+
814+
815+
});
816+
817+
connect(thing, &Thing::settingChanged, cluster, [cluster, thing, this](const ParamTypeId &settingTypeId, const QVariant &value) {
818+
DpValue dp;
819+
820+
if (settingTypeId == thermostatSettingsWindowDetectionParamTypeId) {
821+
dp = DpValue(THERMOSTAT_DP_WINDOW_DETECTION, DpValue::TypeUInt32, value.toUInt(), m_seq++);
822+
}
823+
if (settingTypeId == thermostatSettingsTemperatureCalibrationParamTypeId) {
824+
dp = DpValue(THERMOSTAT_DP_WINDOW_DETECTION, DpValue::TypeUInt32, value.toDouble() * 10, m_seq++);
825+
}
826+
qCDebug(dcZigbeeTuya()) << "setting" << thing->thingClass().settingsTypes().findById(settingTypeId).name() << dp << dp.toData().toHex();
827+
writeDpDelayed(cluster, dp);
828+
});
829+
830+
}
670831
}
671832

672833
void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
@@ -702,6 +863,38 @@ void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
702863
}
703864
}
704865

866+
if (thing->thingClassId() == thermostatThingClassId) {
867+
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(0x01);
868+
ZigbeeCluster *cluster = endpoint->getInputCluster(static_cast<ZigbeeClusterLibrary::ClusterId>(CLUSTER_ID_MANUFACTURER_SPECIFIC_TUYA));
869+
if (!cluster) {
870+
qCWarning(dcZigbeeTuya()) << "Unable to find Tuya manufacturer specific cluuster on endpoint 1 on node" << node;
871+
info->finish(Thing::ThingErrorHardwareFailure);
872+
return;
873+
}
874+
875+
if (info->action().actionTypeId() == thermostatChildLockActionTypeId) {
876+
bool locked = info->action().param(thermostatChildLockActionChildLockParamTypeId).value().toBool();
877+
DpValue dp = DpValue(THERMOSTAT_DP_CHILD_LOCK, DpValue::TypeBool,locked ? 1 : 0, m_seq++);
878+
qCDebug(dcZigbeeTuya()) << "setting child lock:" << dp << dp.toData().toHex();
879+
writeDpDelayed(cluster, dp, info);
880+
return;
881+
}
882+
if (info->action().actionTypeId() == thermostatTargetTemperatureActionTypeId) {
883+
quint16 heatingSetpoint = info->action().param(thermostatTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble() * 10;
884+
DpValue dp = DpValue(THERMOSTAT_DP_HEATING_SETPOINT, DpValue::TypeUInt32, heatingSetpoint, m_seq++);
885+
qCDebug(dcZigbeeTuya()) << "setting heating setpoint:" << dp << dp.toData().toHex();
886+
writeDpDelayed(cluster, dp, info);
887+
return;
888+
}
889+
if (info->action().actionTypeId() == thermostatModeActionTypeId) {
890+
quint8 mode = info->action().param(thermostatModeActionModeParamTypeId).value().toUInt();
891+
DpValue dp = DpValue(THERMOSTAT_DP_MODE, DpValue::TypeUInt32, mode, m_seq++);
892+
qCDebug(dcZigbeeTuya()) << "setting mode:" << dp << dp.toData().toHex();
893+
writeDpDelayed(cluster, dp, info);
894+
return;
895+
}
896+
}
897+
705898
info->finish(Thing::ThingErrorUnsupportedFeature);
706899
}
707900

@@ -733,7 +926,7 @@ bool IntegrationPluginZigbeeTuya::match(ZigbeeNode *node, const QString &modelNa
733926
return node->modelName() == modelName && manufacturerNames.contains(node->manufacturerName());
734927
}
735928

736-
void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp)
929+
void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp, ThingActionInfo *info)
737930
{
738931
DelayedDpWrite op;
739932
op.cluster = cluster;
@@ -743,5 +936,12 @@ void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const D
743936
// Trigger the delayed write asap by trying to read to trigger a lastSeen change
744937
cluster->executeClusterCommand(COMMAND_ID_DATA_QUERY, QByteArray(), ZigbeeClusterLibrary::DirectionClientToServer, true);
745938

939+
if (info) {
940+
m_actionQueue.insert(info, dp);
941+
connect(info, &ThingActionInfo::finished, this, [=](){
942+
m_actionQueue.remove(info);
943+
});
944+
}
945+
746946
}
747947

zigbeetuya/integrationpluginzigbeetuya.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private slots:
6666
private:
6767
bool match(ZigbeeNode *node, const QString &modelName, const QStringList &manufacturerNames);
6868

69-
void writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp);
69+
void writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp, ThingActionInfo *info = nullptr);
7070

7171
private:
7272
struct DelayedDpWrite {
@@ -77,6 +77,7 @@ private slots:
7777
PluginTimer *m_energyPollTimer = nullptr;
7878
quint16 m_seq = 0;
7979
QList<DelayedDpWrite> m_delayedDpWrites;
80+
QHash<ThingActionInfo*, DpValue> m_actionQueue;
8081
};
8182

8283
#endif // INTEGRATIONPLUGINZIGBEETUYA_H

0 commit comments

Comments
 (0)