zigbee-herdsman-converters 15.0.24 → 15.0.26

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.
package/devices/bosch.js CHANGED
@@ -410,7 +410,7 @@ const definition = [
410
410
  exposes.climate()
411
411
  .withLocalTemperature(ea.STATE)
412
412
  .withSetpoint('occupied_heating_setpoint', 5, 30, 0.5)
413
- .withLocalTemperatureCalibration(-12, 12, 0.5)
413
+ .withLocalTemperatureCalibration(-5, 5, 0.1)
414
414
  .withSystemMode(['off', 'heat', 'auto'])
415
415
  .withPiHeatingDemand(ea.STATE),
416
416
  exposes.binary('boost', ea.ALL, 'ON', 'OFF')
package/devices/diyruz.js CHANGED
@@ -63,7 +63,8 @@ module.exports = [
63
63
  vendor: 'DIYRuZ',
64
64
  description: '[DiY 8/12/20 button keypad](http://modkam.ru/?p=1114)',
65
65
  fromZigbee: [fz.diyruz_freepad_clicks, fz.diyruz_freepad_config, fz.battery],
66
- exposes: [e.battery(), e.action(['*_single', '*_double', '*_triple', '*_quadruple', '*_release'])].concat(((enpoinsCount) => {
66
+ exposes: [e.battery(),
67
+ e.action(['*_single', '*_double', '*_triple', '*_quadruple', '*_release', '*_hold'])].concat(((enpoinsCount) => {
67
68
  const features = [];
68
69
  for (let i = 1; i <= enpoinsCount; i++) {
69
70
  const epName = `button_${i}`;
@@ -102,7 +102,7 @@ module.exports = [
102
102
  extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [153, 454]}),
103
103
  },
104
104
  {
105
- zigbeeModel: ['915005996801', '915005996901'],
105
+ zigbeeModel: ['915005996801', '915005996901', '929003574401'],
106
106
  model: '915005996901',
107
107
  vendor: 'Philips',
108
108
  description: 'Hue white ambiance ceiling light Enrave L with Bluetooth',
@@ -143,6 +143,13 @@ module.exports = [
143
143
  description: 'Hue white ambiance suspension Cher with bluetooth 3000lm',
144
144
  extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [153, 500]}),
145
145
  },
146
+ {
147
+ zigbeeModel: ['4076130P6'],
148
+ model: '4076130P6',
149
+ vendor: 'Philips',
150
+ description: 'Hue white ambiance suspension Cher with bluetooth 3000lm',
151
+ extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [153, 500]}),
152
+ },
146
153
  {
147
154
  zigbeeModel: ['929003054301'],
148
155
  model: '929003054301',
@@ -1568,6 +1575,13 @@ module.exports = [
1568
1575
  description: 'Hue Gradient Signe floor lamp (wood)',
1569
1576
  extend: philips.extend.light_onoff_brightness_colortemp_color_gradient({colorTempRange: [153, 500]}),
1570
1577
  },
1578
+ {
1579
+ zigbeeModel: ['929003516101'],
1580
+ model: '929003516101',
1581
+ vendor: 'Philips',
1582
+ description: 'Hue Gradient Signe floor lamp (wood)',
1583
+ extend: philips.extend.light_onoff_brightness_colortemp_color_gradient({colorTempRange: [153, 500]}),
1584
+ },
1571
1585
  {
1572
1586
  zigbeeModel: ['LCT020'],
1573
1587
  model: '4080148P7',
@@ -2217,6 +2231,13 @@ module.exports = [
2217
2231
  description: 'Hue white filament Edison ST19 E26 LED warm-to-cool',
2218
2232
  extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [222, 454]}),
2219
2233
  },
2234
+ {
2235
+ zigbeeModel: ['LTV005'],
2236
+ model: '9290029808',
2237
+ vendor: 'Philips',
2238
+ description: 'Hue white filament Edison ST23 E26 LED warm-to-cool',
2239
+ extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [222, 454]}),
2240
+ },
2220
2241
  {
2221
2242
  zigbeeModel: ['LTO004'],
2222
2243
  model: '9290024785',
@@ -2809,6 +2830,13 @@ module.exports = [
2809
2830
  description: 'Hue white ambiance pendant white Enrave',
2810
2831
  extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [153, 454]}),
2811
2832
  },
2833
+ {
2834
+ zigbeeModel: ['929003054501'],
2835
+ model: '929003054501',
2836
+ vendor: 'Philips',
2837
+ description: 'Hue white ambiance Fair ceiling with Bluetooth (white)',
2838
+ extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [153, 454]}),
2839
+ },
2812
2840
  {
2813
2841
  zigbeeModel: ['929003054601'],
2814
2842
  model: '929003054601',
@@ -2816,6 +2844,13 @@ module.exports = [
2816
2844
  description: 'Hue white ambiance Fair ceiling with Bluetooth white',
2817
2845
  extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [153, 454]}),
