* Copyright (c) 2021-2025 dresden elektronik ingenieurtechnik gmbh.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
*/
#include <QIODevice>
#include <QTimeZone>
#include "deconz/u_assert.h"
#include "device_access_fn.h"
#include "device_descriptions.h"
#include "device_js/device_js.h"
#include "ias_zone.h"
#include "resource.h"
#include "zcl/zcl.h"
#define CMD_ID_ANY 0x100
#define TIME_CLUSTER_ID 0x000A
#define TIME_ATTRID_TIME 0x0000
#define TIME_ATTRID_LOCAL_TIME 0x0007
#define TIME_ATTRID_LAST_SET_TIME 0x0008
Documentation for manufacturer specific Tuya cluster (0xEF00)
https://developer.tuya.com/en/docs/iot-device-dev/tuya-zigbee-universal-docking-access-standard?id=K9ik6zvofpzql
Tuya ZCL insights
https://github.com/TuyaInc/tuya_zigbee_sdk/blob/master/silicon_labs_zigbee/include/zigbee_attr.h
Basic cluster (0x0000)
-------------
0x0001 Application version:: 0b 01 00 0001 = 1.0.1 ie 0x41 for 1.0.1
0x0004 Manufacturer name: XXX…XXX (16 bytes in length, consisting of an 8-byte prefix and an 8-byte PID)
0-7 bytes: _ TZE600_
8-16 bytes: PID (created and provided by the product manager in the platform or self-service)
Tuya cluster (0xEF00)
---------------------
https://developer.tuya.com/en/docs/iot-device-dev/tuya-zigbee-universal-docking-access-standard?id=K9ik6zvofpzql
Zigbee generic docking is suitable for scenarios where the Zigbee standard protocol is not supported or not very suitable.
ZDP Simple Descriptor Device Id (0x0051)
Frame control for outgoing commands:
deCONZ::ZclFCClusterCommand
deCONZ::ZclFCDirectionClientToServer
deCONZ::ZclFCDisableDefaultResponse
DP data format
--------------
DPID U8 Datapoint serial number
Type U8 Datatype in value
Name Id Length
------------------------------
raw 0x00
bool 0x01
value 0x02
string 0x03
enum 0x04
bitmap 0x05
Length U16 Length of Value
Value 1/2/4/N The value as big endian
ZCL Payload of comamnds
-----------------------
Example MoesGo switch TY_DATA_REPORT
00 4c sequence number
02 DPID
02 Type: Value
00 04 Length: 4
00 00 00 15
*/
#define TUYA_CLUSTER_ID 0xEF00
enum TuyaCommandId : unsigned char
{
TY_DATA_REQUEST = 0x00,
TY_DATA_RESPONSE = 0x01,
TY_DATA_REPORT = 0x02,
TY_DATA_QUERY = 0x03,
TY_DATA_STATUS_SEARCH = 0x06,
TUYA_MCU_VERSION_REQ = 0x10,
TUYA_MCU_VERSION_RSP = 0x11,
TUYA_MCU_OTA_NOTIFY = 0x12,
TUYA_MCU_OTA_BLOCK_DATA_REQ = 0x13,
TUYA_MCU_OTA_BLOCK_DATA_RSP = 0x14,
TUYA_MCU_OTA_RESULT = 0x15,
TUYA_MCU_SYNC_TIME = 0x24
};
enum TuyaDataType : unsigned char
{
TuyaDataTypeRaw = 0x00,
TuyaDataTypeBool = 0x01,
TuyaDataTypeValue = 0x02,
TuyaDataTypeString = 0x03,
TuyaDataTypeEnum = 0x04,
TuyaDataTypeBitmap = 0x05
};
enum DA_Constants
{
BroadcastEndpoint = 255,
AutoEndpoint = 0
};
struct ParseFunction
{
ParseFunction(const QString &_name, const int _arity, ParseFunction_t _fn) :
name(_name),
arity(_arity),
fn(_fn)
{ }
QString name;
int arity = 0;
ParseFunction_t fn = nullptr;
};
struct ReadFunction
{
ReadFunction(const QString &_name, const int _arity, ReadFunction_t _fn) :
name(_name),
arity(_arity),
fn(_fn)
{ }
QString name;
int arity = 0;
ReadFunction_t fn = nullptr;
};
struct WriteFunction
{
WriteFunction(const QString &_name, const int _arity, WriteFunction_t _fn) :
name(_name),
arity(_arity),
fn(_fn)
{ }
QString name;
int arity = 0;
WriteFunction_t fn = nullptr;
};
quint8 zclNextSequenceNumber();
uint8_t DEV_ResolveDestinationEndpoint(uint64_t extAddr, uint8_t hintEp, uint16_t cluster, uint8_t frameControl);
\param var - Holds the string or number.
\param max – Upper bound of the allowed value.
\param ok – true if var holds and uint which is <= \p max.
*/
uint variantToUint(const QVariant &var, size_t max, bool *ok)
{
Q_ASSERT(ok);
*ok = false;
if (var.isNull())
{
return 0;
}
const auto val = var.toString().toUInt(ok, 0);
*ok = *ok && val <= max;
return *ok ? val : 0;
}
*/
static ZCL_Param getZclParam(const QVariantMap ¶m)
{
ZCL_Param result{};
if (!param.contains(QLatin1String("cl")))
{
return result;
}
bool ok = true;
result.endpoint = param.contains("ep") ? variantToUint(param["ep"], UINT8_MAX, &ok) : quint8(AutoEndpoint);
result.clusterId = ok ? variantToUint(param["cl"], UINT16_MAX, &ok) : 0;
result.manufacturerCode = ok && param.contains("mf") ? variantToUint(param["mf"], UINT16_MAX, &ok) : 0;
if (param.contains(QLatin1String("cmd")))
{
if (param["cmd"].toString() == QLatin1String("any"))
{
result.commandId = CMD_ID_ANY;
result.hasCommandId = 1;
}
else
{
result.commandId = variantToUint(param["cmd"], UINT32_MAX, &ok);
result.hasCommandId = ok ? 1 : 0;
}
}
else
{
result.hasCommandId = 0;
}
if (param.contains(QLatin1String("fc")))
{
result.frameControl = (uint8_t)variantToUint(param["fc"], UINT8_MAX, &ok);
result.hasFrameControl = ok ? 1 : 0;
}
else
{
result.frameControl = 0;
result.hasFrameControl = 0;
}
const auto ignoreSeqno = QLatin1String("noseq");
if (param.contains(ignoreSeqno))
{
result.ignoreResponseSeq = param.value(ignoreSeqno).toBool() ? 1 : 0;
}
else
{
result.ignoreResponseSeq = 0;
}
result.attributeCount = 0;
const auto attr = param[QLatin1String("at")];
if (!ok)
{ }
else if (attr.type() == QVariant::String)
{
result.attributes[result.attributeCount] = variantToUint(attr, UINT16_MAX, &ok);
result.attributeCount = 1;
}
else if (attr.type() == QVariant::List)
{
const auto arr = attr.toList();
for (const auto &at : arr)
{
if (result.attributeCount == ZCL_Param::MaxAttributes)
{
break;
}
if (ok && at.type() == QVariant::String)
{
result.attributes[result.attributeCount] = variantToUint(at, UINT16_MAX, &ok);
result.attributeCount++;
}
}
ok = result.attributeCount == size_t(arr.size());
}
else if (param["eval"].toString().contains("Attr"))
{
ok = false;
}
result.valid = ok;
return result;
}
quint8 resolveAutoEndpoint(const Resource *r)
{
quint8 result = AutoEndpoint;
U_ASSERT(r);
if (r)
{
const ResourceItem *itemUniqueId = r->item(RAttrUniqueId);
U_ASSERT(itemUniqueId);
if (itemUniqueId)
{
const auto ls = itemUniqueId->toString().split('-', SKIP_EMPTY_PARTS);
if (ls.size() >= 2)
{
bool ok = false;
uint ep = ls[1].toUInt(&ok, 16);
if (ok && ep < BroadcastEndpoint)
{
result = ep;
}
}
}
}
return result;
}
*/
bool evalZclAttribute(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, int attrIndex, const deCONZ::ZclAttribute &attr, const QVariant &parseParameters)
{
bool ok = false;
const auto &zclParam = item->zclParam();
for (size_t i = 0; i < zclParam.attributeCount; i++)
{
if (zclParam.attributes[i] == attr.id())
{
ok = true;
break;
}
}
if (!ok)
{
return false;
}
const auto expr = parseParameters.toMap()["eval"].toString();
if (!expr.isEmpty())
{
DeviceJs &engine = *DeviceJs::instance();
engine.reset();
engine.setResource(r);
engine.setItem(item);
engine.setZclAttribute(attrIndex, attr);
engine.setZclFrame(zclFrame);
engine.setApsIndication(ind);
if (engine.evaluate(expr) == JsEvalResult::Ok)
{
const auto res = engine.result();
if (res.isValid())
{
DBG_Printf(DBG_DDF, "%s/%s expression: %s --> %s\n", r->item(RAttrUniqueId)->toCString(), item->descriptor().suffix, qPrintable(expr), qPrintable(res.toString()));
return true;
}
}
else
{
DBG_Printf(DBG_DDF, "failed to evaluate expression for %s/%s: %s, err: %s\n", r->item(RAttrUniqueId)->toCString(), item->descriptor().suffix, qPrintable(expr), qPrintable(engine.errorString()));
}
}
return false;
}
*/
bool evalZclFrame(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, const QVariant &parseParameters)
{
const auto expr = parseParameters.toMap()["eval"].toString();
if (!expr.isEmpty())
{
DeviceJs &engine = *DeviceJs::instance();
engine.reset();
engine.setResource(r);
engine.setItem(item);
engine.setZclFrame(zclFrame);
engine.setApsIndication(ind);
if (engine.evaluate(expr) == JsEvalResult::Ok)
{
const auto res = engine.result();
if (res.isValid())
{
if (DBG_IsEnabled(DBG_DDF))
{
DBG_Printf(DBG_DDF, "expression: %s --> %s\n", qPrintable(expr), qPrintable(res.toString()));
}
return true;
}
}
else
{
DBG_Printf(DBG_DDF, "failed to evaluate expression for %s/%s: %s, err: %s\n", qPrintable(r->item(RAttrUniqueId)->toString()), item->descriptor().suffix, qPrintable(expr), qPrintable(engine.errorString()));
}
}
return false;
}
The item->parseParameters() is expected to be an object (given in the device description file).
{"fn": "numtostr", "srcitem": suffix, "op": operator, "to": array}
- srcitem: the suffix of the source item which holds the numeric value
- op: (lt | le | eq | gt | ge) the operator used to match the 'to' array
- to: [number, string, [number, string], ...] an sorted array to map 'number -> string' with the given operator
Example: { "parse": {"fn": "numtostr", "srcitem": "state/airqualityppb", "op": "le", "to": [65, "good", 65535, "bad"] }
*/
bool parseNumericToString(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, const QVariant &parseParameters)
{
Q_UNUSED(ind)
Q_UNUSED(zclFrame)
bool result = false;
ResourceItem *srcItem = nullptr;
const auto map = parseParameters.toMap();
enum Op { OpNone, OpLessThan, OpLessEqual, OpEqual, OpGreaterThan, OpGreaterEqual };
Op op = OpNone;
if (!item->parseFunction())
{
if (item->descriptor().type != DataTypeString)
{
return result;
}
if (!map.contains(QLatin1String("to")) || !map.contains(QLatin1String("op")) || !map.contains(QLatin1String("srcitem")))
{
return result;
}
item->setParseFunction(parseNumericToString);
}
ResourceItemDescriptor rid;
if (!getResourceItemDescriptor(map["srcitem"].toString(), rid))
{
return result;
}
srcItem = r->item(rid.suffix);
if (!srcItem)
{
return result;
}
if (!(srcItem->needPushChange() || srcItem->needPushSet()))
{
return result;
}
{
const auto opString = map[QLatin1String("op")].toString();
if (opString == QLatin1String("le")) { op = OpLessEqual; }
else if (opString == QLatin1String("lt")) { op = OpLessThan; }
else if (opString == QLatin1String("eq")) { op = OpEqual; }
else if (opString == QLatin1String("ge")) { op = OpGreaterEqual; }
else if (opString == QLatin1String("gt")) { op = OpGreaterThan; }
else
{
return result;
}
}
const qint64 num = srcItem->toNumber();
const auto to = map["to"].toList();
if (to.size() & 1)
{
return result;
}
auto i = std::find_if(to.cbegin(), to.cend(), [num, op](const QVariant &var)
{
if (var.type() == QVariant::Double || var.type() == QVariant::LongLong)
{
if (op == OpLessEqual) { return num <= var.toInt(); }
if (op == OpLessThan) { return num < var.toInt(); }
if (op == OpEqual) { return num == var.toInt(); }
if (op == OpGreaterEqual) { return num >= var.toInt(); }
if (op == OpGreaterThan) { return num > var.toInt(); }
}
return false;
});
if (i != to.cend())
{
i++;
if (i != to.cend() && i->type() == QVariant::String)
{
const QString str = i->toString();
if (!str.isEmpty())
{
DBG_Printf(DBG_DDF, "%s/%s numtostr: %s %lld --> %s\n", r->item(RAttrUniqueId)->toCString(), item->descriptor().suffix, srcItem->descriptor().suffix, num, qPrintable(str));
item->setValue(str);
item->setLastZclReport(srcItem->lastZclReport());
result = true;
}
}
}
if (result)
{
DeviceJS_ResourceItemValueChanged(item);
}
return result;
}
The item->parseParameters() is expected to be an object (given in the device description file).
{"fn": "zcl:attr", "ep": endpoint, "cl": clusterId, "mf": manufacturerCode, "at": attributeId, "eval": expression}
- endpoint: (optional) 255 means any endpoint, 0 means auto selected from the related resource, defaults to 0
- clusterId: string hex value
- manufacturerCode: (optional) string hex value
- attributeId: string hex value or array of string hex values
- expression: Javascript expression to transform the attribute value to the Item value
Example: { "parse": {"fn": "zcl:attr", "ep:" 1, "cl": "0x0402", "at": "0x0000", "eval": "Attr.val + R.item('config/offset').val" } }
TODO: move code to parse a ZCL command to separate function.
Exmaple: { "parse": {"fn": "zcl:cmd", "ep": 2, "cl": "0xfc00", "mf", "0x100b", "script": "fc00_buttonevent.js" } }
*/
bool parseZclAttribute(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, const QVariant &parseParameters)
{
bool result = false;
if (!item->parseFunction())
{
Q_ASSERT(!parseParameters.isNull());
if (parseParameters.isNull())
{
return result;
}
ZCL_Param param = getZclParam(parseParameters.toMap());
Q_ASSERT(param.valid);
if (!param.valid)
{
return result;
}
if (param.hasCommandId)
{
if (param.commandId == CMD_ID_ANY)
{
}
else if (param.commandId != zclFrame.commandId())
{
return result;
}
}
else if (!param.hasCommandId && param.attributeCount == 0)
{
}
else if (!param.hasCommandId && zclFrame.commandId() != deCONZ::ZclReadAttributesResponseId && zclFrame.commandId() != deCONZ::ZclReportAttributesId)
{
return result;
}
if (param.manufacturerCode != zclFrame.manufacturerCode())
{
return result;
}
if (param.endpoint == AutoEndpoint)
{
param.endpoint = resolveAutoEndpoint(r);
if (param.endpoint == AutoEndpoint)
{
return result;
}
}
item->setParseFunction(parseZclAttribute);
item->setZclProperties(param);
}
const auto &zclParam = item->zclParam();
if (ind.clusterId() != zclParam.clusterId)
{
return result;
}
if (!zclParam.hasCommandId &&
zclFrame.isProfileWideCommand() &&
zclFrame.commandId() != deCONZ::ZclReadAttributesResponseId &&
zclFrame.commandId() != deCONZ::ZclReportAttributesId)
{
return result;
}
if (zclParam.manufacturerCode != zclFrame.manufacturerCode())
{
return result;
}
if (zclParam.endpoint < BroadcastEndpoint && zclParam.endpoint != ind.srcEndpoint())
{
return result;
}
if (zclParam.attributeCount == 0)
{
if (zclParam.hasCommandId)
{
if (zclParam.commandId == CMD_ID_ANY)
{
}
else if (zclParam.commandId != zclFrame.commandId())
{
return result;
}
}
if (evalZclFrame(r, item, ind, zclFrame, parseParameters))
{
result = true;
}
return result;
}
if (zclFrame.payload().isEmpty() && zclParam.attributeCount > 0)
{
return result;
}
QDataStream stream(zclFrame.payload());
stream.setByteOrder(QDataStream::LittleEndian);
int attrIndex = -1;
while (!stream.atEnd())
{
quint16 attrId;
quint8 status;
quint8 dataType;
stream >> attrId;
attrIndex++;
if (zclFrame.commandId() == deCONZ::ZclReadAttributesResponseId)
{
stream >> status;
if (status != deCONZ::ZclSuccessStatus)
{
continue;
}
}
stream >> dataType;
deCONZ::ZclAttribute attr(attrId, dataType, QLatin1String(""), deCONZ::ZclReadWrite, true);
if (!attr.readFromStream(stream))
{
break;
}
if (evalZclAttribute(r, item, ind, zclFrame, attrIndex, attr, parseParameters))
{
if (zclFrame.commandId() == deCONZ::ZclReportAttributesId)
{
item->setLastZclReport(deCONZ::steadyTimeRef().ref);
}
result = true;
}
}
return result;
}
The item->parseParameters() is expected to be an object (given in the device description file).
{"fn": "tuya", "dpid": datapointId, "eval": expression}
- datapointId: 1-255 the datapoint identifier (DPID) to extract
- expression: Javascript expression to transform the raw value
Example: { "parse": {"fn": "tuya", "dpid:" 1, "eval": "Attr.val + R.item('config/offset').val" } }
*/
bool parseTuyaData(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, const QVariant &parseParameters)
{
bool result = false;
if (ind.clusterId() != TUYA_CLUSTER_ID || !(zclFrame.commandId() == TY_DATA_REPORT || zclFrame.commandId() == TY_DATA_RESPONSE || zclFrame.commandId() == TY_DATA_STATUS_SEARCH))
{
return result;
}
if (!item->parseFunction())
{
const auto map = parseParameters.toMap();
if (map.isEmpty())
{
return result;
}
if (!map.contains(QLatin1String("dpid")) || !map.contains(QLatin1String("eval")))
{
return result;
}
bool ok = false;
ZCL_Param param{};
param.attributes[0] = variantToUint(map.value(QLatin1String("dpid")), 255, &ok);
if (!ok)
{
return result;
}
param.valid = 1;
param.endpoint = ind.srcEndpoint();
param.clusterId = ind.clusterId();
param.attributeCount = 1;
item->setParseFunction(parseTuyaData);
item->setZclProperties(param);
}
quint16 seq;
quint8 dpid;
quint8 dataType;
quint16 dataLength;
quint8 zclDataType = 0;
const auto &zclParam = item->zclParam();
QDataStream stream(zclFrame.payload());
stream.setByteOrder(QDataStream::BigEndian);
stream >> seq;
int attrIndex = 0;
while (!stream.atEnd())
{
stream >> dpid;
stream >> dataType;
stream >> dataLength;
if (stream.status() != QDataStream::Ok)
{
return result;
}
deCONZ::NumericUnion num;
num.u64 = 0;
switch (dataType)
{
case TuyaDataTypeRaw:
{
zclDataType = deCONZ::ZclCharacterString;
}
break;
case TuyaDataTypeString:
return result;
case TuyaDataTypeBool:
{ stream >> num.u8; zclDataType = deCONZ::ZclBoolean; }
break;
case TuyaDataTypeEnum:
{ stream >> num.u8; zclDataType = deCONZ::Zcl8BitUint; }
break;
case TuyaDataTypeValue:
{ stream >> num.s32; zclDataType = deCONZ::Zcl32BitInt; }
break;
case TuyaDataTypeBitmap:
{
switch (dataLength)
{
case 1: { stream >> num.u8; zclDataType = deCONZ::Zcl8BitUint; } break;
case 2: { stream >> num.u16; zclDataType = deCONZ::Zcl16BitUint; } break;
case 4: { stream >> num.u32; zclDataType = deCONZ::Zcl32BitUint; } break;
}
}
break;
default:
return result;
}
if (dpid == zclParam.attributes[0])
{
deCONZ::ZclAttribute attr(dpid, zclDataType, QLatin1String(""), deCONZ::ZclReadWrite, true);
if (zclDataType == deCONZ::Zcl32BitInt)
{
attr.setValue(qint64(num.s32));
}
else
{
attr.setValue(quint64(num.u32));
}
if (evalZclAttribute(r, item, ind, zclFrame, attrIndex, attr, parseParameters))
{
item->setLastZclReport(deCONZ::steadyTimeRef().ref);
result = true;
}
}
attrIndex++;
const char *rt = zclFrame.commandId() == TY_DATA_REPORT ? "REPORT" : "RESPONSE";
DBG_Printf(DBG_INFO, "TY_DATA_%s: seq %u, dpid: 0x%02X, type: 0x%02X, length: %u, val: %d\n",
rt, seq, dpid, dataType, dataLength, num.s32);
}
return result;
}
Important: This function should be attached to only one item!
The item->readParameters() is expected to be an object (given in the device description file).
{ "fn": "tuya"}
Example: { "read": {"fn": "tuya"} }
*/
static DA_ReadResult readTuyaAllData(const Resource *r, const ResourceItem *item, deCONZ::ApsController *apsCtrl, const QVariant &readParameters)
{
Q_UNUSED(item)
Q_UNUSED(readParameters);
DA_ReadResult result{};
static deCONZ::SteadyTimeRef lastReadGlobal{};
auto now = deCONZ::steadyTimeRef();
if (now - lastReadGlobal < deCONZ::TimeSeconds{15})
{
return result;
}
lastReadGlobal = now;
auto *rTop = r->parentResource() ? r->parentResource() : r;
const auto *extAddr = rTop->item(RAttrExtAddress);
const auto *nwkAddr = rTop->item(RAttrNwkAddress);
if (!extAddr || !nwkAddr)
{
return result;
}
deCONZ::ApsDataRequest req;
deCONZ::ZclFrame zclFrame;
req.setDstEndpoint(1);
req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission);
req.setDstAddressMode(deCONZ::ApsNwkAddress);
req.dstAddress().setNwk(nwkAddr->toNumber());
req.dstAddress().setExt(extAddr->toNumber());
req.setClusterId(TUYA_CLUSTER_ID);
req.setProfileId(HA_PROFILE_ID);
req.setSrcEndpoint(1);
zclFrame.setSequenceNumber(zclNextSequenceNumber());
zclFrame.setCommandId(TY_DATA_QUERY);
zclFrame.setFrameControl(deCONZ::ZclFCClusterCommand |
deCONZ::ZclFCDirectionClientToServer |
deCONZ::ZclFCDisableDefaultResponse);
{
QDataStream stream(&req.asdu(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
zclFrame.writeToStream(stream);
}
result.isEnqueued = apsCtrl->apsdeDataRequest(req) == deCONZ::Success;
result.apsReqId = req.id();
result.sequenceNumber = zclFrame.sequenceNumber();
result.clusterId = req.clusterId();
return result;
}
The \p writeParameters is expected to contain one object (given in the device description file).
{ "fn": "tuya", "dpid": datapointId, "dt": dataType, "eval": expression }
- datapointId: number
- dataType: string hex value
bool 0x10
s32 value 0x2b
enum 0x30
8-bit bitmap 0x18
16-bit bitmap 0x19
32-bit bitmap 0x1b
- expression: to transform the item value
Example: "write": {"fn":"tuya", "dpid": 1, "dt": "0x10", "eval": "Item.val == 1"}
*/
bool writeTuyaData(const Resource *r, const ResourceItem *item, deCONZ::ApsController *apsCtrl, const QVariant &writeParameters)
{
Q_ASSERT(r);
Q_ASSERT(item);
Q_ASSERT(apsCtrl);
bool result = false;
const auto rParent = r->parentResource() ? r->parentResource() : r;
const auto *extAddr = rParent->item(RAttrExtAddress);
const auto *nwkAddr = rParent->item(RAttrNwkAddress);
if (!extAddr || !nwkAddr)
{
return result;
}
const auto map = writeParameters.toMap();
if (!map.contains(QLatin1String("dpid")) || !map.contains(QLatin1String("dt")) || !map.contains(QLatin1String("eval")))
{
return result;
}
bool ok = false;
const auto dpid = variantToUint(map.value(QLatin1String("dpid")), 255, &ok);
if (!ok)
{
return result;
}
const auto dataType = variantToUint(map.value("dt"), UINT8_MAX, &ok);
switch (dataType)
{
case deCONZ::ZclBoolean:
case deCONZ::Zcl32BitInt:
case deCONZ::Zcl8BitEnum:
case deCONZ::Zcl8BitBitMap:
case deCONZ::Zcl16BitBitMap:
case deCONZ::Zcl32BitBitMap:
break;
default:
return result;
}
const auto expr = map.value("eval").toString();
if (!ok || expr.isEmpty())
{
return result;
}
DBG_Printf(DBG_INFO, "writeTuyaData, dpid: 0x%02X, type: 0x%02X, expr: %s\n",
dpid & 0xFF, dataType & 0xFF, qPrintable(expr));
deCONZ::ApsDataRequest req;
deCONZ::ZclFrame zclFrame;
req.setDstEndpoint(1);
req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission);
req.setDstAddressMode(deCONZ::ApsNwkAddress);
req.dstAddress().setNwk(nwkAddr->toNumber());
req.dstAddress().setExt(extAddr->toNumber());
req.setClusterId(TUYA_CLUSTER_ID);
req.setProfileId(HA_PROFILE_ID);
req.setSrcEndpoint(1);
zclFrame.setSequenceNumber(zclNextSequenceNumber());
zclFrame.setCommandId(TY_DATA_REQUEST);
zclFrame.setFrameControl(deCONZ::ZclFCClusterCommand |
deCONZ::ZclFCDirectionClientToServer |
deCONZ::ZclFCDisableDefaultResponse);
{
QVariant value;
DeviceJs &engine = *DeviceJs::instance();
engine.reset();
engine.setResource(r);
engine.setItem(item);
if (engine.evaluate(expr) == JsEvalResult::Ok)
{
value = engine.result();
DBG_Printf(DBG_INFO, "Tuya write expression: %s --> %s\n", qPrintable(expr), qPrintable(value.toString()));
}
else
{
DBG_Printf(DBG_INFO, "failed to evaluate Tuya write expression for %s/%s: %s, err: %s\n", qPrintable(r->item(RAttrUniqueId)->toString()), item->descriptor().suffix, qPrintable(expr), qPrintable(engine.errorString()));
return result;
}
if (!value.isValid())
{
return result;
}
QDataStream stream(&zclFrame.payload(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::BigEndian);
stream << quint16(req.id());
stream << quint8(dpid);
switch (dataType)
{
case deCONZ::ZclBoolean:
{
stream << quint8(TuyaDataTypeBool);
stream << quint16(1);
stream << quint8(value.toUInt());
}
break;
case deCONZ::Zcl32BitInt:
{
stream << quint8(TuyaDataTypeValue);
stream << quint16(4);
stream << qint32(value.toInt());
}
break;
case deCONZ::Zcl8BitEnum:
{
stream << quint8(TuyaDataTypeEnum);
stream << quint16(1);
stream << quint8(value.toUInt());
}
break;
case deCONZ::Zcl8BitBitMap:
{
stream << quint8(TuyaDataTypeBitmap);
stream << quint16(1);
stream << quint8(value.toUInt());
}
break;
case deCONZ::Zcl16BitBitMap:
{
stream << quint8(TuyaDataTypeBitmap);
stream << quint16(2);
stream << quint16(value.toUInt());
}
break;
case deCONZ::Zcl32BitBitMap:
{
stream << quint8(TuyaDataTypeBitmap);
stream << quint16(4);
stream << quint32(value.toUInt());
}
break;
default:
return result;
}
}
{
QDataStream stream(&req.asdu(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
zclFrame.writeToStream(stream);
}
result = apsCtrl->apsdeDataRequest(req) == deCONZ::Success;
return result;
}
\param zclFrame - Contains the special report with attribute 0xff01, 0xff02 or 0x00f7.
\param rtag - The tag or struct index of the attribute to return.
\returns Parsed attribute, use attr.id() != 0xffff to check for valid result.
*/
deCONZ::ZclAttribute parseXiaomiZclTag(const quint8 rtag, const deCONZ::ZclFrame &zclFrame)
{
deCONZ::ZclAttribute result;
quint16 attrId = 0;
quint8 dataType = 0;
quint8 length = 0;
quint16 structElements = 0;
QDataStream stream(zclFrame.payload());
stream.setByteOrder(QDataStream::LittleEndian);
stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
while (attrId == 0 && !stream.atEnd())
{
quint16 a;
stream >> a;
stream >> dataType;
if (dataType == deCONZ::ZclCharacterString || dataType == deCONZ::ZclOctedString)
{
stream >> length;
}
if (a == 0xff01 && dataType == deCONZ::ZclCharacterString)
{
attrId = a;
}
else if (a == 0xff02 && dataType == 0x4c )
{
attrId = a;
stream >> structElements;
}
else if (a == 0x00f7 && dataType == deCONZ::ZclOctedString)
{
attrId = a;
}
if (dataType == deCONZ::ZclCharacterString && attrId != 0xff01)
{
for (; length > 0; length--)
{
quint8 dummy;
stream >> dummy;
}
}
}
if (stream.atEnd() || attrId == 0)
{
return result;
}
quint8 tag = 0;
while (!stream.atEnd())
{
if (attrId == 0xff01 || attrId == 0x00f7)
{
stream >> tag;
}
stream >> dataType;
deCONZ::ZclAttribute atmp(tag, dataType, QLatin1String(""), deCONZ::ZclRead, true);
if (!atmp.readFromStream(stream))
{
return result;
}
if (tag == rtag)
{
result = atmp;
break;
}
if (structElements > 0)
{
tag++;
}
}
return result;
}
The item->parseParameters() is expected to be an object (given in the device description file).
{"fn": "xiaomi:special", "ep": endpoint, "at": attributeId, "idx": index, "eval": expression}
- endpoint: (optional), 0xff means any endpoint (default: 0xff)
- attributeId: string hex value of 0xff01, 0xff02 or 0x00f7.
- index: string hex value representing the tag or index in the structure
- expression: Javascript expression to transform the raw value (as alternative "script" can be used to reference a external JS script file)
Example: { "parse": {"fn": "xiaomi:special", "at": "0xff01", "idx": "0x01", "eval": "Item.val = Attr.val" } }
*/
bool parseXiaomiSpecial(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, const QVariant &parseParameters)
{
bool result = false;
if (zclFrame.commandId() != deCONZ::ZclReportAttributesId)
{
return result;
}
if (ind.clusterId() != 0x0000 && ind.clusterId() != 0xfcc0)
{
return result;
}
if (!item->parseFunction())
{
Q_ASSERT(!parseParameters.isNull());
if (parseParameters.isNull())
{
return result;
}
const auto map = parseParameters.toMap();
bool ok = true;
ZCL_Param param;
param.endpoint = BroadcastEndpoint;
param.clusterId = 0x0000;
if (ind.clusterId() == 0xfcc0)
{
param.clusterId = 0xfcc0;
param.manufacturerCode = 0x115f;
}
if (map.contains(QLatin1String("ep")))
{
param.endpoint = variantToUint(map["ep"], UINT8_MAX, &ok);
}
const auto at = ok ? variantToUint(map["at"], UINT16_MAX, &ok) : 0;
const auto idx = ok ? variantToUint(map["idx"], UINT16_MAX, &ok) : 0;
DBG_Assert(at == 0xff01 || at == 0xff02 || at == 0x00f7);
if (!ok)
{
return result;
}
param.attributeCount = 2;
param.attributes[0] = at;
param.attributes[1] = idx;
if (param.endpoint == AutoEndpoint)
{
param.endpoint = resolveAutoEndpoint(r);
if (param.endpoint == AutoEndpoint)
{
return result;
}
}
item->setParseFunction(parseXiaomiSpecial);
item->setZclProperties(param);
}
const auto &zclParam = item->zclParam();
if (!(ind.clusterId() == 0x0000 || ind.clusterId() == 0xfcc0) || zclFrame.payload().isEmpty())
{
return result;
}
if (zclParam.endpoint < BroadcastEndpoint && zclParam.endpoint != ind.srcEndpoint())
{
return result;
}
Q_ASSERT(zclParam.attributeCount == 2);
const auto attr = parseXiaomiZclTag(zclParam.attributes[1], zclFrame);
int attrIndex = 0;
if (evalZclAttribute(r, item, ind, zclFrame, attrIndex, attr, parseParameters))
{
result = true;
}
return result;
}
The item->parseParameters() is expected to be an object (given in the device description file).
{"fn": "ias:zonestatus", "mask": expression}
- mask (optional): The bitmask to be applied for Alarm1 and Alarm2 of the IAS zone status value as list of strings
Example: { "parse": {"fn": "ias:zonestatus", "mask": "alarm1,alarm2" } }
*/
bool parseIasZoneNotificationAndStatus(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, const QVariant &parseParameters)
{
bool result = false;
if (ind.clusterId() != IAS_ZONE_CLUSTER_ID)
{
return result;
}
if (ind.srcEndpoint() != resolveAutoEndpoint(r))
{
return result;
}
if (zclFrame.isClusterCommand())
{
if (zclFrame.commandId() != CMD_STATUS_CHANGE_NOTIFICATION)
{
return result;
}
}
else if (zclFrame.commandId() != deCONZ::ZclReadAttributesResponseId && zclFrame.commandId() != deCONZ::ZclReportAttributesId)
{
return result;
}
if (!item->parseFunction())
{
item->setParseFunction(parseIasZoneNotificationAndStatus);
}
QDataStream stream(zclFrame.payload());
stream.setByteOrder(QDataStream::LittleEndian);
quint16 zoneStatus = UINT16_MAX;
while (!stream.atEnd())
{
if (zclFrame.isClusterCommand())
{
quint8 extendedStatus;
quint8 zoneId;
quint16 delay;
stream >> zoneStatus;
stream >> extendedStatus;
stream >> zoneId;
stream >> delay;
DBG_Assert(stream.status() == QDataStream::Ok);
}
else
{
quint16 attrId;
quint8 status;
quint8 dataType;
stream >> attrId;
if (zclFrame.commandId() == deCONZ::ZclReadAttributesResponseId)
{
stream >> status;
if (status != deCONZ::ZclSuccessStatus)
{
continue;
}
}
stream >> dataType;
deCONZ::ZclAttribute attr(attrId, dataType, QLatin1String(""), deCONZ::ZclReadWrite, true);
if (!attr.readFromStream(stream))
{
break;
}
if (attr.id() == 0x0002)
{
zoneStatus = attr.numericValue().u16;
break;
}
}
}
if (zoneStatus != UINT16_MAX)
{
int mask = 0;
const char *suffix = item->descriptor().suffix;
if (suffix == RStateAlarm || suffix == RStateCarbonMonoxide || suffix == RStateFire || suffix == RStateOpen ||
suffix == RStatePresence || suffix == RStateVibration || suffix == RStateWater)
{
const auto map = parseParameters.toMap();
if (map.contains(QLatin1String("mask")))
{
QStringList alarmMask = map["mask"].toString().split(',', SKIP_EMPTY_PARTS);
if (alarmMask.contains(QLatin1String("alarm1"))) { mask |= STATUS_ALARM1; }
if (alarmMask.contains(QLatin1String("alarm2"))) { mask |= STATUS_ALARM2; }
}
}
else if (suffix == RStateTampered)
{
mask |= STATUS_TAMPER;
}
else if (suffix == RStateLowBattery)
{
mask |= STATUS_BATTERY;
}
else if (suffix == RStateTest)
{
mask |= STATUS_TEST;
}
item->setValue((zoneStatus & mask) != 0);
item->setLastZclReport(deCONZ::steadyTimeRef().ref);
DeviceJS_ResourceItemValueChanged(item);
result = true;
}
return result;
}
{"fn": "time"}
- The function does not require any further parameters
Example: { "write": {"fn": "time"} }
*/
bool writeTimeData(const Resource *r, const ResourceItem *item, deCONZ::ApsController *apsCtrl, const QVariant &writeParameters)
{
Q_UNUSED(writeParameters);
Q_UNUSED(item);
Q_ASSERT(r);
Q_ASSERT(apsCtrl);
bool result = false;
const auto rParent = r->parentResource() ? r->parentResource() : r;
const auto *extAddr = rParent->item(RAttrExtAddress);
const auto *nwkAddr = rParent->item(RAttrNwkAddress);
if (!extAddr || !nwkAddr)
{
return result;
}
quint8 endpoint = resolveAutoEndpoint(r);
if (endpoint == AutoEndpoint)
{
return result;
}
DBG_Printf(DBG_DDF, "%s correcting time drift...\n", r->item(RAttrUniqueId)->toCString());
const QDateTime now = QDateTime::currentDateTimeUtc();
const QDateTime yearStart(QDate(QDate::currentDate().year(), 1, 1), QTime(0, 0), Qt::UTC);
const QTimeZone timeZone(QTimeZone::systemTimeZoneId());
QDateTime epoch;
quint32 time_now = 0xFFFFFFFF;
qint8 time_status = 0x0D;
qint32 time_zone = 0xFFFFFFFF;
quint32 time_dst_start = 0xFFFFFFFF;
quint32 time_dst_end = 0xFFFFFFFF;
qint32 time_dst_shift = 0xFFFFFFFF;
quint32 time_valid_until_time = 0xFFFFFFFF;
epoch = QDateTime(QDate(2000, 1, 1), QTime(0, 0), Qt::UTC);;
time_now = epoch.secsTo(now);
time_zone = timeZone.offsetFromUtc(yearStart);
if (timeZone.hasTransitions())
{
const QTimeZone::OffsetData dstStartOffsetData = timeZone.nextTransition(yearStart);
const QTimeZone::OffsetData dstEndOffsetData = timeZone.nextTransition(dstStartOffsetData.atUtc);
time_dst_start = epoch.secsTo(dstStartOffsetData.atUtc);
time_dst_end = epoch.secsTo(dstEndOffsetData.atUtc);
time_dst_shift = dstStartOffsetData.daylightTimeOffset;
}
time_valid_until_time = time_now + (3600 * 24);
deCONZ::ApsDataRequest req;
deCONZ::ZclFrame zclFrame;
req.setDstEndpoint(endpoint);
req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission);
req.setDstAddressMode(deCONZ::ApsExtAddress);
req.dstAddress().setNwk(nwkAddr->toNumber());
req.dstAddress().setExt(extAddr->toNumber());
req.setClusterId(TIME_CLUSTER_ID);
req.setProfileId(HA_PROFILE_ID);
req.setSrcEndpoint(1);
zclFrame.setSequenceNumber(zclNextSequenceNumber());
zclFrame.setCommandId(deCONZ::ZclWriteAttributesId);
zclFrame.setFrameControl(deCONZ::ZclFCProfileCommand |
deCONZ::ZclFCDirectionClientToServer |
deCONZ::ZclFCDisableDefaultResponse);
{
QDataStream stream(&zclFrame.payload(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
stream << (quint16) 0x0000;
stream << (quint8) deCONZ::ZclUtcTime;
stream << time_now;
stream << (quint16) 0x0001;
stream << (quint8) deCONZ::Zcl8BitBitMap;
stream << time_status;
stream << (quint16) 0x0002;
stream << (quint8) deCONZ::Zcl32BitInt;
stream << time_zone;
stream << (quint16) 0x0003;
stream << (quint8) deCONZ::Zcl32BitUint;
stream << time_dst_start;
stream << (quint16) 0x0004;
stream << (quint8) deCONZ::Zcl32BitUint;
stream << time_dst_end;
stream << (quint16) 0x0005;
stream << (quint8) deCONZ::Zcl32BitInt;
stream << time_dst_shift;
stream << (quint16) 0x0009;
stream << (quint8) deCONZ::ZclUtcTime;
stream << time_valid_until_time;
}
{
QDataStream stream(&req.asdu(), QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
zclFrame.writeToStream(stream);
}
result = apsCtrl->apsdeDataRequest(req) == deCONZ::Success;
return result;
}
The item->parseParameters() is expected to be an object (given in the device description file).
{"fn": "time"}
- The function does not require any further parameters
Example: { "parse": {"fn": "time"} }
*/
bool parseAndSyncTime(Resource *r, ResourceItem *item, const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame, const QVariant &parseParameters)
{
Q_UNUSED(parseParameters);
bool result = false;
if (ind.clusterId() != TIME_CLUSTER_ID)
{
return result;
}
if (ind.srcEndpoint() != resolveAutoEndpoint(r))
{
return result;
}
if (zclFrame.commandId() != deCONZ::ZclReadAttributesResponseId && zclFrame.commandId() != deCONZ::ZclReportAttributesId)
{
return result;
}
if (!item->parseFunction())
{
item->setParseFunction(parseAndSyncTime);
}
QDataStream stream(zclFrame.payload());
stream.setByteOrder(QDataStream::LittleEndian);
const QDateTime epoch = QDateTime(QDate(2000, 1, 1), QTime(0, 0), Qt::UTC);
const char *suffix = item->descriptor().suffix;
while (!stream.atEnd())
{
quint16 attrId;
quint8 status;
quint8 dataType;
stream >> attrId;
if (zclFrame.commandId() == deCONZ::ZclReadAttributesResponseId)
{
stream >> status;
if (status != deCONZ::ZclSuccessStatus)
{
continue;
}
}
stream >> dataType;
deCONZ::ZclAttribute attr(attrId, dataType, QLatin1String(""), deCONZ::ZclReadWrite, true);
if (!attr.readFromStream(stream))
{
break;
}
switch (attrId) {
case TIME_ATTRID_TIME:
{
if (suffix == RStateUtc)
{
QDateTime time = epoch.addSecs(attr.numericValue().u32);
const qint32 drift = QDateTime::currentDateTimeUtc().secsTo(time);
if (item->toVariant().toDateTime().toMSecsSinceEpoch() != time.toMSecsSinceEpoch())
{
item->setValue(time, ResourceItem::SourceDevice);
}
if (drift < -10 || drift > 10)
{
DBG_Printf(DBG_DDF, "%s/%s : time drift detected, %d seconds to now\n", r->item(RAttrUniqueId)->toCString(), suffix, drift);
auto *apsCtrl = deCONZ::ApsController::instance();
if (writeTimeData(r, item, apsCtrl, item->toVariant()))
{
const auto &ddfItem = DDF_GetItem(item);
const auto readFunction = DA_GetReadFunction(ddfItem.readParameters);
auto res = readFunction(r, item, apsCtrl, ddfItem.readParameters);
if (res.isEnqueued)
{
DBG_Printf(DBG_DDF, "%s time verification queued...\n", r->item(RAttrUniqueId)->toCString());
}
}
}
else
{
DBG_Printf(DBG_DDF, "%s/%s : NO considerable time drift detected, %d seconds to now\n", r->item(RAttrUniqueId)->toCString(), suffix, drift);
}
item->setLastZclReport(deCONZ::steadyTimeRef().ref);
result = true;
}
}
break;
case TIME_ATTRID_LOCAL_TIME:
{
if (suffix == RStateLocaltime)
{
QDateTime time = epoch.addSecs(attr.numericValue().u32 - QDateTime::currentDateTime().offsetFromUtc());
if (item->toVariant().toDateTime().toMSecsSinceEpoch() != time.toMSecsSinceEpoch())
{
item->setValue(time, ResourceItem::SourceDevice);
}
item->setLastZclReport(deCONZ::steadyTimeRef().ref);
result = true;
}
}
break;
case TIME_ATTRID_LAST_SET_TIME:
{
if (suffix == RStateLastSet)
{
QDateTime time = epoch.addSecs(attr.numericValue().u32);
if (item->toVariant().toDateTime().toMSecsSinceEpoch() != time.toMSecsSinceEpoch())
{
item->setValue(time, ResourceItem::SourceDevice);
}
item->setLastZclReport(deCONZ::steadyTimeRef().ref);
result = true;
}
}
break;
}
}
if (result)
{
DeviceJS_ResourceItemValueChanged(item);
}
return result;
}
The item->readParameters() is expected to be an object (given in the device description file).
{ "fn": "zcl:attr", "ep": endpoint, "cl" : clusterId, "mf": manufacturerCode, "at": attributeId, "noseq": noSequenceNumber }
- endpoint: the destination endpoint, use 0 for auto endpoint (from the uniqueid)
- clusterId: string hex value
- manufacturerCode: (optional) string hex value
- attributeId: string hex value or array of up to 8 string hex values
- noSequenceNumber: (optional) bool must be set to `true` and must only be present if needed
Example: { "read": {"fn": "zcl:attr", "ep": 1, "cl": "0x0402", "mf": "0x110b", "at": "0x0000"} }
*/
static DA_ReadResult readZclAttribute(const Resource *r, const ResourceItem *item, deCONZ::ApsController *apsCtrl, const QVariant &readParameters)
{
Q_UNUSED(item)
DA_ReadResult result{};
Q_ASSERT(!readParameters.isNull());
if (readParameters.isNull())
{
return result;
}
auto *rTop = r->parentResource() ? r->parentResource() : r;
const auto *extAddr = rTop->item(RAttrExtAddress);
const auto *nwkAddr = rTop->item(RAttrNwkAddress);
if (!extAddr || !nwkAddr)
{
return result;
}
auto param = getZclParam(readParameters.toMap());
if (!param.valid)
{
return result;
}
if (param.endpoint == AutoEndpoint)
{
param.endpoint = resolveAutoEndpoint(r);
param.endpoint = DEV_ResolveDestinationEndpoint(extAddr->toNumber(), param.endpoint, param.clusterId, param.frameControl);
if (param.endpoint == AutoEndpoint)
{
return result;
}
}
const auto zclResult = ZCL_ReadAttributes(param, extAddr->toNumber(), nwkAddr->toNumber(), apsCtrl);
result.isEnqueued = zclResult.isEnqueued;
result.apsReqId = zclResult.apsReqId;
result.sequenceNumber = zclResult.sequenceNumber;
result.clusterId = param.clusterId;
result.ignoreResponseSequenceNumber = param.ignoreResponseSeq == 1;
return result;
}
The \p writeParameters is expected to contain one object (given in the device description file).
{ "fn": "zcl:attr", "ep": endpoint, "cl": clusterId, "mf": manufacturerCode, "at": attributeId, "dt": zclDataType, "eval": expression }
- endpoint: the destination endpoint, use 0 for auto endpoint (from the uniqueid)
- clusterId: string hex value
- manufacturerCode: (optional) string hex value
- attributeId: string hex value
- zclDataType: string hex value
- expression: to transform the item value to the attribute value
Example: "write": {"fn": "zcl:attr", "cl": "0x0000", "mf": "0x11F5", "at": "0xff0d", "dt": "0x20", "eval": "Item.val"}
*/
bool writeZclAttribute(const Resource *r, const ResourceItem *item, deCONZ::ApsController *apsCtrl, const QVariant &writeParameters)
{
Q_ASSERT(r);
Q_ASSERT(item);
Q_ASSERT(apsCtrl);
bool result = false;
const auto rParent = r->parentResource() ? r->parentResource() : r;
const auto *extAddr = rParent->item(RAttrExtAddress);
const auto *nwkAddr = rParent->item(RAttrNwkAddress);
if (!extAddr || !nwkAddr)
{
return result;
}
const auto map = writeParameters.toMap();
ZCL_Param param = getZclParam(map);
if (!param.valid)
{
return result;
}
if (param.attributeCount != 1)
{
return result;
}
if (param.endpoint == AutoEndpoint)
{
param.endpoint = resolveAutoEndpoint(r);
param.endpoint = DEV_ResolveDestinationEndpoint(extAddr->toNumber(), param.endpoint, param.clusterId, param.frameControl);
if (param.endpoint == AutoEndpoint)
{
return result;
}
}
if (!map.contains("dt") || !map.contains("eval"))
{
return result;
}
bool ok;
const auto dataType = variantToUint(map.value("dt"), UINT8_MAX, &ok);
const auto expr = map.value("eval").toString();
if (!ok || expr.isEmpty())
{
return result;
}
deCONZ::ZclAttribute attribute(param.attributes[0], dataType, QLatin1String(""), deCONZ::ZclReadWrite, true);
DeviceJs &engine = *DeviceJs::instance();
engine.reset();
engine.setResource(r);
engine.setItem(item);
if (engine.evaluate(expr) == JsEvalResult::Ok)
{
const auto value = engine.result();
DBG_Printf(DBG_DDF, "%s/%s expression: %s --> %s\n", r->item(RAttrUniqueId)->toCString(), item->descriptor().suffix, qPrintable(expr), qPrintable(value.toString()));
attribute.setValue(value);
}
else
{
DBG_Printf(DBG_DDF, "failed to evaluate expression for %s/%s: %s, err: %s\n", qPrintable(r->item(RAttrUniqueId)->toString()), item->descriptor().suffix, qPrintable(expr), qPrintable(engine.errorString()));
return result;
}
const auto zclResult = ZCL_WriteAttribute(param, extAddr->toNumber(), nwkAddr->toNumber(), apsCtrl, &attribute);
result = zclResult.isEnqueued;
return result;
}
The \p cmdParameters is expected to contain one object (given in the device description file).
{ "fn": "zcl:cmd", "ep": endpoint, "cl": clusterId, "mf": manufacturerCode, "cmd": commandId, "fc": frameControl, "eval": expression }
- endpoint: the destination endpoint, use 0 for auto endpoint (from the uniqueid)
- clusterId: string hex value
- manufacturerCode: (optional) string hex value
- commandId: string hex value
- frameControl: (optional) string hex value, OR combined 8-bit bitmap to overwrite ZCL frame control
ZclFCProfileCommand = 0x00,
ZclFCClusterCommand = 0x01,
ZclFCManufacturerSpecific = 0x04,
ZclFCDirectionServerToClient = 0x08,
ZclFCDirectionClientToServer = 0x00,
ZclFCEnableDefaultResponse = 0x00,
ZclFCDisableDefaultResponse = 0x10
- expression: (optional) to transform the item value to the command payload as hex string value
Example: "read": {"fn": "zcl:cmd", "ep": "0x0b", "cl": "0x0000", "mf": "0x100b", "cmd": "0xc0", "eval": "'002d00000040'"}
*/
static DA_ReadResult sendZclCommand(const Resource *r, const ResourceItem *item, deCONZ::ApsController *apsCtrl, const QVariant &cmdParameters)
{
Q_ASSERT(r);
Q_ASSERT(item);
Q_ASSERT(apsCtrl);
DA_ReadResult result{};
const auto rParent = r->parentResource() ? r->parentResource() : r;
const auto *extAddr = rParent->item(RAttrExtAddress);
const auto *nwkAddr = rParent->item(RAttrNwkAddress);
if (!extAddr || !nwkAddr)
{
return result;
}
const auto map = cmdParameters.toMap();
ZCL_Param param = getZclParam(map);
if (!param.valid)
{
return result;
}
std::vector<uint8_t> payload;
if (map.contains("eval"))
{
const auto expr = map.value("eval").toString();
if (expr.isEmpty())
{
return result;
}
DeviceJs &engine = *DeviceJs::instance();
engine.reset();
engine.setResource(r);
engine.setItem(item);
if (engine.evaluate(expr) == JsEvalResult::Ok)
{
const auto value = engine.result();
DBG_Printf(DBG_DDF, "%s/%s expression: %s --> %s\n", r->item(RAttrUniqueId)->toCString(), item->descriptor().suffix, qPrintable(expr), qPrintable(value.toString()));
auto a = QByteArray::fromHex(value.toString().toLatin1());
for (const auto b : a)
{
payload.push_back(b);
}
}
else
{
DBG_Printf(DBG_DDF, "failed to evaluate expression for %s/%s: %s, err: %s\n", qPrintable(r->item(RAttrUniqueId)->toString()), item->descriptor().suffix, qPrintable(expr), qPrintable(engine.errorString()));
return result;
}
}
if (param.endpoint == BroadcastEndpoint || param.endpoint == AutoEndpoint)
{
param.endpoint = resolveAutoEndpoint(r);
param.endpoint = DEV_ResolveDestinationEndpoint(extAddr->toNumber(), param.endpoint, param.clusterId, param.frameControl);
if (param.endpoint == BroadcastEndpoint || param.endpoint == AutoEndpoint)
{
return result;
}
}
const auto zclResult = ZCL_SendCommand(param, extAddr->toNumber(), nwkAddr->toNumber(), apsCtrl, &payload);
result.isEnqueued = zclResult.isEnqueued;
result.apsReqId = zclResult.apsReqId;
result.sequenceNumber = zclResult.sequenceNumber;
result.clusterId = param.clusterId;
result.ignoreResponseSequenceNumber = param.ignoreResponseSeq == 1;
return result;
}
The \p cmdParameters is expected to contain one object (given in the device description file).
{ "fn": "zcl:cmd", "ep": endpoint, "cl": clusterId, "mf": manufacturerCode, "cmd": commandId, "eval": expression }
- endpoint: the destination endpoint, use 0 for auto endpoint (from the uniqueid)
- clusterId: string hex value
- manufacturerCode: (optional) string hex value
- commandId: string hex value
- expression: (optional) to transform the item value to the command payload as hex string value
Example: "write": {"fn": "zcl:cmd", "cl": "0x0000", "mf": "0x100b", "cmd": "0xc0", "eval": "'002d00000040'"}
*/
bool writeZclCommand(const Resource *r, const ResourceItem *item, deCONZ::ApsController *apsCtrl, const QVariant &cmdParameters)
{
const auto result = sendZclCommand(r, item, apsCtrl, cmdParameters);
return result.isEnqueued;
}
ParseFunction_t DA_GetParseFunction(const QVariant ¶ms)
{
ParseFunction_t result = nullptr;
const std::array<ParseFunction, 8> functions =
{
ParseFunction(QLatin1String("zcl"), 1, parseZclAttribute),
ParseFunction(QLatin1String("zcl:attr"), 1, parseZclAttribute),
ParseFunction(QLatin1String("zcl:cmd"), 1, parseZclAttribute),
ParseFunction(QLatin1String("xiaomi:special"), 1, parseXiaomiSpecial),
ParseFunction(QLatin1String("ias:zonestatus"), 1, parseIasZoneNotificationAndStatus),
ParseFunction(QLatin1String("tuya"), 1, parseTuyaData),
ParseFunction(QLatin1String("numtostr"), 1, parseNumericToString),
ParseFunction(QLatin1String("time"), 1, parseAndSyncTime)
};
QString fnName;
if (params.type() == QVariant::Map)
{
const auto params1 = params.toMap();
if (params1.isEmpty())
{ }
else if (params1.contains(QLatin1String("fn")))
{
fnName = params1["fn"].toString();
}
else
{
fnName = QLatin1String("zcl:attr");
}
}
for (const auto &f : functions)
{
if (f.name == fnName)
{
result = f.fn;
break;
}
}
return result;
}
ReadFunction_t DA_GetReadFunction(const QVariant ¶ms)
{
ReadFunction_t result = nullptr;
const std::array<ReadFunction, 4> functions =
{
ReadFunction(QLatin1String("zcl"), 1, readZclAttribute),
ReadFunction(QLatin1String("zcl:attr"), 1, readZclAttribute),
ReadFunction(QLatin1String("zcl:cmd"), 1, sendZclCommand),
ReadFunction(QLatin1String("tuya"), 1, readTuyaAllData)
};
QString fnName;
if (params.type() == QVariant::Map)
{
const auto params1 = params.toMap();
if (params1.isEmpty())
{ }
else if (params1.contains(QLatin1String("fn")))
{
fnName = params1["fn"].toString();
}
else
{
fnName = QLatin1String("zcl:attr");
}
}
for (const auto &f : functions)
{
if (f.name == fnName)
{
result = f.fn;
break;
}
}
return result;
}
WriteFunction_t DA_GetWriteFunction(const QVariant ¶ms)
{
WriteFunction_t result = nullptr;
const std::array<WriteFunction, 4> functions =
{
WriteFunction(QLatin1String("zcl"), 1, writeZclAttribute),
WriteFunction(QLatin1String("zcl:attr"), 1, writeZclAttribute),
WriteFunction(QLatin1String("zcl:cmd"), 1, writeZclCommand),
WriteFunction(QLatin1String("tuya"), 1, writeTuyaData)
};
QString fnName;
if (params.type() == QVariant::Map)
{
const auto params1 = params.toMap();
if (params1.isEmpty())
{ }
else if (params1.contains(QLatin1String("fn")))
{
fnName = params1["fn"].toString();
}
else
{
fnName = QLatin1String("zcl:attr");
}
}
for (const auto &f : functions)
{
if (f.name == fnName)
{
result = f.fn;
break;
}
}
return result;
}
Track the running APS queue to aid scheduling of new APS requests
in order to not overhelm the queue. The aim is to execute low priority tasks,
like polling and binding maintenance, only when the queue is not too busy.
This leaves room to send high priority commands e.g. to control a light
without waiting for low priority APS request to be finished.
*/
#define APS_BUSY_TABLE_SIZE 32
struct DA_ReqBusy
{
uint64_t dstExtAddr;
int64_t tref;
uint16_t clusterId;
uint8_t dstEndpoint;
uint8_t apsRequestId;
};
static unsigned _DA_ApsUnconfirmedCount = 0;
static DA_ReqBusy _DA_BusyTable[APS_BUSY_TABLE_SIZE];
unsigned DA_ApsUnconfirmedRequests()
{
return _DA_ApsUnconfirmedCount;
}
unsigned DA_ApsUnconfirmedRequestsForExtAddress(uint64_t extAddr)
{
unsigned result = 0;
if (_DA_ApsUnconfirmedCount != 0)
{
for (unsigned i = 0; i < APS_BUSY_TABLE_SIZE; i++)
{
DA_ReqBusy *e = &_DA_BusyTable[i];
if (e->tref != 0 && e->dstExtAddr == extAddr)
{
result++;
}
if (result == _DA_ApsUnconfirmedCount)
{
break;
}
}
}
return result;
}
Record it here to track it until it's confirmed aka done.
*/
void DA_ApsRequestEnqueued(const deCONZ::ApsDataRequest &req)
{
if (!req.dstAddress().hasExt())
{
DBG_Assert(!req.dstAddress().isNwkUnicast());
return;
}
const int64_t now = deCONZ::steadyTimeRef().ref / 1000;
for (unsigned i = 0; i < APS_BUSY_TABLE_SIZE; i++)
{
DA_ReqBusy *e = &_DA_BusyTable[i];
if (e->tref != 0 && ((now - e->tref) > 60))
{
DBG_Assert(_DA_ApsUnconfirmedCount > 0);
if (_DA_ApsUnconfirmedCount > 0)
{
_DA_ApsUnconfirmedCount--;
}
memset(e, 0, sizeof(*e));
}
if (e->tref == 0)
{
e->dstExtAddr = req.dstAddress().ext();
e->dstEndpoint = req.dstEndpoint();
e->apsRequestId = req.id();
e->clusterId = req.clusterId();
e->tref = now;
DBG_Assert(_DA_ApsUnconfirmedCount < APS_BUSY_TABLE_SIZE);
if (_DA_ApsUnconfirmedCount < APS_BUSY_TABLE_SIZE)
{
_DA_ApsUnconfirmedCount++;
}
return;
}
}
}
firmware or an error occured. The actual status isn't considered here, we
only care that the APS request is 'done'.
*/
void DA_ApsRequestConfirmed(const deCONZ::ApsDataConfirm &conf)
{
if (!conf.dstAddress().hasExt())
{
return;
}
if (_DA_ApsUnconfirmedCount != 0)
{
for (unsigned i = 0; i < APS_BUSY_TABLE_SIZE; i++)
{
DA_ReqBusy *e = &_DA_BusyTable[i];
if (e->apsRequestId != conf.id()) continue;
if (e->dstExtAddr != conf.dstAddress().ext()) continue;
if (e->dstEndpoint != conf.dstEndpoint()) continue;
memset(e, 0, sizeof(*e));
_DA_ApsUnconfirmedCount--;
return;
}
}
}