iobroker.zigbee2mqtt 3.2.2 → 3.2.5

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.
@@ -6,19 +6,6 @@ const rgb = require('./rgb.js');
6
6
  const ImageController = require('./imageController').ImageController;
7
7
  const dmUtils = require('@iobroker/dm-utils');
8
8
 
9
- /**
10
- * Prüft ob eine ieee_address in der kommaseparierten debugDevices-Liste enthalten ist.
11
- * String.includes() würde Substring-Matches liefern (z.B. "0x12" trifft "0x1234").
12
- *
13
- * @param {string} debugDevices
14
- * @param {string} address
15
- * @returns {boolean}
16
- */
17
- function isDebugDevice(debugDevices, address) {
18
- if (!debugDevices || !address) { return false; }
19
- return String(debugDevices).split(',').map((s) => s.trim()).includes(address);
20
- }
21
-
22
9
  /**
23
10
  * Verwaltet das Erstellen und Aktualisieren von Geräte- und Gruppenobjekten in ioBroker
24
11
  * basierend auf den Zigbee2MQTT-Exposes.
@@ -43,6 +30,31 @@ class DeviceController extends dmUtils.DeviceManagement {
43
30
  this.logCustomizations = logCustomizations;
44
31
  this.createCache = createCache;
45
32
  this.imageController = new ImageController(adapter);
33
+
34
+ // Debug-Device Cache - wird lazy befüllt (siehe isDebugDevice)
35
+ this._debugDeviceCache = null;
36
+ }
37
+
38
+ /**
39
+ * Checks if the given device is a debug device.
40
+ * Der CSV-String wird einmalig geparst und in einem Set gecached (O(1) Lookup).
41
+ * Case-insensitive Vergleich.
42
+ *
43
+ * @param {string} ieeeAddr - IEEE address of the device
44
+ * @returns {boolean}
45
+ */
46
+ isDebugDevice(ieeeAddr) {
47
+ if (!ieeeAddr) { return false; }
48
+ if (this._debugDeviceCache === null) {
49
+ const debugDeviceList = this.logCustomizations.debugDevices || '';
50
+ this._debugDeviceCache = new Set(
51
+ debugDeviceList
52
+ .split(',')
53
+ .map(s => s.trim().toLowerCase())
54
+ .filter(s => s.length > 0)
55
+ );
56
+ }
57
+ return this._debugDeviceCache.has(ieeeAddr.toLowerCase());
46
58
  }
47
59
 