2818
2846
  },
2847
+ {
2848
+ zigbeeModel: ['929003054701'],
2849
+ model: '929003054701',
2850
+ vendor: 'Philips',
2851
+ description: 'Hue white ambiance Fair ceiling with Bluetooth (black)',
2852
+ extend: philips.extend.light_onoff_brightness_colortemp({colorTempRange: [153, 454]}),
2853
+ },
2819
2854
  {
2820
2855
  zigbeeModel: ['929003048301_01', '929003048301_02', '929003048301_03', '929003048301_04'],
2821
2856
  model: '8719514339163',
@@ -54,7 +54,7 @@ module.exports = [
54
54
  fingerprint: [{modelID: 'Dimmer-Switch-ZB3.0', manufacturerName: 'Samotech'}, {modelID: 'HK_DIM_A', manufacturerName: 'Samotech'}],
55
55
  model: 'SM323',
56
56
  vendor: 'Samotech',
57
- description: 'ZigBee retrofit dimmer 250W',
57
+ description: 'Zigbee retrofit dimmer 250W',
58
58
  extend: extend.light_onoff_brightness({noConfigure: true}),
59
59
  configure: async (device, coordinatorEndpoint, logger) => {
60
60
  await extend.light_onoff_brightness().configure(device, coordinatorEndpoint, logger);
@@ -64,4 +64,19 @@ module.exports = [
64
64
  await reporting.brightness(endpoint);
65
65
  },
66
66
  },
67
+ {
68
+ zigbeeModel: ['SM324'],
69
+ model: 'SM324',
70
+ vendor: 'Samotech',
71
+ description: '220V Zigbee CCT LED dimmer',
72
+ extend: extend.light_onoff_brightness_colortemp({noConfigure: true, colorTempRange: [150, 500]}),
73
+ configure: async (device, coordinatorEndpoint, logger) => {
74
+ await extend.light_onoff_brightness_colortemp().configure(device, coordinatorEndpoint, logger);
75
+ const endpoint = device.getEndpoint(1);
76
+ await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'genLevelCtrl', 'lightingColorCtrl']);
77
+ await reporting.onOff(endpoint);
78
+ await reporting.brightness(endpoint);
79
+ await reporting.colorTemperature(endpoint);
80
+ },
81
+ },
67
82
  ];
@@ -144,8 +144,18 @@ module.exports = [
144
144
  model: 'Z01-A19NAE26',
145
145
  vendor: 'Sengled',
146
146
  description: 'Element plus (A19)',
147
- extend: sengledExtend.light_onoff_brightness_colortemp(),
147
+ fromZigbee: sengledExtend.light_onoff_brightness_colortemp_color().fromZigbee.concat([fz.metering]),
148
+ toZigbee: sengledExtend.light_onoff_brightness_colortemp_color().toZigbee,
148
149
  ota: ota.zigbeeOTA,
150
+ configure: async (device, coordinatorEndpoint, logger) => {
151
+ await sengledExtend.light_onoff_brightness_colortemp_color().configure(device, coordinatorEndpoint, logger);
152
+ const endpoint = device.getEndpoint(1);
153
+ await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']);
154
+ await reporting.readMeteringMultiplierDivisor(endpoint);
155
+ await reporting.currentSummDelivered(endpoint);
156
+ await reporting.instantaneousDemand(endpoint);
157
+ },
158
+ exposes: sengledExtend.light_onoff_brightness_colortemp_color().exposes.concat([e.power(), e.energy()]),
149
159
  },
