zigbee-herdsman-converters 14.0.580 → 14.0.583

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1094,9 +1094,9 @@ const converters = {
1094
1094
  cluster: 'genScenes',
1095
1095
  type: 'commandStore',
1096
1096
  convert: (model, msg, publish, options, meta) => {
1097
- if (utils.hasAlreadyProcessedMessage(msg)) return;
1098
- const payload = {action: utils.postfixWithEndpointName(`store_${msg.data.sceneid}`, msg, model, meta)};
1099
- utils.addActionGroup(payload, msg, model);
1097
+ if (hasAlreadyProcessedMessage(msg)) return;
1098
+ const payload = {action: postfixWithEndpointName(`store_${msg.data.sceneid}`, msg, model, meta)};
1099
+ addActionGroup(payload, msg, model);
1100
1100
  return payload;
1101
1101
  },
1102
1102
  },
@@ -2807,21 +2807,21 @@ const converters = {
2807
2807
  if (msg.data.hasOwnProperty('tuyaMovingState')) {
2808
2808
  const value = msg.data['tuyaMovingState'];
2809
2809
  const movingLookup = {0: 'UP', 1: 'STOP', 2: 'DOWN'};
2810
- result.moving = movingLookup[value];
2810
+ result[postfixWithEndpointName('moving', msg, model, meta)] = movingLookup[value];
2811
2811
  }
2812
2812
  if (msg.data.hasOwnProperty('tuyaCalibration')) {
2813
2813
  const value = msg.data['tuyaCalibration'];
2814
2814
  const calibrationLookup = {0: 'ON', 1: 'OFF'};
2815
- result.calibration = calibrationLookup[value];
2815
+ result[postfixWithEndpointName('calibration', msg, model, meta)] = calibrationLookup[value];
2816
2816
  }
2817
2817
  if (msg.data.hasOwnProperty('tuyaMotorReversal')) {
2818
2818
  const value = msg.data['tuyaMotorReversal'];
2819
2819
  const reversalLookup = {0: 'OFF', 1: 'ON'};
2820
- result.motor_reversal = reversalLookup[value];
2820
+ result[postfixWithEndpointName('motor_reversal', msg, model, meta)] = reversalLookup[value];
2821
2821
  }
2822
2822
  if (msg.data.hasOwnProperty('moesCalibrationTime')) {
2823
2823
  const value = parseFloat(msg.data['moesCalibrationTime']) / 10.0;
2824
- result.calibration_time = value;
2824
+ result[postfixWithEndpointName('calibration_time', msg, model, meta)] = value;
2825
2825
  }
2826
2826
  return result;
2827
2827
  },
package/devices/adeo.js CHANGED
@@ -36,6 +36,13 @@ module.exports = [
36
36
  description: 'ENKI LEXMAN E27 LED white',
37
37
  extend: extend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 454]}),
38
38
  },
