iobroker.zigbee2mqtt 2.2.0 → 2.3.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.
package/README.md CHANGED
@@ -30,11 +30,22 @@ This adapter allows to control the data points of the devices of a Zigbee2MQTT i
30
30
  Placeholder for the next version (at the beginning of the line):
31
31
  ### **WORK IN PROGRESS**
32
32
  -->
33
+ ### 2.3.0 (2022-10-30)
34
+
35
+ - (o0shojo0o) added support for the `toggle` of states that support this.
36
+ - (o0shojo0o) added correct handling of `color_move` and `color_temperature_move`
37
+
38
+ ### 2.2.1 (2022-10-25)
39
+
40
+ - (o0shojo0o) fix state roles and access
41
+ - (o0shojo0o) fix state handling
42
+ - (o0shojo0o) fix createZ2MMessage
43
+
33
44
  ### 2.2.0 (2022-10-20)
34
45
 
35
46
  - (o0shojo0o) added support for [Lidl HG06467 effects](https://www.zigbee2mqtt.io/devices/HG06467.html#trigger-effects)
36
47
  - (o0shojo0o) added support for hs color
37
- - (o0shojo0o) simulated_brightness data point is added only for supported devices
48
+ - (o0shojo0o) `simulated_brightness` data point is added only for supported devices
38
49
 
39
50
  ### 2.1.1 (2022-10-16)
40
51
 
@@ -13,7 +13,7 @@
13
13
  "External MQTT-Server Port": "Externer MQTT-Server Port",
14
14
  "MQTT-Server IP-Address bind": "MQTT-Server IP-Adresse binden",
15
15
  "MQTT-Server Port": "MQTT-Server-Port",
16
- "Configure your Zigbee2MQTT WebUi connection": "Zigbee2MQTT WebUi Verbindung Konfiguration",
16
+ "Configure your Zigbee2MQTT WebUi connection": "Konfiguration der Zigbee2MQTT WebUi Verbindung",
17
17
  "WebUi Address": "WebUi Adresse",
18
18
  "WebUi Port": "WebUi Port",
19
19
  "Color configurations": "Farbkonfigurationen",
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zigbee2mqtt",
4
- "version": "2.2.0",
4
+ "version": "2.3.0",
5
5
  "news": {
6
+ "2.3.0": {
7
+ "en": "added support for the `toggle` of states that support this.\nadded correct handling of `color_move` and `color_temperature_move`",
8
+ "de": "zusätzliche unterstützung für den kippen von staaten, die dies unterstützen.\nkorrekte handhabung von color_move und color_temperature_move",
9
+ "ru": "добавлена поддержка в отношении государств, которые поддерживают это.\nдобавлена правильная обработка color_move и color_temperature_move",
10
+ "pt": "acrescentou apoio para o conjunto de estados que apoiam isso.\nadicionado manuseio correto de color_move e color_temperature_move",
11
+ "nl": "steun toegevoegd voor de toggle van staten die dit ondersteunen.\nvoegde de juiste kleding en kleurtemperatuur toe",
12
+ "fr": "a ajouté un soutien pour la lutte des etats qui appuient cela.\nla manipulation correcte de color_move et color_temperature_move",
13
+ "it": "ha aggiunto il sostegno per la lotta di stati che sostengono questo.\naggiunto corretto trattamento di colore_move e colore_temperature_move",
14
+ "es": "agregó apoyo para la lucha de estados que apoyan esto.\nañadido correcto manejo de color_move y color_temperature_move",
15
+ "pl": "dodał wsparcie dla porozumień stanów, które to wspierają.\ndodano poprawne zachowanie koloru i kolor_temperature_move",
16
+ "uk": "додана підтримка для тягових станів, які підтримують це.\nдодано правильну обробку кольором_move та color_ infrastructure_move",
17
+ "zh-cn": "此外,还支持支持这一国家。.\n· 删除白色、流血和色色色色色彩。"
18
+ },
19
+ "2.2.1": {
20
+ "en": "fix state roles and access\nfix state handling\nfix createZ2MMessage",
21
+ "de": "zustandsrollen und zugriff beheben\nmanuelle zustandsbearbeitung\nfix erstellenZ2MMessage",
22
+ "ru": "исправить государственные роли и доступ\nисправить государственную обработку\nисправить CreateZ2MMessage",
23
+ "pt": "corrigir funções estaduais e acesso\ncorrigir o manuseio do estado\ncorrigir createZ2MMessage",
24
+ "nl": "vertaling:\nvertaling:\nvertaling:",
25
+ "fr": "fixer les rôles de l'état et l'accès\nmaniement de l ' état\nfix createZ2MMessage",
26
+ "it": "fissare i ruoli dello stato e l'accesso\ncorretta gestione dello stato\nfix createZ2MMessage",
27
+ "es": "fijar funciones estatales y acceso\ncorrección del estado\nfix createZ2MMessage",
28
+ "pl": "uchwała rola państwowa i dostęp\nnaprawa\ntworzenie Z2MM",
29
+ "uk": "фіксувати державні ролі і доступ\nфіксувати стан обробки\nфіксатор творуZ2MMessage",
30
+ "zh-cn": "fix 国家作用和准入\nfix 国家处理\nfix 建立Z2MMessage"
31
+ },
6
32
  "2.2.0": {
7
33
  "en": "added support for [Lidl HG06467 effects](https://www.zigbee2mqtt.io/devices/HG06467.html#trigger-effects)\nadded support for hs color\nsimulated_brightness data point is added only for supported devices",
8
34
  "de": "unterstützung für [Lidl HG06467-Effekte](https://www.zigbee2mqt.io/devices/HG06467.html#trigger-Effekte)\nunterstützung für hs farbe\nsimuld_brightness-datenpunkt wird nur für unterstützte geräte hinzugefügt",
@@ -66,30 +92,6 @@
66
92
  "es": "*! ¡CAMBIANDO\n\nopciones adicionales para MQTT-Server externo\nconexión a zigbe2mqtt completamente rediseñado y cambiado a MQTT\nmuchos bugfixes\nautomáticamente poner las acciones del botón de nuevo a falso\nsoporte añadido para función Zigbee2MQTT simulado_brightness\ncontrol de configuración añadido\nproducción de registro agregada sobre detalles de coordinadores",
67
93
  "pl": "! BREAKING CHANGE! (ANG.)\n\nobsługa MQTT-Server\nzigbe2mqtt całkowicie przerobił i zmienił MQTT\nwiele błędów\nautomatycznie ustawione akcje przycisku\ndodano obsługę Zigbee2MQTT\nsprawdzać\ndokładna produkcja logów na temat szczegółów koordynacji",
68
94
  "zh-cn": "页:1 BREAKREANGE*\n\n外贸总协定\n与zigbe2mqt完全重新工作,并改装到外贸总协定\n批发\n自动建立但顿行动回到虚假的行动\n对Zigbee2MQTT特征的增援\n增幅检查\n协调员的产出"
69
- },
70
- "0.2.0": {
71
- "en": "group states corrected\nadded option 'Use Kelvin instead of mired for the color temps'\nremove available logic, now will use the information from z2m\nrename noLogDevices to logfilter\nlots of bugfixes\nadded noLogDevices functionality\nadded debugmessage for specific device functionality\nadded some states are default false\nadded support for scenes defined on a device\nfix available state role\nfix subscribeWritableStates",
72
- "de": "gruppenzustände korrigiert\nzusatzoption 'Use Kelvin anstelle von mired für die Farbe Tempos '\ndie verfügbare logik entfernen, jetzt die informationen von z2m verwenden\numbenennen noLogDevices zum Logfilter\nviele bugfixes\nnoLogDevices Funktionalität hinzugefügt\ndebugmessage für spezifische gerätefunktionalität\neinige zustände standardmäßig falsch hinzugefügt\nunterstützung für szenen, die auf einem gerät definiert sind\nfix verfügbare zustandsrolle\nabonnierenWritableState",
73
- "ru": "исправлены состояния группы\nдобавлена опция 'Используйте Kelvin вместо mired для цветовых темп « »\nудалить доступную логику, теперь будет использовать информацию из z2m\nпереименовать noLogDevices для logfilter\nмного багфиксов\nдобавлена функция noLogDevices\nдобавлено debugmessage для конкретной функциональности устройства\nдобавлены некоторые государства по умолчанию false\nдобавлена поддержка сцен, определенных на устройстве\nисправить доступную государственную роль\nисправить подпискуWritableStates",
74
- "pt": "estados do grupo corrigidos\nopção adicionada 'Use Kelvin em vez de mired para os temps de cor '\nremover a lógica disponível, agora usará as informações do z2m\nrenomear noLogDevices para logfilter\nlotes de correções de bugs\nadicionado noLogDevices funcionalidade\nadicionado debugmessage para funcionalidade específica do dispositivo\nadicionado alguns estados são padrão falso\nsuporte adicionado para cenas definidas em um dispositivo\ncorrigir a função estadual disponível\ncorrigir assinantesEstados disponíveis",
75
- "nl": "de groep zegt:\nvoegde optie 'Use Kelvin' in plaats van de kleuren tempen '\nverwijder de logica, nu zal de informatie van z2m gebruiken\nnoLog Devices\nveel insectenfixen\nnoLog Devices functionaliteit\nvoegde debugmesage toe voor specifieke apparaat functionaliteit\nsommige staten zijn vals\nsteun voor scènes gedefinieerd op een apparaat\nbeschikbare staatsrol\nvertaling:",
76
- "fr": "états de groupe corrigés\noption ajoutée 'Utiliser Kelvin au lieu de mired pour le temps de couleur '\nsupprimer la logique disponible, maintenant utilisera les informations de z2m\nrenommer noLogDevices to logfilter\nbeaucoup de bugfixes\najout de fonctionnalité noLogDevices\najout debugmessage pour la fonctionnalité de périphérique spécifique\ncertains états ajoutés sont par défaut faux\nsupport ajouté pour les scènes définies sur un appareil\nle rôle de l ' état\nfixer souscrire",
77
- "it": "gruppo stati corretti\nopzione aggiunta 'Usa Kelvin invece di mired per le tentazioni di colore '\nrimuovere la logica disponibile, ora userà le informazioni da z2m\nrinominare noLogDevices per logfilter\nmolti bugfix\naggiunto noLogDevices funzionalità\ndebugmessage aggiunto per funzionalità specifiche del dispositivo\naggiunto alcuni stati sono falsi di default\nsupporto aggiunto per scene definite su un dispositivo\ncorrezione del ruolo di stato disponibile\nfissare abbonamentiWritableStates",
78
- "es": "estados del grupo corregidos\nopción agregada 'Use Kelvin en lugar de mired para el color temps '\neliminar la lógica disponible, ahora utilizará la información de z2m\nnoLogDevices to logfilter\nmuchos bugfixes\nnoLogDevices funcionalidad\ndebugmesage añadido para la funcionalidad específica del dispositivo\nañadido algunos estados son falsos\nsoporte añadido para escenas definidas en un dispositivo\nfijar la función estatal disponible\nfijar suscriptosEstados disponibles",
79
- "pl": "państwa grupowe poprawiły\ndodał opcję 'Use Kelvin' zamiast śmiać kolorowe tempo. '\nzdejmowanie dostępnej logiki wykorzystuje obecnie informacje z 2 m\nnazwa NoLogDevices (ang.)\nwiele błędów\ndodano obsługę NoLogDevices\ndodanie debugmessage dla konkretnej funkcjonalności\ndodanie niektórych stanów jest fałszywe\ndodano wsparcie dla scen zdefiniowanych na urządzenie\nfunkcja stanowa\nrecovery",
80
- "zh-cn": "更正\n加上“Use Kelvin”的备选办法,而不是对色彩的诱惑。 评 注\n删除现有逻辑,现在将使用兹2m的信息。\n目 录\n批发\n增加无车辆功能\n增加特定装置功能的碎片\n另有一些国家的违约情况\n对一个装置所界定的场地的更多支持\nf 现有国家作用\nfix 有线性的国家"
81
- },
82
- "0.1.0": {
83
- "en": "first release",
84
- "de": "erste veröffentlichung",
85
- "ru": "первый релиз",
86
- "pt": "primeiro lançamento",
87
- "nl": "eerste vrijlating",
88
- "fr": "première version",
89
- "it": "primo rilascio",
90
- "es": "primera liberación",
91
- "pl": "pierwsze wydanie",
92
- "zh-cn": "首次释放"
93
95
  }
94
96
  },
95
97
  "titleLang": {
@@ -3,14 +3,16 @@ const defineDeviceFromExposes = require('./exposes').defineDeviceFromExposes;
3
3
  const utils = require('./utils');
4
4
  const colors = require('./colors.js');
5
5
  const rgb = require('./rgb.js');
6
- const createCache = {};
6
+ //const createCache = {};
7
7
 
8
8
  class DeviceController {
9
- constructor(adapter, deviceCache, groupCache, config) {
9
+ constructor(adapter, deviceCache, groupCache, config, logCustomizations, createCache) {
10
10
  this.adapter = adapter;
11
11
  this.groupCache = groupCache;
12
12
  this.deviceCache = deviceCache;
13
13
  this.config = config;
14
+ this.logCustomizations = logCustomizations;
15
+ this.createCache = createCache;
14
16
  }
15
17
 
16
18
  async createDeviceDefinitions(exposes) {
@@ -24,6 +26,9 @@ class DeviceController {
24
26
  scenes = scenes.concat(expose.endpoints[key].scenes);
25
27
  }
26
28
  }
29
+ if (this.logCustomizations.debugDevices.includes(expose.ieee_address)) {
30
+ this.adapter.log.warn(`--->>> fromZ2M -> ${expose.ieee_address} exposes: ${JSON.stringify(expose)}`);
31
+ }
27
32
  // if the device is already present in the cache, remove it
28
33
  this.removeDeviceByIeee(this.deviceCache, expose.ieee_address);
29
34
  defineDeviceFromExposes(this.deviceCache, expose.friendly_name, expose.ieee_address, expose.definition, expose.power_source, scenes, this.config);
@@ -142,7 +147,7 @@ class DeviceController {
142
147
  async createOrUpdateDevices() {
143
148
  for (const device of this.groupCache.concat(this.deviceCache)) {
144
149
  const deviceName = device.id == device.ieee_address ? '' : device.id;
145
- if (!createCache[device.ieee_address] || createCache[device.ieee_address].common.name != deviceName) {
150
+ if (!this.createCache[device.ieee_address] || this.createCache[device.ieee_address].name != deviceName) {
146
151
  const deviceObj = {
147
152
  type: 'device',
148
153
  common: {
@@ -160,7 +165,7 @@ class DeviceController {
160
165
 
161
166
  //@ts-ignore
162
167
  await this.adapter.extendObjectAsync(device.ieee_address, deviceObj);
163
- createCache[device.ieee_address] = deviceObj;
168
+ this.createCache[device.ieee_address] = { name: deviceName };
164
169
  }
165
170
 
166
171
  // Here it is checked whether the scenes match the current data from z2m.
@@ -175,14 +180,15 @@ class DeviceController {
175
180
  }
176
181
 
177
182
  for (const state of device.states) {
178
- if (!createCache[device.ieee_address][state.id] || createCache[device.ieee_address][state.id].name != state.name) {
179
- const iobState = await this.copyAndCleanStateObj(state);
180
- await this.adapter.extendObjectAsync(`${device.ieee_address}.${state.id}`, {
183
+ if (!this.createCache[device.ieee_address][state.id] || this.createCache[device.ieee_address][state.id].name != state.name) {
184
+ const iobState = {
181
185
  type: 'state',
182
- common: iobState,
186
+ common: await this.copyAndCleanStateObj(state),
183
187
  native: {},
184
- });
185
- createCache[device.ieee_address][state.id] = state.name;
188
+ };
189
+
190
+ await this.adapter.extendObjectAsync(`${device.ieee_address}.${state.id}`, iobState);
191
+ this.createCache[device.ieee_address][state.id] = { name: state.name, created: true };
186
192
  }
187
193
  }
188
194
  }
@@ -212,6 +218,7 @@ class DeviceController {
212
218
  if (ieee_address != undefined) {
213
219
  this.adapter.setStateAsync(`${ieee_address}.available`, false, true);
214
220
  this.adapter.extendObject(`${ieee_address}`, { common: { name: 'Device removed!', } });
221
+ delete this.createCache[ieee_address];
215
222
  }
216
223
  }
217
224
 
package/lib/exposes.js CHANGED
@@ -8,10 +8,38 @@ const utils = require('./utils');
8
8
  const colors = require('./colors');
9
9
  const getNonGenDevStatesDefs = require('./nonGenericDevicesExtension').getStateDefinition;
10
10
 
11
+ // https://www.zigbee2mqtt.io/guide/usage/exposes.html#access
12
+ const z2mAccess = {
13
+ /**
14
+ * Bit 0: The property can be found in the published state of this device
15
+ */
16
+ STATE: 1,
17
+ /**
18
+ * Bit 1: The property can be set with a /set command
19
+ */
20
+ SET: 2,
21
+ /**
22
+ * Bit 2: The property can be retrieved with a /get command
23
+ */
24
+ GET: 4,
25
+ /**
26
+ * Bitwise inclusive OR of STATE and SET : 0b001 | 0b010
27
+ */
28
+ STATE_SET: 3,
29
+ /**
30
+ * Bitwise inclusive OR of STATE and GET : 0b001 | 0b100
31
+ */
32
+ STATE_GET: 5,
33
+ /**
34
+ * Bitwise inclusive OR of STATE and GET and SET : 0b001 | 0b100 | 0b010
35
+ */
36
+ ALL: 7,
37
+ };
38
+
11
39
  function genState(expose, role, name, desc) {
12
40
  let state;
13
- const readable = true; //expose.access > 0;
14
- const writable = expose.access > 1;
41
+ const readable = (expose.access & z2mAccess.STATE) > 0;
42
+ const writable = (expose.access & z2mAccess.SET) > 0;
15
43
  const stname = (name || expose.property);
16
44
 
17
45
  if (typeof stname !== 'string') {
@@ -121,22 +149,13 @@ function genState(expose, role, name, desc) {
121
149
 
122
150
  function createFromExposes(deviceID, ieee_address, definitions, power_source, scenes, config) {
123
151
  const states = [];
124
- // make the different (set and get) part of state is updatable if different exposes is used for get and set
125
- // as example:
126
- // ...
127
- // exposes.binary('some_option', ea.STATE, true, false).withDescription('Some Option'),
128
- // exposes.composite('options', 'options')
129
- // .withDescription('Some composite Options')
130
- // .withFeature(exposes.binary('some_option', ea.SET, true, false).withDescription('Some Option'))
131
- //in this case one state - `some_option` has two different exposes for set an get, we have to combine it ...
132
-
133
152
  function pushToStates(state, access) {
134
153
  if (state === undefined) {
135
154
  return 0;
136
155
  }
137
-
138
- state.readable = true;
139
- state.writable = access > 1;
156
+ if (access === undefined) access = z2mAccess.ALL;
157
+ state.readable = (access & z2mAccess.STATE) > 0;
158
+ state.writable = (access & z2mAccess.SET) > 0;
140
159
  const stateExists = states.findIndex((x, _index, _array) => (x.id === state.id));
141
160
 
142
161
  if (stateExists < 0) {
@@ -264,6 +283,21 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
264
283
  epname: expose.endpoint,
265
284
  setattr: 'state',
266
285
  }, prop.access);
286
+ // features contains TOGGLE?
287
+ if (prop.value_toggle) {
288
+ pushToStates({
289
+ id: `${prop.property}_toggle`,
290
+ prop: `${prop.property}_toggle`,
291
+ name: `Toggle state of the ${prop.property}`,
292
+ icon: undefined,
293
+ role: 'button',
294
+ write: true,
295
+ read: true,
296
+ type: 'boolean',
297
+ setattr: prop.property,
298
+ setter: (value) => (value) ? prop.value_toggle : undefined
299
+ });
300
+ }
267
301
  break;
268
302
  }
269
303
  case 'brightness': {
@@ -379,7 +413,7 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
379
413
  },
380
414
  epname: expose.endpoint,
381
415
  setattr: 'color',
382
- }, 2);
416
+ }, prop.access);
383
417
  break;
384
418
  }
385
419
  case 'color_hs': {
@@ -415,7 +449,7 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
415
449
  },
416
450
  epname: expose.endpoint,
417
451
  setattr: 'color',
418
- }, 2);
452
+ }, prop.access);
419
453
  // pushToStates({
420
454
  // id: expose.endpoint ? `hue_${expose.endpoint}` : 'hue',
421
455
  // prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
@@ -539,7 +573,7 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
539
573
  break;
540
574
  }
541
575
  default:
542
- pushToStates(genState(prop), 2);
576
+ pushToStates(genState(prop), prop.access);
543
577
  break;
544
578
  }
545
579
  }
@@ -552,6 +586,21 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
552
586
  switch (prop.name) {
553
587
  case 'state':
554
588
  pushToStates(genState(prop, 'switch'), prop.access);
589
+ // features contains TOGGLE?
590
+ if (prop.value_toggle) {
591
+ pushToStates({
592
+ id: `${prop.property}_toggle`,
593
+ prop: `${prop.property}_toggle`,
594
+ name: `Toggle state of the ${prop.property}`,
595
+ icon: undefined,
596
+ role: 'button',
597
+ write: true,
598
+ read: true,
599
+ type: 'boolean',
600
+ setattr: prop.property,
601
+ setter: (value) => (value) ? prop.value_toggle : undefined
602
+ });
603
+ }
555
604
  break;
556
605
  default:
557
606
  pushToStates(genState(prop), prop.access);
@@ -631,21 +680,22 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
631
680
  switch (expose.name) {
632
681
  case 'action': {
633
682
 
634
- // Ansatz:
635
-
636
- // Action aufspalten in 2 Blöcke:
637
- // Action (bekommt text ausser hold und release, auto reset nach 250 ms)
638
- // Hold: wird gesetzt bei hold, gelöscht bei passendem Release
683
+ if (!Array.isArray(expose.values)) {
684
+ break;
685
+ }
639
686
 
640
- if (!Array.isArray(expose.values)) break;
641
687
  const hasHold = expose.values.find((actionName) => actionName.includes('hold'));
642
688
  const hasRelease = expose.values.find((actionName) => actionName.includes('release'));
689
+
643
690
  for (const actionName of expose.values) {
644
691
  // is release state ? - skip
645
- if (hasHold && hasRelease && actionName.includes('release')) continue;
692
+ if (hasHold && hasRelease && actionName.includes('release')) {
693
+ continue;
694
+ }
695
+
646
696
  // is hold state ?
647
697
  if (hasHold && hasRelease && actionName.includes('hold')) {
648
- state = {
698
+ pushToStates({
649
699
  id: actionName.replace(/\*/g, ''),
650
700
  prop: 'action',
651
701
  name: actionName,
@@ -667,9 +717,67 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
667
717
  }
668
718
  return undefined;
669
719
  },
670
- };
671
- } else {
672
- state = {
720
+ }, expose.access);
721
+ }
722
+ else if (actionName.includes('color_temperature_move')) {
723
+ pushToStates({
724
+ id: 'color_temperature_move',
725
+ prop: 'action',
726
+ name: 'Color temperature move value',
727
+ icon: undefined,
728
+ role: 'level.color.temperature',
729
+ write: false,
730
+ read: true,
731
+ type: 'number',
732
+ def: config.useKelvin == true ? utils.miredKelvinConversion(150) : 500,
733
+ min: config.useKelvin == true ? utils.miredKelvinConversion(500) : 150,
734
+ max: config.useKelvin == true ? utils.miredKelvinConversion(150) : 500,
735
+ unit: config.useKelvin == true ? 'K' : 'mired',
736
+ getter: (payload) => {
737
+ if (payload.action != 'color_temperature_move') {
738
+ return undefined;
739
+ }
740
+
741
+ if (payload.action_color_temperature) {
742
+ if (config.useKelvin == true) {
743
+ return utils.miredKelvinConversion(payload.action_color_temperature);
744
+ }
745
+ else {
746
+ return payload.action_color_temperature;
747
+ }
748
+ }
749
+ },
750
+ }, expose.access);
751
+
752
+ }
753
+ else if (actionName.includes('color_move')) {
754
+ pushToStates({
755
+ id: 'color_move',
756
+ prop: 'action',
757
+ name: 'Color move value',
758
+ icon: undefined,
759
+ role: 'level.color.rgb',
760
+ write: false,
761
+ read: true,
762
+ type: 'string',
763
+ def: '#ffffff',
764
+ getter: (payload) => {
765
+ if (payload.action != 'color_move') {
766
+ return undefined;
767
+ }
768
+
769
+ if (payload.action_color && payload.action_color.hasOwnProperty('x') && payload.action_color.hasOwnProperty('y')) {
770
+ const colorval = rgb.cie_to_rgb(payload.action_color.x, payload.action_color.y);
771
+ return '#' + utils.decimalToHex(colorval[0]) + utils.decimalToHex(colorval[1]) + utils.decimalToHex(colorval[2]);
772
+ }
773
+ else {
774
+ return undefined;
775
+ }
776
+ }
777
+ }, expose.access);
778
+ }
779
+ else {
780
+ pushToStates({
673
781
  id: actionName.replace(/\*/g, ''),
674
782
  prop: 'action',
675
783
  name: actionName,
@@ -681,27 +789,12 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
681
789
  def: false,
682
790
  isEvent: true,
683
791
  getter: payload => (payload.action === actionName) ? true : undefined,
684
- };
792
+ }, expose.access);
685
793
  }
686
- pushToStates(state, expose.access);
687
794
  }
688
795
  // Can the device simulated_brightness?
689
796
  if (definitions.options && definitions.options.find(x => x.property == 'simulated_brightness')) {
690
- pushToStates({
691
- id: 'simulated_brightness',
692
- prop: 'brightness',
693
- name: 'Simulated brightness',
694
- icon: undefined,
695
- role: 'level.dimmer',
696
- write: true,
697
- read: true,
698
- type: 'number',
699
- unit: '%',
700
- def: 0,
701
- getter: payload => {
702
- return utils.bulbLevelToAdapterLevel(payload.brightness);
703
- },
704
- }, 1);
797
+ pushToStates(statesDefs.simulated_brightness, z2mAccess.STATE);
705
798
  }
706
799
  state = null;
707
800
  break;
@@ -720,7 +813,7 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
720
813
  switch (expose.name) {
721
814
  case 'contact':
722
815
  state = statesDefs.contact;
723
- pushToStates(statesDefs.opened, 1);
816
+ pushToStates(statesDefs.opened, expose.access);
724
817
  break;
725
818
 
726
819
  case 'battery_low':
@@ -763,6 +856,21 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
763
856
  switch (prop.name) {
764
857
  case 'state':
765
858
  pushToStates(genState(prop, 'switch'), prop.access);
859
+ // features contains TOGGLE?
860
+ if (prop.value_toggle) {
861
+ pushToStates({
862
+ id: `${prop.property}_toggle`,
863
+ prop: `${prop.property}_toggle`,
864
+ name: `Toggle state of the ${prop.property}`,
865
+ icon: undefined,
866
+ role: 'button',
867
+ write: true,
868
+ read: true,
869
+ type: 'boolean',
870
+ setattr: prop.property,
871
+ setter: (value) => (value) ? prop.value_toggle : undefined
872
+ });
873
+ }
766
874
  break;
767
875
  default:
768
876
  pushToStates(genState(prop), prop.access);
@@ -783,6 +891,12 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
783
891
  case 'running_mode':
784
892
  pushToStates(statesDefs.climate_running_mode, prop.access);
785
893
  break;
894
+ case 'local_temperature':
895
+ pushToStates(statesDefs.hvacThermostat_local_temp, prop.access);
896
+ break;
897
+ case 'local_temperature_calibration':
898
+ pushToStates(statesDefs.hvacThermostat_local_temp_calibration, prop.access);
899
+ break;
786
900
  default:
787
901
  {
788
902
  if (prop.name.includes('heating_setpoint')) {
@@ -809,7 +923,7 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
809
923
  return result;
810
924
  };
811
925
  // if we have a composite expose, the value have to be an object {expose.property : {prop.property: value}}
812
- if (prop.access & 2) {
926
+ if (prop.access & z2mAccess.SET) {
813
927
  st.setter = (value, options) => {
814
928
  const result = {};
815
929
  options[prop.property] = value;
@@ -819,7 +933,7 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
819
933
  st.setattr = expose.property;
820
934
  }
821
935
  // if we have a composite expose, the payload will be an object {expose.property : {prop.property: value}}
822
- if (prop.access & 1) {
936
+ if (prop.access & z2mAccess.STATE) {
823
937
  st.getter = payload => {
824
938
  if ((payload.hasOwnProperty(expose.property)) && (payload[expose.property] !== null) && payload[expose.property].hasOwnProperty(prop.property)) {
825
939
  return !isNaN(payload[expose.property][prop.property]) ? payload[expose.property][prop.property] : undefined;
@@ -841,22 +955,15 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
841
955
  // If necessary, add states defined for this device model.
842
956
  // Unfortunately this is necessary for some device models because they do not adhere to the standard
843
957
  for (const state of getNonGenDevStatesDefs(definitions.model)) {
844
- pushToStates(state, state.write ? 2 : 1);
958
+ pushToStates(state, state.write ? z2mAccess.SET : z2mAccess.STATE);
845
959
  }
846
960
 
847
961
  // Add default states
848
- pushToStates(statesDefs.available, 1);
849
-
850
- const newDevice = {
851
- id: deviceID,
852
- ieee_address: ieee_address,
853
- power_source: power_source,
854
- states: states,
855
- };
962
+ pushToStates(statesDefs.available, z2mAccess.STATE);
856
963
 
857
964
  // Create buttons for scenes
858
965
  for (const scene of scenes) {
859
- const sceneSate = {
966
+ pushToStates({
860
967
  id: `scene_${scene.id}`,
861
968
  prop: `scene_recall`,
862
969
  name: scene.name,
@@ -866,11 +973,16 @@ function createFromExposes(deviceID, ieee_address, definitions, power_source, sc
866
973
  read: true,
867
974
  type: 'boolean',
868
975
  setter: (value) => (value) ? scene.id : undefined
869
- };
870
- // @ts-ignore
871
- newDevice.states.push(sceneSate);
976
+ });
872
977
  }
873
978
 
979
+ const newDevice = {
980
+ id: deviceID,
981
+ ieee_address: ieee_address,
982
+ power_source: power_source,
983
+ states: states,
984
+ };
985
+
874
986
  return newDevice;
875
987
  }
876
988
 
package/lib/states.js CHANGED
@@ -56,8 +56,8 @@ const unitLookup = {
56
56
  'frequency': 'Hz',
57
57
  'power_factor': 'pf',
58
58
  'illuminance_lux': 'lx',
59
-
60
59
  };
60
+
61
61
  const timers = {};
62
62
 
63
63
  const states = {
@@ -72,7 +72,7 @@ const states = {
72
72
  type: 'number',
73
73
  min: 0,
74
74
  max: 255,
75
- def: 10,
75
+ def: 0,
76
76
  },
77
77
 
78
78
  available: {
@@ -86,6 +86,108 @@ const states = {
86
86
  type: 'boolean',
87
87
  def: false,
88
88
  },
89
+
90
+ color_read: {
91
+ id: 'color',
92
+ prop: 'action',
93
+ name: 'Color',
94
+ icon: undefined,
95
+ role: 'level.color.rgb',
96
+ write: false,
97
+ read: true,
98
+ type: 'string',
99
+ def: '#ffffff',
100
+ getter: (payload) => {
101
+ if (payload.action != 'color_wheel') {
102
+ return undefined;
103
+ }
104
+
105
+ if (payload.color && payload.color.hasOwnProperty('x') && payload.color.hasOwnProperty('y')) {
106
+ const colorval = rgb.cie_to_rgb(payload.color.x, payload.color.y);
107
+ return '#' + utils.decimalToHex(colorval[0]) + utils.decimalToHex(colorval[1]) + utils.decimalToHex(colorval[2]);
108
+ }
109
+ else {
110
+ return undefined;
111
+ }
112
+ },
113
+ },
114
+
115
+ simulated_brightness: {
116
+ id: 'simulated_brightness',
117
+ prop: 'brightness',
118
+ name: 'Simulated brightness',
119
+ icon: undefined,
120
+ role: 'level.dimmer',
121
+ write: true,
122
+ read: true,
123
+ type: 'number',
124
+ unit: '%',
125
+ def: 0,
126
+ getter: payload => {
127
+ return utils.bulbLevelToAdapterLevel(payload.brightness);
128
+ },
129
+ },
130
+
131
+ state: {
132
+ id: 'state',
133
+ name: 'Switch state',
134
+ icon: undefined,
135
+ role: 'switch',
136
+ write: true,
137
+ read: true,
138
+ type: 'boolean',
139
+ getter: payload => (payload.state === 'ON'),
140
+ setter: (value) => (value) ? 'ON' : 'OFF',
141
+ },
142
+
143
+ brightness: {
144
+ id: 'brightness',
145
+ name: 'Brightness',
146
+ icon: undefined,
147
+ role: 'level.dimmer',
148
+ write: true,
149
+ read: true,
150
+ type: 'number',
151
+ unit: '%',
152
+ min: 0,
153
+ max: 100,
154
+ getter: payload => {
155
+ return utils.bulbLevelToAdapterLevel(payload.brightness);
156
+ },
157
+ setter: (value) => {
158
+ return utils.adapterLevelToBulbLevel(value);
159
+ }
160
+ },
161
+
162
+ brightness_move: {
163
+ id: 'brightness_move',
164
+ prop: 'brightness_move',
165
+ name: 'Dimming',
166
+ icon: undefined,
167
+ role: 'state',
168
+ write: true,
169
+ read: false,
170
+ type: 'number',
171
+ min: -50,
172
+ max: 50,
173
+ def: 0
174
+ },
175
+
176
+ colortemp_move: {
177
+ id: 'colortemp_move',
178
+ prop: 'color_temp_move',
179
+ name: 'Colortemp change',
180
+ icon: undefined,
181
+ role: 'state',
182
+ write: true,
183
+ read: false,
184
+ type: 'number',
185
+ min: -50,
186
+ max: 50,
187
+ def: 0
188
+ },
189
+
190
+ //#################################################################
89
191
  device_query: { // button to trigger device read
90
192
  id: 'device_query',
91
193
  prop: 'device_query',
@@ -462,22 +564,7 @@ const states = {
462
564
  isEvent: true,
463
565
  getter: payload => (payload.click === 'both_double') ? true : undefined,
464
566
  },
465
- state: {
466
- id: 'state',
467
- name: 'Switch state',
468
- icon: undefined,
469
- role: 'switch',
470
- write: true,
471
- read: true,
472
- type: 'boolean',
473
- getter: payload => (payload.state === 'ON'),
474
- setter: (value) => (value) ? 'ON' : 'OFF',
475
- setterOpt: (value, options) => {
476
- const stateValue = (value ? 'ON' : 'OFF');
477
- return { ...options, state: stateValue };
478
- },
479
- inOptions: true,
480
- },
567
+
481
568
  stateEp: {
482
569
  id: 'state',
483
570
  name: 'Switch state',
@@ -1182,37 +1269,7 @@ const states = {
1182
1269
  return utils.bulbLevelToAdapterLevel(payload.brightness);
1183
1270
  },
1184
1271
  },
1185
- brightness: {
1186
- id: 'brightness',
1187
- name: 'Brightness',
1188
- icon: undefined,
1189
- role: 'level.dimmer',
1190
- write: true,
1191
- read: true,
1192
- type: 'number',
1193
- unit: '%',
1194
- min: 0,
1195
- max: 100,
1196
- getter: payload => {
1197
- return utils.bulbLevelToAdapterLevel(payload.brightness);
1198
- },
1199
- setter: (value, options) => {
1200
- return utils.adapterLevelToBulbLevel(value);
1201
- },
1202
- setterOpt: (value, options) => {
1203
- const hasTransitionTime = options && options.hasOwnProperty('transition_time');
1204
- const transitionTime = hasTransitionTime ? options.transition_time : 0;
1205
- const preparedOptions = { ...options, transition: transitionTime };
1206
- preparedOptions.brightness = utils.adapterLevelToBulbLevel(value);
1207
- return preparedOptions;
1208
- },
1209
- readResponse: (resp) => {
1210
- const respObj = resp[0];
1211
- if (respObj.status === 0 && respObj.attrData != undefined) {
1212
- return utils.bulbLevelToAdapterLevel(respObj.attrData);
1213
- }
1214
- },
1215
- },
1272
+
1216
1273
  colortemp: {
1217
1274
  id: 'colortemp',
1218
1275
  prop: 'color_temp',
@@ -2153,7 +2210,7 @@ const states = {
2153
2210
 
2154
2211
  // hvac Thermostat cluster - generic states
2155
2212
  hvacThermostat_local_temp: {
2156
- id: 'local_temp',
2213
+ id: 'local_temperature',
2157
2214
  prop: 'local_temperature',
2158
2215
  name: 'Local Temperature',
2159
2216
  icon: undefined,
@@ -2164,7 +2221,7 @@ const states = {
2164
2221
  unit: '°C',
2165
2222
  },
2166
2223
  hvacThermostat_local_temp_calibration: {
2167
- id: 'local_temp_calibration',
2224
+ id: 'local_temperature_calibration',
2168
2225
  prop: 'local_temperature_calibration',
2169
2226
  name: 'Temperature Calibration',
2170
2227
  icon: undefined,
@@ -2173,6 +2230,7 @@ const states = {
2173
2230
  read: true,
2174
2231
  type: 'number',
2175
2232
  unit: '°C',
2233
+ def: 0
2176
2234
  },
2177
2235
  hvacThermostat_remote_sensing: {
2178
2236
  id: 'remote_sensing',
@@ -5292,32 +5350,6 @@ const states = {
5292
5350
  min: -50,
5293
5351
  max: 50
5294
5352
  },
5295
- brightness_move: {
5296
- id: 'brightness_move',
5297
- prop: 'brightness_move',
5298
- name: 'Dimming',
5299
- icon: undefined,
5300
- role: 'state',
5301
- write: true,
5302
- read: false,
5303
- type: 'number',
5304
- min: -50,
5305
- max: 50,
5306
- def: 0
5307
- },
5308
- colortemp_move: {
5309
- id: 'colortemp_move',
5310
- prop: 'color_temp_move',
5311
- name: 'Colortemp change',
5312
- icon: undefined,
5313
- role: 'state',
5314
- write: true,
5315
- read: false,
5316
- type: 'number',
5317
- min: -50,
5318
- max: 50,
5319
- def: 0
5320
- },
5321
5353
  hue_move: {
5322
5354
  id: 'hue_move',
5323
5355
  prop: 'hue_move',
@@ -3,11 +3,12 @@ const incStatsQueue = [];
3
3
  const timeOutCache = {};
4
4
 
5
5
  class StatesController {
6
- constructor(adapter, deviceCache, groupCache, logCustomizations) {
6
+ constructor(adapter, deviceCache, groupCache, logCustomizations, createCache) {
7
7
  this.adapter = adapter;
8
8
  this.groupCache = groupCache;
9
9
  this.deviceCache = deviceCache;
10
10
  this.logCustomizations = logCustomizations;
11
+ this.createCache = createCache;
11
12
  }
12
13
 
13
14
  processDeviceMessage(messageObj) {
@@ -49,6 +50,13 @@ class StatesController {
49
50
 
50
51
  const stateName = `${device.ieee_address}.${state.id}`;
51
52
 
53
+ // It may be that the state has not yet been created!
54
+ if (!this.createCache[device.ieee_address] || !this.createCache[device.ieee_address][state.id] || !this.createCache[device.ieee_address][state.id].created) {
55
+ incStatsQueue[incStatsQueue.length] = messageObj;
56
+ //this.adapter.log.debug(`State ${stateName} is not yet created, queue state in incStatsQueue!`);
57
+ continue;
58
+ }
59
+
52
60
  try {
53
61
  if (state.isEvent) {
54
62
  if (state.getter) {
@@ -36,16 +36,16 @@ class Z2mController {
36
36
  stateID = deviceState.prop;
37
37
  }
38
38
 
39
- let topic = `${device.ieee_address}/set`;
40
- if (device.ieee_address.includes('group_')) {
41
- topic = `${device.id}/set`;
39
+ if (deviceState.setattr) {
40
+ stateID = deviceState.setattr;
42
41
  }
43
42
 
43
+
44
44
  const controlObj = {
45
45
  payload: {
46
46
  [stateID]: stateVal
47
47
  },
48
- topic: topic
48
+ topic: `${device.id}/set`
49
49
  };
50
50
 
51
51
  // set stats with the mentioned role or ids always immediately to ack = true, because these are not reported back by Zigbee2MQTT
package/main.js CHANGED
@@ -23,6 +23,7 @@ let mqttClient;
23
23
  let deviceCache = [];
24
24
  // eslint-disable-next-line prefer-const
25
25
  let groupCache = [];
26
+ const createCache = {};
26
27
  const logCustomizations = { debugDevices: '', logfilter: [] };
27
28
  let showInfo = true;
28
29
  let statesController;
@@ -44,8 +45,8 @@ class Zigbee2mqtt extends core.Adapter {
44
45
  }
45
46
 
46
47
  async onReady() {
47
- statesController = new StatesController(this, deviceCache, groupCache, logCustomizations);
48
- deviceController = new DeviceController(this, deviceCache, groupCache, this.config);
48
+ statesController = new StatesController(this, deviceCache, groupCache, logCustomizations, createCache);
49
+ deviceController = new DeviceController(this, deviceCache, groupCache, this.config, logCustomizations, createCache);
49
50
  z2mController = new Z2mController(this, deviceCache, groupCache, logCustomizations);
50
51
 
51
52
  // Initialize your adapter here
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.zigbee2mqtt",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Zigbee2MQTT adapter for ioBroker",
5
5
  "author": {
6
6
  "name": "Dennis Rathjen",
@@ -24,25 +24,25 @@
24
24
  "aedes-persistence-nedb": "^2.0.3",
25
25
  "mqtt": "^4.3.7",
26
26
  "net": "^1.0.2",
27
- "ws": "^8.9.0"
27
+ "ws": "^8.10.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@alcalzone/release-script-plugin-iobroker": "^3.5.9",
31
31
  "@alcalzone/release-script-plugin-license": "^3.5.9",
32
32
  "@alcalzone/release-script": "^3.5.9",
33
- "@iobroker/adapter-dev": "^1.1.0",
33
+ "@iobroker/adapter-dev": "^1.2.0",
34
34
  "@iobroker/testing": "^4.1.0",
35
35
  "@tsconfig/node14": "^1.0.3",
36
36
  "@types/chai": "^4.3.3",
37
37
  "@types/chai-as-promised": "^7.1.5",
38
38
  "@types/mocha": "^10.0.0",
39
- "@types/node": "^18.11.0",
39
+ "@types/node": "^18.11.7",
40
40
  "@types/proxyquire": "^1.3.28",
41
41
  "@types/sinon": "^10.0.13",
42
42
  "@types/sinon-chai": "^3.2.8",
43
43
  "chai": "^4.3.6",
44
44
  "chai-as-promised": "^7.1.1",
45
- "eslint": "^8.25.0",
45
+ "eslint": "^8.26.0",
46
46
  "eslint-config-prettier": "^8.5.0",
47
47
  "eslint-plugin-prettier": "^4.2.1",
48
48
  "mocha": "^10.1.0",