Skip to content

Commit 65d8c09

Browse files
author
jenkins
committed
Merge PR #75: WIP: Tuya: Add thermostat support
2 parents 72b913d + 95b0cca commit 65d8c09

3 files changed

Lines changed: 325 additions & 3 deletions

File tree

zigbeetuya/integrationpluginzigbeetuya.cpp

Lines changed: 181 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,19 @@ bool IntegrationPluginZigbeeTuya::handleNode(ZigbeeNode *node, const QUuid &/*ne
170192
return true;
171193
}
172194

195+
if (match(node, "TS0601", {
196+
"_TZE200_hhrtiq0x",
197+
"_TZE200_zivfvd7h",
198+
"_TZE200_kfvq6avy",
199+
"_TZE200_ps5v5jor",
200+
"_TZE200_jeaxp72v",
201+
"_TZE200_owwdxjbx",
202+
"_TZE200_2cs6g9i7",
203+
"_TZE200_04yfvweb"})) {
204+
createThing(thermostatThingClassId, node);
205+
return true;
206+
}
207+
173208
return false;
174209
}
175210

@@ -667,6 +702,111 @@ void IntegrationPluginZigbeeTuya::createConnections(Thing *thing)
667702

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

672812
void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
@@ -702,6 +842,38 @@ void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
702842
}
703843
}
704844

845+
if (thing->thingClassId() == thermostatThingClassId) {
846+
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(0x01);
847+
ZigbeeCluster *cluster = endpoint->getInputCluster(static_cast<ZigbeeClusterLibrary::ClusterId>(CLUSTER_ID_MANUFACTURER_SPECIFIC_TUYA));
848+
if (!cluster) {
849+
qCWarning(dcZigbeeTuya()) << "Unable to find Tuya manufacturer specific cluuster on endpoint 1 on node" << node;
850+
info->finish(Thing::ThingErrorHardwareFailure);
851+
return;
852+
}
853+
854+
if (info->action().actionTypeId() == thermostatChildLockActionTypeId) {
855+
bool locked = info->action().param(thermostatChildLockActionChildLockParamTypeId).value().toBool();
856+
DpValue dp = DpValue(THERMOSTAT_DP_CHILD_LOCK, DpValue::TypeBool,locked ? 1 : 0, m_seq++);
857+
qCDebug(dcZigbeeTuya()) << "setting child lock:" << dp << dp.toData().toHex();
858+
writeDpDelayed(cluster, dp, info);
859+
return;
860+
}
861+
if (info->action().actionTypeId() == thermostatTargetTemperatureActionTypeId) {
862+
quint16 heatingSetpoint = info->action().param(thermostatTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble() * 10;
863+
DpValue dp = DpValue(THERMOSTAT_DP_HEATING_SETPOINT, DpValue::TypeUInt32, heatingSetpoint, m_seq++);
864+
qCDebug(dcZigbeeTuya()) << "setting heating setpoint:" << dp << dp.toData().toHex();
865+
writeDpDelayed(cluster, dp, info);
866+
return;
867+
}
868+
if (info->action().actionTypeId() == thermostatModeActionTypeId) {
869+
quint8 mode = info->action().param(thermostatModeActionModeParamTypeId).value().toUInt();
870+
DpValue dp = DpValue(THERMOSTAT_DP_MODE, DpValue::TypeUInt32, mode, m_seq++);
871+
qCDebug(dcZigbeeTuya()) << "setting mode:" << dp << dp.toData().toHex();
872+
writeDpDelayed(cluster, dp, info);
873+
return;
874+
}
875+
}
876+
705877
info->finish(Thing::ThingErrorUnsupportedFeature);
706878
}
707879

@@ -733,7 +905,7 @@ bool IntegrationPluginZigbeeTuya::match(ZigbeeNode *node, const QString &modelNa
733905
return node->modelName() == modelName && manufacturerNames.contains(node->manufacturerName());
734906
}
735907

736-
void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp)
908+
void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const DpValue &dp, ThingActionInfo *info)
737909
{
738910
DelayedDpWrite op;
739911
op.cluster = cluster;
@@ -743,5 +915,12 @@ void IntegrationPluginZigbeeTuya::writeDpDelayed(ZigbeeCluster *cluster, const D
743915
// Trigger the delayed write asap by trying to read to trigger a lastSeen change
744916
cluster->executeClusterCommand(COMMAND_ID_DATA_QUERY, QByteArray(), ZigbeeClusterLibrary::DirectionClientToServer, true);
745917

918+
if (info) {
919+
m_actionQueue.insert(info, dp);
920+
connect(info, &ThingActionInfo::finished, this, [=](){
921+
m_actionQueue.remove(info);
922+
});
923+
}
924+
746925
}
747926

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
@@ -703,6 +703,148 @@
703703
"defaultValue": false
704704
}
705705
]
706+
},
707+
{
708+
"id": "51cf21ce-28a0-4b48-851b-d27a18d00b9c",
709+
"name": "thermostat",
710+
"displayName": "Thermostat",
711+
"createMethods": [ "auto" ],
712+
"interfaces": ["thermostat", "temperaturesensor", "battery", "childlock", "wirelessconnectable"],
713+
"paramTypes": [
714+
{
715+
"id": "2c562d32-0351-4f39-9d33-178817a9d413",
716+
"name": "ieeeAddress",
717+
"displayName": "IEEE adress",
718+
"type": "QString",
719+
"defaultValue": "00:00:00:00:00:00:00:00"
720+
},
721+
{
722+
"id": "35aa5409-43b9-4f66-bb22-225f226427d5",
723+
"name": "networkUuid",
724+
"displayName": "Zigbee network UUID",
725+
"type": "QString",
726+
"defaultValue": ""
727+
}
728+
],
729+
"settingsTypes": [
730+
{
731+
"id": "df9414af-9db3-4ee0-bf31-ff832b437324",
732+
"name": "windowDetection",
733+
"displayName": "Window open detection",
734+
"type": "bool",
735+
"defaultValue": true
736+
},
737+
{
738+
"id": "c94e536e-7bb4-4e66-906f-a370783011d8",
739+
"name": "temperatureCalibration",
740+
"displayName": "Temperature calibration offset",
741+
"type": "double",
742+
"defaultValue": 0
743+
}
744+
],
745+
"stateTypes": [
746+
{
747+
"id": "08e33c27-54cb-4270-a82d-a2b24bea8c45",
748+
"name": "targetTemperature",
749+
"displayName": "Target temperature",
750+
"displayNameAction": "Set target temperature",
751+
"type": "double",
752+
"unit": "DegreeCelsius",
753+
"minValue": 7,
754+
"maxValue": 30,
755+
"defaultValue": 0,
756+
"writable": true
757+
},
758+
{
759+
"id": "6db38434-4f6c-4326-a4c1-deb3868cc402",
760+
"name": "temperature",
761+
"displayName": "Current temperature",
762+
"type": "double",
763+
"unit": "DegreeCelsius",
764+
"defaultValue": 0
765+
},
766+
{
767+
"id": "fb58531f-c650-486c-bef7-b1a63073c681",
768+
"name": "heatingOn",
769+
"displayName": "Heating on",
770+
"type": "bool",
771+
"defaultValue": false
772+
},
773+
{
774+
"id": "31e58198-a7c0-4866-9aac-6cac3a07fdb9",
775+
"name": "connected",
776+
"displayName": "Connected",
777+
"type": "bool",
778+
"cached": false,
779+
"defaultValue": false
780+
},
781+
{
782+
"id": "95e83e40-5f85-41cf-9f22-060bec40392d",
783+
"name": "signalStrength",
784+
"displayName": "Signal strength",
785+
"defaultValue": 0,
786+
"maxValue": 100,
787+
"minValue": 0,
788+
"type": "uint",
789+
"unit": "Percentage"
790+
},
791+
{
792+
"id": "97c0b335-b29f-4297-9039-44ac612dabfb",
793+
"name": "batteryLevel",
794+
"displayName": "Battery level",
795+
"type": "int",
796+
"unit": "Percentage",
797+
"defaultValue": 50,
798+
"minValue": 0,
799+
"maxValue": 100
800+
},
801+
{
802+
"id": "21564d78-d28c-47f2-8e67-5a6330df4bef",
803+
"name": "batteryCritical",
804+
"displayName": "Battery critical",
805+
"type": "bool",
806+
"defaultValue": false
807+
},
808+
{
809+
"id": "81d70a7c-2151-4354-b81e-cf406cf72979",
810+
"name": "windowOpen",
811+
"displayName": "Window open",
812+
"type": "bool",
813+
"defaultValue": false
814+
},
815+
{
816+
"id": "2db4b96b-8892-4864-af6a-3d40c296fb6f",
817+
"name": "childLock",
818+
"displayName": "Child protection lock",
819+
"displayNameAction": "Set child protection lock",
820+
"type": "bool",
821+
"defaultValue": false,
822+
"writable": true
823+
},
824+
{
825+
"id": "288c6bcf-afa4-4851-87b5-d23c008ec771",
826+
"name": "mode",
827+
"displayName": "Mode",
828+
"displayNameAction": "Set mode",
829+
"type": "uint",
830+
"possibleValues": [
831+
{
832+
"value": 0,
833+
"displayName": "Off"
834+
},
835+
{
836+
"value": 1,
837+
"displayName": "Auto"
838+
},
839+
{
840+
"value": 2,
841+
"displayName": "Manual"
842+
}
843+
],
844+
"defaultValue": 1,
845+
"writable": true
846+
}
847+
]
706848
}
707849
]
708850
}

0 commit comments

Comments
 (0)