Skip to content

Commit

Permalink
Adapt TuYa converters to multiple data point change (Koenkk#3572)
Browse files Browse the repository at this point in the history
* Extend `tuya_data_point_dump` to support multiple DP values per payload

Adapt `scripts/read_tuya_dump.py` accordingly and make its output more
helpful (print the numerical DP value and remove the `fn` field).  Its
output for a packet with two DP values now looks like this:

```
2021-12-22T20:50:13 0x867829fffe2de210 (seq:    0, value: #0) [DP   1 unknown     ] => enum: 1
2021-12-22T20:50:13 0x867829fffe2de210 (seq:    0, value: Koenkk#1) [DP   4 unknown     ] => int: 92
```

* Adapt TuYa "from Zigbee" converters to multiple data point change

Types `commandDataReport`, `commandDataResponse`, and
`commandActiveStatusReport` receive an array of DP values now (instead of a
single value).  Adapt existing converters to use the first entry of this
array.

To avoid breakage, additional DP values that may be present in a payload are
ignored (as before).

* Adapt TuYa library functions and "to Zigbee" converters to multiple data point change
  • Loading branch information
gmbnomis authored Jan 2, 2022
1 parent b3a8166 commit 4306b31
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 178 deletions.
268 changes: 154 additions & 114 deletions converters/fromZigbee.js

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions converters/toZigbee.js
Original file line number Diff line number Diff line change
Expand Up @@ -5462,7 +5462,7 @@ const converters = {
}
}

