iobroker.zigbee 3.2.5 → 3.3.1-alpha.0

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.
@@ -20,6 +20,7 @@ class localConfig extends EventEmitter {
20
20
 
21
21
  async onReady()
22
22
  {
23
+ this.init();
23
24
  }
24
25
 
25
26
  async onUnload(callback)
@@ -299,6 +300,21 @@ class localConfig extends EventEmitter {
299
300
  try {
300
301
  const data_js = JSON.parse(content);
301
302
  this.localData = data_js;
303
+ // fix the legacy change. Any 'legacy' option found on a model will be moved to 'options.legacy'
304
+ //
305
+ let changed = false;
306
+ for (const mopt of Object.values(this.localData.by_model)) {
307
+ if (mopt?.hasOwnProperty('legacy')) {
308
+ const opt = mopt?.options || {};
309
+ if (!opt.hasOwnProperty('legacy')) {
310
+ opt.legacy = mopt.legacy;
311
+ mopt.options = opt;
312
+ }
313
+ delete mopt.legacy;
314
+ changed = true;
315
+ }
316
+ }
317
+ if (changed) this.retainData(false);
302
318
  }
303
319
  catch (error) {
304
320
  this.error(`unable to parse data read from ${fn} : ${error.message ? error.message : 'undefined error'}`);
@@ -364,6 +380,32 @@ class localConfig extends EventEmitter {
364
380
  return rv;
365
381
  }
366
382
 
383
+ getOption(source, key, option, defaultvalue) {
384
+ const sourceEntries = source[key] || {};
385
+ if (sourceEntries.hasOwnProperty(option)) return sourceEntries[option];
386
+ if (typeof sourceEntries.options == 'object') {
387
+ return sourceEntries.options.hasOwnProperty(option) ? sourceEntries.options[option] : defaultvalue;
388
+ }
389
+ return defaultvalue;
390
+ }
391
+
392
+ getModelOption(model_id, option, defaultvalue) {
393
+ return this.getOption(this.localData.by_model, model_id, option, defaultvalue);
394
+ }
395
+
396
+ getDeviceOption(device_id, model_id, option, dOnly) {
397
+ const modelOption = dOnly ? this.getModelOption(model_id, option) : undefined;
398
+ const dId = device_id.replace('0x','');
399
+ const deviceOption = this.getOption(this.localData.by_id, dId, option);
400
+ if (modelOption && typeof modelOption == 'object' && deviceOption && typeof deviceOption == 'object') {
401
+ for (const key of Object.keys(modelOption)) {
402
+ if (modelOption[key] != undefined && deviceOption[key]==undefined) deviceOption[key] = modelOption[key];
403
+ }
404
+ return deviceOption
405
+ }
406
+ else return deviceOption || modelOption
407
+ }
408
+
367
409
  getByModel(id) {
368
410
  return this.localData.by_model[id] || {};
369
411
  }
package/lib/models.js ADDED
@@ -0,0 +1,615 @@
1
+ const legacyDevices = require('./legacy/devices.js');
2
+ const legacyStates = require('./legacy/states').states;
3
+ const { getModelRegEx } = require('./utils.js')
4
+ const { applyHerdsmanModel } = require('./exposes.js');
5
+ const { findByDevice } = require('zigbee-herdsman-converters');
6
+
7
+ const herdsmanModelInfo = new Map();
8
+ const UUIDbyDevice = new Map();
9
+
10
+ const { ParseColor } = require('./colors');
11
+ const { rgb_to_cie, cie_to_rgb } = require('./rgb');
12
+ const { decimalToHex, adapterLevelToBulbLevel, bulbLevelToAdapterLevel , toMired } = require('./utils');
13
+
14
+ const states = {
15
+ // common states
16
+ link_quality: {
17
+ id: 'link_quality',
18
+ prop: 'linkquality',
19
+ name: 'Link quality',
20
+ icon: undefined,
21
+ role: 'value',
22
+ write: false,
23
+ read: true,
24
+ type: 'number',
25
+ min: 0,
26
+ max: 255,
27
+ isCommonState:true,
28
+ },
29
+ available: {
30
+ id: 'available',
31
+ prop: 'available',
32
+ name: 'Available',
33
+ icon: undefined,
34
+ role: 'indicator.reachable',
35
+ write: false,
36
+ read: true,
37
+ type: 'boolean',
38
+ isCommonState:true,
39
+ isInternalState: true,
40
+ },
41
+ device_query: { // button to trigger device read
42
+ id: 'device_query',
43
+ prop: 'device_query',
44
+ name: 'Trigger device query',
45
+ icon: undefined,
46
+ role: 'button',
47
+ write: true,
48
+ read: false,
49
+ type: 'boolean',
50
+ isCommonState:true,
51
+ isInternalState: true,
52
+ },
53
+ from_zigbee: {
54
+ id: 'msg_from_zigbee',
55
+ name: 'Message from Zigbee',
56
+ icon: undefined,
57
+ role: 'state',
58
+ write: false,
59
+ read: true,
60
+ type: 'string',
61
+ isCommonState:true,
62
+ isInternalState: true,
63
+ },
64
+ send_payload: {
65
+ id: 'send_payload',
66
+ name: 'Send to Device',
67
+ icon: undefined,
68
+ role: 'state',
69
+ write: true,
70
+ read: true,
71
+ type: 'string',
72
+ isCommonState:true,
73
+ isInternalState: true,
74
+ },
75
+ state: {
76
+ id: 'state',
77
+ name: 'Switch state',
78
+ icon: undefined,
79
+ role: 'switch',
80
+ write: true,
81
+ read: true,
82
+ type: 'boolean',
83
+ getter: payload => (payload.state === 'ON'),
84
+ setter: (value) => (value) ? 'ON' : 'OFF',
85
+ setterOpt: (value, options) => {
86
+ const stateValue = (value ? 'ON' : 'OFF');
87
+ return {...options, state: stateValue};
88
+ },
89
+ inOptions: true,
90
+ },
91
+ // group states
92
+ groupstateupdate: {
93
+ id: 'stateupdate',
94
+ name: 'Set group by member states',
95
+ icon: undefined,
96
+ role: 'state',
97
+ write: true,
98
+ read: true,
99
+ type: 'string',
100
+ states: {off:'off',max:'max',min:'min',avg:'avg',mat:'mat'},
101
+ def:'off',
102
+ isInternalState: true,
103
+ },
104
+ groupmemberupdate: {
105
+ id: 'memberupdate',
106
+ name: 'Read member states',
107
+ icon: undefined,
108
+ role: 'switch',
109
+ write: true,
110
+ read: true,
111
+ type: 'boolean',
112
+ isInternalState: true,
113
+ },
114
+ brightness: {
115
+ id: 'brightness',
116
+ name: 'Brightness',
117
+ icon: undefined,
118
+ role: 'level.dimmer',
119
+ write: true,
120
+ read: true,
121
+ type: 'number',
122
+ unit: '',
123
+ min: 0,
124
+ max: 100,
125
+ getter: payload => {
126
+ return bulbLevelToAdapterLevel(payload.brightness);
127
+ },
128
+ setter: (value, options) => {
129
+ return adapterLevelToBulbLevel(value);
130
+ },
131
+ setterOpt: (value, options) => {
132
+ const hasTransitionTime = options && options.hasOwnProperty('transition_time');
133
+ const transitionTime = hasTransitionTime ? options.transition_time : 0;
134
+ const preparedOptions = {...options, transition: transitionTime};
135
+ preparedOptions.brightness = adapterLevelToBulbLevel(value);
136
+ return preparedOptions;
137
+ },
138
+ readResponse: (resp) => {
139
+ const respObj = resp[0];
140
+ if (respObj.status === 0 && respObj.attrData != undefined) {
141
+ return bulbLevelToAdapterLevel(respObj.attrData);
142
+ }
143
+ },
144
+ },
145
+ brightness_step: {
146
+ id: 'brightness_step',
147
+ prop: 'brightness_step',
148
+ name: 'Brightness stepping',
149
+ icon: undefined,
150
+ role: 'level',
151
+ write: true,
152
+ read: false,
153
+ type: 'number',
154
+ min: -50,
155
+ max: 50
156
+ },
157
+ brightness_move: {
158
+ id: 'brightness_move',
159
+ prop: 'brightness_move',
160
+ name: 'Dimming',
161
+ icon: undefined,
162
+ role: 'level',
163
+ write: true,
164
+ read: false,
165
+ type: 'number',
166
+ min: -50,
167
+ max: 50
168
+ },
169
+ colortemp_move: {
170
+ id: 'colortemp_move',
171
+ prop: 'color_temp_move',
172
+ name: 'Colortemp change',
173
+ icon: undefined,
174
+ role: 'level',
175
+ write: true,
176
+ read: false,
177
+ type: 'number',
178
+ min: -50,
179
+ max: 50
180
+ },
181
+ hue_move: {
182
+ id: 'hue_move',
183
+ prop: 'hue_move',
184
+ name: 'Hue change',
185
+ icon: undefined,
186
+ role: 'level',
187
+ write: true,
188
+ read: false,
189
+ type: 'number',
190
+ min: -50,
191
+ max: 50
192
+ },
193
+ saturation_move: {
194
+ id: 'saturation_move',
195
+ prop: 'saturation_move',
196
+ name: 'Saturation change',
197
+ icon: undefined,
198
+ role: 'level',
199
+ write: true,
200
+ read: false,
201
+ type: 'number',
202
+ min: -50,
203
+ max: 50
204
+ },
205
+ transition_time: {
206
+ id: 'transition_time',
207
+ name: 'Transition time',
208
+ icon: undefined,
209
+ role: 'state',
210
+ write: true,
211
+ read: false,
212
+ type: 'number',
213
+ unit: 'sec',
214
+ isOption: true,
215
+ },
216
+ colortemp: {
217
+ id: 'colortemp',
218
+ prop: 'color_temp',
219
+ name: 'Color temperature',
220
+ icon: undefined,
221
+ role: 'level.color.temperature',
222
+ write: true,
223
+ read: true,
224
+ type: 'number',
225
+ min: undefined,
226
+ max: undefined,
227
+ setter: (value) => {
228
+ return toMired(value);
229
+ },
230
+ setterOpt: (value, options) => {
231
+ const hasTransitionTime = options && options.hasOwnProperty('transition_time');
232
+ const transitionTime = hasTransitionTime ? options.transition_time : 0;
233
+ return {...options, transition: transitionTime};
234
+ },
235
+ },
236
+ color: {
237
+ id: 'color',
238
+ prop: 'color',
239
+ name: 'Color',
240
+ icon: undefined,
241
+ role: 'level.color',
242
+ write: true,
243
+ read: true,
244
+ type: 'string',
245
+ setter: (value) => {
246
+
247
+ // convert RGB to XY for set
248
+ let xy = [0, 0];
249
+ const rgbcolor = ParseColor(value);
250
+ xy = rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
251
+ return {
252
+ x: xy[0],
253
+ y: xy[1]
254
+ };
255
+ },
256
+ setterOpt: (value, options) => {
257
+ const hasTransitionTime = options && options.hasOwnProperty('transition_time');
258
+ const transitionTime = hasTransitionTime ? options.transition_time : 0;
259
+ return {...options, transition: transitionTime};
260
+ },
261
+ getter: payload => {
262
+ if (payload.color && payload.color.hasOwnProperty('x') && payload.color.hasOwnProperty('y')) {
263
+ const colorval = cie_to_rgb(payload.color.x, payload.color.y);
264
+ return '#' + decimalToHex(colorval[0],2) + decimalToHex(colorval[1],2) + decimalToHex(colorval[2],2);
265
+ } else {
266
+ return undefined;
267
+ }
268
+ },
269
+ },
270
+ hex_color: {
271
+ id:`hex_color`,
272
+ name: `Hex Color`,
273
+ icon: undefined,
274
+ role: 'level.color.rgb',
275
+ write: true,
276
+ read: true,
277
+ type: 'string',
278
+ setter: value => {
279
+ // hex color (no named allowed)
280
+ const rgbcolor = ParseColor(value, true);
281
+ return rgbcolor;
282
+ },
283
+ setterOpt: (value, options) => {
284
+ const hasTransitionTime = options && options.hasOwnProperty('transition_time');
285
+ const transitionTime = hasTransitionTime ? options.transition_time : 0;
286
+ return {...options, transition: transitionTime};
287
+ },
288
+
289
+ getter: payload => {
290
+ // Requires testing!
291
+
292
+ try {
293
+ // JSON
294
+ const colorJSON = JSON.parse(payload.replaceAll("'",'"'));
295
+ if (colorJSON.r != undefined && colorJSON.g != undefined && colorJSON.b != undefined) {
296
+ const hexstring = (colorJSON.r*65536 + colorJSON.g * 256 + colorJSON.b).toString(16).padStart(6);
297
+ return `#${hexstring.substring(2)}`;
298
+ }
299
+ return undefined;
300
+ }
301
+ catch {
302
+ // intentionally empty;
303
+ }
304
+ if (payload.color.startsWith('#')) return payload.color;
305
+ const p = payload.replace('0x', '');
306
+ const m = p.match(/[0123456789abcdefABCDEF]+/);
307
+ if (p.length < 7 && m && m[0].length == p.length) return `${'#000000'.substring(0, 7-p.length)}${p}`;
308
+ return undefined;
309
+ },
310
+ setattr: 'color',
311
+
312
+ },
313
+ battery: {
314
+ id: 'battery',
315
+ prop: 'battery',
316
+ name: 'Battery percent',
317
+ icon: 'img/battery_p.png',
318
+ role: 'value.battery',
319
+ write: false,
320
+ read: true,
321
+ type: 'number',
322
+ unit: '%',
323
+ min: 0,
324
+ max: 120
325
+ },
326
+ temperature: {
327
+ id: 'temperature',
328
+ name: 'Temperature',
329
+ icon: undefined,
330
+ role: 'value.temperature',
331
+ write: false,
332
+ read: true,
333
+ type: 'number',
334
+ unit: '°C'
335
+ },
336
+ humidity: {
337
+ id: 'humidity',
338
+ name: 'Humidity',
339
+ icon: undefined,
340
+ role: 'value.humidity',
341
+ write: false,
342
+ read: true,
343
+ type: 'number',
344
+ unit: '%',
345
+ min: 0,
346
+ max: 100
347
+ },
348
+ pressure: {
349
+ id: 'pressure',
350
+ name: 'Pressure',
351
+ icon: undefined,
352
+ role: 'value.pressure',
353
+ write: false,
354
+ read: true,
355
+ type: 'number',
356
+ unit: 'hPa',
357
+ min: 0,
358
+ max: 10000
359
+ },
360
+ illuminance: {
361
+ id: 'illuminance',
362
+ prop: 'illuminance_lux',
363
+ name: 'Illuminance',
364
+ icon: undefined,
365
+ role: 'value.brightness',
366
+ write: false,
367
+ read: true,
368
+ type: 'number',
369
+ unit: 'lux'
370
+ },
371
+ illuminance_raw: {
372
+ id: 'illuminance_raw',
373
+ prop: 'illuminance',
374
+ name: 'Illuminance raw',
375
+ icon: undefined,
376
+ role: 'value.brightness',
377
+ write: false,
378
+ read: true,
379
+ type: 'number',
380
+ unit: ''
381
+ },
382
+ occupancy: {
383
+ id: 'occupancy',
384
+ name: 'Occupancy',
385
+ icon: undefined,
386
+ role: 'sensor.motion',
387
+ write: false,
388
+ read: true,
389
+ type: 'boolean',
390
+ },
391
+ load_power: {
392
+ id: 'load_power',
393
+ prop: 'power',
394
+ name: 'Load power',
395
+ icon: undefined,
396
+ role: 'value.power',
397
+ write: false,
398
+ read: true,
399
+ type: 'number',
400
+ unit: 'W'
401
+ },
402
+ contact: {
403
+ id: 'contact',
404
+ name: 'Contact event',
405
+ icon: undefined,
406
+ role: 'state',
407
+ write: false,
408
+ read: true,
409
+ type: 'boolean'
410
+ },
411
+ opened: {
412
+ id: 'opened',
413
+ prop: 'contact',
414
+ name: 'Is open',
415
+ icon: undefined,
416
+ role: 'state',
417
+ write: false,
418
+ read: true,
419
+ type: 'boolean',
420
+ getter: payload => !payload.contact,
421
+ },
422
+ heiman_batt_low: {
423
+ id: 'battery_low',
424
+ prop: 'battery_low',
425
+ name: 'Battery Status Low',
426
+ icon: undefined,
427
+ role: 'indicator.lowbat',
428
+ write: false,
429
+ read: true,
430
+ type: 'boolean'
431
+ },
432
+ tamper: {
433
+ id: 'tampered',
434
+ prop: 'tamper',
435
+ name: 'Is tampered',
436
+ icon: undefined,
437
+ role: 'state',
438
+ write: false,
439
+ read: true,
440
+ type: 'boolean',
441
+ },
442
+ water_detected: {
443
+ id: 'detected',
444
+ prop: 'water_leak',
445
+ name: 'Water leak detected',
446
+ icon: undefined,
447
+ role: 'indicator.leakage',
448
+ write: false,
449
+ read: true,
450
+ type: 'boolean'
451
+ },
452
+ climate_away_mode: {
453
+ id: 'away_mode',
454
+ prop: 'away_mode',
455
+ name: 'Away',
456
+ icon: undefined,
457
+ role: 'state',
458
+ write: true,
459
+ read: true,
460
+ type: 'boolean',
461
+ isEvent: true,
462
+ getter: payload => (payload.action === 'ON') ? true : false,
463
+ setter: (value) => (value) ? 'ON' : 'OFF',
464
+ },
465
+ climate_system_mode: {
466
+ id: 'mode',
467
+ name: 'Mode',
468
+ prop: 'system_mode',
469
+ icon: undefined,
470
+ role: 'state',
471
+ write: true,
472
+ read: true,
473
+ type: 'string',
474
+ states: 'auto:auto;off:off;heat:heat',
475
+ },
476
+ climate_running_mode: {
477
+ id: 'running_state',
478
+ name: 'Running Mode',
479
+ prop: 'running_state',
480
+ icon: undefined,
481
+ role: 'state',
482
+ write: true,
483
+ read: true,
484
+ type: 'string', // valid: low, medium, high
485
+ states: 'idle;idle;heat:heat',
486
+ },
487
+ }
488
+
489
+
490
+
491
+ const lightStates = [states.state, states.brightness, states.brightness_move, states.transition_time, states.brightness_step],
492
+ lightStatesWithColor=[...lightStates, states.color, states.hex_color, states.colortemp, states.colortemp_move],
493
+ onOffStates=[states.state],
494
+ lightStatesWithColortemp = [...lightStates, states.colortemp, states.colortemp_move],
495
+ lightStatesWithColor_hue= [...lightStatesWithColor, states.hue_move, states.transition_time, states.effect_type_hue],
496
+ lightStatesWithColorNoTemp= [...lightStates, states.color],
497
+ commonStates=Object.values(states).filter((candidate) => candidate.isCommonState),
498
+ commonGroupStates=[states.groupstateupdate, states.groupmemberupdate],
499
+ groupStates=[states.groupstateupdate, states.groupmemberupdate, ...lightStatesWithColor, ...onOffStates];
500
+
501
+ /**
502
+ * Returns a model description, if one exists
503
+ * @param {string} model the model name
504
+ * @param {string} UUID a UUID for models with device specific model definitions
505
+ * @param {boolean} legacy true: legacy devices only. false: exposed devices only. undefined: Exposed devices preferred, legacy devices else
506
+ * @returns {object: adapterModelDefinition}
507
+ */
508
+ function findModel(model, deviceId, legacy) {
509
+ const UUID = deviceId ? UUIDbyDevice.get(deviceId) : undefined;
510
+ if (legacy) return legacyDevices.findModel(model, true);
511
+ const m = UUID ? herdsmanModelInfo.get(UUID) : herdsmanModelInfo.get(getModelRegEx(model));
512
+ if (m) return m;
513
+ if (legacy === undefined) return legacyDevices.findModel(model, true)
514
+ }
515
+
516
+
517
+ /**
518
+ * Returns a 16 digit hex hash
519
+ * @param {string} hashMe the item to hash
520
+ * @returns {string} the hash.
521
+ */
522
+ function toHash(hashMe) {
523
+ const hashStr = JSON.stringify(hashMe);
524
+ let hash = 0;
525
+ if (hashStr.length == 0) return hash;
526
+ for (let i = 0; i < hashStr.length; i++) {
527
+ const char = hashStr.charCodeAt(i);
528
+ hash = ((hash << 5) - hash) + char;
529
+ hash = hash & hash;
530
+ }
531
+ if (hash < 0) hash+= Number.MAX_SAFE_INTEGER;
532
+ return `00000000000000${hash.toString(16)}`.slice(-13);
533
+ }
534
+
535
+ /**
536
+ * Returns a 16 digit hex hash
537
+ * @param {string} hashMe the item to hash
538
+ * @returns {string} the hash.
539
+ */
540
+ async function addExposeToDevices(device, model, logger) {
541
+
542
+ // grab the model definition from herdsman
543
+ //
544
+ const hM = await findByDevice(device);
545
+
546
+ if (hM) {
547
+ const adapterModelDefinition = { model:model, device:device, key: getModelRegEx(model), icon:hM.icon }
548
+
549
+ adapterModelDefinition.herdsmanModel = hM
550
+ if (typeof hM.exposes == 'function') {
551
+ adapterModelDefinition.exposes = hM.exposes(device);
552
+ const endpoints = typeof hM.endpoints == 'function' ? hM.endpoint(device) || {} : {};
553
+ adapterModelDefinition.UUID = toHash(`${adapterModelDefinition.exposes.map((expose) => JSON.stringify(expose)).join('.')}.${Object.keys(endpoints).join('.')}`);
554
+ UUIDbyDevice.set(device.ieeeAddr,adapterModelDefinition.UUID);
555
+ }
556
+ else adapterModelDefinition.exposes = (typeof hM.exposes == 'object') ? hM.exposes : {};
557
+ adapterModelDefinition.states = commonStates;
558
+ const adapterModel = await findModel(model, device.ieeeAddr, false);
559
+ if (adapterModel) return adapterModel;
560
+ const { newModel, message, error } = await applyHerdsmanModel(adapterModelDefinition);
561
+ if (error) {
562
+ if (logger) logger.warn(`${message} : ${error?.message}`);
563
+ }
564
+ else {
565
+ herdsmanModelInfo.set(adapterModelDefinition.UUID || adapterModelDefinition.key, newModel);
566
+ // default to legacy device if exposes are empty ?
567
+ // currently: not.
568
+ //
569
+ if (adapterModelDefinition.UUID) newModel.UUID = adapterModelDefinition.UUID;
570
+ return newModel;
571
+ }
572
+ }
573
+ return {};
574
+ }
575
+
576
+ async function clearModelDefinitions() {
577
+ UUIDbyDevice.clear();
578
+ herdsmanModelInfo.clear();
579
+ }
580
+
581
+ function getStateDefinition(name, type, prop) {
582
+ if (typeof name != 'string') return getStateDefinition('ilstate', type, prop);
583
+ if (legacyStates.hasOwnProperty(name)) return legacyStates[name];
584
+ return states[name] || {
585
+ id: name,
586
+ prop: prop || name,
587
+ name: name.replace('_',' '),
588
+ icon: undefined,
589
+ role: 'state',
590
+ write: true,
591
+ read: true,
592
+ type: type || 'string',
593
+ }
594
+ }
595
+
596
+ module.exports = {
597
+ addExposeToDevices,
598
+ findModel,
599
+ getStateDefinition,
600
+ clearModelDefinitions,
601
+ getIconforLegacyModel:legacyDevices.getIconforLegacyModel,
602
+ hasLegacyDevice:legacyDevices.hasLegacyDevice,
603
+ lightStates,
604
+ lightStatesWithColor,
605
+ states,
606
+ commonStates,
607
+ commonGroupStates,
608
+ groupStates,
609
+ lightStatesWithColortemp,
610
+ lightStatesWithColor_hue,
611
+ lightStatesWithColorNoTemp,
612
+ onOffStates,
613
+ }
614
+ // this stores the Herdsman model info for each device. Note that each model also stores
615
+ // which devices use this model as reference