Skip to content

Commit

Permalink
Merge PR #75: WIP: Tuya: Add thermostat support
Browse files Browse the repository at this point in the history
  • Loading branch information
jenkins committed Nov 27, 2023
2 parents b6b4c8c + 30df1a7 commit 542189d
Show file tree
Hide file tree
Showing 7 changed files with 458 additions and 18 deletions.
14 changes: 0 additions & 14 deletions zigbeegeneric/integrationpluginzigbeegeneric.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,20 +345,6 @@ void IntegrationPluginZigbeeGeneric::createConnections(Thing *thing)

if (thing->thingClassId() == doorSensorThingClassId) {
connectToIasZoneInputCluster(thing, endpoint, "closed", true);
ZigbeeClusterIasZone *iasZoneCluster = endpoint->inputCluster<ZigbeeClusterIasZone>(ZigbeeClusterLibrary::ClusterIdIasZone);
if (!iasZoneCluster) {
qCWarning(dcZigbeeGeneric()) << "Could not find IAS zone cluster on" << thing << endpoint;
} else {
if (iasZoneCluster->hasAttribute(ZigbeeClusterIasZone::AttributeZoneStatus)) {
qCDebug(dcZigbeeGeneric()) << thing << iasZoneCluster->zoneStatus();
ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus = iasZoneCluster->zoneStatus();
thing->setStateValue(doorSensorClosedStateTypeId, !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) && !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
}
connect(iasZoneCluster, &ZigbeeClusterIasZone::zoneStatusChanged, thing, [=](ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus, quint8 extendedStatus, quint8 zoneId, quint16 delays) {
qCDebug(dcZigbeeGeneric()) << "Zone status changed to:" << zoneStatus << extendedStatus << zoneId << delays;
thing->setStateValue(doorSensorClosedStateTypeId, !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) && !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
});
}
}

