zigbee-herdsman-converters 14.0.319 → 14.0.323

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.
@@ -1425,11 +1425,11 @@ const converters = {
1425
1425
  const invert = model.meta && model.meta.coverInverted ? !options.invert_cover : options.invert_cover;
1426
1426
  if (msg.data.hasOwnProperty('currentPositionLiftPercentage') && msg.data['currentPositionLiftPercentage'] <= 100) {
1427
1427
  const value = msg.data['currentPositionLiftPercentage'];
1428
- result.position = invert ? value : 100 - value;
1428
+ result[postfixWithEndpointName('position', msg, model)] = invert ? value : 100 - value;
1429
1429
  }
1430
1430
  if (msg.data.hasOwnProperty('currentPositionTiltPercentage') && msg.data['currentPositionTiltPercentage'] <= 100) {
1431
1431
  const value = msg.data['currentPositionTiltPercentage'];
1432
- result.tilt = invert ? value : 100 - value;
1432
+ result[postfixWithEndpointName('tilt', msg, model)] = invert ? value : 100 - value;
1433
1433
  }
1434
1434
  return result;
1435
1435
  },
@@ -1574,6 +1574,10 @@ const converters = {
1574
1574
  const result = {};
1575
1575
  const data = msg.data;
1576
1576
 
1577
+ if (data.hasOwnProperty(0x0401)) { // Load
1578
+ result.load = data[0x0401];
1579
+ }
1580
+
1577
1581
  if (data.hasOwnProperty(0x0402)) { // Display text
1578
1582
  result.display_text = data[0x0402];
1579
1583
  }
@@ -3551,24 +3555,6 @@ const converters = {
3551
3555
  return {child_lock: value ? 'LOCK' : 'UNLOCK'};
3552
3556
  case tuya.dataPoints.moesSbattery:
3553
3557
  return {battery: value};
3554
- case tuya.dataPoints.moesSschedule:
3555
- return {
3556
- program_weekday:
3557
- {weekday: ' ' + value[0] + 'h:' + value[1] + 'm ' + value[2]/2 + '°C' +
3558
- ', ' + value[3] + 'h:' + value[4] + 'm ' + value[5]/2 + '°C' +
3559
- ', ' + value[6] + 'h:' + value[7] + 'm ' + value[8]/2 + '°C' +
3560
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[11]/2 + '°C '},
3561
- program_saturday:
3562
- {saturday: '' + value[12] + 'h:' + value[13] + 'm ' + value[14]/2 + '°C' +
3563
- ', ' + value[15] + 'h:' + value[16] + 'm ' + value[17]/2 + '°C' +
3564
- ', ' + value[18] + 'h:' + value[19] + 'm ' + value[20]/2 + '°C' +
3565
- ', ' + value[21] + 'h:' + value[22] + 'm ' + value[23]/2 + '°C '},
3566
- program_sunday:
3567
- {sunday: ' ' + value[24] + 'h:' + value[25] + 'm ' + value[26]/2 + '°C' +
3568
- ', ' + value[27] + 'h:' + value[28] + 'm ' + value[29]/2 + '°C' +
3569
- ', ' + value[30] + 'h:' + value[31] + 'm ' + value[32]/2 + '°C' +
3570
- ', ' + value[33] + 'h:' + value[34] + 'm ' + value[35]/2 + '°C '},
3571
- };
3572
3558
  case tuya.dataPoints.moesSboostHeatingCountdownTimeSet:
3573
3559
  return {boost_heating_countdown_time_set: (value)};
3574
3560
  case tuya.dataPoints.moesSvalvePosition:
@@ -3583,6 +3569,18 @@ const converters = {
3583
3569
  return {max_temperature: value};
3584
3570
  case tuya.dataPoints.moesSminTempSet:
3585
3571
  return {min_temperature: value};
3572
+ case tuya.dataPoints.moesSschedule: {
3573
+ const items = [];
3574
+ const pMode = [];
3575
+ for (let i = 0; i < 12; i++) {
3576
+ const item = {h: value[i*3], m: value[i*3+1], temp: value[i*3+2] / 2};
3577
+ items[i] = item;
3578
+ pMode[i] = item['h'].toString().padStart(2, '0') + ':' +
3579
+ item['m'].toString().padStart(2, '0') + '/' +
3580
+ item['temp'] + '°C';
3581
+ }
3582
+ return {programming_mode: pMode.join(' ')};
3583
+ }
3586
3584
  default:
3587
3585
  meta.logger.warn(`zigbee-herdsman-converters:moesS_thermostat: NOT RECOGNIZED DP #${
3588
3586
  dp} with data ${JSON.stringify(msg.data)}`);
@@ -4295,79 +4293,6 @@ const converters = {
4295
4293
  }
4296
4294
  },
4297
4295
  },
4298
- ikea_air_purifier: {
4299
- cluster: 'manuSpecificIkeaAirPurifier',
4300
- type: ['attributeReport', 'readResponse'],
4301
- options: [exposes.options.precision('pm25'), exposes.options.calibration('pm25')],
4302
- convert: (model, msg, publish, options, meta) => {
4303
- const state = {};
4304
-
4305
- if (msg.data.hasOwnProperty('particulateMatter25Measurement')) {
4306
- const pm25Property = postfixWithEndpointName('pm25', msg, model);
4307
- let pm25 = parseFloat(msg.data['particulateMatter25Measurement']);
4308
-
4309
- // Air Quality Scale (ikea app):
4310
- // 0-35=Good, 35-80=OK, 80+=Not Good
4311
- let airQuality;
4312
- const airQualityProperty = postfixWithEndpointName('air_quality', msg, model);
4313
- if (pm25 <= 35) {
4314
- airQuality = 'good';
4315
- } else if (pm25 <= 80) {
4316
- airQuality = 'ok';
4317
- } else if (pm25 < 65535) {
4318
- airQuality = 'not_good';
4319
- } else {
4320
- airQuality = 'unknown';
4321
- }
4322
-
4323
- // calibrate and round pm25 unless invalid
4324
- pm25 = (pm25 == 65535) ? -1 : calibrateAndPrecisionRoundOptions(pm25, options, 'pm25');
4325
-
4326
- state[pm25Property] = calibrateAndPrecisionRoundOptions(pm25, options, 'pm25');
4327
- state[airQualityProperty] = airQuality;
4328
- }
4329
-
4330
- if (msg.data.hasOwnProperty('filterRunTime')) {
4331
- // Filter needs to be replaced after 6 months
4332
- state['replace_filter'] = (parseInt(msg.data['filterRunTime']) >= 259200);
4333
- }
4334
-
4335
- if (msg.data.hasOwnProperty('controlPanelLight')) {
4336
- state['led_enable'] = (msg.data['controlPanelLight'] == 0);
4337
- }
4338
-
4339
- if (msg.data.hasOwnProperty('childLock')) {
4340
- state['child_lock'] = (msg.data['childLock'] > 0 ? 'LOCK' : 'UNLOCK');
4341
- }
4342
-
4343
- if (msg.data.hasOwnProperty('fanSpeed')) {
4344
- let fanSpeed = msg.data['fanSpeed'];
4345
- if (fanSpeed >= 10) {
4346
- fanSpeed = (((fanSpeed - 5) * 2) / 10);
4347
- } else {
4348
- fanSpeed = 0;
4349
- }
4350
-
4351
- state['fan_speed'] = fanSpeed;
4352
- }
4353
-
4354
- if (msg.data.hasOwnProperty('fanMode')) {
4355
- let fanMode = msg.data['fanMode'];
4356
- if (fanMode >= 10) {
4357
- fanMode = (((fanMode - 5) * 2) / 10).toString();
4358
- } else if (fanMode == 1) {
4359
- fanMode = 'auto';
4360
- } else {
4361
- fanMode = 'off';
4362
- }
4363
-
4364
- state['fan_mode'] = fanMode;
4365
- state['fan_state'] = (fanMode === 'off' ? 'OFF' : 'ON');
4366
- }
4367
-
4368
- return state;
4369
- },
4370
- },
4371
4296
  E1524_E1810_levelctrl: {
4372
4297
  cluster: 'genLevelCtrl',
4373
4298
  type: [
@@ -1379,6 +1379,16 @@ const converters = {
1379
1379
  // #endregion
1380
1380
 
1381
1381
  // #region Non-generic converters
1382
+ elko_load: {
1383
+ key: ['load'],
1384
+ convertSet: async (entity, key, value, meta) => {
1385
+ await entity.write('hvacThermostat', {'elkoLoad': value});
1386
+ return {state: {load: value}};
1387
+ },
1388
+ convertGet: async (entity, key, meta) => {
1389
+ await entity.read('hvacThermostat', ['elkoLoad']);
1390
+ },
1391
+ },
1382
1392
  elko_display_text: {
1383
1393
  key: ['display_text'],
1384
1394
  convertSet: async (entity, key, value, meta) => {
@@ -1912,71 +1922,6 @@ const converters = {
1912
1922
  await entity.read('genBasic', [0x0033], manufacturerOptions.hue);
1913
1923
  },
1914
1924
  },
1915
- ikea_air_purifier_fan_mode: {
1916
- key: ['fan_mode', 'fan_state'],
1917
- convertSet: async (entity, key, value, meta) => {
1918
- if (key == 'fan_state' && value.toLowerCase() == 'on') {
1919
- value = utils.getMetaValue(entity, meta.mapped, 'fanStateOn', 'allEqual', 'on');
1920
- }
1921
-
1922
- let fanMode;
1923
- switch (value.toLowerCase()) {
1924
- case 'off':
1925
- fanMode = 0;
1926
- break;
1927
- case 'auto':
1928
- fanMode = 1;
1929
- break;
1930
- default:
1931
- fanMode = parseInt(((parseInt(value) / 2.0) * 10) + 5);
1932
- }
1933
-
1934
- await entity.write('manuSpecificIkeaAirPurifier', {'fanMode': fanMode}, manufacturerOptions.ikea);
1935
- return {state: {fan_mode: value.toLowerCase(), fan_state: value.toLowerCase() === 'off' ? 'OFF' : 'ON'}};
1936
- },
1937
- convertGet: async (entity, key, meta) => {
1938
- await entity.read('manuSpecificIkeaAirPurifier', ['fanMode']);
1939
- },
1940
- },
1941
- ikea_air_purifier_fan_speed: {
1942
- key: ['fan_speed'],
1943
- convertGet: async (entity, key, meta) => {
1944
- await entity.read('manuSpecificIkeaAirPurifier', ['fanSpeed']);
1945
- },
1946
- },
1947
- ikea_air_purifier_pm25: {
1948
- key: ['pm25', 'air_quality'],
1949
- convertGet: async (entity, key, meta) => {
1950
- await entity.read('manuSpecificIkeaAirPurifier', ['particulateMatter25Measurement']);
1951
- },
1952
- },
1953
- ikea_air_purifier_replace_filter: {
1954
- key: ['replace_filter'],
1955
- convertGet: async (entity, key, meta) => {
1956
- await entity.read('manuSpecificIkeaAirPurifier', ['filterRunTime']);
1957
- },
1958
- },
1959
- ikea_air_purifier_child_lock: {
1960
- key: ['child_lock'],
1961
- convertSet: async (entity, key, value, meta) => {
1962
- await entity.write('manuSpecificIkeaAirPurifier', {'childLock': ((value.toLowerCase() === 'lock') ? 1 : 0)},
1963
- manufacturerOptions.ikea);
1964
- return {state: {child_lock: ((value.toLowerCase() === 'lock') ? 'LOCK' : 'UNLOCK')}};
1965
- },
1966
- convertGet: async (entity, key, meta) => {
1967
- await entity.read('manuSpecificIkeaAirPurifier', ['childLock']);
1968
- },
1969
- },
1970
- ikea_air_purifier_led_enable: {
1971
- key: ['led_enable'],
1972
- convertSet: async (entity, key, value, meta) => {
1973
- await entity.write('manuSpecificIkeaAirPurifier', {'controlPanelLight': ((value) ? 0 : 1)}, manufacturerOptions.ikea);
1974
- return {state: {led_enable: ((value) ? true : false)}};
1975
- },
1976
- convertGet: async (entity, key, meta) => {
1977
- await entity.read('manuSpecificIkeaAirPurifier', ['controlPanelLight']);
1978
- },
1979
- },
1980
1925
  RTCGQ13LM_motion_sensitivity: {
1981
1926
  key: ['motion_sensitivity'],
1982
1927
  convertSet: async (entity, key, value, meta) => {
@@ -2022,7 +1967,7 @@ const converters = {
2022
1967
  xiaomi_switch_power_outage_memory: {
2023
1968
  key: ['power_outage_memory'],
2024
1969
  convertSet: async (entity, key, value, meta) => {
2025
- if (['ZNCZ04LM', 'QBKG25LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'QBKG39LM', 'QBKG41LM', 'ZNCZ15LM',
1970
+ if (['ZNCZ04LM', 'QBKG25LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'DLKZMK12LM', 'QBKG39LM', 'QBKG41LM', 'ZNCZ15LM',
2026
1971
  'WS-EUK01', 'WS-EUK02', 'WS-EUK03', 'WS-EUK04', 'QBKG31LM', 'QBCZ15LM', 'QBKG20LM', 'QBKG38LM',
2027
1972
  'QBKG34LM', 'QBCZ14LM', 'QBKG19LM'].includes(meta.mapped.model)) {
2028
1973
  await entity.write('aqaraOpple', {0x0201: {value: value ? 1 : 0, type: 0x10}}, manufacturerOptions.xiaomi);
@@ -2045,7 +1990,7 @@ const converters = {
2045
1990
  return {state: {power_outage_memory: value}};
2046
1991
  },
2047
1992
  convertGet: async (entity, key, meta) => {
2048
- if (['ZNCZ04LM', 'QBKG25LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'QBKG39LM', 'QBKG41LM', 'ZNCZ15LM',
1993
+ if (['ZNCZ04LM', 'QBKG25LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'DLKZMK12LM', 'QBKG39LM', 'QBKG41LM', 'ZNCZ15LM',
2049
1994
  'WS-EUK02', 'WS-EUK01', 'QBKG31LM', 'QBCZ15LM', 'QBCZ14LM', 'QBKG20LM', 'QBKG34LM', 'QBKG19LM',
2050
1995
  'QBKG38LM'].includes(meta.mapped.model)) {
2051
1996
  await entity.read('aqaraOpple', [0x0201]);
@@ -2830,6 +2775,25 @@ const converters = {
2830
2775
  await tuya.sendDataPointValue(entity, tuya.dataPoints.moesSminTempSet, temp);
2831
2776
  },
2832
2777
  },
2778
+ moesS_thermostat_schedule_programming: {
2779
+ key: ['programming_mode'],
2780
+ convertSet: async (entity, key, value, meta) => {
2781
+ const payload = [];
2782
+ const items = value.split(' ');
2783
+ for (let i = 0; i < 12; i++) {
2784
+ const hourTemperature = items[i].split('/');
2785
+ const hourMinute = hourTemperature[0].split(':', 2);
2786
+ const h = parseInt(hourMinute[0]);
2787
+ const m = parseInt(hourMinute[1]);
2788
+ const temp = parseInt(hourTemperature[1]);
2789
+ if (h < 0 || h >= 24 || m < 0 || m >= 60 || temp < 5 || temp >= 35) {
2790
+ throw new Error('Invalid hour, minute or temperature of:' + items[i]);
2791
+ }
2792
+ payload[i*3] = h; payload[i*3+1] = m; payload[i*3+2] = temp * 2;
2793
+ }
2794
+ return tuya.sendDataPointRaw(entity, tuya.dataPoints.moesSschedule, payload);
2795
+ },
2796
+ },
2833
2797
  tvtwo_thermostat: {
2834
2798
  key: [
2835
2799
  'window_detection', 'frost_protection', 'child_lock',
package/devices/elko.js CHANGED
@@ -27,11 +27,15 @@ module.exports = [
27
27
  vendor: 'ELKO',
28
28
  description: 'ESH Plus Super TR RF PH',
29
29
  fromZigbee: [fz.elko_thermostat, fz.thermostat],
30
- toZigbee: [tz.thermostat_occupied_heating_setpoint, tz.thermostat_occupied_heating_setpoint,
30
+ toZigbee: [tz.thermostat_occupied_heating_setpoint, tz.thermostat_occupied_heating_setpoint, tz.elko_load,
31
31
  tz.elko_display_text, tz.elko_power_status, tz.elko_external_temp, tz.elko_mean_power, tz.elko_child_lock, tz.elko_frost_guard,
32
32
  tz.elko_relay_state, tz.elko_sensor_mode, tz.elko_local_temperature_calibration, tz.elko_max_floor_temp,
33
33
  tz.elko_regulator_mode, tz.elko_regulator_time, tz.elko_night_switching],
34
34
  exposes: [exposes.text('display_text', ea.ALL).withDescription('Displayed text on thermostat display (zone). Max 14 characters'),
35
+ exposes.numeric('load', ea.ALL).withUnit('W')
36
+ .withDescription('Load in W when heating is on (between 0-2000 W). The thermostat uses the value as input to the ' +
37
+ 'mean_power calculation.')
38
+ .withValueMin(0).withValueMax(2000),
35
39
  exposes.binary('regulator_mode', ea.ALL, 'regulator', 'thermostat')
36
40
  .withDescription('Device in regulator or thermostat mode.'),
37
41
  exposes.numeric('regulator_time', ea.ALL).withUnit('min')
@@ -69,6 +73,13 @@ module.exports = [
69
73
  await reporting.thermostatOccupiedHeatingSetpoint(endpoint);
70
74
 
71
75
  // ELKO attributes
76
+ // Load value
77
+ await endpoint.configureReporting('hvacThermostat', [{
78
+ attribute: 'elkoLoad',
79
+ minimumReportInterval: 0,
80
+ maximumReportInterval: constants.repInterval.HOUR,
81
+ reportableChange: null,
82
+ }]);
72
83
  // Power status
73
84
  await endpoint.configureReporting('hvacThermostat', [{
74
85
  attribute: 'elkoPowerStatus',
package/devices/ikea.js CHANGED
@@ -8,6 +8,10 @@ const {repInterval} = require('../lib/constants');
8
8
  const extend = require('../lib/extend');
9
9
  const e = exposes.presets;
10
10
  const ea = exposes.access;
11
+ const herdsman = require('zigbee-herdsman');
12
+ const {
13
+ calibrateAndPrecisionRoundOptions, postfixWithEndpointName, getMetaValue,
14
+ } = require('../lib/utils');
11
15
 
12
16
  const bulbOnEvent = async (type, data, device) => {
13
17
  /**
@@ -49,6 +53,161 @@ const tradfriExtend = {
49
53
  }),
50
54
  };
51
55
 
56
+ const manufacturerOptions = {manufacturerCode: herdsman.Zcl.ManufacturerCode.IKEA_OF_SWEDEN};
57
+
58
+ const ikea = {
59
+ fz: {
60
+ air_purifier: {
61
+ cluster: 'manuSpecificIkeaAirPurifier',
62
+ type: ['attributeReport', 'readResponse'],
63
+ options: [exposes.options.precision('pm25'), exposes.options.calibration('pm25')],
64
+ convert: (model, msg, publish, options, meta) => {
65
+ const state = {};
66
+
67
+ if (msg.data.hasOwnProperty('particulateMatter25Measurement')) {
68
+ const pm25Property = postfixWithEndpointName('pm25', msg, model);
69
+ let pm25 = parseFloat(msg.data['particulateMatter25Measurement']);
70
+
71
+ // Air Quality
72
+ // Scale based on EU AQI (https://www.eea.europa.eu/themes/air/air-quality-index)
73
+ // Using German IAQ labels to match the Develco Air Quality Sensor
74
+ let airQuality;
75
+ const airQualityProperty = postfixWithEndpointName('air_quality', msg, model);
76
+ if (pm25 <= 10) {
77
+ airQuality = 'excellent';
78
+ } else if (pm25 <= 20) {
79
+ airQuality = 'good';
80
+ } else if (pm25 <= 25) {
81
+ airQuality = 'moderate';
82
+ } else if (pm25 <= 50) {
83
+ airQuality = 'poor';
84
+ } else if (pm25 <= 75) {
85
+ airQuality = 'unhealthy';
86
+ } else if (pm25 <= 800) {
87
+ airQuality = 'hazardous';
88
+ } else if (pm25 < 65535) {
89
+ airQuality = 'out_of_range';
90
+ } else {
91
+ airQuality = 'unknown';
92
+ }
93
+
94
+ // calibrate and round pm25 unless invalid
95
+ pm25 = (pm25 == 65535) ? -1 : calibrateAndPrecisionRoundOptions(pm25, options, 'pm25');
96
+
97
+ state[pm25Property] = calibrateAndPrecisionRoundOptions(pm25, options, 'pm25');
98
+ state[airQualityProperty] = airQuality;
99
+ }
100
+
101
+ if (msg.data.hasOwnProperty('filterRunTime')) {
102
+ // Filter needs to be replaced after 6 months
103
+ state['replace_filter'] = (parseInt(msg.data['filterRunTime']) >= 259200);
104
+ }
105
+
106
+ if (msg.data.hasOwnProperty('controlPanelLight')) {
107
+ state['led_enable'] = (msg.data['controlPanelLight'] == 0);
108
+ }
109
+
110
+ if (msg.data.hasOwnProperty('childLock')) {
111
+ state['child_lock'] = (msg.data['childLock'] > 0 ? 'LOCK' : 'UNLOCK');
112
+ }
113
+
114
+ if (msg.data.hasOwnProperty('fanSpeed')) {
115
+ let fanSpeed = msg.data['fanSpeed'];
116
+ if (fanSpeed >= 10) {
117
+ fanSpeed = (((fanSpeed - 5) * 2) / 10);
118
+ } else {
119
+ fanSpeed = 0;
120
+ }
121
+
122
+ state['fan_speed'] = fanSpeed;
123
+ }
124
+
125
+ if (msg.data.hasOwnProperty('fanMode')) {
126
+ let fanMode = msg.data['fanMode'];
127
+ if (fanMode >= 10) {
128
+ fanMode = (((fanMode - 5) * 2) / 10).toString();
129
+ } else if (fanMode == 1) {
130
+ fanMode = 'auto';
131
+ } else {
132
+ fanMode = 'off';
133
+ }
134
+
135
+ state['fan_mode'] = fanMode;
136
+ state['fan_state'] = (fanMode === 'off' ? 'OFF' : 'ON');
137
+ }
138
+
139
+ return state;
140
+ },
141
+ },
142
+ },
143
+ tz: {
144
+ air_purifier_fan_mode: {
145
+ key: ['fan_mode', 'fan_state'],
146
+ convertSet: async (entity, key, value, meta) => {
147
+ if (key == 'fan_state' && value.toLowerCase() == 'on') {
148
+ value = getMetaValue(entity, meta.mapped, 'fanStateOn', 'allEqual', 'on');
149
+ }
150
+
151
+ let fanMode;
152
+ switch (value.toLowerCase()) {
153
+ case 'off':
154
+ fanMode = 0;
155
+ break;
156
+ case 'auto':
157
+ fanMode = 1;
158
+ break;
159
+ default:
160
+ fanMode = parseInt(((parseInt(value) / 2.0) * 10) + 5);
161
+ }
162
+
163
+ await entity.write('manuSpecificIkeaAirPurifier', {'fanMode': fanMode}, manufacturerOptions.ikea);
164
+ return {state: {fan_mode: value.toLowerCase(), fan_state: value.toLowerCase() === 'off' ? 'OFF' : 'ON'}};
165
+ },
166
+ convertGet: async (entity, key, meta) => {
167
+ await entity.read('manuSpecificIkeaAirPurifier', ['fanMode']);
168
+ },
169
+ },
170
+ air_purifier_fan_speed: {
171
+ key: ['fan_speed'],
172
+ convertGet: async (entity, key, meta) => {
173
+ await entity.read('manuSpecificIkeaAirPurifier', ['fanSpeed']);
174
+ },
175
+ },
176
+ air_purifier_pm25: {
177
+ key: ['pm25', 'air_quality'],
178
+ convertGet: async (entity, key, meta) => {
179
+ await entity.read('manuSpecificIkeaAirPurifier', ['particulateMatter25Measurement']);
180
+ },
181
+ },
182
+ air_purifier_replace_filter: {
183
+ key: ['replace_filter'],
184
+ convertGet: async (entity, key, meta) => {
185
+ await entity.read('manuSpecificIkeaAirPurifier', ['filterRunTime']);
186
+ },
187
+ },
188
+ air_purifier_child_lock: {
189
+ key: ['child_lock'],
190
+ convertSet: async (entity, key, value, meta) => {
191
+ await entity.write('manuSpecificIkeaAirPurifier', {'childLock': ((value.toLowerCase() === 'lock') ? 1 : 0)},
192
+ manufacturerOptions);
193
+ return {state: {child_lock: ((value.toLowerCase() === 'lock') ? 'LOCK' : 'UNLOCK')}};
194
+ },
195
+ convertGet: async (entity, key, meta) => {
196
+ await entity.read('manuSpecificIkeaAirPurifier', ['childLock']);
197
+ },
198
+ },
199
+ air_purifier_led_enable: {
200
+ key: ['led_enable'],
201
+ convertSet: async (entity, key, value, meta) => {
202
+ await entity.write('manuSpecificIkeaAirPurifier', {'controlPanelLight': ((value) ? 0 : 1)}, manufacturerOptions);
203
+ return {state: {led_enable: ((value) ? true : false)}};
204
+ },
205
+ convertGet: async (entity, key, meta) => {
206
+ await entity.read('manuSpecificIkeaAirPurifier', ['controlPanelLight']);
207
+ },
208
+ },
209
+ },
210
+ };
52
211
 
53
212
  module.exports = [
54
213
  {
@@ -632,7 +791,9 @@ module.exports = [
632
791
  .withDescription('Current fan speed'),
633
792
  e.pm25().withAccess(ea.STATE_GET),
634
793
  exposes.enum('air_quality', ea.STATE_GET, [
635
- 'good', 'ok', 'not_good', 'unknown',
794
+ 'excellent', 'good', 'moderate', 'poor',
795
+ 'unhealthy', 'hazardous', 'out_of_range',
796
+ 'unknown',
636
797
  ]).withDescription('Measured air quality'),
637
798
  exposes.binary('led_enable', ea.ALL, true, false).withDescription('Enabled LED'),
638
799
  exposes.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'),
@@ -640,29 +801,28 @@ module.exports = [
640
801
  .withDescription('Filter is older than 6 months and needs replacing'),
641
802
  ],
642
803
  meta: {fanStateOn: 'auto'},
643
- fromZigbee: [fz.ikea_air_purifier],
804
+ fromZigbee: [ikea.fz.air_purifier],
644
805
  toZigbee: [
645
- tz.ikea_air_purifier_fan_mode, tz.ikea_air_purifier_fan_speed,
646
- tz.ikea_air_purifier_pm25, tz.ikea_air_purifier_child_lock, tz.ikea_air_purifier_led_enable,
647
- tz.ikea_air_purifier_replace_filter,
806
+ ikea.tz.air_purifier_fan_mode, ikea.tz.air_purifier_fan_speed,
807
+ ikea.tz.air_purifier_pm25, ikea.tz.air_purifier_child_lock, ikea.tz.air_purifier_led_enable,
808
+ ikea.tz.air_purifier_replace_filter,
648
809
  ],
649
810
  configure: async (device, coordinatorEndpoint, logger) => {
650
- const options = {manufacturerCode: 0x117c};
651
811
  const endpoint = device.getEndpoint(1);
652
812
 
653
813
  await reporting.bind(endpoint, coordinatorEndpoint, ['manuSpecificIkeaAirPurifier']);
654
814
  await endpoint.configureReporting('manuSpecificIkeaAirPurifier', [{attribute: 'particulateMatter25Measurement',
655
815
  minimumReportInterval: repInterval.MINUTE, maximumReportInterval: repInterval.HOUR, reportableChange: 1}],
656
- options);
816
+ manufacturerOptions);
657
817
  await endpoint.configureReporting('manuSpecificIkeaAirPurifier', [{attribute: 'filterRunTime',
658
818
  minimumReportInterval: repInterval.HOUR, maximumReportInterval: repInterval.HOUR, reportableChange: 0}],
659
- options);
819
+ manufacturerOptions);
660
820
  await endpoint.configureReporting('manuSpecificIkeaAirPurifier', [{attribute: 'fanMode',
661
821
  minimumReportInterval: 0, maximumReportInterval: repInterval.HOUR, reportableChange: 1}],
662
- options);
822
+ manufacturerOptions);
663
823
  await endpoint.configureReporting('manuSpecificIkeaAirPurifier', [{attribute: 'fanSpeed',
664
824
  minimumReportInterval: 0, maximumReportInterval: repInterval.HOUR, reportableChange: 1}],
665
- options);
825
+ manufacturerOptions);
666
826
 
667
827
  await endpoint.read('manuSpecificIkeaAirPurifier', ['controlPanelLight', 'childLock', 'filterRunTime']);
668
828
  },
@@ -41,7 +41,7 @@ module.exports = [
41
41
  exposes: [e.cover_position().setAccess('state', ea.ALL), e.temperature(), e.battery(), e.pressure()],
42
42
  },
43
43
  {
44
- zigbeeModel: ['SV02-410-MP-1.3', 'SV02-610-MP-1.3', 'SV02-612-MP-1.3', 'SV02-410-MP-1.0'],
44
+ zigbeeModel: ['SV02-410-MP-1.3', 'SV02-412-MP-1.3', 'SV02-610-MP-1.3', 'SV02-612-MP-1.3', 'SV02-410-MP-1.0'],
45
45
  model: 'SV02',
46
46
  vendor: 'Keen Home',
47
47
  description: 'Smart vent',
package/devices/moes.js CHANGED
@@ -202,7 +202,8 @@ module.exports = [
202
202
  tz.moesS_thermostat_boost_heating_countdown, tz.moesS_thermostat_system_mode,
203
203
  tz.moesS_thermostat_boost_heating, tz.moesS_thermostat_boostHeatingCountdownTimeSet,
204
204
  tz.moesS_thermostat_eco_temperature, tz.moesS_thermostat_max_temperature,
205
- tz.moesS_thermostat_min_temperature, tz.moesS_thermostat_moesSecoMode],
205
+ tz.moesS_thermostat_min_temperature, tz.moesS_thermostat_moesSecoMode,
206
+ tz.moesS_thermostat_schedule_programming],
206
207
  exposes: [
207
208
  e.battery(), e.child_lock(), e.eco_mode(), e.eco_temperature(), e.max_temperature(), e.min_temperature(),
208
209
  e.position(), e.window_detection(),
@@ -218,11 +219,9 @@ module.exports = [
218
219
  'to 15 degrees Celsius. After 10 days, the device will automatically switch to programming mode. ' +
219
220
  'TEMPORARY MANUAL MODE - In this mode, ☝ icon will flash. At this time, the device executes the manually set ' +
220
221
  'temperature and returns to the weekly programming mode in the next time period. '),
221
- exposes.composite('programming_mode').withDescription('PROGRAMMING MODE ⏱ - In this mode, ' +
222
- 'the device executes a preset week programming temperature time and temperature. ')
223
- .withFeature(exposes.text('program_weekday', ea.STATE))
224
- .withFeature(exposes.text('program_saturday', ea.STATE))
225
- .withFeature(exposes.text('program_sunday', ea.STATE)),
222
+ exposes.text('programming_mode', ea.STATE_SET).withDescription('PROGRAMMING MODE ⏱ - In this mode, ' +
223
+ 'the device executes a preset week programming temperature time and temperature. ' +
224
+ 'You can set up to 4 stages of temperature every for WEEKDAY ➀➁➂➃➄, SATURDAY ➅ and SUNDAY ➆.'),
226
225
  exposes.binary('boost_heating', ea.STATE_SET, 'ON', 'OFF').withDescription('Boost Heating: press and hold "+" for 3 seconds, ' +
227
226
  'the device will enter the boost heating mode, and the ▷╵◁ will flash. The countdown will be displayed in the APP'),
228
227
  exposes.numeric('boost_heating_countdown', ea.STATE_SET).withUnit('min').withDescription('Countdown in minutes'),
package/devices/osram.js CHANGED
@@ -30,6 +30,14 @@ module.exports = [
30
30
  extend: extend.ledvance.light_onoff_brightness_colortemp_color(),
31
31
  ota: ota.ledvance,
32
32
  },
33
+ {
34
+ zigbeeModel: ['LIGHTIFY RT RGBW'],
35
+ model: '73741_LIGHTIFY',
36
+ vendor: 'OSRAM',
37
+ description: 'LIGHTIFY RT5/6 LED',
38
+ extend: extend.ledvance.light_onoff_brightness_colortemp_color(),
39
+ ota: ota.ledvance,
40
+ },
33
41
  {
34
42
  zigbeeModel: ['Classic A60 RGBW'],
35
43
  model: 'AA69697',
@@ -735,6 +735,15 @@ module.exports = [
735
735
  extend: hueExtend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 500]}),
736
736
  ota: ota.zigbeeOTA,
737
737
  },
738
+ {
739
+ zigbeeModel: ['LCA009'],
740
+ model: '9290024717',
741
+ vendor: 'Philips',
742
+ description: 'Hue white and color ambiance E26/A19 1600lm',
743
+ meta: {turnsOffAtBrightness1: true},
744
+ extend: hueExtend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 500]}),
745
+ ota: ota.zigbeeOTA,
746
+ },
738
747
  {
739
748
  zigbeeModel: ['LCT001', 'LCT007', 'LCT010', 'LCT012', 'LCT014', 'LCT015', 'LCT016', 'LCT021'],
740
749
  model: '9290012573A',
@@ -1306,7 +1315,7 @@ module.exports = [
1306
1315
  vendor: 'Philips',
1307
1316
  description: 'Hue Iris (generation 2, black)',
1308
1317
  meta: {turnsOffAtBrightness1: true},
1309
- extend: hueExtend.light_onoff_brightness_color(),
1318
+ extend: hueExtend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 500]}),
1310
1319
  ota: ota.zigbeeOTA,
1311
1320
  },
1312
1321
  {
@@ -1408,6 +1417,15 @@ module.exports = [
1408
1417
  extend: hueExtend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 500]}),
1409
1418
  ota: ota.zigbeeOTA,
1410
1419
  },
1420
+ {
1421
+ zigbeeModel: ['LCT020'],
1422
+ model: '4080148P7',
1423
+ vendor: 'Philips',
1424
+ description: 'Hue Signe table light',
1425
+ meta: {turnsOffAtBrightness1: true},
1426
+ extend: hueExtend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 500]}),
1427
+ ota: ota.zigbeeOTA,
1428
+ },
1411
1429
  {
1412
1430
  zigbeeModel: ['4080148P9'],
1413
1431
  model: '4080148P9',
@@ -2104,7 +2122,7 @@ module.exports = [
2104
2122
  vendor: 'Philips',
2105
2123
  description: 'Hue Play gradient lightstrip 65',
2106
2124
  meta: {turnsOffAtBrightness1: true},
2107
- extend: hueExtend.light_onoff_brightness_colortemp({colorTempRange: [153, 500]}),
2125
+ extend: hueExtend.light_onoff_brightness_colortemp_color({colorTempRange: [153, 500]}),
2108
2126
  ota: ota.zigbeeOTA,
2109
2127
  },
2110
2128
  {
package/devices/tuya.js CHANGED
@@ -976,7 +976,8 @@ module.exports = [
976
976
  {modelID: 'TS011F', manufacturerName: '_TZ3000_ps3dmato'},
977
977
  {modelID: 'TS011F', manufacturerName: '_TZ3000_mraovvmm'},
978
978
  {modelID: 'TS011F', manufacturerName: '_TZ3000_jvzvulen'},
979
- {modelID: 'TS011F', manufacturerName: '_TZ3000_w0qqde0g'}],
979
+ {modelID: 'TS011F', manufacturerName: '_TZ3000_w0qqde0g'},
980
+ {modelID: 'TS011F', manufacturerName: '_TZ3000_nfnmi125'}],
980
981
  model: 'TS011F_plug_1',
981
982
  description: 'Smart plug (with power monitoring)',
982
983
  vendor: 'TuYa',
@@ -1013,11 +1014,15 @@ module.exports = [
1013
1014
  .withDescription('Recover state after power outage')],
1014
1015
  },
1015
1016
  {
1016
- fingerprint: [{modelID: 'TS011F', manufacturerName: '_TZ3000_dpo1ysak'},
1017
- {modelID: 'TS011F', manufacturerName: '_TZ3000_cphmq0q7', applicationVersion: 69}],
1017
+ fingerprint: [
1018
+ {modelID: 'TS011F', manufacturerName: '_TZ3000_dpo1ysak'},
1019
+ {modelID: 'TS011F', manufacturerName: '_TZ3000_cphmq0q7', applicationVersion: 69},
1020
+ {modelID: 'TS011F', manufacturerName: '_TZ3000_5f43h46b'},
1021
+ ],
1018
1022
  model: 'TS011F_plug_3',
1019
1023
  description: 'Smart plug (with power monitoring by polling)',
1020
1024
  vendor: 'TuYa',
1025
+ whiteLabel: [{vendor: 'VIKEFON', model: 'TS011F'}],
1021
1026
  fromZigbee: [fz.on_off, fz.electrical_measurement, fz.metering, fz.ignore_basic_report, fz.tuya_switch_power_outage_memory],
1022
1027
  toZigbee: [tz.on_off, tz.tuya_switch_power_outage_memory],
1023
1028
  configure: async (device, coordinatorEndpoint, logger) => {
package/devices/xiaomi.js CHANGED
@@ -1357,7 +1357,7 @@ module.exports = [
1357
1357
  await reporting.batteryVoltage(endpoint);
1358
1358
  await reporting.illuminance(endpoint, {min: 15, max: constants.repInterval.HOUR, change: 500});
1359
1359
  },
1360
- exposes: [e.battery(), e.illuminance(), e.illuminance_lux()],
1360
+ exposes: [e.battery(), e.battery_voltage(), e.illuminance(), e.illuminance_lux()],
1361
1361
  },
1362
1362
  {
1363
1363
  zigbeeModel: ['lumi.light.rgbac1'],
@@ -1426,6 +1426,22 @@ module.exports = [
1426
1426
  device.save();
1427
1427
  },
1428
1428
  },
1429
+ {
1430
+ zigbeeModel: ['lumi.switch.l0acn1'],
1431
+ model: 'DLKZMK12LM',
1432
+ vendor: 'Xiaomi',
1433
+ description: 'Aqara single switch module T1 (without neutral). Doesn\'t work as a router and doesn\'t support power meter',
1434
+ fromZigbee: [fz.on_off, fz.xiaomi_switch_type, fz.xiaomi_switch_power_outage_memory],
1435
+ exposes: [e.switch(), e.power_outage_memory(), e.switch_type()],
1436
+ toZigbee: [tz.xiaomi_switch_type, tz.on_off, tz.xiaomi_switch_power_outage_memory],
1437
+ configure: async (device, coordinatorEndpoint, logger) => {
1438
+ const endpoint = device.getEndpoint(1);
1439
+ await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']);
1440
+ await reporting.onOff(endpoint);
1441
+ device.powerSource = 'Mains (single phase)';
1442
+ device.save();
1443
+ },
1444
+ },
1429
1445
  {
1430
1446
  zigbeeModel: ['lumi.switch.n4acn4'],
1431
1447
  model: 'ZNCJMB14LM',
package/lib/exposes.js CHANGED
@@ -503,7 +503,7 @@ module.exports = {
503
503
  battery: () => new Numeric('battery', access.STATE).withUnit('%').withDescription('Remaining battery in %').withValueMin(0).withValueMax(100),
504
504
  battery_low: () => new Binary('battery_low', access.STATE, true, false).withDescription('Indicates if the battery of this device is almost empty'),
505
505
  battery_voltage: () => new Numeric('voltage', access.STATE).withUnit('mV').withDescription('Voltage of the battery in millivolts'),
506
- boost_time: () => new Numeric('boost_time', access.STATE_SET).withUnit('s').withDescription('Boost time'),
506
+ boost_time: () => new Numeric('boost_time', access.STATE_SET).withUnit('s').withDescription('Boost time').withValueMin(0).withValueMax(600),
507
507
  button_lock: () => new Binary('button_lock', access.ALL, 'ON', 'OFF').withDescription('Disables the physical switch button'),
508
508
  carbon_monoxide: () => new Binary('carbon_monoxide', access.STATE, true, false).withDescription('Indicates if CO (carbon monoxide) is detected'),
509
509
  child_lock: () => new Lock().withState('child_lock', 'LOCK', 'UNLOCK', 'Enables/disables physical input on the device', access.STATE_SET),
package/lib/utils.js CHANGED
@@ -72,7 +72,7 @@ function hasAlreadyProcessedMessage(msg, ID=null, key=null) {
72
72
  return false;
73
73
  }
74
74
 
75
- const defaultPrecision = {temperature: 2, humidity: 2, pressure: 1, pm25: 2};
75
+ const defaultPrecision = {temperature: 2, humidity: 2, pressure: 1, pm25: 0};
76
76
  function calibrateAndPrecisionRoundOptions(number, options, type) {
77
77
  // Calibrate
78
78
  const calibrateKey = `${type}_calibration`;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigbee-herdsman-converters",
3
- "version": "14.0.319",
3
+ "version": "14.0.323",
4
4
  "lockfileVersion": 1,
5
5
  "requires": true,
6
6
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigbee-herdsman-converters",
3
- "version": "14.0.319",
3
+ "version": "14.0.323",
4
4
  "description": "Collection of device converters to be used with zigbee-herdsman",
5
5
  "main": "index.js",
6
6
  "files": [