150
160
  {
151
161
  zigbeeModel: ['Z01-A60EAE27'],
package/devices/sonoff.js CHANGED
@@ -177,21 +177,16 @@ module.exports = [
177
177
  model: 'SNZB-02D',
178
178
  vendor: 'SONOFF',
179
179
  description: 'Temperature and humidity sensor with screen',
180
- exposes: [e.battery(), e.temperature(), e.humidity(), e.battery_voltage()],
180
+ exposes: [e.battery(), e.temperature(), e.humidity()],
181
181
  fromZigbee: [fz.temperature, fz.humidity, fz.battery],
182
182
  toZigbee: [],
183
183
  configure: async (device, coordinatorEndpoint, logger) => {
184
- try {
185
- const endpoint = device.getEndpoint(1);
186
- const bindClusters = ['msTemperatureMeasurement', 'msRelativeHumidity', 'genPowerCfg'];
187
- await reporting.bind(endpoint, coordinatorEndpoint, bindClusters);
188
- await reporting.temperature(endpoint, {min: 5, max: constants.repInterval.MINUTES_30, change: 20});
189
- await reporting.humidity(endpoint);
190
- await reporting.batteryVoltage(endpoint);
191
- await reporting.batteryPercentageRemaining(endpoint);
192
- } catch (e) {/* Not required for all: https://github.com/Koenkk/zigbee2mqtt/issues/5562 */
193
- logger.error(`Configure failed: ${e}`);
194
- }
184
+ const endpoint = device.getEndpoint(1);
185
+ const bindClusters = ['msTemperatureMeasurement', 'msRelativeHumidity', 'genPowerCfg'];
186
+ await reporting.bind(endpoint, coordinatorEndpoint, bindClusters);
187
+ await reporting.temperature(endpoint, {min: 5, max: constants.repInterval.MINUTES_30, change: 20});
188
+ await reporting.humidity(endpoint);
189
+ await reporting.batteryPercentageRemaining(endpoint, {min: 3600, max: 7200});
195
190
  },
196
191
  },
197
192
  {
package/devices/tuya.js CHANGED
@@ -1321,14 +1321,25 @@ module.exports = [
1321
1321
  fromZigbee: [tuya.fz.datapoints],
1322
1322
  toZigbee: [tuya.tz.datapoints],
1323
1323
  configure: tuya.configureMagicPacket,
1324
- exposes: [tuya.exposes.lightBrightness().withEndpoint('l1'), tuya.exposes.lightBrightness().withEndpoint('l2')],
1324
+ exposes: [
1325
+ tuya.exposes.lightBrightnessWithMinMax().withEndpoint('l1'),
1326
+ tuya.exposes.lightBrightnessWithMinMax().withEndpoint('l2'),
1327
+ tuya.exposes.countdown().withEndpoint('l1'),
1328
+ tuya.exposes.countdown().withEndpoint('l2'),
1329
+ ],
1325
1330
  meta: {
1326
1331
  multiEndpoint: true,
1327
1332
  tuyaDatapoints: [
1328
1333
  [1, 'state_l1', tuya.valueConverter.onOff, {skip: tuya.skip.stateOnAndBrightnessPresent}],
1329
1334
  [2, 'brightness_l1', tuya.valueConverter.scale0_254to0_1000],
1335
+ [3, 'min_brightness_l1', tuya.valueConverter.scale0_254to0_1000],
1336
+ [5, 'max_brightness_l1', tuya.valueConverter.scale0_254to0_1000],
1337
+ [6, 'countdown_l1', tuya.valueConverter.countdown],
1330
1338
  [7, 'state_l2', tuya.valueConverter.onOff, {skip: tuya.skip.stateOnAndBrightnessPresent}],
1331
1339
  [8, 'brightness_l2', tuya.valueConverter.scale0_254to0_1000],
1340
+ [9, 'min_brightness_l2', tuya.valueConverter.scale0_254to0_1000],
1341
+ [11, 'max_brightness_l2', tuya.valueConverter.scale0_254to0_1000],
1342
+ [12, 'countdown_l2', tuya.valueConverter.countdown],
1332
1343
  ],
1333
1344
  },
1334
1345
  endpoint: (device) => {
@@ -2027,8 +2038,7 @@ module.exports = [
2027
2038
  },
2028
2039
  },
2029
2040
  {
2030
- fingerprint: [{modelID: 'TS0003', manufacturerName: '_TZ3000_vsasbzkf'},
2031
- {modelID: 'TS0003', manufacturerName: '_TZ3000_odzoiovu'}],
2041
+ fingerprint: tuya.fingerprint('TS0003', ['_TZ3000_vsasbzkf', '_TZ3000_odzoiovu', '_TZ3000_4o16jdca']),
2032
2042
  model: 'TS0003_switch_module',
2033
2043
  vendor: 'TuYa',
2034
2044
  description: '3 gang switch module',
package/devices/xiaomi.js CHANGED
@@ -10,6 +10,8 @@ const ea = exposes.access;
10
10
  const globalStore = require('../lib/store');
11
11
  const xiaomi = require('../lib/xiaomi');
12
12
  const utils = require('../lib/utils');
13
+ const {printNumberAsHex, printNumbersAsHexSequence} = utils;
14
+ const {fp1, manufacturerCode} = xiaomi;
13
15
 
14
16
  const xiaomiExtend = {
15
17
  light_onoff_brightness_colortemp: (options={disableColorTempStartup: true}) => ({
@@ -211,6 +213,73 @@ const fzLocal = {
211
213
  return result;
212
214
  },
213
215
  },
216
+ aqara_fp1_region_events: {
217
+ cluster: 'aqaraOpple',
218
+ type: ['attributeReport', 'readResponse'],
219
+ convert: (model, msg, publish, options, meta) => {
220
+ /**
221
+ * @type {{ region_event?: string; }}
222
+ */
223
+ const payload = {};
224
+ const log = utils.createLogger(meta.logger, 'xiaomi', 'aqara_fp1');
225
+
226
+ Object.entries(msg.data).forEach(([key, value]) => {
227
+ const eventKey = parseInt(key);
228
+ const eventKeyHex = printNumberAsHex(eventKey, 4);
229
+
230
+ switch (eventKey) {
231
+ case fp1.constants.region_event_key: {
232
+ if (
233
+ !Buffer.isBuffer(value) ||
234
+ !(typeof value[0] === 'string' || typeof value[0] === 'number') ||
235
+ !(typeof value[1] === 'string' || typeof value[1] === 'number')
236
+ ) {
237
+ log('warn', `region_event: Unrecognized payload structure '${JSON.stringify(value)}'`);
238
+ break;
239
+ }
240
+
241
+ /**
242
+ * @type {[ regionId: number | string, eventTypeCode: number | string ]}
243
+ */
244
+ const [regionIdRaw, eventTypeCodeRaw] = value;
245
+ const regionId = parseInt(regionIdRaw, 10);
246
+ const eventTypeCode = parseInt(eventTypeCodeRaw, 10);
247
+
248
+ if (Number.isNaN(regionId)) {
249
+ log('warn', `region_event: Invalid regionId "${regionIdRaw}"`);
250
+ break;
251
+ }
252
+ if (!Object.values(fp1.constants.region_event_types).includes(eventTypeCode)) {
253
+ log('warn', `region_event: Unknown region event type "${eventTypeCode}"`);
254
+ break;
255
+ }
256
+
257
+ const eventTypeName = fp1.mappers.aqara_fp1.region_event_type_names[eventTypeCode];
258
+ log('debug', `region_event: Triggered event (region "${regionId}", type "${eventTypeName}")`);
259
+ payload.region_event = `region_${regionId}_${eventTypeName}`;
260
+ break;
261
+ }
262
+ case 0xf7: {
263
+ const valueHexSequence = printNumbersAsHexSequence(value, 2);
264
+ log('debug', `Unhandled key ${eventKeyHex} = ${valueHexSequence}`);
265
+ break;
266
+ }
267
+ case 0x0142:
268
+ case 0x0143:
269
+ case 0x0144:
270
+ case 0x0146: {
271
+ log('debug', `Unhandled key ${eventKeyHex} = ${value}`);
272
+ break;
273
+ }
274
+ default: {
275
+ log('warn', `Unknown key ${eventKeyHex} = ${value}`);
276
+ }
277
+ }
278
+ });
279
+
280
+ return payload;
281
+ },
282
+ },
214
283
  };
215
284
 
216
285
  const tzLocal = {
@@ -415,6 +484,113 @@ const tzLocal = {
415
484
  return {state: {[key]: value}};
416
485
  },
417
486
  },
487
+ aqara_fp1_region_upsert: {
488
+ key: ['region_upsert'],
489
+ convertSet: async (entity, key, value, meta) => {
490
+ const log = utils.createLogger(meta.logger, 'xiaomi', 'aqara_fp1:region_upsert');
491
+ const commandWrapper = fp1.parseAqaraFp1RegionUpsertInput(value);
492
+
493
+ if (!commandWrapper.isSuccess) {
494
+ log('warn',
495
+ `encountered an error (${commandWrapper.error.reason}) ` +
496
+ `while parsing configuration commands (input: ${JSON.stringify(value)})`,
497
+ );
498
+
499
+ return;
500
+ }
501
+
502
+ const command = commandWrapper.payload.command;
503
+
504
+ log('debug', `trying to create region ${command.region_id}`);
505
+
506
+ /** @type {Record<string, Set<number>>} */
507
+ const sortedZonesAccumulator = {};
508
+ const sortedZones = command.zones
509
+ .reduce(
510
+ (accumulator, zone) => {
511
+ if (!accumulator[zone.y]) {
512
+ accumulator[zone.y] = new Set();
513
+ }
514
+
515
+ accumulator[zone.y].add(zone.x);
516
+
517
+ return accumulator;
518
+ },
519
+ sortedZonesAccumulator,
520
+ );
521
+
522
+ const deviceConfig = new Uint8Array(7);
523
+
524
+ // Command parameters
525
+ deviceConfig[0] = fp1.constants.region_config_cmds.create;
526
+ deviceConfig[1] = command.region_id;
527
+ deviceConfig[6] = fp1.constants.region_config_cmd_suffix_upsert;
528
+ // Zones definition
529
+ deviceConfig[2] |= fp1.encodeXCellsDefinition(sortedZones['1']);
530
+ deviceConfig[2] |= fp1.encodeXCellsDefinition(sortedZones['2']) << 4;
531
+ deviceConfig[3] |= fp1.encodeXCellsDefinition(sortedZones['3']);
532
+ deviceConfig[3] |= fp1.encodeXCellsDefinition(sortedZones['4']) << 4;
533
+ deviceConfig[4] |= fp1.encodeXCellsDefinition(sortedZones['5']);
534
+ deviceConfig[4] |= fp1.encodeXCellsDefinition(sortedZones['6']) << 4;
535
+ deviceConfig[5] |= fp1.encodeXCellsDefinition(sortedZones['7']);
536
+
537
+ log('info', `create region ${command.region_id} ${printNumbersAsHexSequence([...deviceConfig], 2)}`);
538
+
539
+ const payload = {
540
+ [fp1.constants.region_config_write_attribute]: {
541
+ value: deviceConfig,
542
+ type: fp1.constants.region_config_write_attribute_type,
543
+ },
544
+ };
545
+
546
+ await entity.write('aqaraOpple', payload, {manufacturerCode});
547
+ },
548
+ },
549
+ aqara_fp1_region_delete: {
550
+ key: ['region_delete'],
551
+ convertSet: async (entity, key, value, meta) => {
552
+ const log = utils.createLogger(meta.logger, 'xiaomi', 'aqara_fp1:region_delete');
553
+ const commandWrapper = fp1.parseAqaraFp1RegionDeleteInput(value);
554
+
555
+ if (!commandWrapper.isSuccess) {
556
+ log('warn',
557
+ `encountered an error (${commandWrapper.error.reason}) ` +
558
+ `while parsing configuration commands (input: ${JSON.stringify(value)})`,
559
+ );
560
+ return;
561
+ }
562
+
563
+ const command = commandWrapper.payload.command;
564
+
565
+ log('debug', `trying to delete region ${command.region_id}`);
566
+
567
+ const deviceConfig = new Uint8Array(7);
568
+
569
+ // Command parameters
570
+ deviceConfig[0] = fp1.constants.region_config_cmds.delete;
571
+ deviceConfig[1] = command.region_id;
572
+ deviceConfig[6] = fp1.constants.region_config_cmd_suffix_delete;
573
+ // Zones definition
574
+ deviceConfig[2] = 0;
575
+ deviceConfig[3] = 0;
576
+ deviceConfig[4] = 0;
577
+ deviceConfig[5] = 0;
578
+
579
+ log('info',
580
+ `delete region ${command.region_id} ` +
581
+ `(${printNumbersAsHexSequence([...deviceConfig], 2)})`,
582
+ );
583
+
584
+ const payload = {
585
+ [fp1.constants.region_config_write_attribute]: {
586
+ value: deviceConfig,
587
+ type: fp1.constants.region_config_write_attribute_type,
588
+ },
589
+ };
590
+
591
+ await entity.write('aqaraOpple', payload, {manufacturerCode});
592
+ },
593
+ },
418
594
  };
419
595
 
420
596
  module.exports = [
@@ -1471,11 +1647,14 @@ module.exports = [
1471
1647
  zigbeeModel: ['lumi.motion.ac01'],
1472
1648
  model: 'RTCZCGQ11LM',
1473
1649
  vendor: 'Xiaomi',
1474
- description: 'Aqara presence detector FP1 (regions not supported for now)',
1475
- fromZigbee: [fz.aqara_opple],
1476
- toZigbee: [tz.RTCZCGQ11LM_presence, tz.RTCZCGQ11LM_monitoring_mode, tz.RTCZCGQ11LM_approach_distance,
1477
- tz.aqara_motion_sensitivity, tz.RTCZCGQ11LM_reset_nopresence_status],
1478
- exposes: [e.presence().withAccess(ea.STATE_GET),
1650
+ description: 'Aqara presence detector FP1',
1651
+ fromZigbee: [fz.aqara_opple, fzLocal.aqara_fp1_region_events],
1652
+ toZigbee: [
1653
+ tz.RTCZCGQ11LM_presence, tz.RTCZCGQ11LM_monitoring_mode, tz.RTCZCGQ11LM_approach_distance, tz.aqara_motion_sensitivity,
1654
+ tz.RTCZCGQ11LM_reset_nopresence_status, tzLocal.aqara_fp1_region_upsert, tzLocal.aqara_fp1_region_delete,
1655
+ ],
1656
+ exposes: [
1657
+ e.presence().withAccess(ea.STATE_GET), e.device_temperature(), e.power_outage_count(),
1479
1658
  exposes.enum('presence_event', ea.STATE, ['enter', 'leave', 'left_enter', 'right_leave', 'right_enter', 'left_leave',
1480
1659
  'approach', 'away']).withDescription('Presence events: "enter", "leave", "left_enter", "right_leave", ' +
1481
1660
  '"right_enter", "left_leave", "approach", "away"'),
@@ -1486,7 +1665,41 @@ module.exports = [
1486
1665
  exposes.enum('motion_sensitivity', ea.ALL, ['low', 'medium', 'high']).withDescription('Different sensitivities ' +
1487
1666
  'means different static human body recognition rate and response speed of occupied'),
1488
1667
  exposes.enum('reset_nopresence_status', ea.SET, ['']).withDescription('Reset the status of no presence'),
1489
- e.device_temperature(), e.power_outage_count()],
1668
+ exposes.enum('action', ea.STATE, ['region_*_enter', 'region_*_leave', 'region_*_occupied',
1669
+ 'region_*_unoccupied']).withDescription('Most recent region event. Event template is "region_<REGION_ID>_<EVENT_TYPE>", ' +
1670
+ 'where <REGION_ID> is region number (1-10), <EVENT_TYPE> is one of "enter", "leave", "occupied", "unoccupied". ' +
1671
+ '"enter" / "leave" events are usually triggered first, followed by "occupied" / "unoccupied" after a couple of seconds.'),
1672
+ exposes.composite('region_upsert', 'region_upsert', ea.SET)
1673
+ .withDescription(
1674
+ 'Definition of a new region to be added (or replace existing one). ' +
1675
+ 'Creating or modifying a region requires you to define which zones of a 7x4 detection grid ' +
1676
+ 'should be active for that zone. Regions can overlap, meaning that a zone can be defined ' +
1677
+ 'in more than one region (eg. "zone x = 1 & y = 1" can be added to region 1 & 2). ' +
1678
+ '"Zone x = 1 & y = 1" is the nearest zone on the right (from sensor\'s perspective, along the detection path).',
1679
+ )
1680
+ .withFeature(
1681
+ exposes.numeric('region_id', ea.SET)
1682
+ .withValueMin(fp1.constants.region_config_regionId_min)
1683
+ .withValueMax(fp1.constants.region_config_regionId_max),
1684
+ )
1685
+ .withFeature(
1686
+ exposes.list('zones', ea.SET,
1687
+ exposes.composite('zone_position', ea.SET)
1688
+ .withFeature(exposes.numeric('x', ea.SET)
1689
+ .withValueMin(fp1.constants.region_config_zoneX_min)
1690
+ .withValueMax(fp1.constants.region_config_zoneX_max))
1691
+ .withFeature(exposes.numeric('y', ea.SET)
1692
+ .withValueMin(fp1.constants.region_config_zoneY_min)
1693
+ .withValueMax(fp1.constants.region_config_zoneY_max)),
1694
+ ),
1695
+ ),
1696
+ exposes.composite('region_delete', 'region_delete', ea.SET)
1697
+ .withDescription('Region definition to be deleted from the device.')
1698
+ .withFeature(exposes.numeric('region_id', ea.SET)
1699
+ .withValueMin(fp1.constants.region_config_regionId_min)
1700
+ .withValueMax(fp1.constants.region_config_regionId_max),
1701
+ ),
1702
+ ],
1490
1703
  configure: async (device, coordinatorEndpoint, logger) => {
1491
1704
  const endpoint = device.getEndpoint(1);
1492
1705
  await endpoint.read('aqaraOpple', [0x010c], {manufacturerCode: 0x115f});
@@ -2710,7 +2923,7 @@ module.exports = [
2710
2923
  fromZigbee: [fzLocal.aqara_feeder],
2711
2924
  toZigbee: [tzLocal.aqara_feeder],
2712
2925
  exposes: [
2713
- exposes.enum('feed', ea.STATE_SET, ['START']).withDescription('Start feeding'),
2926
+ exposes.enum('feed', ea.STATE_SET, ['', 'START']).withDescription('Start feeding'),
2714
2927
  exposes.enum('feeding_source', ea.STATE, ['schedule', 'manual', 'remote']).withDescription('Feeding source'),
2715
2928
  exposes.numeric('feeding_size', ea.STATE).withDescription('Feeding size').withUnit('portion'),
2716
2929
  exposes.numeric('portions_per_day', ea.STATE).withDescription('Portions per day'),
@@ -2734,6 +2947,10 @@ module.exports = [
2734
2947
  .withUnit('g'),
2735
2948
  ],
2736
2949
  ota: ota.zigbeeOTA,
2950
+ configure: async (device, coordinatorEndpoint, logger) => {
2951
+ const endpoint = device.getEndpoint(1);
2952
+ await endpoint.read('aqaraOpple', [0xfff1], {manufacturerCode: 0x115f});
2953
+ },
2737
2954
  },
2738
2955
  {
2739
2956
  zigbeeModel: ['lumi.remote.acn007'],
package/index.js CHANGED
@@ -228,5 +228,15 @@ module.exports = {
228
228
  const payload = {0xf00: {value: 23, type: 35}};
229
229
  await endpoint.readResponse('genBasic', data.meta.zclTransactionSequenceNumber, payload, options);
230
230
  }
231
+ // Aqara feeder C1 polls the time during the interview, need to send back the local time instead of the UTC.
232
+ // The device.definition has not yet been set - therefore the device.definition.onEvent method does not work.
233
+ if (type === 'message' && data.type === 'read' && data.cluster === 'genTime' &&
234
+ device.modelID === 'aqara.feeder.acn001') {
235
+ device.skipTimeResponse = true;
236
+ const oneJanuary2000 = new Date('January 01, 2000 00:00:00 UTC+00:00').getTime();
237
+ const secondsUTC = Math.round(((new Date()).getTime() - oneJanuary2000) / 1000);
238
+ const secondsLocal = secondsUTC - (new Date()).getTimezoneOffset() * 60;
239
+ await device.getEndpoint(1).readResponse('genTime', data.meta.zclTransactionSequenceNumber, {time: secondsLocal});
240
+ }
231
241
  },
232
242
  };
package/lib/utils.js CHANGED
@@ -454,6 +454,37 @@ function attachOutputCluster(device, clusterKey) {
454
454
  }
455
455
  }
456
456
 
457
+ /**
458
+ * @param {number} value
459
+ * @param {number} hexLength
460
+ * @return {string}
461
+ */
462
+ function printNumberAsHex(value, hexLength) {
463
+ const hexValue = value.toString(16).padStart(hexLength, '0');
464
+ return `0x${hexValue}`;
465
+ }
466
+
467
+ /**
468
+ * @param {number[]} numbers
469
+ * @param {number} hexLength
470
+ * @return {string}
471
+ */
472
+ function printNumbersAsHexSequence(numbers, hexLength) {
473
+ return numbers.map((v) => v.toString(16).padStart(hexLength, '0')).join(':');
474
+ }
475
+
476
+ // Note: this is valid typescript-flavored JSDoc
477
+ // eslint-disable-next-line valid-jsdoc
478
+ /**
479
+ * @param {logger} logger
480
+ * @param {vendor} vendor
481
+ * @param {key} key
482
+ * @returns {(level: string, message: string) => void}
483
+ */
484
+ const createLogger = (logger, vendor, key) => (level, message) => {
485
+ logger[level](`zigbee-herdsman-converters:${vendor}:${key}: ${message}`);
486
+ };
487
+
457
488
  module.exports = {
458
489
  noOccupancySince,
459
490
  getOptions,
@@ -490,4 +521,7 @@ module.exports = {
490
521
  getSceneState,
491
522
  extendDevice,
492
523
  attachOutputCluster,
524
+ printNumberAsHex,
525
+ printNumbersAsHexSequence,
526
+ createLogger,
493
527
  };
package/lib/xiaomi.js CHANGED
@@ -684,9 +684,188 @@ const numericAttributes2Options = (definition) => {
684
684
  return result;
685
685
  };
686
686
 
687
+ // For RTCZCGQ11LM
688
+ /**
689
+ * @typedef {{
690
+ * x: number,
691
+ * y: number,
692
+ * }} AqaraFP1RegionZone
693
+ */
694
+ const fp1Constants = {
695
+ region_event_key: 0x0151,
696
+ region_event_types: {
697
+ Enter: 1,
698
+ Leave: 2,
699
+ Occupied: 4,
700
+ Unoccupied: 8,
701
+ },
702
+ region_config_write_attribute: 0x0150,
703
+ region_config_write_attribute_type: 0x41,
704
+ region_config_cmds: {
705
+ /**
706
+ * Creates new region (or force replaces existing one)
707
+ * with new zones definition.
708
+ */
709
+ create: 1,
710
+ /**
711
+ * Modifies existing region.
712
+ * Note: unused, as it seems to break existing regions
713
+ * (region stops reporting new detection events).
714
+ * Use "create" instead, as it replaces existing region with new one.
715
+ */
716
+ modify: 2,
717
+ /**
718
+ * Deletes existing region.
719
+ */
720
+ delete: 3,
721
+ },
722
+ region_config_regionId_min: 1,
723
+ region_config_regionId_max: 10,
724
+ region_config_zoneY_min: 1,
725
+ region_config_zoneY_max: 7,
726
+ region_config_zoneX_min: 1,
727
+ region_config_zoneX_max: 4,
728
+ region_config_cmd_suffix_upsert: 0xff,
729
+ region_config_cmd_suffix_delete: 0x00,
730
+ };
731
+ const fp1Mappers = {
732
+ aqara_fp1: {
733
+ region_event_type_names: {
734
+ [fp1Constants.region_event_types.Enter]: 'enter',
735
+ [fp1Constants.region_event_types.Leave]: 'leave',
736
+ [fp1Constants.region_event_types.Occupied]: 'occupied',
737
+ [fp1Constants.region_event_types.Unoccupied]: 'unoccupied',
738
+ },
739
+ },
740
+ };
741
+ const fp1 = {
742
+ constants: fp1Constants,
743
+ mappers: fp1Mappers,
744
+ /**
745
+ * @param {undefined | Set<number>} xCells
746
+ * @return {number}
747
+ */
748
+ encodeXCellsDefinition: (xCells) => {
749
+ if (!xCells || !xCells.size) {
750
+ return 0;
751
+ }
752
+ return [...xCells.values()].reduce((accumulator, marker) => accumulator + fp1.encodeXCellIdx(marker), 0);
753
+ },
754
+ /**
755
+ * @param {number} cellXIdx
756
+ * @return {number}
757
+ */
758
+ encodeXCellIdx: (cellXIdx) => {
759
+ return 2 ** (cellXIdx - 1);
760
+ },
761
+ // Note: let TypeScript infer the return type to enable union discrimination
762
+ // eslint-disable-next-line valid-jsdoc
763
+ /**
764
+ * @param {unknown} input
765
+ */
766
+ parseAqaraFp1RegionDeleteInput: (input) => {
767
+ if (!input || typeof input !== 'object') {
768
+ return fp1.failure({reason: 'NOT_OBJECT'});
769
+ }
770
+
771
+ if (!('region_id' in input) || !fp1.isAqaraFp1RegionId(input.region_id)) {
772
+ return fp1.failure({reason: 'INVALID_REGION_ID'});
773
+ }
774
+
775
+ return {
776
+ /** @type true */
777
+ isSuccess: true,
778
+ payload: {
779
+ command: {
780
+ region_id: input.region_id,
781
+ },
782
+ },
783
+ };
784
+ },
785
+ // Note: let TypeScript infer the return type to enable union discrimination
786
+ // eslint-disable-next-line valid-jsdoc
787
+ /**
788
+ * @param {unknown} input
789
+ */
790
+ parseAqaraFp1RegionUpsertInput: (input) => {
791
+ if (!input || typeof input !== 'object') {
792
+ return fp1.failure({reason: 'NOT_OBJECT'});
793
+ }
794
+
795
+ if (!('region_id' in input) || !fp1.isAqaraFp1RegionId(input.region_id)) {
796
+ return fp1.failure({reason: 'INVALID_REGION_ID'});
797
+ }
798
+
799
+ if (!('zones' in input) || !Array.isArray(input.zones) || !input.zones.length) {
800
+ return fp1.failure({reason: 'ZONES_LIST_EMPTY'});
801
+ }
802
+
803
+ if (!input.zones.every(fp1.isAqaraFp1RegionZoneDefinition)) {
804
+ return fp1.failure({reason: 'INVALID_ZONES'});
805
+ }
806
+
807
+ return {
808
+ /** @type true */
809
+ isSuccess: true,
810
+ payload: {
811
+ command: {
812
+ region_id: input.region_id,
813
+ zones: input.zones,
814
+ },
815
+ },
816
+ };
817
+ },
818
+ // Note: this is valid typescript JSDoc
819
+ // eslint-disable-next-line valid-jsdoc
820
+ /**
821
+ * @param {unknown} value
822
+ * @returns {value is number}
823
+ */
824
+ isAqaraFp1RegionId: (value) => {
825
+ return (
826
+ typeof value === 'number' &&
827
+ value >= fp1.constants.region_config_regionId_min &&
828
+ value <= fp1.constants.region_config_regionId_max
829
+ );
830
+ },
831
+ // Note: this is valid typescript JSDoc
832
+ // eslint-disable-next-line valid-jsdoc
833
+ /**
834
+ * @param {unknown} value
835
+ * @returns {value is AqaraFP1RegionZone}
836
+ */
837
+ isAqaraFp1RegionZoneDefinition: (value) => {
838
+ return (
839
+ value &&
840
+ typeof value === 'object' &&
841
+ 'x' in value &&
842
+ 'y' in value &&
843
+ typeof value.x === 'number' &&
844
+ typeof value.y === 'number' &&
845
+ value.x >= fp1.constants.region_config_zoneX_min &&
846
+ value.x <= fp1.constants.region_config_zoneX_max &&
847
+ value.y >= fp1.constants.region_config_zoneY_min &&
848
+ value.y <= fp1.constants.region_config_zoneY_max
849
+ );
850
+ },
851
+ /**
852
+ * @template {Record<string, unknown>} ErrorType
853
+ * @param {ErrorType} error
854
+ * @return { { isSuccess: false, error: ErrorType } }
855
+ */
856
+ failure: (error) => {
857
+ return {
858
+ isSuccess: false,
859
+ error,
860
+ };
861
+ },
862
+ };
863
+
687
864
  module.exports = {
688
865
  buffer2DataObject,
689
866
  numericAttributes2Payload,
690
867
  numericAttributes2Options,
691
868
  VOCKQJK11LMDisplayUnit,
869
+ fp1,
870
+ manufacturerCode: 0x115f,
692
871
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigbee-herdsman-converters",
3
- "version": "15.0.24",
3
+ "version": "15.0.26",
4
4
  "description": "Collection of device converters to be used with zigbee-herdsman",
5
5
  "main": "index.js",
6
6
  "files": [