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 +1 -1
- package/devices/diyruz.js +2 -1
- package/devices/philips.js +36 -1
- package/devices/samotech.js +16 -1
- package/devices/sengled.js +11 -1
- package/devices/sonoff.js +7 -12
- package/devices/tuya.js +13 -3
- package/devices/xiaomi.js +224 -7
- package/index.js +10 -0
- package/lib/utils.js +34 -0
- package/lib/xiaomi.js +179 -0
- package/package.json +1 -1
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(-
|
|
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(),
|
|
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}`;
|
package/devices/philips.js
CHANGED
|
@@ -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',
|
package/devices/samotech.js
CHANGED
|
@@ -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: '
|
|
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
|
];
|
package/devices/sengled.js
CHANGED
|
@@ -144,8 +144,18 @@ module.exports = [
|
|
|
144
144
|
model: 'Z01-A19NAE26',
|
|
145
145
|
vendor: 'Sengled',
|
|
146
146
|
description: 'Element plus (A19)',
|
|
147
|
-
|
|
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()
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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: [
|
|
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:
|
|
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
|
|
1475
|
-
fromZigbee: [fz.aqara_opple],
|
|
1476
|
-
toZigbee: [
|
|
1477
|
-
tz.
|
|
1478
|
-
|
|
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
|
-
|
|
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
|
};
|