zigbee-herdsman-converters 14.0.581 → 14.0.584

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.
@@ -1544,6 +1544,8 @@ const converters = {
1544
1544
  if (msg.data.hasOwnProperty('currentPositionLiftPercentage') && msg.data['currentPositionLiftPercentage'] <= 100) {
1545
1545
  const value = msg.data['currentPositionLiftPercentage'];
1546
1546
  result[postfixWithEndpointName('position', msg, model, meta)] = invert ? value : 100 - value;
1547
+ result[postfixWithEndpointName('state', msg, model, meta)] =
1548
+ invert ? (value > 0 ? 'CLOSE' : 'OPEN') : (value > 0 ? 'OPEN' : 'CLOSE');
1547
1549
  }
1548
1550
  if (msg.data.hasOwnProperty('currentPositionTiltPercentage') && msg.data['currentPositionTiltPercentage'] <= 100) {
1549
1551
  const value = msg.data['currentPositionTiltPercentage'];
@@ -2807,21 +2809,21 @@ const converters = {
2807
2809
  if (msg.data.hasOwnProperty('tuyaMovingState')) {
2808
2810
  const value = msg.data['tuyaMovingState'];
2809
2811
  const movingLookup = {0: 'UP', 1: 'STOP', 2: 'DOWN'};
2810
- result.moving = movingLookup[value];
2812
+ result[postfixWithEndpointName('moving', msg, model, meta)] = movingLookup[value];
2811
2813
  }
2812
2814
  if (msg.data.hasOwnProperty('tuyaCalibration')) {
2813
2815
  const value = msg.data['tuyaCalibration'];
2814
2816
  const calibrationLookup = {0: 'ON', 1: 'OFF'};
2815
- result.calibration = calibrationLookup[value];
2817
+ result[postfixWithEndpointName('calibration', msg, model, meta)] = calibrationLookup[value];
2816
2818
  }
2817
2819
  if (msg.data.hasOwnProperty('tuyaMotorReversal')) {
2818
2820
  const value = msg.data['tuyaMotorReversal'];
2819
2821
  const reversalLookup = {0: 'OFF', 1: 'ON'};
2820
- result.motor_reversal = reversalLookup[value];
2822
+ result[postfixWithEndpointName('motor_reversal', msg, model, meta)] = reversalLookup[value];
2821
2823
  }
2822
2824
  if (msg.data.hasOwnProperty('moesCalibrationTime')) {
2823
2825
  const value = parseFloat(msg.data['moesCalibrationTime']) / 10.0;
2824
- result.calibration_time = value;
2826
+ result[postfixWithEndpointName('calibration_time', msg, model, meta)] = value;
2825
2827
  }
2826
2828
  return result;
2827
2829
  },
@@ -115,7 +115,7 @@ module.exports = [
115
115
  model: '5128.10',
116
116
  vendor: 'Iluminize',
117
117
  description: 'Zigbee 3.0 switch shutter SW with level control',
118
- fromZigbee: [fz.cover_position_via_brightness, fz.cover_state_via_onoff],
118
+ fromZigbee: [fz.cover_position_via_brightness, fz.cover_state_via_onoff, fz.cover_position_tilt],
119
119
  toZigbee: [tz.cover_state, tz.cover_via_brightness],
120
120
  exposes: [e.cover_position()],
121
121
  ota: ota.zigbeeOTA,
@@ -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',
@@ -3,6 +3,8 @@ const fz = {...require('../converters/fromZigbee'), legacy: require('../lib/lega
3
3
  const tz = require('../converters/toZigbee');
4
4
  const ota = require('../lib/ota');
5
5
  const reporting = require('../lib/reporting');
6
+ const globalStore = require('../lib/store');
7
+ const utils = require('../lib/utils');
6
8
  const e = exposes.presets;
7
9
  const ea = exposes.access;
8
10
 
@@ -40,6 +42,48 @@ const hueExtend = {
40
42
  }),
41
43
  };
42
44
 
45
+ const fzLocal = {
46
+ hue_tap_dial: {
47
+ cluster: 'manuSpecificPhilips',
48
+ type: 'commandHueNotification',
49
+ options: [exposes.options.simulated_brightness()],
50
+ convert: (model, msg, publish, options, meta) => {
51
+ const buttonLookup = {1: 'button_1', 2: 'button_2', 3: 'button_3', 4: 'button_4', 20: 'dial'};
52
+ const button = buttonLookup[msg.data['button']];
53
+ const typeLookup = {0: 'press', 1: 'hold', 2: 'press_release', 3: 'hold_release'};
54
+ const type = typeLookup[msg.data['type']];
55
+ const direction = msg.data['unknown2'] <127 ? 'right' : 'left';
56
+ const time = msg.data['time'];
57
+ const payload = {};
58
+
59
+ if (button === 'dial') {
60
+ const adjustedTime = direction === 'right' ? time : 256 - time;
61
+ const dialType = 'rotate';
62
+ const speed = adjustedTime <= 25 ? 'step' : adjustedTime <= 75 ? 'slow' : 'fast';
63
+ payload.action = `${button}_${dialType}_${direction}_${speed}`;
64
+
65
+ // simulated brightness
66
+ if (options.simulated_brightness) {
67
+ const opts = options.simulated_brightness;
68
+ const deltaOpts = typeof opts === 'object' && opts.hasOwnProperty('delta') ? opts.delta : 35;
69
+ const delta = direction === 'right' ? deltaOpts : deltaOpts * -1;
70
+ const brightness = globalStore.getValue(msg.endpoint, 'brightness', 255) + delta;
71
+ payload.brightness = utils.numberWithinRange(brightness, 0, 255);
72
+ globalStore.putValue(msg.endpoint, 'brightness', payload.brightness);
73
+ }
74
+ } else {
75
+ payload.action = `${button}_${type}`;
76
+ // duration
77
+ if (type === 'press') globalStore.putValue(msg.endpoint, 'press_start', Date.now());
78
+ else if (type === 'hold' || type === 'hold_release') {
79
+ payload.action_duration = (Date.now() - globalStore.getValue(msg.endpoint, 'press_start')) / 1000;
80
+ }
81
+ }
82
+ return payload;
83
+ },
84
+ },
85
+ };
86
+
43
87
  module.exports = [
44
88
  {
45
89
  zigbeeModel: ['929003055801'],
@@ -1866,7 +1910,7 @@ module.exports = [
1866
1910
  ota: ota.zigbeeOTA,
1867
1911
  },
1868
1912
  {
1869
- zigbeeModel: ['LOM002', 'LOM004'],
1913
+ zigbeeModel: ['LOM002', 'LOM004', 'LOM010'],
1870
1914
  model: '046677552343',
1871
1915
  vendor: 'Philips',
1872
1916
  description: 'Hue smart plug bluetooth',
@@ -2103,6 +2147,20 @@ module.exports = [
2103
2147
  description: 'Hue White Ambinance G25 E26 Edison Filament Globe',
2104
2148
  extend: hueExtend.light_onoff_brightness_colortemp({colorTempRange: [222, 454]}),
2105
2149
  },
2150
+ {
2151
+ zigbeeModel: ['LWE005'],
2152
+ model: '9290024796',
2153
+ vendor: 'Philips',
2154
+ description: 'Hue Filament White E12',
2155
+ extend: hueExtend.light_onoff_brightness(),
2156
+ },
2157
+ {
2158
+ zigbeeModel: ['LTA007'],
2159
+ model: '9290024783',
2160
+ vendor: 'Philips',
2161
+ description: 'Hue Filament White Ambiance A60/E27 Bluetooth',
2162
+ extend: hueExtend.light_onoff_brightness_colortemp({colorTempRange: [222, 454]}),
2163
+ },
2106
2164
  {
2107
2165
  zigbeeModel: ['LWV002'],
2108
2166
  model: '046677551780',
@@ -2299,6 +2357,29 @@ module.exports = [
2299
2357
  description: 'Hue White and Color Ambiance A19 1100 lumen',
2300
2358
  extend: hueExtend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 500]}),
2301
2359
  },
2360
+ {
2361
+ zigbeeModel: ['RDM002'],
2362
+ model: '8719514440937/8719514440999',
2363
+ vendor: 'Philips',
2364
+ description: 'Hue Tap dial switch',
2365
+ fromZigbee: [fz.ignore_command_step, fzLocal.hue_tap_dial, fz.battery],
2366
+ toZigbee: [],
2367
+ exposes: [e.battery(), e.action(['button_1_press', 'button_1_press_release', 'button_1_hold', 'button_1_hold_release',
2368
+ 'button_2_press', 'button_2_press_release', 'button_2_hold', 'button_2_hold_release',
2369
+ 'button_3_press', 'button_3_press_release', 'button_3_hold', 'button_3_hold_release',
2370
+ 'button_4_press', 'button_4_press_release', 'button_4_hold', 'button_4_hold_release',
2371
+ 'dial_rotate_left_step', 'dial_rotate_left_slow', 'dial_rotate_left_fast',
2372
+ 'dial_rotate_right_step', 'dial_rotate_right_slow', 'dial_rotate_right_fast']),
2373
+ ],
2374
+ configure: async (device, coordinatorEndpoint, logger) => {
2375
+ const endpoint = device.getEndpoint(1);
2376
+ await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'genLevelCtrl', 'manuSpecificPhilips', 'genPowerCfg']);
2377
+ const options = {manufacturerCode: 0x100B, disableDefaultResponse: true};
2378
+ await endpoint.write('genBasic', {0x0031: {value: 0x000B, type: 0x19}}, options);
2379
+ await reporting.batteryPercentageRemaining(endpoint);
2380
+ },
2381
+ ota: ota.zigbeeOTA,
2382
+ },
2302
2383
  {
2303
2384
  fingerprint: [{modelID: 'GreenPower_2', ieeeAddr: /^0x00000000004.....$/}],
2304
2385
  model: '8718696743133',
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
  ];
@@ -3,18 +3,27 @@ const fz = {...require('../converters/fromZigbee'), legacy: require('../lib/lega
3
3
  const tz = require('../converters/toZigbee');
4
4
  const ota = require('../lib/ota');
5
5
  const reporting = require('../lib/reporting');
6
- const extend = require('../lib/extend');
7
6
  const e = exposes.presets;
8
7
 
8
+ // Make sure extend.ldight_* is not used (sengledExtend should be used instead)
9
+ const extendDontUse = require('../lib/extend');
10
+ const extend = {switch: extendDontUse.switch};
11
+ const sengledExtend = {
12
+ light_onoff_brightness: (options={}) => extendDontUse.light_onoff_brightness({disableEffect: true, ...options}),
13
+ light_onoff_brightness_colortemp: (options={}) => extendDontUse.light_onoff_brightness_colortemp({disableEffect: true, ...options}),
14
+ light_onoff_brightness_colortemp_color: (options={}) =>
15
+ extendDontUse.light_onoff_brightness_colortemp_color({disableEffect: true, ...options}),
16
+ };
17
+
9
18
  module.exports = [
10
19
  {
11
20
  zigbeeModel: ['E13-N11'],
12
21
  model: 'E13-N11',
13
22
  vendor: 'Sengled',
14
23
  description: 'Flood light with motion sensor light outdoor',
15
- fromZigbee: extend.light_onoff_brightness().fromZigbee.concat([fz.ias_occupancy_alarm_1]),
16
- toZigbee: extend.light_onoff_brightness().toZigbee,
17
- exposes: extend.light_onoff_brightness().exposes.concat([e.occupancy()]),
24
+ fromZigbee: sengledExtend.light_onoff_brightness().fromZigbee.concat([fz.ias_occupancy_alarm_1]),
25
+ toZigbee: sengledExtend.light_onoff_brightness().toZigbee,
26
+ exposes: sengledExtend.light_onoff_brightness().exposes.concat([e.occupancy()]),
18
27
  ota: ota.zigbeeOTA,
19
28
  },
20
29
  {
@@ -22,7 +31,7 @@ module.exports = [
22
31
  model: 'E21-N13A',
23
32
  vendor: 'Sengled',
24
33
  description: 'Smart LED (A19)',
25
- extend: extend.light_onoff_brightness(),
34
+ extend: sengledExtend.light_onoff_brightness(),
26
35
  ota: ota.zigbeeOTA,
27
36
  },
28
37
  {
@@ -30,12 +39,12 @@ module.exports = [
30
39
  model: 'E21-N1EA',
31
40
  vendor: 'Sengled',
32
41
  description: 'Smart LED multicolor A19 bulb',
33
- fromZigbee: extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).fromZigbee.concat([fz.metering]),
34
- toZigbee: extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).toZigbee,
35
- exposes: extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).exposes.concat([e.power(), e.energy()]),
42
+ fromZigbee: sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).fromZigbee.concat([fz.metering]),
43
+ toZigbee: sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).toZigbee,
44
+ exposes: sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).exposes.concat([e.power(), e.energy()]),
36
45
  ota: ota.zigbeeOTA,
37
46
  configure: async (device, coordinatorEndpoint, logger) => {
38
- await extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]})
47
+ await sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]})
39
48
  .configure(device, coordinatorEndpoint, logger);