48
60
  /**
@@ -58,12 +70,15 @@ class DeviceController extends dmUtils.DeviceManagement {
58
70
  return;
59
71
  }
60
72
  utils.clearArray(this.deviceCache);
73
+ // Topic->Device Map-Cache invalidieren, da deviceCache neu aufgebaut wird
74
+ // (verhindert veraltete Referenzen -> u.a. available-State bleibt sonst false)
75
+ this.invalidateStatesDeviceMap();
61
76
  for (const devicesMessag of devicesMessage) {
62
77
  if (!devicesMessag || !devicesMessag.ieee_address) {
63
78
  continue;
64
79
  }
65
80
 
66
- if (isDebugDevice(this.logCustomizations.debugDevices, devicesMessag.ieee_address)) {
81
+ if (this.isDebugDevice(devicesMessag.ieee_address)) {
67
82
  this.adapter.log.warn(
68
83
  `--->>> fromZ2M -> ${devicesMessag.ieee_address} exposes: ${JSON.stringify(devicesMessag)}`
69
84
  );
@@ -76,7 +91,11 @@ class DeviceController extends dmUtils.DeviceManagement {
76
91
  if (Array.isArray(devicesMessag.definition.exposes)) {
77
92
  try {
78
93
  const newDevice = await createDeviceFromExposes(devicesMessag, this.adapter);
79
- newDevice.icon = await this.imageController.getDeviceIcon(devicesMessag);
94
+ try {
95
+ newDevice.icon = await this.imageController.getDeviceIcon(devicesMessag);
96
+ } catch (e) {
97
+ this.adapter.log.warn(`[deviceController] Could not load icon for device ${newDevice.common?.name || 'unknown'}: ${e.message}`);
98
+ }
80
99
  newDevice.manufacturer = devicesMessag.definition.vendor || '';
81
100
  newDevice.model = devicesMessag.definition.model || '';
82
101
  newDevice.modelDescription = devicesMessag.definition.description || '';
@@ -292,12 +311,14 @@ class DeviceController extends dmUtils.DeviceManagement {
292
311
  return;
293
312
  }
294
313
  utils.clearArray(this.groupCache);
314
+ // Topic->Device Map-Cache invalidieren, da groupCache neu aufgebaut wird
315
+ this.invalidateStatesDeviceMap();
295
316
  for (const groupMessage of groupsMessage) {
296
317
  // Fix 5: fehlende Pflichtfelder abfangen
297
318
  if (!groupMessage || groupMessage.id == null || !groupMessage.friendly_name) {
298
319
  continue;
299
320
  }
300
- if (isDebugDevice(this.logCustomizations.debugDevices, String(groupMessage.id))) {
321
+ if (this.isDebugDevice(String(groupMessage.id))) {
301
322
  this.adapter.log.warn(`--->>> fromZ2M -> ${groupMessage.id} exposes: ${JSON.stringify(groupMessage)}`);
302
323
  }
303
324
  this.defineGroupDevice(groupMessage.friendly_name, `group_${groupMessage.id}`, groupMessage.scenes || []);
@@ -469,6 +490,8 @@ class DeviceController extends dmUtils.DeviceManagement {
469
490
  .find((x) => x.id === messageObj.payload.data.from);
470
491
  if (renamedDevice) {
471
492
  renamedDevice.id = messageObj.payload.data.to;
493
+ // Topic (device.id) hat sich geändert -> Map-Cache invalidieren
494
+ this.invalidateStatesDeviceMap();
472
495
  }
473
496
  }
474
497
 
@@ -485,6 +508,18 @@ class DeviceController extends dmUtils.DeviceManagement {
485
508
  }
486
509
  }
487
510
 
511
+ /**
512
+ * Invalidiert den Topic->Device Map-Cache des StatesController.
513
+ * Muss nach jedem Neuaufbau von deviceCache/groupCache aufgerufen werden,
514
+ * damit findDeviceByTopic() keine veralteten Geräteobjekte liefert
515
+ * (sonst bleibt z.B. das available-State neu angelernter Geräte auf false).
516
+ */
517
+ invalidateStatesDeviceMap() {
518
+ if (this.adapter.statesController && typeof this.adapter.statesController.clearDeviceMap === 'function') {
519
+ this.adapter.statesController.clearDeviceMap();
520
+ }
521
+ }
522
+
488
523
  /**
489
524
  * Erstellt eine bereinigte Kopie eines State-Objekts ohne interne Laufzeit-Felder
490
525
  * (getter, setter, prop, etc.) für die ioBroker-Objektdefinition.
@@ -765,11 +800,9 @@ class DeviceController extends dmUtils.DeviceManagement {
765
800
  const detailItems = device._detailItems || {};
766
801
  const hasDetails = Object.keys(detailItems).length > 0;
767
802
 
768
- /** @type {Record<string, any>} */
769
803
  const data = {};
770
804
 
771
805
  // ── Tab 1: Gerät ─────────────────────────────────────────────────────
772
- /** @type {Record<string, any>} */
773
806
  const infoItems = {};
774
807
  infoItems['_h1'] = { type: 'header', text: 'Device Identity', size: 4, newLine: true };
775
808
  infoItems['_d1'] = { type: 'divider', color: 'primary' };