await tuya.sendDataPoint(entity, tuya.dataTypes.string, tuya.dataPoints.silvercrestSetEffect, data);
await tuya.sendDataPointStringBuffer(entity, tuya.dataPoints.silvercrestSetEffect, data);
} else if (key === 'brightness') {
await tuya.sendDataPointEnum(entity, tuya.dataPoints.silvercrestChangeMode, tuya.silvercrestModes.white);
// It expects 2 leading zero's.
Expand All @@ -5472,7 +5472,10 @@ const converters = {
const scaled = utils.mapNumberRange(value, 0, 255, 0, 1000);
data = data.concat(tuya.convertDecimalValueTo2ByteHexArray(scaled));

await tuya.sendDataPoint(entity, tuya.dataTypes.value, tuya.dataPoints.silvercrestSetBrightness, data);
await tuya.sendDataPoint(
entity,
{dp: tuya.dataPoints.silvercrestSetBrightness, datatype: tuya.dataTypes.value, data: data},
);
} else if (key === 'color') {
await tuya.sendDataPointEnum(entity, tuya.dataPoints.silvercrestChangeMode, tuya.silvercrestModes.color);

Expand Down Expand Up @@ -5544,7 +5547,7 @@ const converters = {
data = data.concat(tuya.convertStringToHexArray(hsb.s));
data = data.concat(tuya.convertStringToHexArray(hsb.b));

await tuya.sendDataPoint(entity, tuya.dataTypes.string, tuya.dataPoints.silvercrestSetColor, data);
await tuya.sendDataPointStringBuffer(entity, tuya.dataPoints.silvercrestSetColor, data);
}
},
},
Expand Down
7 changes: 4 additions & 3 deletions devices/lidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ const fzLocal = {
cluster: 'manuSpecificTuya',
type: ['commandDataResponse', 'commandDataReport'],
convert: (model, msg, publish, options, meta) => {
const dp = msg.data.dp;
const value = tuya.getDataValue(msg.data.datatype, msg.data.data);
const dpValue = tuya.firstDpValue(msg, meta, 'zs_thermostat');
const dp = dpValue.dp;
const value = tuya.getDataValue(dpValue);
const ret = {};
const daysMap = {1: 'monday', 2: 'tuesday', 3: 'wednesday', 4: 'thursday', 5: 'friday', 6: 'saturday', 7: 'sunday'};
const day = daysMap[value[0]];
Expand Down Expand Up @@ -135,7 +136,7 @@ const fzLocal = {
ret.away_preset_days = (value[6]<<8)+value[7];
return ret;
default:
meta.logger.warn(`zigbee-herdsman-converters:zsThermostat: Unrecognized DP #${dp} with data ${JSON.stringify(msg.data)}`);
meta.logger.warn(`zigbee-herdsman-converters:zsThermostat: Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`);
}
},
},
Expand Down
5 changes: 3 additions & 2 deletions devices/zemismart.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ const fzLocal = {
cluster: 'manuSpecificTuya',
type: ['commandGetData', 'commandSetDataResponse'],
convert: (model, msg, publish, options, meta) => {
const button = msg.data.dp;
const actionValue = tuya.getDataValue(msg.data.datatype, msg.data.data);
const dpValue = tuya.firstDpValue(msg, meta, 'ZMRM02');
const button = dpValue.dp;
const actionValue = tuya.getDataValue(dpValue);
const lookup = {0: 'single', 1: 'double', 2: 'hold'};
const action = lookup[actionValue];
return {action: `button_${button}_${action}`};
Expand Down
5 changes: 3 additions & 2 deletions lib/legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2052,8 +2052,9 @@ const fromZigbee = {
return fromZigbeeConverters.tuya_thermostat_weekly_schedule.convert(model, msg, publish, options, meta);
}

const dp = msg.data.dp;
const value = tuyaGetDataValue(msg.data.datatype, msg.data.data);
const dpValue = tuya.firstDpValue(msg, meta, 'tuya_thermostat_weekly_schedule');
const dp = dpValue.dp;
const value = tuyaGetDataValue(dpValue.datatype, dpValue.data);

const thermostatMeta = getMetaValue(msg.endpoint, model, 'thermostat');
const firstDayDpId = thermostatMeta.weeklyScheduleFirstDayDpId;
Expand Down
125 changes: 85 additions & 40 deletions lib/tuya.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,38 @@ const convertMultiByteNumberPayloadToSingleDecimalNumber = (chunks) => {
return value;
};

function getDataValue(dataType, data) {
switch (dataType) {
function firstDpValue(msg, meta, converterName) {
const dpValues = msg.data.dpValues;
for (let index = 1; index < dpValues.length; index++) {
meta.logger.warn(`zigbee-herdsman-converters:${converterName}: Additional DP #${
dpValues[index].dp} with data ${JSON.stringify(dpValues[index])} will be ignored! ` +
'Use a for loop in the fromZigbee converter (see ' +
'https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html)');
}
return dpValues[0];
}


function getDataValue(dpValue) {
switch (dpValue.datatype) {
case dataTypes.raw:
return data;
return dpValue.data;
case dataTypes.bool:
return data[0] === 1;
return dpValue.data[0] === 1;
case dataTypes.value:
return convertMultiByteNumberPayloadToSingleDecimalNumber(data);
return convertMultiByteNumberPayloadToSingleDecimalNumber(dpValue.data);
case dataTypes.string:
// eslint-disable-next-line
let dataString = '';
// Don't use .map here, doesn't work: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1799/files#r530377091
for (let i = 0; i < data.length; ++i) {
dataString += String.fromCharCode(data[i]);
for (let i = 0; i < dpValue.data.length; ++i) {
dataString += String.fromCharCode(dpValue.data[i]);
}
return dataString;
case dataTypes.enum:
return data[0];
return dpValue.data[0];
case dataTypes.bitmap:
return convertMultiByteNumberPayloadToSingleDecimalNumber(data);
return convertMultiByteNumberPayloadToSingleDecimalNumber(dpValue.data);
}
}

Expand Down Expand Up @@ -606,98 +618,131 @@ const tvThermostatPreset = {
};

// Return `seq` - transaction ID for handling concrete response
async function sendDataPoint(entity, datatype, dp, data, cmd, seq=undefined) {
async function sendDataPoints(entity, dpValues, cmd, seq=undefined) {
if (seq === undefined) {
if (sendDataPoint.seq === undefined) {
sendDataPoint.seq = 0;
if (sendDataPoints.seq === undefined) {
sendDataPoints.seq = 0;
} else {
sendDataPoint.seq++;
sendDataPoint.seq %= 0xFFFF;
sendDataPoints.seq++;
sendDataPoints.seq %= 0xFFFF;
}
seq = sendDataPoint.seq;
seq = sendDataPoints.seq;
}

await entity.command(
'manuSpecificTuya',
cmd || 'dataRequest',
{
seq,
dp: dp,
datatype: datatype,
length_hi: (data.length >> 8) & 0xFF,
length_lo: data.length & 0xFF,
data: data,
dpValues,
},
{disableDefaultResponse: true},
);
return seq;
}

function dpValueFromIntValue(dp, value) {
return {dp, datatype: dataTypes.value, data: convertDecimalValueTo4ByteHexArray(value)};
}

function dpValueFromBool(dp, value) {
return {dp, datatype: dataTypes.bool, data: [value ? 1 : 0]};
}

function dpValueFromEnum(dp, value) {
return {dp, datatype: dataTypes.enum, data: [value]};
}

function dpValueFromStringBuffer(dp, stringBuffer) {
return {dp, datatype: dataTypes.string, data: stringBuffer};
}

function dpValueFromRaw(dp, rawBuffer) {
return {dp, datatype: dataTypes.raw, data: rawBuffer};
}

function dpValueFromBitmap(dp, bitmapBuffer) {
return {dp, datatype: dataTypes.bitmap, data: bitmapBuffer};
}

// Return `seq` - transaction ID for handling concrete response
async function sendDataPoint(entity, dpValue, cmd, seq=undefined) {
return await sendDataPoints(entity, [dpValue], cmd, seq);
}

async function sendDataPointValue(entity, dp, value, cmd, seq=undefined) {
return await sendDataPoint(
return await sendDataPoints(
entity,
dataTypes.value,
dp,
convertDecimalValueTo4ByteHexArray(value),
[dpValueFromIntValue(dp, value)],
cmd,
seq,
);
}

async function sendDataPointBool(entity, dp, value, cmd, seq=undefined) {
return await sendDataPoint(
return await sendDataPoints(
entity,
dataTypes.bool,
dp,
[value ? 1 : 0],
[dpValueFromBool(dp, value)],
cmd,
seq,
);
}

async function sendDataPointEnum(entity, dp, value, cmd, seq=undefined) {
return await sendDataPoint(
return await sendDataPoints(
entity,
dataTypes.enum,
dp,
[value],
[dpValueFromEnum(dp, value)],
cmd,
seq,
);
}

async function sendDataPointRaw(entity, dp, value, cmd, seq=undefined) {
return await sendDataPoint(
return await sendDataPoints(
entity,
dataTypes.raw,
dp,
value,
[dpValueFromRaw(dp, value)],
cmd,
seq,
);
}

async function sendDataPointBitmap(entity, dp, value, cmd, seq=undefined) {
return await sendDataPoint(
return await sendDataPoints(
entity,
[dpValueFromBitmap(dp, value)],
cmd,
seq,
);
}

async function sendDataPointStringBuffer(entity, dp, value, cmd, seq=undefined) {
return await sendDataPoints(
entity,
dataTypes.bitmap,
dp,
value,
[dpValueFromStringBuffer(dp, value)],
cmd,
seq,
);
}

module.exports = {
sendDataPoint,
sendDataPoints,
sendDataPointValue,
sendDataPointBool,
sendDataPointEnum,
sendDataPointBitmap,
sendDataPointRaw,
sendDataPointStringBuffer,
firstDpValue,
getDataValue,
dataTypes,
dataPoints,
dpValueFromIntValue,
dpValueFromBool,
dpValueFromEnum,
dpValueFromStringBuffer,
dpValueFromRaw,
dpValueFromBitmap,
convertDecimalValueTo4ByteHexArray,
convertDecimalValueTo2ByteHexArray,
onEventSetTime,
Expand Down
26 changes: 12 additions & 14 deletions scripts/read_tuya_dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,28 +122,25 @@ def parse_saswell_schedule(length: int, data: bytearray):

DATA_POINT_NAMES = SASWELL_DATA_POINT_NAMES
DATA_TYPE_PARSERS = [parse_raw, parse_bool, parse_value, parse_string, parse_enum, parse_bitmap]
DATA_TYPE_NAME = ["raw", "bool", "int", "string", "enum", "bitmap"]
EXTRA_PARSERS = SASWELL_PARSERS


def get_values(numbers):
data = bytearray()
dp = int(numbers[0], 16)
dtype = int(numbers[1], 16)
fn = int(numbers[2], 16)
for number in numbers[3:]:
data += bytes([int(number, 16)])

data = bytes.fromhex("".join(numbers[2:]))
name = "DP {:3d} ".format(dp)
if dp in DATA_POINT_NAMES.keys():
name = DATA_POINT_NAMES[dp]
name += DATA_POINT_NAMES[dp]
else:
name = "unknown "
name += "unknown "
length = len(data)

value = "{:d} ".format(fn)
if dp in EXTRA_PARSERS.keys():
value += EXTRA_PARSERS[dp](length, data)
value = EXTRA_PARSERS[dp](length, data)
else:
value += DATA_TYPE_PARSERS[dtype](length, data)
value = DATA_TYPE_NAME[dtype] + ": " + DATA_TYPE_PARSERS[dtype](length, data)
return (name, value)


Expand All @@ -155,16 +152,17 @@ def main():
line = line[:-1]
if len(line) == 0:
continue
tokens = line.split(" ")
tokens = line.split()
time = datetime.datetime.fromtimestamp(int(int(tokens[0]) / 1000))
address = tokens[1]

seq = int(tokens[2], 16)
seq_dp = int(tokens[3], 16)

(name, value) = get_values(tokens[3:])
(name, value) = get_values(tokens[4:])
print(
"{} {} (seq: {:4d}) [{}] => {}".format(
time.isoformat(), address, seq, name, value
"{} {} (seq: {:4d}, value: #{}) [{}] => {}".format(
time.isoformat(), address, seq, seq_dp, name, value
)
)

Expand Down

0 comments on commit 4306b31

Please sign in to comment.