40
49
  device.powerSource = 'Mains (single phase)';
41
50
  device.save();
@@ -52,7 +61,7 @@ module.exports = [
52
61
  model: 'E12-N1E',
53
62
  vendor: 'Sengled',
54
63
  description: 'Smart LED multicolor (BR30)',
55
- extend: extend.light_onoff_brightness_colortemp_color(),
64
+ extend: sengledExtend.light_onoff_brightness_colortemp_color(),
56
65
  ota: ota.zigbeeOTA,
57
66
  },
58
67
  {
@@ -60,7 +69,7 @@ module.exports = [
60
69
  model: 'E1G-G8E',
61
70
  vendor: 'Sengled',
62
71
  description: 'Multicolor light strip (2M)',
63
- extend: extend.light_onoff_brightness_colortemp_color(),
72
+ extend: sengledExtend.light_onoff_brightness_colortemp_color(),
64
73
  ota: ota.zigbeeOTA,
65
74
  },
66
75
  {
@@ -68,7 +77,7 @@ module.exports = [
68
77
  model: 'E11-U21U31',
69
78
  vendor: 'Sengled',
70
79
  description: 'Element touch (A19)',
71
- extend: extend.light_onoff_brightness(),
80
+ extend: sengledExtend.light_onoff_brightness(),
72
81
  ota: ota.zigbeeOTA,
73
82
  },
74
83
  {
@@ -76,11 +85,11 @@ module.exports = [
76
85
  model: 'E11-G13',
77
86
  vendor: 'Sengled',
78
87
  description: 'Element classic (A19)',
79
- fromZigbee: extend.light_onoff_brightness().fromZigbee.concat([fz.metering]),
80
- toZigbee: extend.light_onoff_brightness().toZigbee,
88
+ fromZigbee: sengledExtend.light_onoff_brightness().fromZigbee.concat([fz.metering]),
89
+ toZigbee: sengledExtend.light_onoff_brightness().toZigbee,
81
90
  ota: ota.zigbeeOTA,
82
91
  configure: async (device, coordinatorEndpoint, logger) => {
83
- await extend.light_onoff_brightness().configure(device, coordinatorEndpoint, logger);
92
+ await sengledExtend.light_onoff_brightness().configure(device, coordinatorEndpoint, logger);
84
93
  device.powerSource = 'Mains (single phase)';
85
94
  device.save();
86
95
 
@@ -90,14 +99,14 @@ module.exports = [
90
99
  await reporting.currentSummDelivered(endpoint);
91
100
  await reporting.instantaneousDemand(endpoint);
92
101
  },
93
- exposes: extend.light_onoff_brightness().exposes.concat([e.power(), e.energy()]),
102
+ exposes: sengledExtend.light_onoff_brightness().exposes.concat([e.power(), e.energy()]),
94
103
  },
95
104
  {
96
105
  zigbeeModel: ['E11-G23', 'E11-G33'],
97
106
  model: 'E11-G23/E11-G33',
98
107
  vendor: 'Sengled',
99
108
  description: 'Element classic (A60)',
100
- extend: extend.light_onoff_brightness(),
109
+ extend: sengledExtend.light_onoff_brightness(),
101
110
  ota: ota.zigbeeOTA,
102
111
  },
103
112
  {
@@ -105,7 +114,7 @@ module.exports = [
105
114
  model: 'E11-N13/E11-N13A/E11-N14/E11-N14A',
106
115
  vendor: 'Sengled',
107
116
  description: 'Element extra bright (A19)',
108
- extend: extend.light_onoff_brightness(),
117
+ extend: sengledExtend.light_onoff_brightness(),
109
118
  ota: ota.zigbeeOTA,
110
119
  },
111
120
  {
@@ -113,7 +122,7 @@ module.exports = [
113
122
  model: 'Z01-CIA19NAE26',
114
123
  vendor: 'Sengled',
115
124
  description: 'Element touch (A19)',
116
- extend: extend.light_onoff_brightness(),
125
+ extend: sengledExtend.light_onoff_brightness(),
117
126
  ota: ota.zigbeeOTA,
118
127
  },
119
128
  {
@@ -121,7 +130,7 @@ module.exports = [
121
130
  model: 'Z01-A19NAE26',
122
131
  vendor: 'Sengled',
123
132
  description: 'Element plus (A19)',
124
- extend: extend.light_onoff_brightness_colortemp(),
133
+ extend: sengledExtend.light_onoff_brightness_colortemp(),
125
134
  ota: ota.zigbeeOTA,
126
135
  },
127
136
  {
@@ -129,7 +138,7 @@ module.exports = [
129
138
  model: 'Z01-A60EAE27',
130
139
  vendor: 'Sengled',
131
140
  description: 'Element Plus (A60)',
132
- extend: extend.light_onoff_brightness_colortemp(),
141
+ extend: sengledExtend.light_onoff_brightness_colortemp(),
133
142
  ota: ota.zigbeeOTA,
134
143
  },
135
144
  {
@@ -137,11 +146,11 @@ module.exports = [
137
146
  model: 'E11-N1EA',
138
147
  vendor: 'Sengled',
139
148
  description: 'Element plus color (A19)',
140
- fromZigbee: extend.light_onoff_brightness().fromZigbee.concat([fz.metering]),
141
- toZigbee: extend.light_onoff_brightness().toZigbee,
149
+ fromZigbee: sengledExtend.light_onoff_brightness().fromZigbee.concat([fz.metering]),
150
+ toZigbee: sengledExtend.light_onoff_brightness().toZigbee,
142
151
  ota: ota.zigbeeOTA,
143
152
  configure: async (device, coordinatorEndpoint, logger) => {
144
- await extend.light_onoff_brightness().configure(device, coordinatorEndpoint, logger);
153
+ await sengledExtend.light_onoff_brightness().configure(device, coordinatorEndpoint, logger);
145
154
  device.powerSource = 'Mains (single phase)';
146
155
  device.save();
147
156
 
@@ -151,14 +160,14 @@ module.exports = [
151
160
  await reporting.currentSummDelivered(endpoint);
152
161
  await reporting.instantaneousDemand(endpoint);
153
162
  },
154
- exposes: extend.light_onoff_brightness().exposes.concat([e.power(), e.energy()]),
163
+ exposes: sengledExtend.light_onoff_brightness().exposes.concat([e.power(), e.energy()]),
155
164
  },
156
165
  {
157
166
  zigbeeModel: ['E11-U2E'],
158
167
  model: 'E11-U2E',
159
168
  vendor: 'Sengled',
160
169
  description: 'Element color plus E27',
161
- extend: extend.light_onoff_brightness_colortemp_color(),
170
+ extend: sengledExtend.light_onoff_brightness_colortemp_color(),
162
171
  ota: ota.zigbeeOTA,
163
172
  },
164
173
  {
@@ -166,7 +175,7 @@ module.exports = [
166
175
  model: 'E11-U3E',
167
176
  vendor: 'Sengled',
168
177
  description: 'Element color plus B22',
169
- extend: extend.light_onoff_brightness_colortemp_color(),
178
+ extend: sengledExtend.light_onoff_brightness_colortemp_color(),
170
179
  ota: ota.zigbeeOTA,
171
180
  },
172
181
  {
@@ -174,12 +183,12 @@ module.exports = [
174
183
  model: 'E1F-N5E',
175
184
  vendor: 'Sengled',
176
185
  description: 'Element color plus E12',
177
- fromZigbee: extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).fromZigbee.concat([fz.metering]),
178
- toZigbee: extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).toZigbee,
179
- exposes: extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).exposes.concat([e.power(), e.energy()]),
186
+ fromZigbee: sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).fromZigbee.concat([fz.metering]),
187
+ toZigbee: sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).toZigbee,
188
+ exposes: sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]}).exposes.concat([e.power(), e.energy()]),
180
189
  ota: ota.zigbeeOTA,
181
190
  configure: async (device, coordinatorEndpoint, logger) => {
182
- await extend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]})
191
+ await sengledExtend.light_onoff_brightness_colortemp_color({colorTempRange: [154, 500]})
183
192
  .configure(device, coordinatorEndpoint, logger);
184
193
  device.powerSource = 'Mains (single phase)';
185
194
  device.save();
@@ -196,7 +205,7 @@ module.exports = [
196
205
  model: 'E12-N14',
197
206
  vendor: 'Sengled',
198
207
  description: 'Element Classic (BR30)',
199
- extend: extend.light_onoff_brightness(),
208
+ extend: sengledExtend.light_onoff_brightness(),
200
209
  ota: ota.zigbeeOTA,
201
210
  },
202
211
  {
@@ -204,7 +213,7 @@ module.exports = [
204
213
  model: 'E1ACA4ABE38A',
205
214
  vendor: 'Sengled',
206
215
  description: 'Element downlight smart LED bulb',
207
- extend: extend.light_onoff_brightness(),
216
+ extend: sengledExtend.light_onoff_brightness(),
208
217
  ota: ota.zigbeeOTA,
209
218
  },
210
219
  {
@@ -261,7 +270,7 @@ module.exports = [
261
270
  model: 'E11-N1G',
262
271
  vendor: 'Sengled',
263
272
  description: 'Vintage LED edison bulb (ST19)',
264
- extend: extend.light_onoff_brightness(),
273
+ extend: sengledExtend.light_onoff_brightness(),
265
274
  ota: ota.zigbeeOTA,
266
275
  },
267
276
  {
@@ -269,7 +278,7 @@ module.exports = [
269
278
  model: 'E1F-N9G',
270
279
  vendor: 'Sengled',
271
280
  description: 'Smart LED filament candle (E12)',
272
- extend: extend.light_onoff_brightness(),
281
+ extend: sengledExtend.light_onoff_brightness(),
273
282
  ota: ota.zigbeeOTA,
274
283
  },
275
284
  ];
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'],
@@ -559,7 +793,8 @@ module.exports = [
559
793
  {modelID: 'TS0503B', manufacturerName: '_TZ3210_a5fxguxr'},
560
794
  {modelID: 'TS0503B', manufacturerName: '_TZ3210_778drfdt'},
561
795
  {modelID: 'TS0503B', manufacturerName: '_TZ3000_g5xawfcq'},
562
- {modelID: 'TS0503B', manufacturerName: '_TZ3210_trm3l2aw'}],
796
+ {modelID: 'TS0503B', manufacturerName: '_TZ3210_trm3l2aw'},
797
+ {modelID: 'TS0503B', manufacturerName: '_TZ3210_odlghna1'}],
563
798
  model: 'TS0503B',
564
799
  vendor: 'TuYa',
565
800
  description: 'Zigbee RGB light',
@@ -1967,13 +2202,20 @@ module.exports = [
1967
2202
  vendor: 'TuYa',
1968
2203
  description: 'Smart light switch - 3 gang without neutral wire',
1969
2204
  extend: extend.switch(),
1970
- exposes: [e.switch().withEndpoint('left'), e.switch().withEndpoint('center'), e.switch().withEndpoint('right')],
2205
+ toZigbee: extend.switch().toZigbee.concat([tz.moes_power_on_behavior, tz.tuya_backlight_mode]),
2206
+ fromZigbee: extend.switch().fromZigbee.concat([fz.moes_power_on_behavior, fz.tuya_backlight_mode]),
2207
+ exposes: [e.switch().withEndpoint('left'), e.switch().withEndpoint('center'), e.switch().withEndpoint('right'),
2208
+ exposes.enum('power_on_behavior', ea.ALL, Object.values(tuya.moesSwitch.powerOnBehavior)),
2209
+ exposes.enum('backlight_mode', ea.ALL, ['LOW', 'MEDIUM', 'HIGH'])
2210
+ .withDescription('Indicator light status: LOW: Off | MEDIUM: On| HIGH: Inverted')],
1971
2211
  endpoint: (device) => {
1972
2212
  return {'left': 1, 'center': 2, 'right': 3};
1973
2213
  },
1974
2214
  whiteLabel: [{vendor: 'TUYATEC', model: 'GDKES-03TZXD'}],
1975
2215
  meta: {multiEndpoint: true},
1976
2216
  configure: async (device, coordinatorEndpoint, logger) => {
2217
+ await device.getEndpoint(1).read('genBasic',
2218
+ ['manufacturerName', 'zclVersion', 'appVersion', 'modelId', 'powerSource', 0xfffe]);
1977
2219
  try {
1978
2220
  for (const ID of [1, 2, 3]) {
1979
2221
  const endpoint = device.getEndpoint(ID);
@@ -2214,6 +2456,39 @@ module.exports = [
2214
2456
  .withLocalTemperature(ea.STATE)
2215
2457
  .withSystemMode(['off', 'auto', 'heat'], ea.STATE_SET).withRunningState(['idle', 'heat'], ea.STATE)],
2216
2458
  },
2459
+ {
2460
+ fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_2ekuz3dz'}],
2461
+ model: 'X5H-GB-B',
2462
+ vendor: 'TuYa',
2463
+ description: 'Wall-mount thermostat',
2464
+ fromZigbee: [fz.ignore_basic_report, fzLocal.x5h_thermostat],
2465
+ toZigbee: [tzLocal.x5h_thermostat],
2466
+ whiteLabel: [{vendor: 'Beok', model: 'TGR85-ZB'}],
2467
+ exposes: [
2468
+ exposes.climate().withSetpoint('current_heating_setpoint', 5, 60, 0.5, ea.STATE_SET)
2469
+ .withLocalTemperature(ea.STATE).withLocalTemperatureCalibration(-9.9, 9.9, 0.1, ea.STATE_SET)
2470
+ .withSystemMode(['off', 'heat'], ea.STATE_SET).withRunningState(['idle', 'heat'], ea.STATE)
2471
+ .withPreset(['manual', 'program']).withSensor(['internal', 'external', 'both'], ea.STATE_SET),
2472
+ exposes.text('schedule', ea.STATE_SET), e.child_lock(), e.week(),
2473
+ exposes.enum('brightness_state', ea.STATE_SET, ['off', 'low', 'medium', 'high'])
2474
+ .withDescription('Screen brightness'),
2475
+ exposes.binary('sound', ea.STATE_SET, 'ON', 'OFF')
2476
+ .withDescription('Switches beep sound when interacting with thermostat'),
2477
+ exposes.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
2478
+ .withDescription('Antifreeze function'),
2479
+ exposes.binary('factory_reset', ea.STATE_SET, 'ON', 'OFF')
2480
+ .withDescription('Resets all settings to default. Doesn\'t unpair device.'),
2481
+ exposes.numeric('heating_temp_limit', ea.STATE_SET).withUnit('°C').withValueMax(60)
2482
+ .withValueMin(5).withValueStep(1).withPreset('default', 35, 'Default value')
2483
+ .withDescription('Heating temperature limit'),
2484
+ exposes.numeric('deadzone_temperature', ea.STATE_SET).withUnit('°C').withValueMax(9.5)
2485
+ .withValueMin(0.5).withValueStep(0.5).withPreset('default', 1, 'Default value')
2486
+ .withDescription('The delta between local_temperature and current_heating_setpoint to trigger Heat'),
2487
+ exposes.numeric('upper_temp', ea.STATE_SET).withUnit('°C').withValueMax(95)
2488
+ .withValueMin(35).withValueStep(1).withPreset('default', 60, 'Default value'),
2489
+ ],
2490
+ onEvent: tuya.onEventSetTime,
2491
+ },
2217
2492
  {
2218
2493
  fingerprint: [{modelID: 'TS0222', manufacturerName: '_TYZB01_4mdqxxnn'},
2219
2494
  {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.581",
3
+ "version": "14.0.584",
4
4
  "description": "Collection of device converters to be used with zigbee-herdsman",
5
5
  "main": "index.js",
6
6
  "files": [