39
+ {
40
+ zigbeeModel: ['ZBEK-6'],
41
+ model: 'IG-CDZB2AG009RA-MZN-01',
42
+ vendor: 'ADEO',
43
+ description: 'ENKI LEXMAN E27 Led white bulb',
44
+ extend: extend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 454]}),
45
+ },
39
46
  {
40
47
  zigbeeModel: ['ZBEK-4'],
41
48
  model: 'IM-CDZDGAAA0005KA_MAN',
@@ -1,8 +1,9 @@
1
1
  const exposes = require('../lib/exposes');
2
- const fz = {...require('../converters/fromZigbee'), legacy: require('../lib/legacy').fromZigbee};
2
+ const fz = require('../converters/fromZigbee');
3
3
  const tz = require('../converters/toZigbee');
4
4
  const reporting = require('../lib/reporting');
5
5
  const e = exposes.presets;
6
+ const ota = require('../lib/ota');
6
7
 
7
8
  module.exports = [
8
9
  {
@@ -11,13 +12,16 @@ module.exports = [
11
12
  vendor: 'Home Control AS',
12
13
  description: 'Heimgard (Wattle) door lock pro',
13
14
  fromZigbee: [fz.lock, fz.battery],
14
- toZigbee: [tz.lock],
15
+ toZigbee: [tz.lock, tz.lock_auto_relock_time, tz.lock_sound_volume],
16
+ ota: ota.zigbeeOTA,
15
17
  configure: async (device, coordinatorEndpoint, logger) => {
16
18
  const endpoint = device.getEndpoint(1);
17
19
  await reporting.bind(endpoint, coordinatorEndpoint, ['closuresDoorLock', 'genPowerCfg']);
18
20
  await reporting.lockState(endpoint);
19
21
  await reporting.batteryPercentageRemaining(endpoint);
22
+ await endpoint.read('closuresDoorLock', ['lockState', 'soundVolume']);
20
23
  },
21
- exposes: [e.lock(), e.battery()],
24
+ exposes: [
25
+ e.lock(), e.battery(), e.auto_relock_time().withValueMin(0).withValueMax(3600), e.sound_volume()],
22
26
  },
23
27
  ];
package/devices/lellki.js CHANGED
@@ -115,6 +115,7 @@ module.exports = [
115
115
  toZigbee: [tz.on_off, tz.tuya_switch_power_outage_memory],
116
116
  configure: async (device, coordinatorEndpoint, logger) => {
117
117
  const endpoint = device.getEndpoint(1);
118
+ await endpoint.read('genBasic', ['manufacturerName', 'zclVersion', 'appVersion', 'modelId', 'powerSource', 0xfffe]);
118
119
  await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']);
119
120
  endpoint.saveClusterAttributeKeyValue('haElectricalMeasurement', {acCurrentDivisor: 1000, acCurrentMultiplier: 1});
120
121
  endpoint.saveClusterAttributeKeyValue('seMetering', {divisor: 100, multiplier: 1});
@@ -82,7 +82,8 @@ module.exports = [
82
82
  return {'left': 1, 'right': 2};
83
83
  },
84
84
  exposes: [
85
- exposes.enum('moving', ea.STATE, ['UP', 'STOP', 'DOWN']),
85
+ exposes.enum('moving', ea.STATE, ['UP', 'STOP', 'DOWN']).withEndpoint('left'),
86
+ exposes.enum('moving', ea.STATE, ['UP', 'STOP', 'DOWN']).withEndpoint('right'),
86
87
  exposes.numeric('calibration_time', ea.STATE).withUnit('S').withDescription('Calibration time'),
87
88
  e.cover_position().withEndpoint('left'), exposes.binary('calibration', ea.ALL, 'ON', 'OFF')
88
89
  .withEndpoint('left'), exposes.binary('motor_reversal', ea.ALL, 'ON', 'OFF').withEndpoint('left'),
package/devices/moes.js CHANGED
@@ -247,7 +247,7 @@ module.exports = [
247
247
  tz.moesS_thermostat_min_temperature, tz.moesS_thermostat_moesSecoMode,
248
248
  tz.moesS_thermostat_system_mode, tz.moesS_thermostat_schedule_programming],
249
249
  exposes: [
250
- e.battery(), e.child_lock(), e.eco_mode(), e.eco_temperature(), e.max_temperature(), e.min_temperature(),
250
+ e.battery(), e.child_lock(), e.eco_mode(), e.eco_temperature(), e.max_temperature().withValueMax(45), e.min_temperature(),
251
251
  e.valve_state(), e.position(), e.window_detection(),
252
252
  exposes.binary('window', ea.STATE, 'CLOSED', 'OPEN').withDescription('Window status closed or open '),
253
253
  exposes.climate()
package/devices/namron.js CHANGED
@@ -28,6 +28,19 @@ module.exports = [
28
28
  await reporting.onOff(endpoint);
29
29
  },
30
30
  },
31
+ {
32
+ zigbeeModel: ['4512708'],
33
+ model: '4512708',
34
+ vendor: 'Namron',
35
+ description: 'Zigbee LED dimmer',
36
+ extend: extend.light_onoff_brightness({noConfigure: true}),
37
+ configure: async (device, coordinatorEndpoint, logger) => {
38
+ await extend.light_onoff_brightness().configure(device, coordinatorEndpoint, logger);
39
+ const endpoint = device.getEndpoint(1);
40
+ await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'genLevelCtrl']);
41
+ await reporting.onOff(endpoint);
42
+ },
43
+ },
31
44
  {
32
45
  zigbeeModel: ['4512733'],
33
46
  model: '4512733',
package/devices/orvibo.js CHANGED
@@ -5,7 +5,32 @@ const reporting = require('../lib/reporting');
5
5
  const extend = require('../lib/extend');
6
6
  const e = exposes.presets;
7
7
 
8
+ const tzLocal = {
9
+ DD10Z_brightness: {
10
+ key: ['brightness'],
11
+ options: [exposes.options.transition()],
12
+ convertSet: async (entity, key, value, meta) => {
13
+ // Device doesn't support moveToLevelWithOnOff therefore this converter is needed.
14
+ await entity.command('genLevelCtrl', 'moveToLevel', {level: Number(value), transtime: 0}, {disableDefaultResponse: true});
15
+ return {state: {brightness: value}};
16
+ },
17
+ convertGet: async (entity, key, meta) => {
18
+ await entity.read('genLevelCtrl', ['currentLevel']);
19
+ },
20
+ },
21
+ };
22
+
8
23
  module.exports = [
24
+ {
25
+ zigbeeModel: ['ccb9f56837ab41dcad366fb1452096b6'],
26
+ model: 'DD10Z',
27
+ vendor: 'ORVIBO',
28
+ description: 'Smart spotlight',
29
+ // https://github.com/Koenkk/zigbee2mqtt/issues/13123#issuecomment-1198793749
30
+ meta: {disableDefaultResponse: true},
31
+ toZigbee: [tz.on_off, tzLocal.DD10Z_brightness, tz.light_colortemp, tz.effect],
32
+ extend: extend.light_onoff_brightness_colortemp({colorTempRange: [153, 370], disableColorTempStartup: true}),
33
+ },
9
34
  {
10
35
  zigbeeModel: ['4a33f5ea766a4c96a962b371ffde9943'],
11
36
  model: 'DS20Z07B',
@@ -1866,7 +1866,7 @@ module.exports = [
1866
1866
  ota: ota.zigbeeOTA,
1867
1867
  },
1868
1868
  {
1869
- zigbeeModel: ['LOM002', 'LOM004'],
1869
+ zigbeeModel: ['LOM002', 'LOM004', 'LOM010'],
1870
1870
  model: '046677552343',
1871
1871
  vendor: 'Philips',
1872
1872
  description: 'Hue smart plug bluetooth',
@@ -2103,6 +2103,20 @@ module.exports = [
2103
2103
  description: 'Hue White Ambinance G25 E26 Edison Filament Globe',
2104
2104
  extend: hueExtend.light_onoff_brightness_colortemp({colorTempRange: [222, 454]}),
2105
2105
  },
2106
+ {
2107
+ zigbeeModel: ['LWE005'],
2108
+ model: '9290024796',
2109
+ vendor: 'Philips',
2110
+ description: 'Hue Filament White E12',
2111
+ extend: hueExtend.light_onoff_brightness(),
2112
+ },
2113
+ {
2114
+ zigbeeModel: ['LTA007'],
2115
+ model: '9290024783',
2116
+ vendor: 'Philips',
2117
+ description: 'Hue Filament White Ambiance A60/E27 Bluetooth',
2118
+ extend: hueExtend.light_onoff_brightness_colortemp({colorTempRange: [222, 454]}),
2119
+ },
2106
2120
  {
2107
2121
  zigbeeModel: ['LWV002'],
2108
2122
  model: '046677551780',
package/devices/robb.js CHANGED
@@ -248,4 +248,30 @@ module.exports = [
248
248
  'recall_*', 'on', 'off', 'toggle', 'brightness_stop', 'brightness_move_up', 'brightness_move_down',
249
249
  'color_loop_set', 'enhanced_move_to_hue_and_saturation', 'hue_stop'])],
250
250
  },
251
+ {
252
+ zigbeeModel: ['ROB_200-026-0'],
253
+ model: 'ROB_200-026-0',
254
+ vendor: 'ROBB',
255
+ description: '2-gang in-wall switch',
256
+ fromZigbee: [fz.on_off, fz.electrical_measurement, fz.metering, fz.power_on_behavior],
257
+ toZigbee: [tz.on_off, tz.power_on_behavior, tz.electrical_measurement_power],
258
+ exposes: [e.switch().withEndpoint('l1'), e.switch().withEndpoint('l2'), e.energy()],
259
+ endpoint: (device) => {
260
+ return {'l1': 1, 'l2': 2};
261
+ },
262
+ meta: {multiEndpoint: true},
263
+ configure: async (device, coordinatorEndpoint, logger) => {
264
+ const endpoint1 = device.getEndpoint(1);
265
+ const endpoint2 = device.getEndpoint(2);
266
+ await reporting.bind(endpoint1, coordinatorEndpoint, ['genOnOff']);
267
+ await reporting.bind(endpoint2, coordinatorEndpoint, ['genOnOff']);
268
+ await reporting.onOff(endpoint1);
269
+ await reporting.onOff(endpoint2);
270
+ await endpoint1.read('haElectricalMeasurement', ['acPowerMultiplier', 'acPowerDivisor']);
271
+ await reporting.bind(endpoint1, coordinatorEndpoint, ['haElectricalMeasurement', 'seMetering']);
272
+ await reporting.activePower(endpoint1);
273
+ await reporting.readMeteringMultiplierDivisor(endpoint1);
274
+ await reporting.currentSummDelivered(endpoint1, {min: 60, change: 1});
275
+ },
276
+ },
251
277
  ];
package/devices/tuya.js CHANGED
@@ -13,6 +13,7 @@ const zosung = require('../lib/zosung');
13
13
  const fzZosung = zosung.fzZosung;
14
14
  const tzZosung = zosung.tzZosung;
15
15
  const ez = zosung.presetsZosung;
16
+ const globalStore = require('../lib/store');
16
17
 
17
18
  const TS011Fplugs = ['_TZ3000_5f43h46b', '_TZ3000_cphmq0q7', '_TZ3000_dpo1ysak', '_TZ3000_ew3ldmgx', '_TZ3000_gjnozsaz',
18
19
  '_TZ3000_jvzvulen', '_TZ3000_mraovvmm', '_TZ3000_nfnmi125', '_TZ3000_ps3dmato', '_TZ3000_w0qqde0g', '_TZ3000_u5u4cakc',
@@ -128,6 +129,131 @@ const tzLocal = {
128
129
  }
129
130
  },
130
131
  },
132
+ x5h_thermostat: {
133
+ key: ['system_mode', 'current_heating_setpoint', 'sensor', 'brightness_state', 'sound', 'frost_protection', 'week', 'factory_reset',
134
+ 'local_temperature_calibration', 'heating_temp_limit', 'deadzone_temperature', 'upper_temp', 'preset', 'child_lock',
135
+ 'schedule'],
136
+ convertSet: async (entity, key, value, meta) => {
137
+ switch (key) {
138
+ case 'system_mode':
139
+ await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hState, value === 'heat');
140
+ break;
141
+ case 'preset': {
142
+ value = value.toLowerCase();
143
+ const lookup = {manual: 0, program: 1};
144
+ utils.validateValue(value, Object.keys(lookup));
145
+ value = lookup[value];
146
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hMode, value);
147
+ break;
148
+ }
149
+ case 'upper_temp':
150
+ if (value >= 35 && value <= 95) {
151
+ await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTempCeiling, value);
152
+ const setpoint = globalStore.getValue(entity, 'currentHeatingSetpoint', 20);
153
+ const setpointRaw = Math.round(setpoint * 10);
154
+ await new Promise((r) => setTimeout(r, 500));
155
+ await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTemp, setpointRaw);
156
+ } else {
157
+ throw new Error('Supported values are in range [35, 95]');
158
+ }
159
+ break;
160
+ case 'deadzone_temperature':
161
+ if (value >= 0.5 && value <= 9.5) {
162
+ value = Math.round(value * 10);
163
+ await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hTempDiff, value);
164
+ } else {
165
+ throw new Error('Supported values are in range [0.5, 9.5]');
166
+ }
167
+ break;
168
+ case 'heating_temp_limit':
169
+ if (value >= 5 && value <= 60) {
170
+ await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hProtectionTempLimit, value);
171
+ } else {
172
+ throw new Error('Supported values are in range [5, 60]');
173
+ }
174
+ break;
175
+ case 'local_temperature_calibration':
176
+ if (value >= -9.9 && value <= 9.9) {
177
+ value = Math.round(value * 10);
178
+
179
+ if (value < 0) {
180
+ value = 0xFFFFFFFF + value + 1;
181
+ }
182
+
183
+ await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hTempCorrection, value);
184
+ } else {
185
+ throw new Error('Supported values are in range [-9.9, 9.9]');
186
+ }
187
+ break;
188
+ case 'factory_reset':
189
+ await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hFactoryReset, value === 'ON');
190
+ break;
191
+ case 'week':
192
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hWorkingDaySetting,
193
+ utils.getKey(tuya.thermostatWeekFormat, value, value, Number));
194
+ break;
195
+ case 'frost_protection':
196
+ await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hFrostProtection, value === 'ON');
197
+ break;
198
+ case 'sound':
199
+ await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hSound, value === 'ON');
200
+ break;
201
+ case 'brightness_state': {
202
+ value = value.toLowerCase();
203
+ const lookup = {off: 0, low: 1, medium: 2, high: 3};
204
+ utils.validateValue(value, Object.keys(lookup));
205
+ value = lookup[value];
206
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hBackplaneBrightness, value);
207
+ break;
208
+ }
209
+ case 'sensor': {
210
+ value = value.toLowerCase();
211
+ const lookup = {'internal': 0, 'external': 1, 'both': 2};
212
+ utils.validateValue(value, Object.keys(lookup));
213
+ value = lookup[value];
214
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.x5hSensorSelection, value);
215
+ break;
216
+ }
217
+ case 'current_heating_setpoint':
218
+ if (value >= 5 && value <= 60) {
219
+ value = Math.round(value * 10);
220
+ await tuya.sendDataPointValue(entity, tuya.dataPoints.x5hSetTemp, value);
221
+ } else {
222
+ throw new Error(`Unsupported value: ${value}`);
223
+ }
224
+ break;
225
+ case 'child_lock':
226
+ await tuya.sendDataPointBool(entity, tuya.dataPoints.x5hChildLock, value === 'LOCK');
227
+ break;
228
+ case 'schedule': {
229
+ const periods = value.split(' ');
230
+ const periodsNumber = 8;
231
+ const payload = [];
232
+
233
+ for (let i = 0; i < periodsNumber; i++) {
234
+ const timeTemp = periods[i].split('/');
235
+ const hm = timeTemp[0].split(':', 2);
236
+ const h = parseInt(hm[0]);
237
+ const m = parseInt(hm[1]);
238
+ const temp = parseFloat(timeTemp[1]);
239
+
240
+ if (h < 0 || h >= 24 || m < 0 || m >= 60 || temp < 5 || temp > 60) {
241
+ throw new Error('Invalid hour, minute or temperature of: ' + periods[i]);
242
+ }
243
+
244
+ const tempHexArray = tuya.convertDecimalValueTo2ByteHexArray(Math.round(temp * 10));
245
+ // 1 byte for hour, 1 byte for minutes, 2 bytes for temperature
246
+ payload.push(h, m, ...tempHexArray);
247
+ }
248
+
249
+ await tuya.sendDataPointRaw(entity, tuya.dataPoints.x5hWeeklyProcedure, payload);
250
+ break;
251
+ }
252
+ default:
253
+ break;
254
+ }
255
+ },
256
+ },
131
257
  temperature_unit: {
132
258
  key: ['temperature_unit'],
133
259
  convertSet: async (entity, key, value, meta) => {
@@ -275,6 +401,114 @@ const fzLocal = {
275
401
  return result;
276
402
  },
277
403
  },
404
+ x5h_thermostat: {
405
+ cluster: 'manuSpecificTuya',
406
+ type: ['commandDataResponse', 'commandDataReport'],
407
+ convert: (model, msg, publish, options, meta) => {
408
+ const dpValue = tuya.firstDpValue(msg, meta, 'x5h_thermostat');
409
+ const dp = dpValue.dp;
410
+ const value = tuya.getDataValue(dpValue);
411
+
412
+ switch (dp) {
413
+ case tuya.dataPoints.x5hState: {
414
+ return {system_mode: value ? 'heat' : 'off'};
415
+ }
416
+ case tuya.dataPoints.x5hWorkingStatus: {
417
+ return {running_state: value ? 'heat' : 'idle'};
418
+ }
419
+ case tuya.dataPoints.x5hSound: {
420
+ return {sound: value ? 'ON' : 'OFF'};
421
+ }
422
+ case tuya.dataPoints.x5hFrostProtection: {
423
+ return {frost_protection: value ? 'ON' : 'OFF'};
424
+ }
425
+ case tuya.dataPoints.x5hWorkingDaySetting: {
426
+ return {week: tuya.thermostatWeekFormat[value]};
427
+ }
428
+ case tuya.dataPoints.x5hFactoryReset: {
429
+ if (value) {
430
+ clearTimeout(globalStore.getValue(msg.endpoint, 'factoryResetTimer'));
431
+ const timer = setTimeout(() => publish({factory_reset: 'OFF'}), 60 * 1000);
432
+ globalStore.putValue(msg.endpoint, 'factoryResetTimer', timer);
433
+ meta.logger.info('The thermostat is resetting now. It will be available in 1 minute.');
434
+ }
435
+
436
+ return {factory_reset: value ? 'ON' : 'OFF'};
437
+ }
438
+ case tuya.dataPoints.x5hTempDiff: {
439
+ return {deadzone_temperature: parseFloat((value / 10).toFixed(1))};
440
+ }
441
+ case tuya.dataPoints.x5hProtectionTempLimit: {
442
+ return {heating_temp_limit: value};
443
+ }
444
+ case tuya.dataPoints.x5hBackplaneBrightness: {
445
+ const lookup = {0: 'off', 1: 'low', 2: 'medium', 3: 'high'};
446
+
447
+ if (value >= 0 && value <= 3) {
448
+ globalStore.putValue(msg.endpoint, 'brightnessState', value);
449
+ return {brightness_state: lookup[value]};
450
+ }
451
+
452
+ // Sometimes, for example on thermostat restart, it sends message like:
453
+ // {"dpValues":[{"data":{"data":[90],"type":"Buffer"},"datatype":4,"dp":104}
454
+ // It doesn't represent any brightness value and brightness remains the previous value
455
+ const lastValue = globalStore.getValue(msg.endpoint, 'brightnessState') || 1;
456
+ return {brightness_state: lookup[lastValue]};
457
+ }
458
+ case tuya.dataPoints.x5hWeeklyProcedure: {
459
+ const periods = [];
460
+ const periodSize = 4;
461
+ const periodsNumber = 8;
462
+
463
+ for (let i = 0; i < periodsNumber; i++) {
464
+ const hours = value[i * periodSize];
465
+ const minutes = value[i * periodSize + 1];
466
+ const tempHexArray = [value[i * periodSize + 2], value[i * periodSize + 3]];
467
+ const tempRaw = Buffer.from(tempHexArray).readUIntBE(0, tempHexArray.length);
468
+ const strHours = hours.toString().padStart(2, '0');
469
+ const strMinutes = minutes.toString().padStart(2, '0');
470
+ const temp = parseFloat((tempRaw / 10).toFixed(1));
471
+ periods.push(`${strHours}:${strMinutes}/${temp}`);
472
+ }
473
+
474
+ const schedule = periods.join(' ');
475
+ return {schedule};
476
+ }
477
+ case tuya.dataPoints.x5hChildLock: {
478
+ return {child_lock: value ? 'LOCK' : 'UNLOCK'};
479
+ }
480
+ case tuya.dataPoints.x5hSetTemp: {
481
+ const setpoint = parseFloat((value / 10).toFixed(1));
482
+ globalStore.putValue(msg.endpoint, 'currentHeatingSetpoint', setpoint);
483
+ return {current_heating_setpoint: setpoint};
484
+ }
485
+ case tuya.dataPoints.x5hSetTempCeiling: {
486
+ return {upper_temp: value};
487
+ }
488
+ case tuya.dataPoints.x5hCurrentTemp: {
489
+ const temperature = value & (1 << 15) ? value - (1 << 16) + 1 : value;
490
+ return {local_temperature: parseFloat((temperature / 10).toFixed(1))};
491
+ }
492
+ case tuya.dataPoints.x5hTempCorrection: {
493
+ return {local_temperature_calibration: parseFloat((value / 10).toFixed(1))};
494
+ }
495
+ case tuya.dataPoints.x5hMode: {
496
+ const lookup = {0: 'manual', 1: 'program'};
497
+ return {preset: lookup[value]};
498
+ }
499
+ case tuya.dataPoints.x5hSensorSelection: {
500
+ const lookup = {0: 'internal', 1: 'external', 2: 'both'};
501
+ return {sensor: lookup[value]};
502
+ }
503
+ case tuya.dataPoints.x5hOutputReverse: {
504
+ return {output_reverse: value};
505
+ }
506
+ default: {
507
+ meta.logger.warn(`fromZigbee:x5h_thermostat: Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`);
508
+ }
509
+ }
510
+ },
511
+ },
278
512
  humidity10: {
279
513
  cluster: 'msRelativeHumidity',
280
514
  type: ['attributeReport', 'readResponse'],
@@ -438,7 +672,8 @@ module.exports = [
438
672
  {modelID: 'TS0001', manufacturerName: '_TZ3000_rmjr4ufz'},
439
673
  {modelID: 'TS0001', manufacturerName: '_TZ3000_v7gnj3ad'},
440
674
  {modelID: 'TS0001', manufacturerName: '_TZ3000_mx3vgyea'},
441
- {modelID: 'TS0001', manufacturerName: '_TZ3000_qsp2pwtf'}],
675
+ {modelID: 'TS0001', manufacturerName: '_TZ3000_qsp2pwtf'},
676
+ {modelID: 'TS0001', manufacturerName: '_TZ3000_46t1rvdu'}],
442
677
  model: 'WHD02',
443
678
  vendor: 'TuYa',
444
679
  whiteLabel: [{vendor: 'TuYa', model: 'iHSW02'}],
@@ -558,7 +793,8 @@ module.exports = [
558
793
  {modelID: 'TS0503B', manufacturerName: '_TZ3210_a5fxguxr'},
559
794
  {modelID: 'TS0503B', manufacturerName: '_TZ3210_778drfdt'},
560
795
  {modelID: 'TS0503B', manufacturerName: '_TZ3000_g5xawfcq'},
561
- {modelID: 'TS0503B', manufacturerName: '_TZ3210_trm3l2aw'}],
796
+ {modelID: 'TS0503B', manufacturerName: '_TZ3210_trm3l2aw'},
797
+ {modelID: 'TS0503B', manufacturerName: '_TZ3210_odlghna1'}],
562
798
  model: 'TS0503B',
