zigbee2mqtt
zigbee2mqtt copied to clipboard
Add time sync to X5H-GB-B wall mounted thermostat
Is your feature request related to a problem? Please describe
I bought myself a floor heating thermostat model TGR85-ZB-WP from Beok. The thermostat is recognized as TuYa X5H-GB-B. It uses a TS0601 zigbee device. Everything seems to work well so far. Only the clock of the device is off and I have not found a way set to set it correctly.
Describe the solution you'd like
a) Time on the device is either synchronized/set regularly by z2m (preferred), or b) time can be set via the z2m front-end / home assistant through mqtt.
It appears the problem has already been solved for other thermostats depending on the TS0601 zigbee device (see issue #5620). Potentially, the same fix can be applied.
Describe alternatives you've considered
None
Additional context
None
Only the clock of the device is off and I have not found a way set to set it correctly.
Is it off by exactly X number of hours?
That is possible. The device appears to be off by exactly 6 hours. Funnily the displayed time even exceeds the 24 hours. At the moment, the device is showing 25:22.
Am 19. September 2022 17:27:16 MESZ schrieb Koen Kanters @.***>:
Only the clock of the device is off and I have not found a way set to set it correctly.
Is it off by exactly X number of hours?
-- Reply to this email directly or view it on GitHub: https://github.com/Koenkk/zigbee2mqtt/issues/14037#issuecomment-1251179429 You are receiving this because you authored the thread.
Message ID: @.***>
Could you check if the issue is fixed with the following external converter:
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const ota = require('zigbee-herdsman-converters/lib/ota');
const tuya = require('zigbee-herdsman-converters/lib/tuya');
const utils = require('zigbee-herdsman-converters/lib/utils');
const e = exposes.presets;
const ea = exposes.access;
const fzLocal = {
x5h_thermostat: {
cluster: 'manuSpecificTuya',
type: ['commandDataResponse', 'commandDataReport'],
convert: (model, msg, publish, options, meta) => {
const dpValue = tuya.firstDpValue(msg, meta, 'x5h_thermostat');
const dp = dpValue.dp;
const value = tuya.getDataValue(dpValue);
switch (dp) {
case tuya.dataPoints.x5hState: {
return {system_mode: value ? 'heat' : 'off'};
}
case tuya.dataPoints.x5hWorkingStatus: {
return {running_state: value ? 'heat' : 'idle'};
}
case tuya.dataPoints.x5hSound: {
return {sound: value ? 'ON' : 'OFF'};
}
case tuya.dataPoints.x5hFrostProtection: {
return {frost_protection: value ? 'ON' : 'OFF'};
}
case tuya.dataPoints.x5hWorkingDaySetting: {
return {week: tuya.thermostatWeekFormat[value]};
}
case tuya.dataPoints.x5hFactoryReset: {
if (value) {
clearTimeout(globalStore.getValue(msg.endpoint, 'factoryResetTimer'));
const timer = setTimeout(() => publish({factory_reset: 'OFF'}), 60 * 1000);
globalStore.putValue(msg.endpoint, 'factoryResetTimer', timer);
meta.logger.info('The thermostat is resetting now. It will be available in 1 minute.');
}
return {factory_reset: value ? 'ON' : 'OFF'};
}
case tuya.dataPoints.x5hTempDiff: {
return {deadzone_temperature: parseFloat((value / 10).toFixed(1))};
}
case tuya.dataPoints.x5hProtectionTempLimit: {
return {heating_temp_limit: value};
}
case tuya.dataPoints.x5hBackplaneBrightness: {
const lookup = {0: 'off', 1: 'low', 2: 'medium', 3: 'high'};
if (value >= 0 && value <= 3) {
globalStore.putValue(msg.endpoint, 'brightnessState', value);
return {brightness_state: lookup[value]};
}
// Sometimes, for example on thermostat restart, it sends message like:
// {"dpValues":[{"data":{"data":[90],"type":"Buffer"},"datatype":4,"dp":104}
// It doesn't represent any brightness value and brightness remains the previous value
const lastValue = globalStore.getValue(msg.endpoint, 'brightnessState') || 1;
return {brightness_state: lookup[lastValue]};
}
case tuya.dataPoints.x5hWeeklyProcedure: {
const periods = [];
const periodSize = 4;
const periodsNumber = 8;
for (let i = 0; i < periodsNumber; i++) {
const hours = value[i * periodSize];
const minutes = value[i * periodSize + 1];
const tempHexArray = [value[i * periodSize + 2], value[i * periodSize + 3]];
const tempRaw = Buffer.from(tempHexArray).readUIntBE(0, tempHexArray.length);
const strHours = hours.toString().padStart(2, '0');
const strMinutes = minutes.toString().padStart(2, '0');
const temp = parseFloat((tempRaw / 10).toFixed(1));
periods.push(`${strHours}:${strMinutes}/${temp}`);
}
const schedule = periods.join(' ');
return {schedule};
}
case tuya.dataPoints.x5hChildLock: {
return {child_lock: value ? 'LOCK' : 'UNLOCK'};
}
case tuya.dataPoints.x5hSetTemp: {
const setpoint = parseFloat((value / 10).toFixed(1));
globalStore.putValue(msg.endpoint, 'currentHeatingSetpoint', setpoint);
return {current_heating_setpoint: setpoint};
}
case tuya.dataPoints.x5hSetTempCeiling: {
return {upper_temp: value};
}
case tuya.dataPoints.x5hCurrentTemp: {
const temperature = value & (1 << 15) ? value - (1 << 16) + 1 : value;
return {local_temperature: parseFloat((temperature / 10).toFixed(1))};
}
case tuya.dataPoints.x5hTempCorrection: {
return {local_temperature_calibration: parseFloat((value / 10).toFixed(1))};
}
case tuya.dataPoints.x5hMode: {
const lookup = {0: 'manual', 1: 'program'};
return {preset: lookup[value]};
}
case tuya.dataPoints.x5hSensorSelection: {
const lookup = {0: 'internal', 1: 'external', 2: 'both'};
return {sensor: lookup[value]};
}
case tuya.dataPoints.x5hOutputReverse: {
return {output_reverse: value};
}
default: {
meta.logger.warn(`fromZigbee:x5h_thermostat: Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`);
}
}
},
},
}
const tzLocal = {
x5h_thermostat: {
key: ['system_mode', 'current_heating_setpoint', 'sensor', 'brightness_state', 'sound', 'frost_protection', 'week', 'factory_reset',
'local_temperature_calibration', 'heating_temp_limit', 'deadzone_temperature', 'upper_temp', 'preset', 'child_lock',
'schedule'],
convertSet: async (entity, key, value, meta) => {
switch (key) {
case 'system_mode':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hState, value === 'heat');
break;
case 'preset': {
value = value.toLowerCase();
const lookup = {manual: 0, program: 1};
utils.validateValue(value, Object.keys(lookup));
value = lookup[value];
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hMode, value);
break;
}
case 'upper_temp':
if (value >= 35 && value <= 95) {
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTempCeiling, value);
const setpoint = globalStore.getValue(entity, 'currentHeatingSetpoint', 20);
const setpointRaw = Math.round(setpoint * 10);
await new Promise((r) => setTimeout(r, 500));
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTemp, setpointRaw);
} else {
throw new Error('Supported values are in range [35, 95]');
}
break;
case 'deadzone_temperature':
if (value >= 0.5 && value <= 9.5) {
value = Math.round(value * 10);
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hTempDiff, value);
} else {
throw new Error('Supported values are in range [0.5, 9.5]');
}
break;
case 'heating_temp_limit':
if (value >= 5 && value <= 60) {
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hProtectionTempLimit, value);
} else {
throw new Error('Supported values are in range [5, 60]');
}
break;
case 'local_temperature_calibration':
if (value >= -9.9 && value <= 9.9) {
value = Math.round(value * 10);
if (value < 0) {
value = 0xFFFFFFFF + value + 1;
}
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hTempCorrection, value);
} else {
throw new Error('Supported values are in range [-9.9, 9.9]');
}
break;
case 'factory_reset':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hFactoryReset, value === 'ON');
break;
case 'week':
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hWorkingDaySetting,
utils.getKey(tuya.thermostatWeekFormat, value, value, Number));
break;
case 'frost_protection':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hFrostProtection, value === 'ON');
break;
case 'sound':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hSound, value === 'ON');
break;
case 'brightness_state': {
value = value.toLowerCase();
const lookup = {off: 0, low: 1, medium: 2, high: 3};
utils.validateValue(value, Object.keys(lookup));
value = lookup[value];
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hBackplaneBrightness, value);
break;
}
case 'sensor': {
value = value.toLowerCase();
const lookup = {'internal': 0, 'external': 1, 'both': 2};
utils.validateValue(value, Object.keys(lookup));
value = lookup[value];
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hSensorSelection, value);
break;
}
case 'current_heating_setpoint':
if (value >= 5 && value <= 60) {
value = Math.round(value * 10);
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTemp, value);
} else {
throw new Error(`Unsupported value: ${value}`);
}
break;
case 'child_lock':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hChildLock, value === 'LOCK');
break;
case 'schedule': {
const periods = value.split(' ');
const periodsNumber = 8;
const payload = [];
for (let i = 0; i < periodsNumber; i++) {
const timeTemp = periods[i].split('/');
const hm = timeTemp[0].split(':', 2);
const h = parseInt(hm[0]);
const m = parseInt(hm[1]);
const temp = parseFloat(timeTemp[1]);
if (h < 0 || h >= 24 || m < 0 || m >= 60 || temp < 5 || temp > 60) {
throw new Error('Invalid hour, minute or temperature of: ' + periods[i]);
}
const tempHexArray = tuya.convertDecimalValueTo2ByteHexArray(Math.round(temp * 10));
// 1 byte for hour, 1 byte for minutes, 2 bytes for temperature
payload.push(h, m, ...tempHexArray);
}
await tuya.sendDataPointRaw(entity, tuya.dataPoints.x5hWeeklyProcedure, payload);
break;
}
default:
break;
}
},
},
}
const definition = {
fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_2ekuz3dz'}],
model: 'X5H-GB-B',
vendor: 'TuYa',
description: 'Wall-mount thermostat',
fromZigbee: [fz.ignore_basic_report, fzLocal.x5h_thermostat],
toZigbee: [tzLocal.x5h_thermostat],
whiteLabel: [{vendor: 'Beok', model: 'TGR85-ZB'}],
exposes: [
exposes.climate().withSetpoint('current_heating_setpoint', 5, 60, 0.5, ea.STATE_SET)
.withLocalTemperature(ea.STATE).withLocalTemperatureCalibration(-9.9, 9.9, 0.1, ea.STATE_SET)
.withSystemMode(['off', 'heat'], ea.STATE_SET).withRunningState(['idle', 'heat'], ea.STATE)
.withPreset(['manual', 'program']).withSensor(['internal', 'external', 'both'], ea.STATE_SET),
exposes.text('schedule', ea.STATE_SET).withDescription('There are 8 periods in the schedule in total. ' +
'6 for workdays and 2 for holidays. It should be set in the following format for each of the periods: ' +
'`hours:minutes/temperature`. All periods should be set at once and delimited by the space symbol. ' +
'For example: `06:00/20.5 08:00/15 11:30/15 13:30/15 17:00/22 22:00/15 06:00/20 22:00/15`. ' +
'The thermostat doesn\'t report the schedule by itself even if you change it manually from device'),
e.child_lock(), e.week(),
exposes.enum('brightness_state', ea.STATE_SET, ['off', 'low', 'medium', 'high'])
.withDescription('Screen brightness'),
exposes.binary('sound', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Switches beep sound when interacting with thermostat'),
exposes.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Antifreeze function'),
exposes.binary('factory_reset', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Resets all settings to default. Doesn\'t unpair device.'),
exposes.numeric('heating_temp_limit', ea.STATE_SET).withUnit('°C').withValueMax(60)
.withValueMin(5).withValueStep(1).withPreset('default', 35, 'Default value')
.withDescription('Heating temperature limit'),
exposes.numeric('deadzone_temperature', ea.STATE_SET).withUnit('°C').withValueMax(9.5)
.withValueMin(0.5).withValueStep(0.5).withPreset('default', 1, 'Default value')
.withDescription('The delta between local_temperature and current_heating_setpoint to trigger Heat'),
exposes.numeric('upper_temp', ea.STATE_SET).withUnit('°C').withValueMax(95)
.withValueMin(35).withValueStep(1).withPreset('default', 60, 'Default value'),
],
onEvent: tuya.onEventSetLocalTime,
};
module.exports = definition;
- save this as file next to
configuration.yaml
asext_converter.js
- add it to
configuration.yaml
:
external_converters:
- ext_converter.js
- start z2m, wait 24 hours and check if issue is fixed
I have that thermostat, it also had synchronization problems. I put the thermostat on the correct time and after a while, the time was totally out of sync. Both the time that was exactly 6 hours ahead, and the day that was two days back from the established date.
With the ext_converter file, something seems to have been fixed. The day no longer changes, but the time is exactly 6 hours ahead on the device. It even goes past 24:00, which is very strange.
$ date Sat Sep 24 08:41:21 PM CEST 2022 $ date -u Sat Sep 24 06:41:27 PM UTC 2022
In the thermostat: Day: 6, Hour: 26:41
https://www.dropbox.com/s/g1mpr1xpnyvnymu/CM220924-204056001.jpg?dl=0
What could I do to solve this problem?
Same here. In configured the external converter, which koenkk posted. For a while the delta seemed to have decreased to +4 hours. At 21:00 CEST (I was by coincident watching in front of the device), the delta change back to +6 hours. I assume, it is the line "onEvent: tuya.onEventSetLocalTime", which is supposed to do the trick? Would it be possible to specify a delta for the time and optionally the date/day of week?
@kalbfuss how are you running z2m, is the time on your machine you are running z2m on correct?
I run z2m as a docker container in Ubuntu Linux on a Raspberry Pi Zero W. This is the command I have used to create the container:
$ docker run -d \
--name zigbee2mqtt \
--restart unless-stopped \
--publish 8080:8080 \
--volume /data/zigbee2mqtt:/app/data \
--volume /run/udev:/run/udev:ro \
--volume /etc/ssl/certs/:/etc/ssl/certs:ro \
--env TZ=Europe/Berlin \
--device=dev/ttyS0:/dev/ttyS0 \
--platform linux/arm/v6 \
--label=com.centurylinklabs.watchtower.monitor-only=true \
koenkk/zigbee2mqtt
Date and time within the container are accurate (verified at the time of testing). The timezone is CEST (same time zone as the Ubuntu Linux system).
$ docker exec -it zigbee2mqtt /bin/sh
$ date
Sun Sep 25 12:25:30 CEST 2022
The time shown by the thermostat appears to be 6 hours later. If I am not mistaken the displayed time corresponds to Chinese time, which is UTC+8, while CEST corresponds to UTC+2. This could explain the delta, but not why the displayed time sometimes exceeds 24:00.
Any solution for this issue?
Can you check if the time is correct with the following external converter? https://gist.github.com/Koenkk/ee621c8913f698c432d05587ba12d9c7
Note that I've also migrated it to the new TuYa integration, you need to update to z2m 1.28.0 first and not all features are supported yet (the ones listed in tuyaDatapoints
)
Do you mean, the new proposed external converter only works in combination with z2m v1.28.0? Or do you mean to say the new converter has already been integrated into version 1.28.0? I am asking because v1.28.0 is not yet available as docker container. If it is number one I will have to wait before I can do the next test.
Ok, I believe I can answer this question myself. I tried the new proposed converter and z2m fails to load due to absence of the globalStore. Will wait for release of the new container and test afterwards. Thank you very much in any case.
I update z2m to v1.28.0, and use new candidate caonverter, with a few changes to get z2m started.
This is the converter:
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const utils = require('zigbee-herdsman-converters/lib/utils');
const ota = require('zigbee-herdsman-converters/lib/ota');
const tuya = require('zigbee-herdsman-converters/lib/tuya');
const e = exposes.presets;
const ea = exposes.access;
const globalStore = require('zigbee-herdsman-converters/lib/store');
const tzDataPoints = {
...tuya.tzDataPoints,
keys: ['system_mode', 'preset', 'running_state', 'current_heating_setpoint', 'upper_temperature', 'local_temperature', 'deadzone_temperature', 'heating_temperature_limit'],
}
const valueConverter = {
systemMode: tuya.valueConverterBasic.lookup({'heat': true, 'off': false}),
runningState: tuya.valueConverterBasic.lookup({'heat': true, 'idle': false}),
preset: tuya.valueConverterBasic.lookup({'manual': 0, 'program': 1}),
divideBy10: tuya.valueConverterBasic.divideBy(10),
};
async function onEventSetLocalTime(type, data, device) {
// FIXME: What actually nextLocalTimeUpdate/forceTimeUpdate do?
// I did not find any timers or something else where it was used.
// Actually, there are two ways to set time on TuYa MCU devices:
// 1. Respond to the `commandMcuSyncTime` event
// 2. Just send `mcuSyncTime` anytime (by 1-hour timer or something else)
const nextLocalTimeUpdate = globalStore.getValue(device, 'nextLocalTimeUpdate');
const forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < new Date().getTime();
if ((data.type === 'commandMcuSyncTime' && data.cluster === 'manuSpecificTuya') || forceTimeUpdate) {
globalStore.putValue(device, 'nextLocalTimeUpdate', new Date().getTime() + 3600 * 1000);
try {
const utcTime = Math.round(((new Date()).getTime()) / 1000);
const localTime = utcTime - ((new Date()).getTimezoneOffset() + 360) * 60;
const endpoint = device.getEndpoint(1);
const payload = {
payloadSize: 8,
payload: [
...convertDecimalValueTo4ByteHexArray(utcTime),
...convertDecimalValueTo4ByteHexArray(localTime),
],
};
await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
} catch (error) {
// endpoint.command can throw an error which needs to
// be caught or the zigbee-herdsman may crash
// Debug message is handled in the zigbee-herdsman
}
}
}
const definition = {
fingerprint: tuya.fingerprint('TS0601', ['_TZE200_2ekuz3dz']),
model: 'TS0601_thermostat',
vendor: 'TuYa',
description: 'Thermostat',
fromZigbee: [tuya.fzDataPoints],
toZigbee: [tzDataPoints],
whiteLabel: [{vendor: 'Beok', model: 'TGR85-ZB'}, {vendor: 'TuYa', model: 'X5H-GB-B'}],
configure: tuya.configureMagicPacket,
meta: {
tuyaDatapoints: [
[1, 'system_mode', valueConverter.systemMode],
[2, 'preset', valueConverter.preset],
[3, 'running_state', valueConverter.runningState],
[16, 'current_heating_setpoint', valueConverter.divideBy10],
[19, 'upper_temperature', tuya.valueConverterBasic.raw],
[24, 'local_temperature', valueConverter.divideBy10],
[101, 'deadzone_temperature', valueConverter.divideBy10],
[102, 'heating_temperature_limit', valueConverter.raw],
],
},
exposes: [
exposes.climate().withSetpoint('current_heating_setpoint', 5, 60, 0.5, ea.STATE_SET)
.withLocalTemperature(ea.STATE).withLocalTemperatureCalibration(-9.9, 9.9, 0.1, ea.STATE_SET)
.withSystemMode(['off', 'heat'], ea.STATE_SET).withRunningState(['idle', 'heat'], ea.STATE)
.withPreset(['manual', 'program']).withSensor(['internal', 'external', 'both'], ea.STATE_SET),
exposes.text('schedule', ea.STATE_SET).withDescription('There are 8 periods in the schedule in total. ' +
'6 for workdays and 2 for holidays. It should be set in the following format for each of the periods: ' +
'`hours:minutes/temperature`. All periods should be set at once and delimited by the space symbol. ' +
'For example: `06:00/20.5 08:00/15 11:30/15 13:30/15 17:00/22 22:00/15 06:00/20 22:00/15`. ' +
'The thermostat doesn\'t report the schedule by itself even if you change it manually from device'),
e.child_lock(), e.week(),
exposes.enum('brightness_state', ea.STATE_SET, ['off', 'low', 'medium', 'high'])
.withDescription('Screen brightness'),
exposes.binary('sound', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Switches beep sound when interacting with thermostat'),
exposes.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Antifreeze function'),
exposes.binary('factory_reset', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Resets all settings to default. Doesn\'t unpair device.'),
exposes.numeric('heating_temperature_limit', ea.STATE_SET).withUnit('°C').withValueMax(60)
.withValueMin(5).withValueStep(1).withPreset('default', 35, 'Default value')
.withDescription('Heating temperature limit'),
exposes.numeric('deadzone_temperature', ea.STATE_SET).withUnit('°C').withValueMax(9.5)
.withValueMin(0.5).withValueStep(0.5).withPreset('default', 1, 'Default value')
.withDescription('The delta between local_temperature and current_heating_setpoint to trigger Heat'),
exposes.numeric('upper_temperature', ea.STATE_SET).withUnit('°C').withValueMax(95)
.withValueMin(35).withValueStep(1).withPreset('default', 60, 'Default value'),
],
onEvent: onEventSetLocalTime,
};
module.exports = definition;
The thermostat works correctly, but I think it does not synchronize the time.
In log of z2m I see this:
debug 2022-10-02 13:55:42: Received Zigbee message from 'Beok', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":80}' from endpoint 1 with groupID 0
debug 2022-10-02 13:55:42: No converter available for 'TS0601_thermostat' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":80}'
Is there any way to force a time synchronization? Or do you have to wait for the process that does it every hour.
I fixed the globalStore issue, please check again with https://gist.github.com/Koenkk/ee621c8913f698c432d05587ba12d9c7
I'm running zigbee2mqtt as a home assistant addon. Version 1.28.0-1.
I have added this https://gist.github.com/Koenkk/ee621c8913f698c432d05587ba12d9c7 as TS0601_thermostat.js in the same directory as configuration.yaml and added to configuration.yaml:
external_converters:
- TS0601_thermostat.js
I now get the following error when starting the addon.
[22:29:00] INFO: Preparing to start...
[22:29:01] INFO: Socat not enabled
[22:29:03] INFO: Starting Zigbee2MQTT...
/app/dist/util/externally-loaded.js:20
systemMode: valueConverterBasic.lookup({'heat': true, 'off': false}),
^
ReferenceError: valueConverterBasic is not defined
at /app/dist/util/externally-loaded.js:20:17
at Script.runInContext (node:vm:139:12)
at Script.runInNewContext (node:vm:144:17)
at Object.runInNewContext (node:vm:298:38)
at loadModuleFromText (/app/lib/util/utils.ts:148:8)
at loadModuleFromFile (/app/lib/util/utils.ts:155:12)
at Object.getExternalConvertersDefinitions (/app/lib/util/utils.ts:165:25)
at getExternalConvertersDefinitions.next (
I'm probably doing something obviously wrong - but am happy to test this fix as I have just received a couple of these thermostats.
I fixed the globalStore issue, please check again with https://gist.github.com/Koenkk/ee621c8913f698c432d05587ba12d9c7
globalStore issue fixed. But don't start correctly z2m.
Log error:
Using '/app/data' as data directory
/app/dist/util/externally-loaded.js:20
systemMode: valueConverterBasic.lookup({'heat': true, 'off': false}),
^
ReferenceError: valueConverterBasic is not defined
at /app/dist/util/externally-loaded.js:20:17
at Script.runInContext (node:vm:141:12)
at Script.runInNewContext (node:vm:146:17)
at Object.runInNewContext (node:vm:306:38)
at loadModuleFromText (/app/lib/util/utils.ts:148:8)
at loadModuleFromFile (/app/lib/util/utils.ts:155:12)
at Object.getExternalConvertersDefinitions (/app/lib/util/utils.ts:165:25)
at getExternalConvertersDefinitions.next (<anonymous>)
at new ExternalConverters (/app/lib/extension/externalConverters.ts:12:20)
at new Controller (/app/lib/controller.ts:84:58)
Solve with:
diff ext_converter.js ext_converter.js~
20,22c20,22
< systemMode: tuya.valueConverterBasic.lookup({'heat': true, 'off': false}),
< runningState: tuya.valueConverterBasic.lookup({'heat': true, 'idle': false}),
< preset: tuya.valueConverterBasic.lookup({'manual': 0, 'program': 1}),
---
> systemMode: valueConverterBasic.lookup({'heat': true, 'off': false}),
> runningState: valueConverterBasic.lookup({'heat': true, 'idle': false}),
> preset: valueConverterBasic.lookup({'manual': 0, 'program': 1}),
With that fix, yet start correctly z2m, but this converter fail with severals functions:
debug 2022-10-04 21:41:35: Received MQTT message on 'zigbee2mqtt/Beok/set' with data '{"brightness_state":"high"}'
error 2022-10-04 21:41:35: No converter available for 'brightness_state' ("high")
--
debug 2022-10-04 21:41:52: Received MQTT message on 'zigbee2mqtt/Beok/set' with data '{"heating_temperature_limit":35}'
error 2022-10-04 21:41:52: No converter available for 'heating_temperature_limit' (35)
--
debug 2022-10-04 21:41:56: Received MQTT message on 'zigbee2mqtt/Beok/set' with data '{"upper_temperature":60}'
error 2022-10-04 21:41:56: No converter available for 'upper_temperature' (60)
Time sync, don't work correctly.
debug 2022-10-04 21:59:48: Received Zigbee message from 'Beok', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":163}' from endpoint 1 with groupID 0
debug 2022-10-04 21:59:48: No converter available for 'TS0601_thermostat' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":163}'
**** const localTime = utcTime - ((new Date()).getTimezoneOffset() + 360) * 60;
Date: 2022-10-04 (Thuesday (2)) 22:01:18
Thermostat: (1) 00:01
https://www.dropbox.com/s/9lk9b9tmdt8sgoy/Screenshot_20221004-220121~2.png?dl=0
Any new ideas or changes to try?
More data about issue. Debug log for 'commandMcuSyncTime'
Zigbee2MQTT:debug 2022-10-04 23:59:18: Received Zigbee message from 'Beok', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":103}' from endpoint 1 with groupID 0
Zigbee2MQTT:debug 2022-10-04 23:59:18: No converter available for 'TS0601_thermostat' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":103}'
2022-10-04T21:59:18.595Z zigbee-herdsman:controller:endpoint DefaultResponse 0xa4c138c2a86d3a20/1 61184(36, {"sendWhen":"immediate","timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":1,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false})
2022-10-04T21:59:18.595Z zigbee-herdsman:adapter:zStack:adapter sendZclFrameToEndpointInternal 0xa4c138c2a86d3a20:45754/1 (0,0,1)
2022-10-04T21:59:18.595Z zigbee-herdsman:adapter:zStack:znp:SREQ --> AF - dataRequest - {"dstaddr":45754,"destendpoint":1,"srcendpoint":1,"clusterid":61184,"transid":27,"options":0,"radius":30,"len":5,"data":{"type":"Buffer","data":[24,125,11,36,0]}}
2022-10-04T21:59:18.595Z zigbee-herdsman:adapter:zStack:unpi:writer --> frame [254,15,36,1,186,178,1,1,0,239,27,0,30,5,24,125,11,36,0,135]
2022-10-04T21:59:18.595Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext []
2022-10-04T21:59:18.606Z zigbee-herdsman:adapter:zStack:unpi:parser <-- [254,1,100,1,0,100]
2022-10-04T21:59:18.606Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext [254,1,100,1,0,100]
2022-10-04T21:59:18.606Z zigbee-herdsman:adapter:zStack:unpi:parser --> parsed 1 - 3 - 4 - 1 - [0] - 100
2022-10-04T21:59:18.606Z zigbee-herdsman:adapter:zStack:znp:SRSP <-- AF - dataRequest - {"status":0}
2022-10-04T21:59:18.606Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext []
2022-10-04T21:59:18.609Z zigbee-herdsman:adapter:zStack:unpi:parser <-- [254,3,68,128,0,1,27,221]
2022-10-04T21:59:18.609Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext [254,3,68,128,0,1,27,221]
2022-10-04T21:59:18.609Z zigbee-herdsman:adapter:zStack:unpi:parser --> parsed 3 - 2 - 4 - 128 - [0,1,27] - 221
2022-10-04T21:59:18.609Z zigbee-herdsman:adapter:zStack:znp:AREQ <-- AF - dataConfirm - {"status":0,"endpoint":1,"transid":27}
2022-10-04T21:59:18.609Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext []
2022-10-04T21:59:28.563Z zigbee-herdsman:adapter:zStack:unpi:parser <-- [254,33,68,129,0,0,0,239,186,178,1,1,0,207,0,36,107,175,0,0,13,9,126,2,0,104,24,2,0,4,0,0,0,217,186,178,29,238]
2022-10-04T21:59:28.563Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext [254,33,68,129,0,0,0,239,186,178,1,1,0,207,0,36,107,175,0,0,13,9,126,2,0,104,24,2,0,4,0,0,0,217,186,178,29,238]
2022-10-04T21:59:28.563Z zigbee-herdsman:adapter:zStack:unpi:parser --> parsed 33 - 2 - 4 - 129 - [0,0,0,239,186,178,1,1,0,207,0,36,107,175,0,0,13,9,126,2,0,104,24,2,0,4,0,0,0,217,186,178,29] - 238
2022-10-04T21:59:28.563Z zigbee-herdsman:adapter:zStack:znp:AREQ <-- AF - incomingMsg - {"groupid":0,"clusterid":61184,"srcaddr":45754,"srcendpoint":1,"dstendpoint":1,"wasbroadcast":0,"linkquality":207,"securityuse":0,"timestamp":11496228,"transseqnumber":0,"len":13,"data":{"type":"Buffer","data":[9,126,2,0,104,24,2,0,4,0,0,0,217]}}
- Updated https://gist.github.com/Koenkk/ee621c8913f698c432d05587ba12d9c7
- How much hours is it ahead now? If it is e.g. 2 hours ahead in the line
const localTime = utcTime - ((new Date()).getTimezoneOffset() + 360) * 60;
change360
to480
The problem must be another. Regardless of the value you put in, the thermostat does not synchronize the time.
I am using this converter:
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const ota = require('zigbee-herdsman-converters/lib/ota');
const tuya = require('zigbee-herdsman-converters/lib/tuya');
const utils = require('zigbee-herdsman-converters/lib/utils');
const globalStore = require('zigbee-herdsman-converters/lib/store');
const e = exposes.presets;
const ea = exposes.access;
// set UTC and Local Time as total number of seconds from 00: 00: 00 on January 01, 1970
// force to update every device time every hour due to very poor clock
async function onEventBeokSetLocalTime(type, data, device) {
// FIXME: What actually nextLocalTimeUpdate/forceTimeUpdate do?
// I did not find any timers or something else where it was used.
// Actually, there are two ways to set time on TuYa MCU devices:
// 1. Respond to the `commandMcuSyncTime` event
// 2. Just send `mcuSyncTime` anytime (by 1-hour timer or something else)
const nextLocalTimeUpdate = globalStore.getValue(device, 'nextLocalTimeUpdate');
const forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < new Date().getTime();
if ((data.type === 'commandMcuSyncTime' && data.cluster === 'manuSpecificTuya') || forceTimeUpdate) {
globalStore.putValue(device, 'nextLocalTimeUpdate', new Date().getTime() + 300 * 1000);
try {
const utcTime = Math.round(((new Date()).getTime()) / 1000);
const localTime = utcTime - ((new Date()).getTimezoneOffset()+480) * 60;
const endpoint = device.getEndpoint(1);
const payload = {
payloadSize: 8,
payload: [
...convertDecimalValueTo4ByteHexArray(utcTime),
...convertDecimalValueTo4ByteHexArray(localTime),
],
};
await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
} catch (error) {
// endpoint.command can throw an error which needs to
// be caught or the zigbee-herdsman may crash
// Debug message is handled in the zigbee-herdsman
}
}
}
const fzLocal = {
x5h_thermostat: {
cluster: 'manuSpecificTuya',
type: ['commandDataResponse', 'commandDataReport'],
convert: (model, msg, publish, options, meta) => {
const dpValue = tuya.firstDpValue(msg, meta, 'x5h_thermostat');
const dp = dpValue.dp;
const value = tuya.getDataValue(dpValue);
switch (dp) {
case tuya.dataPoints.x5hState: {
return {system_mode: value ? 'heat' : 'off'};
}
case tuya.dataPoints.x5hWorkingStatus: {
return {running_state: value ? 'heat' : 'idle'};
}
case tuya.dataPoints.x5hSound: {
return {sound: value ? 'ON' : 'OFF'};
}
case tuya.dataPoints.x5hFrostProtection: {
return {frost_protection: value ? 'ON' : 'OFF'};
}
case tuya.dataPoints.x5hWorkingDaySetting: {
return {week: tuya.thermostatWeekFormat[value]};
}
case tuya.dataPoints.x5hFactoryReset: {
if (value) {
clearTimeout(globalStore.getValue(msg.endpoint, 'factoryResetTimer'));
const timer = setTimeout(() => publish({factory_reset: 'OFF'}), 60 * 1000);
globalStore.putValue(msg.endpoint, 'factoryResetTimer', timer);
meta.logger.info('The thermostat is resetting now. It will be available in 1 minute.');
}
return {factory_reset: value ? 'ON' : 'OFF'};
}
case tuya.dataPoints.x5hTempDiff: {
return {deadzone_temperature: parseFloat((value / 10).toFixed(1))};
}
case tuya.dataPoints.x5hProtectionTempLimit: {
return {heating_temp_limit: value};
}
case tuya.dataPoints.x5hBackplaneBrightness: {
const lookup = {0: 'off', 1: 'low', 2: 'medium', 3: 'high'};
if (value >= 0 && value <= 3) {
globalStore.putValue(msg.endpoint, 'brightnessState', value);
return {brightness_state: lookup[value]};
}
// Sometimes, for example on thermostat restart, it sends message like:
// {"dpValues":[{"data":{"data":[90],"type":"Buffer"},"datatype":4,"dp":104}
// It doesn't represent any brightness value and brightness remains the previous value
const lastValue = globalStore.getValue(msg.endpoint, 'brightnessState') || 1;
return {brightness_state: lookup[lastValue]};
}
case tuya.dataPoints.x5hWeeklyProcedure: {
const periods = [];
const periodSize = 4;
const periodsNumber = 8;
for (let i = 0; i < periodsNumber; i++) {
const hours = value[i * periodSize];
const minutes = value[i * periodSize + 1];
const tempHexArray = [value[i * periodSize + 2], value[i * periodSize + 3]];
const tempRaw = Buffer.from(tempHexArray).readUIntBE(0, tempHexArray.length);
const strHours = hours.toString().padStart(2, '0');
const strMinutes = minutes.toString().padStart(2, '0');
const temp = parseFloat((tempRaw / 10).toFixed(1));
periods.push(`${strHours}:${strMinutes}/${temp}`);
}
const schedule = periods.join(' ');
return {schedule};
}
case tuya.dataPoints.x5hChildLock: {
return {child_lock: value ? 'LOCK' : 'UNLOCK'};
}
case tuya.dataPoints.x5hSetTemp: {
const setpoint = parseFloat((value / 10).toFixed(1));
globalStore.putValue(msg.endpoint, 'currentHeatingSetpoint', setpoint);
return {current_heating_setpoint: setpoint};
}
case tuya.dataPoints.x5hSetTempCeiling: {
return {upper_temp: value};
}
case tuya.dataPoints.x5hCurrentTemp: {
const temperature = value & (1 << 15) ? value - (1 << 16) + 1 : value;
return {local_temperature: parseFloat((temperature / 10).toFixed(1))};
}
case tuya.dataPoints.x5hTempCorrection: {
return {local_temperature_calibration: parseFloat((value / 10).toFixed(1))};
}
case tuya.dataPoints.x5hMode: {
const lookup = {0: 'manual', 1: 'program'};
return {preset: lookup[value]};
}
case tuya.dataPoints.x5hSensorSelection: {
const lookup = {0: 'internal', 1: 'external', 2: 'both'};
return {sensor: lookup[value]};
}
case tuya.dataPoints.x5hOutputReverse: {
return {output_reverse: value};
}
default: {
meta.logger.warn(`fromZigbee:x5h_thermostat: Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`);
}
}
},
},
}
const tzLocal = {
x5h_thermostat: {
key: ['system_mode', 'current_heating_setpoint', 'sensor', 'brightness_state', 'sound', 'frost_protection', 'week', 'factory_reset',
'local_temperature_calibration', 'heating_temp_limit', 'deadzone_temperature', 'upper_temp', 'preset', 'child_lock',
'schedule'],
convertSet: async (entity, key, value, meta) => {
switch (key) {
case 'system_mode':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hState, value === 'heat');
break;
case 'preset': {
value = value.toLowerCase();
const lookup = {manual: 0, program: 1};
utils.validateValue(value, Object.keys(lookup));
value = lookup[value];
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hMode, value);
break;
}
case 'upper_temp':
if (value >= 35 && value <= 95) {
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTempCeiling, value);
const setpoint = globalStore.getValue(entity, 'currentHeatingSetpoint', 20);
const setpointRaw = Math.round(setpoint * 10);
await new Promise((r) => setTimeout(r, 500));
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTemp, setpointRaw);
} else {
throw new Error('Supported values are in range [35, 95]');
}
break;
case 'deadzone_temperature':
if (value >= 0.5 && value <= 9.5) {
value = Math.round(value * 10);
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hTempDiff, value);
} else {
throw new Error('Supported values are in range [0.5, 9.5]');
}
break;
case 'heating_temp_limit':
if (value >= 5 && value <= 60) {
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hProtectionTempLimit, value);
} else {
throw new Error('Supported values are in range [5, 60]');
}
break;
case 'local_temperature_calibration':
if (value >= -9.9 && value <= 9.9) {
value = Math.round(value * 10);
if (value < 0) {
value = 0xFFFFFFFF + value + 1;
}
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hTempCorrection, value);
} else {
throw new Error('Supported values are in range [-9.9, 9.9]');
}
break;
case 'factory_reset':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hFactoryReset, value === 'ON');
break;
case 'week':
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hWorkingDaySetting,
utils.getKey(tuya.thermostatWeekFormat, value, value, Number));
break;
case 'frost_protection':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hFrostProtection, value === 'ON');
break;
case 'sound':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hSound, value === 'ON');
break;
case 'brightness_state': {
value = value.toLowerCase();
const lookup = {off: 0, low: 1, medium: 2, high: 3};
utils.validateValue(value, Object.keys(lookup));
value = lookup[value];
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hBackplaneBrightness, value);
break;
}
case 'sensor': {
value = value.toLowerCase();
const lookup = {'internal': 0, 'external': 1, 'both': 2};
utils.validateValue(value, Object.keys(lookup));
value = lookup[value];
await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hSensorSelection, value);
break;
}
case 'current_heating_setpoint':
if (value >= 5 && value <= 60) {
value = Math.round(value * 10);
await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTemp, value);
} else {
throw new Error(`Unsupported value: ${value}`);
}
break;
case 'child_lock':
await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hChildLock, value === 'LOCK');
break;
case 'schedule': {
const periods = value.split(' ');
const periodsNumber = 8;
const payload = [];
for (let i = 0; i < periodsNumber; i++) {
const timeTemp = periods[i].split('/');
const hm = timeTemp[0].split(':', 2);
const h = parseInt(hm[0]);
const m = parseInt(hm[1]);
const temp = parseFloat(timeTemp[1]);
if (h < 0 || h >= 24 || m < 0 || m >= 60 || temp < 5 || temp > 60) {
throw new Error('Invalid hour, minute or temperature of: ' + periods[i]);
}
const tempHexArray = tuya.convertDecimalValueTo2ByteHexArray(Math.round(temp * 10));
// 1 byte for hour, 1 byte for minutes, 2 bytes for temperature
payload.push(h, m, ...tempHexArray);
}
await tuya.sendDataPointRaw(entity, tuya.dataPoints.x5hWeeklyProcedure, payload);
break;
}
default:
break;
}
},
},
}
const definition = {
fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_2ekuz3dz'}],
model: 'X5H-GB-B*',
vendor: 'TuYa',
description: 'Wall-mount thermostat',
fromZigbee: [fz.ignore_basic_report, fzLocal.x5h_thermostat],
toZigbee: [tzLocal.x5h_thermostat],
whiteLabel: [{vendor: 'Beok', model: 'TGR85-ZB'}],
exposes: [
exposes.climate().withSetpoint('current_heating_setpoint', 5, 60, 0.5, ea.STATE_SET)
.withLocalTemperature(ea.STATE).withLocalTemperatureCalibration(-9.9, 9.9, 0.1, ea.STATE_SET)
.withSystemMode(['off', 'heat'], ea.STATE_SET).withRunningState(['idle', 'heat'], ea.STATE)
.withPreset(['manual', 'program']).withSensor(['internal', 'external', 'both'], ea.STATE_SET),
exposes.text('schedule', ea.STATE_SET).withDescription('There are 8 periods in the schedule in total. ' +
'6 for workdays and 2 for holidays. It should be set in the following format for each of the periods: ' +
'`hours:minutes/temperature`. All periods should be set at once and delimited by the space symbol. ' +
'For example: `06:00/20.5 08:00/15 11:30/15 13:30/15 17:00/22 22:00/15 06:00/20 22:00/15`. ' +
'The thermostat doesn\'t report the schedule by itself even if you change it manually from device'),
e.child_lock(), e.week(),
exposes.enum('brightness_state', ea.STATE_SET, ['off', 'low', 'medium', 'high'])
.withDescription('Screen brightness'),
exposes.binary('sound', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Switches beep sound when interacting with thermostat'),
exposes.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Antifreeze function'),
exposes.binary('factory_reset', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Resets all settings to default. Doesn\'t unpair device.'),
exposes.numeric('heating_temp_limit', ea.STATE_SET).withUnit('°C').withValueMax(60)
.withValueMin(5).withValueStep(1).withPreset('default', 35, 'Default value')
.withDescription('Heating temperature limit'),
exposes.numeric('deadzone_temperature', ea.STATE_SET).withUnit('°C').withValueMax(9.5)
.withValueMin(0.5).withValueStep(0.5).withPreset('default', 1, 'Default value')
.withDescription('The delta between local_temperature and current_heating_setpoint to trigger Heat'),
exposes.numeric('upper_temp', ea.STATE_SET).withUnit('°C').withValueMax(95)
.withValueMin(35).withValueStep(1).withPreset('default', 60, 'Default value'),
],
onEvent: onEventBeokSetLocalTime,
};
module.exports = definition;
With it, all the functions of the thermostat are correct, except for the synchronization problem. Which makes it impossible to run scheduled tasks.
I put the value of 480 instead of 360, but it did nothing when it came time to sync.
Before message commandMcuSyncTime ( https://www.dropbox.com/s/gy8qhcgs15rzmq4/Screenshot_20221005-175323~2.png?dl=0 ):
Log message
debug 2022-10-05 17:54:36: Received Zigbee message from 'Beok', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":144}' from endpoint 1 with groupID 0
debug 2022-10-05 17:54:36: No converter available for 'X5H-GB-B*' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":144}'
Debug message:
Zigbee2MQTT:debug 2022-10-05 17:54:36: Received Zigbee message from 'Beok', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":144}' from endpoint 1 with groupID 0
Zigbee2MQTT:debug 2022-10-05 17:54:36: No converter available for 'X5H-GB-B*' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":144}'
2022-10-05T15:54:36.706Z zigbee-herdsman:controller:endpoint DefaultResponse 0xa4c138c2a86d3a20/1 61184(36, {"sendWhen":"immediate","timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":1,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false})
2022-10-05T15:54:36.706Z zigbee-herdsman:adapter:zStack:adapter sendZclFrameToEndpointInternal 0xa4c138c2a86d3a20:45754/1 (0,0,1)
2022-10-05T15:54:36.706Z zigbee-herdsman:adapter:zStack:znp:SREQ --> AF - dataRequest - {"dstaddr":45754,"destendpoint":1,"srcendpoint":1,"clusterid":61184,"transid":20,"options":0,"radius":30,"len":5,"data":{"type":"Buffer","data":[24,68,11,36,0]}}
2022-10-05T15:54:36.706Z zigbee-herdsman:adapter:zStack:unpi:writer --> frame [254,15,36,1,186,178,1,1,0,239,20,0,30,5,24,68,11,36,0,177]
2022-10-05T15:54:36.706Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext []
2022-10-05T15:54:36.718Z zigbee-herdsman:adapter:zStack:unpi:parser <-- [254,1,100,1,0,100]
2022-10-05T15:54:36.718Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext [254,1,100,1,0,100]
2022-10-05T15:54:36.718Z zigbee-herdsman:adapter:zStack:unpi:parser --> parsed 1 - 3 - 4 - 1 - [0] - 100
2022-10-05T15:54:36.718Z zigbee-herdsman:adapter:zStack:znp:SRSP <-- AF - dataRequest - {"status":0}
2022-10-05T15:54:36.719Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext []
2022-10-05T15:54:36.724Z zigbee-herdsman:adapter:zStack:unpi:parser <-- [254,3,68,128,0,1,20,210]
2022-10-05T15:54:36.724Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext [254,3,68,128,0,1,20,210]
2022-10-05T15:54:36.724Z zigbee-herdsman:adapter:zStack:unpi:parser --> parsed 3 - 2 - 4 - 128 - [0,1,20] - 210
2022-10-05T15:54:36.725Z zigbee-herdsman:adapter:zStack:znp:AREQ <-- AF - dataConfirm - {"status":0,"endpoint":1,"transid":20}
2022-10-05T15:54:36.725Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext []
2022-10-05T15:54:44.672Z zigbee-herdsman:adapter:zStack:unpi:parser <-- [254,33,68,129,0,0,0,239,186,178,1,1,0,207,0,226,110,22,0,0,13,9,69,2,0,145,24,2,0,4,0,0,0,209,186,178,29,94]
2022-10-05T15:54:44.672Z zigbee-herdsman:adapter:zStack:unpi:parser --- parseNext [254,33,68,129,0,0,0,239,186,178,1,1,0,207,0,226,110,22,0,0,13,9,69,2,0,145,24,2,0,4,0,0,0,209,186,178,29,94]
2022-10-05T15:54:44.673Z zigbee-herdsman:adapter:zStack:unpi:parser --> parsed 33 - 2 - 4 - 129 - [0,0,0,239,186,178,1,1,0,207,0,226,110,22,0,0,13,9,69,2,0,145,24,2,0,4,0,0,0,209,186,178,29] - 94
2022-10-05T15:54:44.673Z zigbee-herdsman:adapter:zStack:znp:AREQ <-- AF - incomingMsg - {"groupid":0,"clusterid":61184,"srcaddr":45754,"srcendpoint":1,"dstendpoint":1,"wasbroadcast":0,"linkquality":207,"securityuse":0,"timestamp":1470178,"transseqnumber":0,"len":13,"data":{"type":"Buffer","data":[9,69,2,0,145,24,2,0,4,0,0,0,209]}}
After message commandMcuSyncTime are processed: ( https://www.dropbox.com/s/vtn8d1zm9pxex5p/Screenshot_20221005-175453~2.png?dl=0 )
Both the day of the week on the thermostat (1), which should be (3), and the time, have not changed.
@Koenkk , Is there a way to inject a packet with, for example, Node Red, to send the commandMcuSyncTime message and not have to wait for it to be sent, which occurs every hour?
Updated https://gist.github.com/Koenkk/ee621c8913f698c432d05587ba12d9c7 , a time update will now be send at z2m startup. To verify check if you see SENDING TIME UPDATE
in your log
Updated https://gist.github.com/Koenkk/ee621c8913f698c432d05587ba12d9c7 , a time update will now be send at z2m startup. To verify check if you see
SENDING TIME UPDATE
in your log
At z2m startup, don't see the message "SENDING TIME UPDATE". Waiting for "commandMcuSyncTime" message.
Problem detected, in try-catch, the call to function ...convertDecimalValueTo4ByteHexArray(), not work. I change ...convertDecimalValueTo4ByteHexArray by tuya.convertDecimalValueTo4ByteHexArray.
And when restart z2m, yet I see "SENDING TIME UPDATE"
Debug log:
Zigbee2MQTT:info 2022-10-05 23:58:51: Connected to MQTT server
Zigbee2MQTT:info 2022-10-05 23:58:51: MQTT publish: topic 'zigbee2mqtt/bridge/state', payload 'online'
=== ENTERING FUNCTION TO TIME UPDATE ===
data.type: undefined , data.cluster: undefined
Date.getTime: 1665007131774
nextLocalTimeUpdate: undefined , forceTimeUpdate: true
=== SENDING TIME UPDATE ===
2022-10-05T21:58:51.775Z zigbee-herdsman:controller:endpoint Command 0xa4c138c2a86d3a20/1 manuSpecificTuya.mcuSyncTime({"payloadSize":8,"payload":[[99,61,254,28],[99,61,197,220]]}, {"sendWhen":"immediate","timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":false,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false})
My Timesyn function, with debug options are:
// set UTC and Local Time as total number of seconds from 00: 00: 00 on January 01, 1970
// force to update every device time every hour due to very poor clock
async function onEventSetLocalTime(type, data, device) {
// FIXME: What actually nextLocalTimeUpdate/forceTimeUpdate do?
// I did not find any timers or something else where it was used.
// Actually, there are two ways to set time on TuYa MCU devices:
// 1. Respond to the `commandMcuSyncTime` event
// 2. Just send `mcuSyncTime` anytime (by 1-hour timer or something else)
console.log('\n\nENTERING FUNCTION TO TIME UPDATE\n\n');
const nextLocalTimeUpdate = globalStore.getValue(device, 'nextLocalTimeUpdate');
const forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < new Date().getTime();
console.log('\ndata.typ: ' + data.type + ', data.cluster: ' + data.cluster + ', forceTimeUpdate: ' + forceTimeUpdate + '\n\n')
if ((data.type === 'commandMcuSyncTime' && data.cluster === 'manuSpecificTuya') || forceTimeUpdate) {
globalStore.putValue(device, 'nextLocalTimeUpdate', new Date().getTime() + 3600 * 1000);
try {
const utcTime = Math.round(((new Date()).getTime()) / 1000);
const localTime = utcTime - ((new Date()).getTimezoneOffset() - 240) * 60;
const endpoint = device.getEndpoint(1);
const payload = {
payloadSize: 8,
payload: [
tuya.convertDecimalValueTo4ByteHexArray(utcTime),
tuya.convertDecimalValueTo4ByteHexArray(localTime),
],
};
console.log('\n\nSENDING TIME UPDATE\n\n');
await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
} catch (error) {
// endpoint.command can throw an error which needs to
// be caught or the zigbee-herdsman may crash
// Debug message is handled in the zigbee-herdsman
}
}
}
The mcuSyncTime
message is already sent with its proper payload.
But the thermostat is still out of sync. Ignore the mcuSyncTime
message.
The same payload is not correct.
That I can continue investigating @Koenkk ?
@gargarse the time sync is requested by the thermostat every time you switch its mains power supply off and then on.
BTW, no success with the time synchronization for this BEOK clone in another platform too.. :( The time sync function works fine on AVATTO thermostat, on multiple Tuya T/H sensors with LCD display that shows the date and time - but not on this device.
The weirdest thing is that I can't set the correct time on _TZE200_2ekuz3dz using Tuya own Zigbee gateways as well. I have 2 different Tuya gateways, both fail to set the correct time. The time is always 8 hours ahead of GMT.
@gargarse does the timeshift in hours change when you modify the 240
value in const localTime = utcTime - ((new Date()).getTimezoneOffset() - 240) * 60;
to e.g. 480
?
@gargarse. If you are getting the following in your logs:
> `debug 2022-10-05 17:54:36: Received Zigbee message from 'Beok', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":144}' from endpoint 1 with groupID 0
> debug 2022-10-05 17:54:36: No converter available for 'X5H-GB-B*' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":144}'
Doesn't it mean that there is no code handling the 'commandMcuSyncTime' request from the thermostat? So, the function onEventSetLocalTime will never get called?
I added the following, referenced in the fromZigbee in the definition as fzLocal.tuya_set_time.
`tuya_set_time: {
cluster: 'manuSpecificTuya',
type: ['commandMcuSyncTime'],
convert: (model, msg, publish, options, meta) => {
meta.logger.warn('*** tuya_set_time called ***');
},
},`
This now gets called and I don't get an error in the logs for no converter available - but I don't know how to send the time setting packet - my coding skills aren't up to it.
Any thoughts?
`
@gargarse¿Cambia el cambio de hora en horas cuando modifica el
240
valor en,const localTime = utcTime - ((new Date()).getTimezoneOffset() - 240) * 60;
por ejemplo480
,?
No. I have tried changing that value several times, but the thermostat ignores the changes. It seems to ignore the message commandMcuSyncTime
Thats strange, maybe the firmware is bugged.
The weirdest thing is that I can't set the correct time on _TZE200_2ekuz3dz using Tuya own Zigbee gateways as well. I have 2 different Tuya gateways, both fail to set the correct time. The time is always 8 hours ahead of GMT.
If it doesn't even work with the official gateway there is little chance we can make it works with z2m.
Definitely, the thermostat is a "shit".
Not even with a GW Tuya do they work correctly.
With Smart Life App: https://www.dropbox.com/s/rac3jvj50z2v2sg/Screenshot_20221007-205654~2.png?dl=0
And the time in thermostat: https://www.dropbox.com/s/1jr7wwu945e43he/IMG_20221007_205528.jpg?dl=0
Thank you all for the help offered.