if (thing->thingClassId() == motionSensorThingClassId) {
Expand Down
10 changes: 9 additions & 1 deletion zigbeetuya/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ This plugin adds support for ZigBee devices by Tuya.

## Supported Things

* Smart plugs with energy metering (TS011 plugs)
* Smart plugs with energy metering
* PIR motion sensors
* Mm Wave presence sensors
* Vibration sensors
* H&T sensors
* H&T Display sensors
* AirHouseKeeper
* Smoke sensors
* Door/Window sensors
21 changes: 21 additions & 0 deletions zigbeetuya/descriptors/_TZ3000_7d8yme6f.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--> ZigbeeNode(0x1577, A4:C1:38:EB:34:5E:C9:DB, _TZ3000_7d8yme6f (0x1141), TS0203, End device, RxOn:false)
ZigbeeNodeEndpoint(0x01, Zigbee::ZigbeeProfileHomeAutomation, Zigbee::HomeAutomationDeviceIasZone)
Manufacturer: "_TZ3000_7d8yme6f"
Model "TS0203"
Input clusters ( 4 )
- ZigbeeCluster(0x0000, Basic, Server)
- ZigbeeClusterAttribute(0x0004, ZigbeeDataType(Character string, _TZ3000_7d8yme6f))
- ZigbeeClusterAttribute(0x0005, ZigbeeDataType(Character string, TS0203))
- ZigbeeCluster(0x0001, PowerConfiguration, Server)
- ZigbeeCluster(0x0003, Identify, Server)
- ZigbeeCluster(0x0500, IasZone, Server)
- ZigbeeClusterAttribute(0x0000, ZigbeeDataType(16-bit bitmap, 0x04 0x00))
Output clusters ( 8 )
- ZigbeeCluster(0x0008, LevelControl, Client)
- ZigbeeCluster(0x000a, Time, Client)
- ZigbeeCluster(0x0019, OtaUpgrade, Client)
- ZigbeeCluster(0x0004, Groups, Client)
- ZigbeeCluster(0x0005, Scenes, Client)
- ZigbeeCluster(0x0006, OnOff, Client)
- ZigbeeCluster(0x1000, TouchlinkCommissioning, Client)
- ZigbeeCluster(0x0003, Identify, Client)
203 changes: 201 additions & 2 deletions zigbeetuya/integrationpluginzigbeetuya.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@
#define SMOKE_SENSOR_DP_BATTERY 15
#define SMOKE_SENSOR_DP_TEST 101

#define THERMOSTAT_DP_HEATING_SETPOINT 2
#define THERMOSTAT_DP_LOCAL_TEMP 3
#define THERMOSTAT_DP_MODE 4
#define THERMOSTAT_DP_CHILD_LOCK 7
#define THERMOSTAT_DP_WINDOW_OPEN_SITERWELL 18
#define THERMOSTAT_DP_TEMP_CALIBRATION 44
#define THERMOSTAT_DP_VALVE_OPEN 20
#define THERMOSTAT_DP_BATTERY 21
#define THERMOSTAT_DP_WINDOW_DETECTION 104
#define THERMOSTAT_DP_BATTERY_LOW 110
#define THERMOSTAT_DP_WINDOW_OPEN 115


IntegrationPluginZigbeeTuya::IntegrationPluginZigbeeTuya(): ZigbeeIntegrationPlugin(ZigbeeHardwareResource::HandlerTypeVendor, dcZigbeeTuya())
{
}
Expand Down Expand Up @@ -125,7 +138,16 @@ bool IntegrationPluginZigbeeTuya::handleNode(ZigbeeNode *node, const QUuid &/*ne
return true;
}

if (node->nodeDescriptor().manufacturerCode == 0x1002 && node->modelName() == "TS0601") {
if (match(node, "TS0601", {
"_TZE200_auin8mzr",
"_TZE200_lyetpprm",
"_TZE200_jva8ink8",
"_TZE200_holel4dk",
"_TZE200_xpq2rzhq",
"_TZE200_wukb7rhc",
"_TZE204_xsm7l9xa",
"_TZE204_ztc6ggyl",
"_TZE200_ztc6ggyl"})) {
createThing(presenceSensorThingClassId, node);
return true;
}
Expand Down Expand Up @@ -170,6 +192,30 @@ bool IntegrationPluginZigbeeTuya::handleNode(ZigbeeNode *node, const QUuid &/*ne
return true;
}

if (match(node, "TS0203", {"_TZ3000_7d8yme6f"})) {
// Implements IAS Zone spec, but doesn't reply to ZoneType attribute, thus not handled properly by generic plugin
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(0x01);
configurePowerConfigurationInputClusterAttributeReporting(endpoint);
bindCluster(endpoint, ZigbeeClusterLibrary::ClusterIdIasZone);
configureIasZoneInputClusterAttributeReporting(endpoint);
enrollIasZone(endpoint, 0x42);
createThing(closableSensorThingClassId, node);
return true;
}

if (match(node, "TS0601", {
"_TZE200_hhrtiq0x",
"_TZE200_zivfvd7h",
"_TZE200_kfvq6avy",
"_TZE200_ps5v5jor",
"_TZE200_jeaxp72v",
"_TZE200_owwdxjbx",
"_TZE200_2cs6g9i7",
"_TZE200_04yfvweb"})) {
createThing(thermostatThingClassId, node);
return true;
}

return false;
}

Expand Down Expand Up @@ -667,6 +713,120 @@ void IntegrationPluginZigbeeTuya::createConnections(Thing *thing)

});
}

if (thing->thingClassId() == closableSensorThingClassId) {
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(1);
if (!endpoint) {
qCWarning(dcZigbeeTuya()) << "Unable to find endpoint 1 on node" << node;
return;
}
connectToIasZoneInputCluster(thing, endpoint, "closed", true);
}

if (thing->thingClassId() == thermostatThingClassId) {
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(1);
if (!endpoint) {
qCWarning(dcZigbeeTuya()) << "Unable to find endpoint 1 on node" << node;
return;
}
ZigbeeCluster *cluster = endpoint->getInputCluster(static_cast<ZigbeeClusterLibrary::ClusterId>(CLUSTER_ID_MANUFACTURER_SPECIFIC_TUYA));
if (!cluster) {
qCWarning(dcZigbeeTuya()) << "Unable to find Tuya manufacturer specific cluuster on endpoint 1 on node" << node;
return;
}

if (node->reachable()) {
cluster->executeClusterCommand(COMMAND_ID_DATA_QUERY, QByteArray(), ZigbeeClusterLibrary::DirectionClientToServer, true);
}
connect(node, &ZigbeeNode::reachableChanged, thing, [=](bool reachable){
if (reachable) {
cluster->executeClusterCommand(COMMAND_ID_DATA_QUERY, QByteArray(), ZigbeeClusterLibrary::DirectionClientToServer, true);
}
});

connect(cluster, &ZigbeeCluster::dataIndication, thing, [this, thing](const ZigbeeClusterLibrary::Frame &frame){

if (frame.header.command == COMMAND_ID_DATA_REPORT || frame.header.command == COMMAND_ID_DATA_RESPONSE) {
DpValue dpValue = DpValue::fromData(frame.payload);

switch (dpValue.dp()) {
case THERMOSTAT_DP_HEATING_SETPOINT:
qCDebug(dcZigbeeTuya()) << "Heating setpoint changed:" << dpValue;
thing->setStateValue(thermostatTargetTemperatureStateTypeId, dpValue.value().toUInt() / 10.0);
break;
case THERMOSTAT_DP_LOCAL_TEMP:
qCDebug(dcZigbeeTuya()) << "Local temp changed:" << dpValue;
thing->setStateValue(thermostatTemperatureStateTypeId, dpValue.value().toUInt() / 10);
break;
case THERMOSTAT_DP_MODE:
qCDebug(dcZigbeeTuya()) << "System mode changed:" << dpValue;
thing->setStateValue(thermostatModeStateTypeId, dpValue.value().toUInt());
break;
case THERMOSTAT_DP_CHILD_LOCK:
qCDebug(dcZigbeeTuya()) << "Child lock changed:" << dpValue;
thing->setStateValue(thermostatChildLockStateTypeId, dpValue.value().toUInt() == 1);
break;
case THERMOSTAT_DP_WINDOW_OPEN:
case THERMOSTAT_DP_WINDOW_OPEN_SITERWELL:
qCDebug(dcZigbeeTuya()) << "Window open changed:" << dpValue;
thing->setStateValue(thermostatWindowOpenStateTypeId, dpValue.value().toUInt() == 0);
break;
case THERMOSTAT_DP_WINDOW_DETECTION:
qCDebug(dcZigbeeTuya()) << "Window detection enabled changed:" << dpValue;
thing->setSettingValue(thermostatSettingsWindowDetectionParamTypeId, dpValue.value().toUInt());
break;
case THERMOSTAT_DP_TEMP_CALIBRATION:
qCDebug(dcZigbeeTuya()) << "Temp calibration changed:" << dpValue;
thing->setSettingValue(thermostatSettingsTemperatureCalibrationParamTypeId, dpValue.value().toUInt() / 10);
break;
case THERMOSTAT_DP_VALVE_OPEN:
qCDebug(dcZigbeeTuya()) << "Valve open changed:" << dpValue;
thing->setStateValue(thermostatHeatingOnStateTypeId, dpValue.value().toUInt() == 1);
break;
case THERMOSTAT_DP_BATTERY:
qCDebug(dcZigbeeTuya()) << "Battery changed:" << dpValue;
thing->setStateValue(thermostatBatteryLevelStateTypeId, dpValue.value().toUInt());
break;
case THERMOSTAT_DP_BATTERY_LOW:
qCDebug(dcZigbeeTuya()) << "Battery low changed:" << dpValue;
thing->setStateValue(thermostatBatteryCriticalStateTypeId, dpValue.value().toUInt() == 1);
break;
default:
qCWarning(dcZigbeeTuya()) << "Unhandled data point" << dpValue;
}

if (frame.header.command == COMMAND_ID_DATA_RESPONSE) {
qCDebug(dcZigbeeTuya()) << "Command response:" << dpValue;;
foreach (ThingActionInfo *info, m_actionQueue.keys()) {
if (info->thing() == thing && m_actionQueue.value(info).dp() == dpValue.dp()) {
qCDebug(dcZigbeeTuya()) << "Finishing action";
info->finish(Thing::ThingErrorNoError);
return;
}
}
qCWarning(dcZigbeeTuya) << "No pending action for command response found!";
}
} else {
qCWarning(dcZigbeeTuya()) << "Unhandled thermostat command:" << frame.header.command;
}


});

connect(thing, &Thing::settingChanged, cluster, [cluster, thing, this](const ParamTypeId &settingTypeId, const QVariant &value) {
DpValue dp;

if (settingTypeId == thermostatSettingsWindowDetectionParamTypeId) {
dp = DpValue(THERMOSTAT_DP_WINDOW_DETECTION, DpValue::TypeUInt32, value.toUInt(), m_seq++);
}
if (settingTypeId == thermostatSettingsTemperatureCalibrationParamTypeId) {
dp = DpValue(THERMOSTAT_DP_WINDOW_DETECTION, DpValue::TypeUInt32, value.toDouble() * 10, m_seq++);
}
qCDebug(dcZigbeeTuya()) << "setting" << thing->thingClass().settingsTypes().findById(settingTypeId).name() << dp << dp.toData().toHex();
writeDpDelayed(cluster, dp);
});

}
}

void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
Expand Down Expand Up @@ -702,6 +862,38 @@ void IntegrationPluginZigbeeTuya::executeAction(ThingActionInfo *info)
}
}

if (thing->thingClassId() == thermostatThingClassId) {
ZigbeeNodeEndpoint *endpoint = node->getEndpoint(0x01);
ZigbeeCluster *cluster = endpoint->getInputCluster(static_cast<ZigbeeClusterLibrary::ClusterId>(CLUSTER_ID_MANUFACTURER_SPECIFIC_TUYA));
if (!cluster) {
qCWarning(dcZigbeeTuya()) << "Unable to find Tuya manufacturer specific cluuster on endpoint 1 on node" << node;
info->finish(Thing::ThingErrorHardwareFailure);
return;
}

if (info->action().actionTypeId() == thermostatChildLockActionTypeId) {
bool locked = info->action().param(thermostatChildLockActionChildLockParamTypeId).value().toBool();
DpValue dp = DpValue(THERMOSTAT_DP_CHILD_LOCK, DpValue::TypeBool,locked ? 1 : 0, m_seq++);
qCDebug(dcZigbeeTuya()) << "setting child lock:" << dp << dp.toData().toHex();
writeDpDelayed(cluster, dp, info);
return;
}
if (info->action().actionTypeId() == thermostatTargetTemperatureActionTypeId) {
quint16 heatingSetpoint = info->action().param(thermostatTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble() * 10;
DpValue dp = DpValue(THERMOSTAT_DP_HEATING_SETPOINT, DpValue::TypeUInt32, heatingSetpoint, m_seq++);
qCDebug(dcZigbeeTuya()) << "setting heating setpoint:" << dp << dp.toData().toHex();
writeDpDelayed(cluster, dp, info);
return;
}
if (info->action().actionTypeId() == thermostatModeActionTypeId) {
quint8 mode = info->action().param(thermostatModeActionModeParamTypeId).value().toUInt();
DpValue dp = DpValue(THERMOSTAT_DP_MODE, DpValue::TypeUInt32, mode, m_seq++);
qCDebug(dcZigbeeTuya()) << "setting mode:" << dp << dp.toData().toHex();
writeDpDelayed(cluster, dp, info);
return;
}
}

info->finish(Thing::ThingErrorUnsupportedFeature);
}

Expand Down Expand Up @@ -733,7 +925,7 @@ bool IntegrationPluginZigbeeTuya::match(ZigbeeNode *node, const QString &modelNa
return node->modelName() == modelName && manufacturerNames.contains(node->manufacturerName());
}

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

if (info) {
m_actionQueue.insert(info, dp);
connect(info, &ThingActionInfo::finished, this, [=](){
m_actionQueue.remove(info);
});
}

}

3 changes: 2 additions & 1 deletion zigbeetuya/integrationpluginzigbeetuya.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private slots:
private:
bool match(ZigbeeNode *node, const QString &modelName, const QStringList &manufacturerNames);

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

private:
struct DelayedDpWrite {
Expand All @@ -77,6 +77,7 @@ private slots:
PluginTimer *m_energyPollTimer = nullptr;
quint16 m_seq = 0;
QList<DelayedDpWrite> m_delayedDpWrites;
QHash<ThingActionInfo*, DpValue> m_actionQueue;
};

#endif // INTEGRATIONPLUGINZIGBEETUYA_H
Loading

0 comments on commit 542189d

Please sign in to comment.