563
799
  vendor: 'TuYa',
564
800
  description: 'Zigbee RGB light',
@@ -628,6 +864,7 @@ module.exports = [
628
864
  {modelID: 'TS0202', manufacturerName: '_TZ3000_tiwq83wk'},
629
865
  {modelID: 'TS0202', manufacturerName: '_TZ3000_ykwcwxmz'},
630
866
  {modelID: 'TS0202', manufacturerName: '_TZ3000_hgu1dlak'},
867
+ {modelID: 'TS0202', manufacturerName: '_TZ3000_h4wnrtck'},
631
868
  {modelID: 'WHD02', manufacturerName: '_TZ3000_hktqahrq'}],
632
869
  model: 'TS0202',
633
870
  vendor: 'TuYa',
@@ -2212,6 +2449,39 @@ module.exports = [
2212
2449
  .withLocalTemperature(ea.STATE)
2213
2450
  .withSystemMode(['off', 'auto', 'heat'], ea.STATE_SET).withRunningState(['idle', 'heat'], ea.STATE)],
2214
2451
  },
2452
+ {
2453
+ fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_2ekuz3dz'}],
2454
+ model: 'X5H-GB-B',
2455
+ vendor: 'TuYa',
2456
+ description: 'Wall-mount thermostat',
2457
+ fromZigbee: [fz.ignore_basic_report, fzLocal.x5h_thermostat],
2458
+ toZigbee: [tzLocal.x5h_thermostat],
2459
+ whiteLabel: [{vendor: 'Beok', model: 'TGR85-ZB'}],
2460
+ exposes: [
2461
+ exposes.climate().withSetpoint('current_heating_setpoint', 5, 60, 0.5, ea.STATE_SET)
2462
+ .withLocalTemperature(ea.STATE).withLocalTemperatureCalibration(-9.9, 9.9, 0.1, ea.STATE_SET)
2463
+ .withSystemMode(['off', 'heat'], ea.STATE_SET).withRunningState(['idle', 'heat'], ea.STATE)
2464
+ .withPreset(['manual', 'program']).withSensor(['internal', 'external', 'both'], ea.STATE_SET),
2465
+ exposes.text('schedule', ea.STATE_SET), e.child_lock(), e.week(),
2466
+ exposes.enum('brightness_state', ea.STATE_SET, ['off', 'low', 'medium', 'high'])
2467
+ .withDescription('Screen brightness'),
2468
+ exposes.binary('sound', ea.STATE_SET, 'ON', 'OFF')
2469
+ .withDescription('Switches beep sound when interacting with thermostat'),
2470
+ exposes.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
2471
+ .withDescription('Antifreeze function'),
2472
+ exposes.binary('factory_reset', ea.STATE_SET, 'ON', 'OFF')
2473
+ .withDescription('Resets all settings to default. Doesn\'t unpair device.'),
2474
+ exposes.numeric('heating_temp_limit', ea.STATE_SET).withUnit('°C').withValueMax(60)
2475
+ .withValueMin(5).withValueStep(1).withPreset('default', 35, 'Default value')
2476
+ .withDescription('Heating temperature limit'),
2477
+ exposes.numeric('deadzone_temperature', ea.STATE_SET).withUnit('°C').withValueMax(9.5)
2478
+ .withValueMin(0.5).withValueStep(0.5).withPreset('default', 1, 'Default value')
2479
+ .withDescription('The delta between local_temperature and current_heating_setpoint to trigger Heat'),
2480
+ exposes.numeric('upper_temp', ea.STATE_SET).withUnit('°C').withValueMax(95)
2481
+ .withValueMin(35).withValueStep(1).withPreset('default', 60, 'Default value'),
2482
+ ],
2483
+ onEvent: tuya.onEventSetTime,
2484
+ },
2215
2485
  {
2216
2486
  fingerprint: [{modelID: 'TS0222', manufacturerName: '_TYZB01_4mdqxxnn'},
2217
2487
  {modelID: 'TS0222', manufacturerName: '_TYZB01_m6ec2pgj'}],
package/devices/ubisys.js CHANGED
@@ -4,6 +4,7 @@ const tz = require('../converters/toZigbee');
4
4
  const ota = require('../lib/ota');
5
5
  const utils = require('../lib/utils');
6
6
  const reporting = require('../lib/reporting');
7
+ const constants = require('../lib/constants');
7
8
  const herdsman = require('zigbee-herdsman');
8
9
  const e = exposes.presets;
9
10
  const ea = exposes.access;
@@ -90,6 +91,15 @@ const ubisys = {
90
91
  return {configure_device_setup: result};
91
92
  },
92
93
  },
