Skip to content

Commit 30df1a7

Browse files
committed
Tuya: Add thermostat support
1 parent 77a1238 commit 30df1a7

3 files changed

Lines changed: 326 additions & 3 deletions

File tree

zigbeetuya/integrationpluginzigbeetuya.cpp

Lines changed: 182 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
}
@@ -180,6 +202,20 @@ bool IntegrationPluginZigbeeTuya::handleNode(ZigbeeNode *node, const QUuid &/*ne
180202
createThing(closableSensorThingClassId, node);
181203
return true;
182204
}
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+
183219
return false;
184220
}
185221

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

691832
void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
@@ -721,6 +862,38 @@ void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
721862
}
722863
}
723864

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

@@ -752,7 +925,7 @@ bool IntegrationPluginZigbeeTuya::match(ZigbeeNode *node, const QString &modelNa
752925
return node->modelName() == modelName && manufacturerNames.contains(node->manufacturerName());
753926
}
754927

755-
void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp)
928+
void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp, ThingActionInfo *info)
756929
{
757930
DelayedDpWrite op;
758931
op.cluster = cluster;
@@ -762,5 +935,12 @@ void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const D
762935
// Trigger the delayed write asap by trying to read to trigger a lastSeen change
763936
cluster->executeClusterCommand(COMMAND_ID_DATA_QUERY, QByteArray(), ZigbeeClusterLibrary::DirectionClientToServer, true);
764937

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

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

zigbeetuya/integrationpluginzigbeetuya.json

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,148 @@
783783
"defaultValue": 0
784784
}
785785
]
786+
},
787+
{
788+
"id": "51cf21ce-28a0-4b48-851b-d27a18d00b9c",
789+
"name": "thermostat",
790+
"displayName": "Thermostat",
791+
"createMethods": [ "auto" ],
792+
"interfaces": ["thermostat", "temperaturesensor", "battery", "childlock", "wirelessconnectable"],
793+
"paramTypes": [
794+
{
795+
"id": "2c562d32-0351-4f39-9d33-178817a9d413",
796+
"name": "ieeeAddress",
797+
"displayName": "IEEE adress",
798+
"type": "QString",
799+
"defaultValue": "00:00:00:00:00:00:00:00"
800+
},
801+
{
802+
"id": "35aa5409-43b9-4f66-bb22-225f226427d5",
803+
"name": "networkUuid",
804+
"displayName": "Zigbee network UUID",
805+
"type": "QString",
806+
"defaultValue": ""
807+
}
808+
],
809+
"settingsTypes": [
810+
{
811+
"id": "df9414af-9db3-4ee0-bf31-ff832b437324",
812+
"name": "windowDetection",
813+
"displayName": "Window open detection",
814+
"type": "bool",
815+
"defaultValue": true
816+
},
817+
{
818+
"id": "c94e536e-7bb4-4e66-906f-a370783011d8",
819+
"name": "temperatureCalibration",
820+
"displayName": "Temperature calibration offset",
821+
"type": "double",
822+
"defaultValue": 0
823+
}
824+
],
825+
"stateTypes": [
826+
{
827+
"id": "08e33c27-54cb-4270-a82d-a2b24bea8c45",
828+
"name": "targetTemperature",
829+
"displayName": "Target temperature",
830+
"displayNameAction": "Set target temperature",
831+
"type": "double",
832+
"unit": "DegreeCelsius",
833+
"minValue": 7,
834+
"maxValue": 30,
835+
"defaultValue": 0,
836+
"writable": true
837+
},
838+
{
839+
"id": "6db38434-4f6c-4326-a4c1-deb3868cc402",
840+
"name": "temperature",
841+
"displayName": "Current temperature",
842+
"type": "double",
843+
"unit": "DegreeCelsius",
844+
"defaultValue": 0
845+
},
846+
{
847+
"id": "fb58531f-c650-486c-bef7-b1a63073c681",
848+
"name": "heatingOn",
849+
"displayName": "Heating on",
850+
"type": "bool",
851+
"defaultValue": false
852+
},
853+
{
854+
"id": "31e58198-a7c0-4866-9aac-6cac3a07fdb9",
855+
"name": "connected",
856+
"displayName": "Connected",
857+
"type": "bool",
858+
"cached": false,
859+
"defaultValue": false
860+
},
861+
{
862+
"id": "95e83e40-5f85-41cf-9f22-060bec40392d",
863+
"name": "signalStrength",
864+
"displayName": "Signal strength",
865+
"defaultValue": 0,
866+
"maxValue": 100,
867+
"minValue": 0,
868+
"type": "uint",
869+
"unit": "Percentage"
870+
},
871+
{
872+
"id": "97c0b335-b29f-4297-9039-44ac612dabfb",
873+
"name": "batteryLevel",
874+
"displayName": "Battery level",
875+
"type": "int",
876+
"unit": "Percentage",
877+
"defaultValue": 50,
878+
"minValue": 0,
879+
"maxValue": 100
880+
},
881+
{
882+
"id": "21564d78-d28c-47f2-8e67-5a6330df4bef",
883+
"name": "batteryCritical",
884+
"displayName": "Battery critical",
885+
"type": "bool",
886+
"defaultValue": false
887+
},
888+
{
889+
"id": "81d70a7c-2151-4354-b81e-cf406cf72979",
890+
"name": "windowOpen",
891+
"displayName": "Window open",
892+
"type": "bool",
893+
"defaultValue": false
894+
},
895+
{
896+
"id": "2db4b96b-8892-4864-af6a-3d40c296fb6f",
897+
"name": "childLock",
898+
"displayName": "Child protection lock",
899+
"displayNameAction": "Set child protection lock",
900+
"type": "bool",
901+
"defaultValue": false,
902+
"writable": true
903+
},
904+
{
905+
"id": "288c6bcf-afa4-4851-87b5-d23c008ec771",
906+
"name": "mode",
907+
"displayName": "Mode",
908+
"displayNameAction": "Set mode",
909+
"type": "uint",
910+
"possibleValues": [
911+
{
912+
"value": 0,
913+
"displayName": "Off"
914+
},
915+
{
916+
"value": 1,
917+
"displayName": "Auto"
918+
},
919+
{
920+
"value": 2,
921+
"displayName": "Manual"
922+
}
923+
],
924+
"defaultValue": 1,
925+
"writable": true
926+
}
927+
]
786928
}
787929
]
788930
}

0 commit comments

Comments
 (0)