@@ -781,7 +814,6 @@ class DeviceController extends dmUtils.DeviceManagement {
781
814
  infoItems['pwr'] = { type: 'staticInfo', label: 'Power Source', data: device.powerSource || '—', size: 16, addColon: true, newLine: true };
782
815
 
783
816
  // ── Tab 2: Verbindung ─────────────────────────────────────────────────
784
- /** @type {Record<string, any>} */
785
817
  const connItems = {};
786
818
  connItems['_h1'] = { type: 'header', text: 'Zigbee Connection', size: 4, newLine: true };
787
819
  connItems['_d1'] = { type: 'divider', color: 'primary' };
@@ -793,7 +825,6 @@ class DeviceController extends dmUtils.DeviceManagement {
793
825
  }
794
826
 
795
827
  // ── Tab 3: Technisch ──────────────────────────────────────────────────
796
- /** @type {Record<string, any>} */
797
828
  const techItems = {};
798
829
  techItems['_h1'] = { type: 'header', text: 'Zigbee Status', size: 4, newLine: true };
799
830
  techItems['_d1'] = { type: 'divider', color: 'primary' };
@@ -806,7 +837,6 @@ class DeviceController extends dmUtils.DeviceManagement {
806
837
  data.disabled = !!device.disabled;
807
838
 
808
839
  // ── Tabs zusammenbauen ────────────────────────────────────────────────
809
- /** @type {Record<string, any>} */
810
840
  const tabs = {
811
841
  _tab_info: { type: 'panel', label: 'Device', items: infoItems },
812
842
  _tab_conn: { type: 'panel', label: 'Connection', items: connItems },
package/lib/exposes.js CHANGED
@@ -111,6 +111,7 @@ function genState(expose, role, name, desc) {
111
111
  states: {},
112
112
  };
113
113
 
114
+ state.type = 'string';
114
115
  for (const val of expose.values) {
115
116
  // if a definition of a button (eg. Aqara presence detector FP1)
116
117
  if (val === '') {
@@ -118,7 +119,6 @@ function genState(expose, role, name, desc) {
118
119
  } else {
119
120
  state.states[val] = val;
120
121
  }
121
- state.type = typeof val;
122
122
  }
123
123
  // Fix 4: wenn values leer → type bleibt undefined → Fallback auf 'string'
124
124
  if (!state.type) {
@@ -153,6 +153,7 @@ function genState(expose, role, name, desc) {
153
153
  break;
154
154
 
155
155
  default:
156
+ console.warn(`[genState] Unknown expose type: ${expose.type}`);
156
157
  break;
157
158
  }
158
159
 
@@ -203,13 +204,12 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
203
204
  state = Object.assign({}, state);
204
205
 
205
206
  if (access === undefined) {access = z2mAccess.ALL;}
206
- state.readable = (access & z2mAccess.STATE) > 0;
207
- state.writable = (access & z2mAccess.SET) > 0;
207
+ state.read = (access & z2mAccess.STATE) > 0;
208
+ state.write = (access & z2mAccess.SET) > 0;
208
209
  const stateExists = states.findIndex((x, _index, _array) => x.id === state.id);
209
210
 
210
211
  if (stateExists < 0) {
211
- state.write = state.writable;
212
- if (!state.writable) {
212
+ if (!state.write) {
213
213
  if (state.hasOwnProperty('setter')) {
214
214
  delete state.setter;
215
215
  }
@@ -219,7 +219,7 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
219
219
  }
220
220
  }
221
221
 
222
- if (!state.readable) {
222
+ if (!state.read) {
223
223
  if (state.hasOwnProperty('getter')) {
224
224
  //to awid some worning on unprocessed data
225
225
  state.getter = (_payload) => undefined;
@@ -228,7 +228,7 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
228
228
 
229
229
  return states.push(state);
230
230
  }
231
- if (state.readable && !states[stateExists].readable) {
231
+ if (state.read && !states[stateExists].read) {
232
232
  states[stateExists].read = state.read;
233
233
  // as state is readable, it can't be button or event
234
234
  if (states[stateExists].role === 'button') {
@@ -259,11 +259,11 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
259
259
  } else if (state.hasOwnProperty('prop')) {
260
260
  states[stateExists].prop = state.prop;
261
261
  }
262
- states[stateExists].readable = true;
262
+ states[stateExists].read = true;
263
263
  }
264
264
 
265
- if (state.writable && !states[stateExists].writable) {
266
- states[stateExists].write = state.writable;
265
+ if (state.write && !states[stateExists].write) {
266
+ states[stateExists].write = state.write;
267
267
  // use new state `setter`
268
268
  if (state.hasOwnProperty('setter')) {
269
269
  states[stateExists].setter = state.setter;
@@ -303,7 +303,7 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
303
303
  if (state.hasOwnProperty('epname')) {
304
304
  states[stateExists].epname = state.epname;
305
305
  }
306
- states[stateExists].writable = true;
306
+ states[stateExists].write = true;
307
307
  }
308
308
 
309
309
  return states.length;
@@ -1289,9 +1289,8 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
1289
1289
  return !isNaN(payload[expose.property][prop.property])
1290
1290
  ? payload[expose.property][prop.property]
1291
1291
  : undefined;
1292
- }
1293
- return undefined;
1294
-
1292
+ }
1293
+ return undefined;
1295
1294
  };
1296
1295
  } else {
1297
1296
  state.getter = (payload) => {
@@ -106,7 +106,7 @@ class ImageController {
106
106
  return '';
107
107
  }
108
108
 
109
- const imageSize = this.adapter.config.deviceIconsSize;
109
+ const imageSize = this.adapter.config.deviceIconsSize || 120;
110
110
  const z2mModel = device.definition.model || '';
111
111
  const z2mIconFileNameJPG = `${this.sanitizeZ2MDeviceName(z2mModel)}.jpg`;
112
112
  const z2mIconFileNamePNG = `${this.sanitizeZ2MDeviceName(z2mModel)}.png`;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Aedes = require('aedes');
4
4
  const net = require('node:net');
5
+ const MemoryPersistence = require('aedes-persistence');
5
6
 
6
7
  /**
7
8
  * Verwaltet den internen oder Dummy-MQTT-Server (Aedes) für den Zigbee2MQTT-Adapter.
@@ -58,10 +59,9 @@ class MqttServerController {
58
59
  */
59
60
  async createMQTTServer() {
60
61
  if (!this._checkConfig()) {
61
- return;
62
+ return false;
62
63
  }
63
64
  try {
64
- const MemoryPersistence = require('aedes-persistence');
65
65
  const db = MemoryPersistence();
66
66
  const aedes = Aedes({ persistence: db });
67
67
  this.aedesBroker = aedes;
@@ -82,10 +82,20 @@ class MqttServerController {
82
82
  }
83
83
  );
84
84
  });
85
+ return true;
85
86
  } catch (err) {
86
- this.adapter.log.error(`createMQTTServer error: ${err && err.message ? err.message : String(err)}`);
87
+ const code = err && err.code ? err.code : '';
88
+ if (code === 'EACCES' || code === 'EADDRINUSE') {
89
+ this.adapter.log.error(
90
+ `Port ${this.adapter.config.mqttServerPort} is already in use or requires admin privileges. ` +
91
+ `Please change the port in the adapter settings or stop the other service.`
92
+ );
93
+ } else {
94
+ this.adapter.log.error(`createMQTTServer error: ${err && err.message ? err.message : String(err)}`);
95
+ }
87
96
  // Sicherstellen dass kein halboffener Server übrig bleibt
88
97
  this.closeServer();
98
+ return false;
89
99
  }
90
100
  }
91
101
 
@@ -95,7 +105,7 @@ class MqttServerController {
95
105
  */
96
106
  async createDummyMQTTServer() {
97
107
  if (!this._checkConfig()) {
98
- return;
108
+ return false;
99
109
  }
100
110
  try {
101
111
  const aedes = Aedes();
@@ -117,9 +127,19 @@ class MqttServerController {
117
127
  }
118
128
  );
119
129
  });
130
+ return true;
120
131
  } catch (err) {
121
- this.adapter.log.error(`createDummyMQTTServer error: ${err && err.message ? err.message : String(err)}`);
132
+ const code = err && err.code ? err.code : '';
133
+ if (code === 'EACCES' || code === 'EADDRINUSE') {
134
+ this.adapter.log.error(
135
+ `Port ${this.adapter.config.mqttServerPort} is already in use or requires admin privileges. ` +
136
+ `Please change the port in the adapter settings or stop the other service.`
137
+ );
138
+ } else {
139
+ this.adapter.log.error(`createDummyMQTTServer error: ${err && err.message ? err.message : String(err)}`);
140
+ }
122
141
  this.closeServer();
142
+ return false;
123
143
  }
124
144
  }
125
145
 
package/lib/rgb.js CHANGED
@@ -152,6 +152,11 @@ function rgb_to_cie(red, green, blue) {
152
152
  * @param v
153
153
  */
154
154
  function hsvToRGB(h, s, v) {
155
+ if (arguments.length === 1) {
156
+ s = h.s;
157
+ v = h.v;
158
+ h = h.h;
159
+ }
155
160
  h = (h % 360) / 360;
156
161
  s = s / 100;
157
162
  v = v / 100;
@@ -159,11 +164,6 @@ function hsvToRGB(h, s, v) {
159
164
  let r;
160
165
  let g;
161
166
  let b;
162
- if (arguments.length === 1) {
163
- s = h.s;
164
- v = h.v;
165
- h = h.h;
166
- }
167
167
  const i = Math.floor(h * 6);
168
168
  const f = h * 6 - i;
169
169
  const p = v * (1 - s);
package/lib/states.js CHANGED
@@ -37,6 +37,17 @@ const nameLookup = {
37
37
  Hz: 'frequency',
38
38
  pf: 'power_factor',
39
39
  lx: 'illuminance_lux',
40
+ '°C': 'temperature_celsius',
41
+ '°F': 'temperature_fahrenheit',
42
+ mA: 'milliampere',
43
+ mV: 'millivolt',
44
+ 'μg/m³': 'microgram_per_cubic_meter',
45
+ dB: 'decibel',
46
+ '%rH': 'percent_rh',
47
+ 'km/h': 'kilometer_per_hour',
48
+ 'm/s': 'meter_per_second',
49
+ 'mg/m³': 'milligram_per_cubic_meter',
50
+ K: 'kelvin',
40
51
  };
41
52
 
42
53
  const unitLookup = {
@@ -53,6 +64,17 @@ const unitLookup = {
53
64
  frequency: 'Hz',
54
65
  power_factor: 'pf',
55
66
  illuminance_lux: 'lx',
67
+ temperature_celsius: '°C',
68
+ temperature_fahrenheit: '°F',
69
+ milliampere: 'mA',
70
+ millivolt: 'mV',
71
+ microgram_per_cubic_meter: 'μg/m³',
72
+ decibel: 'dB',
73
+ percent_rh: '%rH',
74
+ kilometer_per_hour: 'km/h',
75
+ meter_per_second: 'm/s',
76
+ milligram_per_cubic_meter: 'mg/m³',
77
+ kelvin: 'K',
56
78
  };
57
79
 
58
80
  const states = {
@@ -396,7 +418,7 @@ const states = {
396
418
  unit: '',
397
419
  getter: (payload) => {
398
420
  // Wenn illuminance_raw bereits im Payload → illuminance_raw_direct setzt ihn direkt
399
- if (payload.illuminance_raw != null) { return undefined; }
421
+ if (payload.illuminance_raw != null) { return Number(payload.illuminance_raw); }
400
422
  const lux = payload.illuminance;
401
423
  if (lux == null) { return undefined; }
402
424
  if (lux <= 0) { return 0; }