Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Tuya: Add thermostat support #75

Open
wants to merge 1 commit into
base: tuya-cloasblesensor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 182 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 @@ -180,6 +202,20 @@ bool IntegrationPluginZigbeeTuya::handleNode(ZigbeeNode *node, const QUuid &/*ne
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 @@ -686,6 +722,111 @@ void IntegrationPluginZigbeeTuya::createConnections(Thing *thing)
}
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 @@ -721,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 @@ -752,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 @@ -762,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
142 changes: 142 additions & 0 deletions zigbeetuya/integrationpluginzigbeetuya.json
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,148 @@
"defaultValue": 0
}
]
},
{
"id": "51cf21ce-28a0-4b48-851b-d27a18d00b9c",
"name": "thermostat",
"displayName": "Thermostat",
"createMethods": [ "auto" ],
"interfaces": ["thermostat", "temperaturesensor", "battery", "childlock", "wirelessconnectable"],
"paramTypes": [
{
"id": "2c562d32-0351-4f39-9d33-178817a9d413",
"name": "ieeeAddress",
"displayName": "IEEE adress",
"type": "QString",
"defaultValue": "00:00:00:00:00:00:00:00"
},
{
"id": "35aa5409-43b9-4f66-bb22-225f226427d5",
"name": "networkUuid",
"displayName": "Zigbee network UUID",
"type": "QString",
"defaultValue": ""
}
],
"settingsTypes": [
{
"id": "df9414af-9db3-4ee0-bf31-ff832b437324",
"name": "windowDetection",
"displayName": "Window open detection",
"type": "bool",
"defaultValue": true
},
{
"id": "c94e536e-7bb4-4e66-906f-a370783011d8",
"name": "temperatureCalibration",
"displayName": "Temperature calibration offset",
"type": "double",
"defaultValue": 0
}
],
"stateTypes": [
{
"id": "08e33c27-54cb-4270-a82d-a2b24bea8c45",
"name": "targetTemperature",
"displayName": "Target temperature",
"displayNameAction": "Set target temperature",
"type": "double",
"unit": "DegreeCelsius",
"minValue": 7,
"maxValue": 30,
"defaultValue": 0,
"writable": true
},
{
"id": "6db38434-4f6c-4326-a4c1-deb3868cc402",
"name": "temperature",
"displayName": "Current temperature",
"type": "double",
"unit": "DegreeCelsius",
"defaultValue": 0
},
{
"id": "fb58531f-c650-486c-bef7-b1a63073c681",
"name": "heatingOn",
"displayName": "Heating on",
"type": "bool",
"defaultValue": false
},
{
"id": "31e58198-a7c0-4866-9aac-6cac3a07fdb9",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"cached": false,
"defaultValue": false
},
{
"id": "95e83e40-5f85-41cf-9f22-060bec40392d",
"name": "signalStrength",
"displayName": "Signal strength",
"defaultValue": 0,
"maxValue": 100,
"minValue": 0,
"type": "uint",
"unit": "Percentage"
},
{
"id": "97c0b335-b29f-4297-9039-44ac612dabfb",
"name": "batteryLevel",
"displayName": "Battery level",
"type": "int",
"unit": "Percentage",
"defaultValue": 50,
"minValue": 0,
"maxValue": 100
},
{
"id": "21564d78-d28c-47f2-8e67-5a6330df4bef",
"name": "batteryCritical",
"displayName": "Battery critical",
"type": "bool",
"defaultValue": false
},
{
"id": "81d70a7c-2151-4354-b81e-cf406cf72979",
"name": "windowOpen",
"displayName": "Window open",
"type": "bool",
"defaultValue": false
},
{
"id": "2db4b96b-8892-4864-af6a-3d40c296fb6f",
"name": "childLock",
"displayName": "Child protection lock",
"displayNameAction": "Set child protection lock",
"type": "bool",
"defaultValue": false,
"writable": true
},
{
"id": "288c6bcf-afa4-4851-87b5-d23c008ec771",
"name": "mode",
"displayName": "Mode",
"displayNameAction": "Set mode",
"type": "uint",
"possibleValues": [
{
"value": 0,
"displayName": "Off"
},
{
"value": 1,
"displayName": "Auto"
},
{
"value": 2,
"displayName": "Manual"
}
],
"defaultValue": 1,
"writable": true
}
]
}
]
}
Expand Down