94
+ thermostat_vacation_mode: {
95
+ cluster: 'hvacThermostat',
96
+ type: ['attributeReport', 'readResponse'],
97
+ convert: (model, msg, publish, options, meta) => {
98
+ if (msg.data.hasOwnProperty('ocupancy')) {
99
+ return {vacation_mode: msg.data.ocupancy === 0};
100
+ }
101
+ },
102
+ },
93
103
  },
94
104
  tz: {
95
105
  configure_j1: {
@@ -503,6 +513,12 @@ const ubisys = {
503
513
  manufacturerOptions.ubisysNull);
504
514
  },
505
515
  },
516
+ thermostat_vacation_mode: {
517
+ key: ['vacation_mode'],
518
+ convertGet: async (entity, key, meta) => {
519
+ await entity.read('hvacThermostat', ['ocupancy']);
520
+ },
521
+ },
506
522
  },
507
523
  };
508
524
 
@@ -771,4 +787,65 @@ module.exports = [
771
787
  },
772
788
  ota: ota.ubisys,
773
789
  },
790
+ {
791
+ zigbeeModel: ['H1'],
792
+ model: 'H1',
793
+ vendor: 'Ubisys',
794
+ description: 'Heating regulator',
795
+ fromZigbee: [fz.battery, fz.thermostat, fz.thermostat_weekly_schedule, ubisys.fz.thermostat_vacation_mode],
796
+ toZigbee: [
797
+ tz.thermostat_occupied_heating_setpoint, tz.thermostat_unoccupied_heating_setpoint,
798
+ tz.thermostat_local_temperature, tz.thermostat_system_mode,
799
+ tz.thermostat_weekly_schedule, tz.thermostat_clear_weekly_schedule,
800
+ tz.thermostat_running_mode, ubisys.tz.thermostat_vacation_mode,
801
+ tz.thermostat_pi_heating_demand,
802
+ ],
803
+ exposes: [
804
+ e.battery(),
805
+ exposes.climate()
806
+ .withSystemMode(['off', 'heat'], ea.ALL)
807
+ .withRunningMode(['off', 'heat'])
808
+ .withSetpoint('occupied_heating_setpoint', 7, 30, 0.5)
809
+ .withLocalTemperature()
810
+ .withPiHeatingDemand(ea.STATE_GET),
811
+ exposes.binary('vacation_mode', ea.STATE_GET, true, false)
812
+ .withDescription('When Vacation Mode is active the schedule is disabled and unoccupied_heating_setpoint is used.'),
813
+ ],
814
+ configure: async (device, coordinatorEndpoint, logger) => {
815
+ const endpoint = device.getEndpoint(1);
816
+ const binds = ['genBasic', 'genPowerCfg', 'genTime', 'hvacThermostat'];
817
+ await reporting.bind(endpoint, coordinatorEndpoint, binds);
818
+
819
+ // reporting
820
+ // NOTE: temperature is 0.5 deg steps
821
+ // NOTE: unoccupied_heating_setpoint cannot be set via the device itself
822
+ // so we do not need to setup reporting for this, as reporting slots
823
+ // seem to be limited.
824
+ await reporting.thermostatSystemMode(endpoint);
825
+ await reporting.thermostatRunningMode(endpoint);
826
+ await reporting.thermostatTemperature(endpoint,
827
+ {min: 0, max: constants.repInterval.HOUR, change: 50});
828
+ await reporting.thermostatOccupiedHeatingSetpoint(endpoint,
829
+ {min: 0, max: constants.repInterval.HOUR, change: 50});
830
+ await reporting.thermostatPIHeatingDemand(endpoint);
831
+ await reporting.thermostatOcupancy(endpoint);
832
+ await reporting.batteryPercentageRemaining(endpoint,
833
+ {min: constants.repInterval.HOUR, max: 43200, change: 1});
834
+
835
+
836
+ // read attributes
837
+ // NOTE: configuring reporting on hvacThermostat seems to trigger an imediat
838
+ // report, so the values are available after configure has run.
839
+ // this does not seem to be the case for genPowerCfg, so we read
840
+ // the battery percentage
841
+ await endpoint.read('genPowerCfg', ['batteryPercentageRemaining']);
842
+
843
+ // write attributes
844
+ // NOTE: device checks in every 1h once the device has entered deepsleep
845
+ // this might be a bit long if you want to set the temperature remotely
846
+ // update this to every 15 minutes. (value is in 1/4th of a second)
847
+ await endpoint.write('genPollCtrl', {'checkinInterval': (4 * 60 * 15)});
848
+ },
849
+ ota: ota.ubisys,
850
+ },
774
851
  ];
package/devices/vimar.js CHANGED
@@ -6,6 +6,17 @@ const extend = require('../lib/extend');
6
6
  const e = exposes.presets;
7
7
 
8
8
  module.exports = [
9
+ {
10
+ zigbeeModel: ['On_Off_Switch_Module_v1.0'],
11
+ model: '03981',
12
+ vendor: 'Vimar',
13
+ description: 'IoT connected relay module',
14
+ extend: extend.switch(),
15
+ configure: async (device, coordinatorEndpoint, logger) => {
16
+ const endpoint = device.getEndpoint(10);
17
+ await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']);
18
+ },
19
+ },
9
20
  {
10
21
  zigbeeModel: ['2_Way_Switch_v1.0', 'On_Off_Switch_v1.0'],
11
22
  model: '14592.0',
package/lib/exposes.js CHANGED
@@ -407,6 +407,14 @@ class Climate extends Base {
407
407
  return this;
408
408
  }
409
409
 
410
+ withRunningMode(modes, access=a.STATE_GET) {
411
+ assert(!this.endpoint, 'Cannot add feature after adding endpoint');
412
+ const allowed = ['off', 'cool', 'heat'];
413
+ modes.forEach((m) => assert(allowed.includes(m)));
414
+ this.features.push(new Enum('running_mode', access, modes).withDescription('The current running mode'));
415
+ return this;
416
+ }
417
+
410
418
  withFanMode(modes, access=a.ALL) {
411
419
  assert(!this.endpoint, 'Cannot add feature after adding endpoint');
412
420
  const allowed = ['off', 'low', 'medium', 'high', 'on', 'auto', 'smart'];
package/lib/reporting.js CHANGED
@@ -170,6 +170,14 @@ module.exports = {
170
170
  const p = payload('runningState', 0, repInterval.HOUR, 0, overrides);
171
171
  await endpoint.configureReporting('hvacThermostat', p);
172
172
  },
173
+ thermostatRunningMode: async (endpoint, overrides) => {
174
+ const p = payload('runningMode', 0, repInterval.HOUR, 0, overrides);
175
+ await endpoint.configureReporting('hvacThermostat', p);
176
+ },
177
+ thermostatOcupancy: async (endpoint, overrides) => {
178
+ const p = payload('ocupancy', 0, repInterval.HOUR, 0, overrides);
179
+ await endpoint.configureReporting('hvacThermostat', p);
180
+ },
173
181
  thermostatTemperatureSetpointHold: async (endpoint, overrides) => {
174
182
  const p = payload('tempSetpointHold', 0, repInterval.HOUR, 0, overrides);
175
183
  await endpoint.configureReporting('hvacThermostat', p);
package/lib/tuya.js CHANGED
@@ -669,6 +669,26 @@ const dataPoints = {
669
669
  // Moes switch with optional neutral
670
670
  moesSwitchPowerOnBehavior: 14,
671
671
  moesSwitchIndicateLight: 15,
672
+ // X5H thermostat
673
+ x5hState: 1,
674
+ x5hMode: 2,
675
+ x5hWorkingStatus: 3,
676
+ x5hSound: 7,
677
+ x5hFrostProtection: 10,
678
+ x5hSetTemp: 16,
679
+ x5hSetTempCeiling: 19,
680
+ x5hCurrentTemp: 24,
681
+ x5hTempCorrection: 27,
682
+ x5hWeeklyProcedure: 30,
683
+ x5hWorkingDaySetting: 31,
684
+ x5hFactoryReset: 39,
685
+ x5hChildLock: 40,
686
+ x5hSensorSelection: 43,
687
+ x5hFaultAlarm: 45,
688
+ x5hTempDiff: 101,
689
+ x5hProtectionTempLimit: 102,
690
+ x5hOutputReverse: 103,
691
+ x5hBackplaneBrightness: 104,
672
692
  // Connecte thermostat
673
693
  connecteState: 1,
674
694
  connecteMode: 2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigbee-herdsman-converters",
3
- "version": "14.0.580",
3
+ "version": "14.0.583",
4
4
  "description": "Collection of device converters to be used with zigbee-herdsman",
5
5
  "main": "index.js",
6
6
  "files": [