iobroker.zigbee2mqtt 3.0.21 → 3.1.1

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.
@@ -18,6 +18,7 @@ class ImageController {
18
18
  * @param modelName
19
19
  */
20
20
  sanitizeModelIDForImageUrl(modelName) {
21
+ if (!modelName) {return 'NA';}
21
22
  const modelNameString = modelName.replace('/', '_');
22
23
  // eslint-disable-next-line no-control-regex
23
24
  return modelNameString.replace(/\u0000/g, '');
@@ -28,33 +29,37 @@ class ImageController {
28
29
  * @param deviceName
29
30
  */
30
31
  sanitizeZ2MDeviceName(deviceName) {
32
+ if (!deviceName) {return 'NA';}
31
33
  const deviceNameString = deviceName.replace(/:|\s|\//g, '-');
32
34
  // eslint-disable-next-line no-control-regex
33
- return deviceName ? deviceNameString.replace(/\u0000/g, '') : 'NA';
35
+ return deviceNameString.replace(/\u0000/g, '');
34
36
  }
35
37
 
36
38
  /**
37
- *
38
39
  * @param device
40
+ * @param ext
39
41
  */
40
- getZ2mDeviceImageModelJPG(device) {
42
+ _buildZ2mImageUrl(device, ext) {
41
43
  if (device && device.definition && device.definition.model) {
42
- const icoString = `https://www.zigbee2mqtt.io/images/devices/${this.sanitizeZ2MDeviceName(device.definition.model)}.jpg`;
44
+ const icoString = `https://www.zigbee2mqtt.io/images/devices/${this.sanitizeZ2MDeviceName(device.definition.model)}.${ext}`;
43
45
  // eslint-disable-next-line no-control-regex
44
46
  return icoString.replace(/\u0000/g, '');
45
47
  }
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * @param device
53
+ */
54
+ getZ2mDeviceImageModelJPG(device) {
55
+ return this._buildZ2mImageUrl(device, 'jpg');
46
56
  }
47
57
 
48
58
  /**
49
- *
50
59
  * @param device
51
60
  */
52
61
  getZ2mDeviceImageModelPNG(device) {
53
- if (device && device.definition && device.definition.model) {
54
- const icoString = `https://www.zigbee2mqtt.io/images/devices/${this.sanitizeZ2MDeviceName(device.definition.model)}.png`;
55
- // eslint-disable-next-line no-control-regex
56
- return icoString.replace(/\u0000/g, '');
57
- }
62
+ return this._buildZ2mImageUrl(device, 'png');
58
63
  }
59
64
 
60
65
 
@@ -76,14 +81,17 @@ class ImageController {
76
81
  */
77
82
  async getDeviceIcon(device) {
78
83
  if (!this.adapter.config.useDeviceIcons) {
79
- return '';
80
- }
81
-
82
- const imageSize = this.adapter.config.deviceIconsSize;
84
+ return '';
85
+ }
86
+ if (!device || !device.definition || !device.definition.model) {
87
+ return '';
88
+ }
83
89
 
84
- const z2mIconFileNameJPG = `${this.sanitizeZ2MDeviceName(device.definition.model)}.jpg`;
85
- const z2mIconFileNamePNG = `${this.sanitizeZ2MDeviceName(device.definition.model)}.png`;
86
- const slsIconFileName = `${this.sanitizeModelIDForImageUrl(device.model_id)}.png`;
90
+ const imageSize = this.adapter.config.deviceIconsSize;
91
+ const z2mModel = device.definition.model || '';
92
+ const z2mIconFileNameJPG = `${this.sanitizeZ2MDeviceName(z2mModel)}.jpg`;
93
+ const z2mIconFileNamePNG = `${this.sanitizeZ2MDeviceName(z2mModel)}.png`;
94
+ const slsIconFileName = `${this.sanitizeModelIDForImageUrl(device.model_id || z2mModel)}.png`;
87
95
 
88
96
  let iconFileName = await this.getExistingIconFileName(z2mIconFileNameJPG, z2mIconFileNamePNG, slsIconFileName);
89
97
  let iconFound = true;
@@ -93,7 +101,7 @@ return '';
93
101
  this.getZ2mDeviceImageModelJPG(device),
94
102
  this.getZ2mDeviceImageModelPNG(device),
95
103
  this.getSlsDeviceImage(device)
96
- ];
104
+ ].filter(Boolean);
97
105
 
98
106
  for (const iconUrl of iconUrls) {
99
107
  try {
@@ -108,38 +116,32 @@ return '';
108
116
  }
109
117
  }
110
118
 
111
- if (!iconFound) {
119
+ if (!iconFound || !iconFileName) {
112
120
  this.adapter.log.warn(`Failed to download image for device model: ${device.definition.model} - ${device.definition.description}`);
113
121
  return '';
114
- }
115
- // Load image from the Meta-Store
116
- const icon = await this.adapter.readFileAsync(this.adapter.namespace, iconFileName);
117
- // Load Image Metadata
118
- const origIconMeta = await sharp(icon.file).metadata();
119
- // Check whether the image needs to be resized
120
- if (
121
- (origIconMeta.height && origIconMeta.height > imageSize) ||
122
- (origIconMeta.width && origIconMeta.width > imageSize)
123
- ) {
124
- // Resize image to 28x28 pixel
125
- this.adapter.log.info(
126
- `Resize image for device model ${device.definition.model} from: ${origIconMeta.width}x${origIconMeta.height} to ${imageSize}x${imageSize}`
127
- );
128
- icon.file = await sharp(icon.file)
129
- .resize({
130
- width: imageSize,
131
- height: imageSize,
132
- fit: sharp.fit.cover,
133
- position: sharp.strategy.entropy,
134
- })
135
- .toBuffer();
136
- // Replace the original image with the resize image.
137
- await this.adapter.writeFileAsync(this.adapter.namespace, iconFileName, icon.file);
138
- }
122
+ }
123
+
124
+ const icon = await this.adapter.readFileAsync(this.adapter.namespace, iconFileName);
125
+ const origIconMeta = await sharp(icon.file).metadata();
126
+ if (
127
+ (origIconMeta.height && origIconMeta.height > imageSize) ||
128
+ (origIconMeta.width && origIconMeta.width > imageSize)
129
+ ) {
130
+ this.adapter.log.info(
131
+ `Resize image for device model ${device.definition.model} from: ${origIconMeta.width}x${origIconMeta.height} to ${imageSize}x${imageSize}`
132
+ );
133
+ icon.file = await sharp(icon.file)
134
+ .resize({
135
+ width: imageSize,
136
+ height: imageSize,
137
+ fit: sharp.fit.cover,
138
+ position: sharp.strategy.entropy,
139
+ })
140
+ .toBuffer();
141
+ await this.adapter.writeFileAsync(this.adapter.namespace, iconFileName, icon.file);
142
+ }
139
143
 
140
- // Create and output Base64
141
- return `data:image/png;base64,${icon.file.toString('base64')}`;
142
-
144
+ return `data:image/png;base64,${icon.file.toString('base64')}`;
143
145
  }
144
146
  /**
145
147
  *
package/lib/messages.js CHANGED
@@ -4,6 +4,9 @@
4
4
  * @param log
5
5
  */
6
6
  async function adapterInfo(config, log) {
7
+ if (!config || !log) {
8
+ return;
9
+ }
7
10
  log.info('================================= Adapter Config =================================');
8
11
  log.info(`|| Zigbee2MQTT Frontend Scheme: ${config.webUIScheme}`);
9
12
  log.info(`|| Zigbee2MQTT Frontend Server: ${config.webUIServer}`);
@@ -52,14 +55,19 @@ async function adapterInfo(config, log) {
52
55
  * @param log
53
56
  */
54
57
  async function zigbee2mqttInfo(payload, log) {
58
+ if (!payload || !log) {
59
+ return;
60
+ }
55
61
  log.info('============================ Zigbee2MQTT Information =============================');
56
62
  log.info(`|| Zigbee2MQTT Version: ${payload.version} `);
57
- log.info(
58
- `|| Coordinator type: ${payload.coordinator.type} Version: ${payload.coordinator.meta.revision} Serial: ${payload.config.serial.port}`
59
- );
60
- log.info(
61
- `|| Network panid ${payload.network.pan_id} channel: ${payload.network.channel} ext_pan_id: ${payload.network.extended_pan_id}`
62
- );
63
+ const coordType = payload.coordinator && payload.coordinator.type ? payload.coordinator.type : 'unknown';
64
+ const coordRev = payload.coordinator && payload.coordinator.meta ? payload.coordinator.meta.revision : 'unknown';
65
+ const serialPort = payload.config && payload.config.serial ? payload.config.serial.port : 'unknown';
66
+ const panId = payload.network ? payload.network.pan_id : 'unknown';
67
+ const channel = payload.network ? payload.network.channel : 'unknown';
68
+ const extPanId = payload.network ? payload.network.extended_pan_id : 'unknown';
69
+ log.info(`|| Coordinator type: ${coordType} Version: ${coordRev} Serial: ${serialPort}`);
70
+ log.info(`|| Network panid ${panId} channel: ${channel} ext_pan_id: ${extPanId}`);
63
71
  log.info('==================================================================================');
64
72
  }
65
73
 
@@ -1,65 +1,156 @@
1
+ 'use strict';
2
+
1
3
  const core = require('@iobroker/adapter-core');
2
- const Aedes = require('aedes');
3
- const net = require('net');
4
- let mqttServer;
4
+ const { Aedes } = require('aedes');
5
+ const net = require('node:net');
5
6
 
6
7
  /**
7
8
  *
8
9
  */
9
10
  class MqttServerController {
10
11
  /**
11
- *
12
12
  * @param adapter
13
13
  */
14
14
  constructor(adapter) {
15
- this.adapter = adapter;
15
+ this.adapter = adapter;
16
+ this.mqttServer = null;
17
+ this.aedesBroker = null;
16
18
  }
17
19
 
18
20
  /**
21
+ * Prüft ob Port und Bind-Adresse konfiguriert sind.
19
22
  *
23
+ * @returns {boolean}
24
+ */
25
+ _checkConfig() {
26
+ if (!this.adapter.config.mqttServerPort || !this.adapter.config.mqttServerIPBind) {
27
+ this.adapter.log.error('MQTT server config incomplete (mqttServerPort / mqttServerIPBind missing).');
28
+ return false;
29
+ }
30
+ return true;
31
+ }
32
+
33
+ /**
34
+ * Hängt error- und close-Events an einen net.Server.
35
+ *
36
+ * @param {string} label
37
+ */
38
+ _attachServerEvents(label) {
39
+ if (!this.mqttServer) {
40
+ return;
41
+ }
42
+ this.mqttServer.on('error', (err) => {
43
+ this.adapter.log.error(`${label} error: ${err && err.message ? err.message : String(err)}`);
44
+ });
45
+ this.mqttServer.on('close', () => {
46
+ this.adapter.log.debug(`${label} closed.`);
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Startet den internen MQTT-Server mit Persistenz (aedes + nedb).
20
52
  */
21
53
  async createMQTTServer() {
54
+ if (!this._checkConfig()) {
55
+ return;
56
+ }
22
57
  try {
23
58
  const NedbPersistence = require('aedes-persistence-nedb');
24
59
  const db = new NedbPersistence({
25
60
  path: `${core.getAbsoluteInstanceDataDir(this.adapter)}/mqttData`,
26
61
  prefix: '',
27
- });
28
- const aedes = Aedes({ persistence: db });
29
- mqttServer = net.createServer(aedes.handle);
30
- mqttServer.listen(this.adapter.config.mqttServerPort, this.adapter.config.mqttServerIPBind, () => {
31
- this.adapter.log.info(
32
- `Starting MQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
62
+ });
63
+ // aedes-persistence-nedb does not implement the async setup() interface
64
+ // required by aedes v1.x → no-op shim
65
+ if (!db.setup) {
66
+ db.setup = async () => {};
67
+ }
68
+ this.aedesBroker = await Aedes.createBroker({ persistence: db });
69
+ this.mqttServer = net.createServer(this.aedesBroker.handle);
70
+ this._attachServerEvents('MQTT-Server');
71
+
72
+ await new Promise((resolve, reject) => {
73
+ this.mqttServer.listen(
74
+ this.adapter.config.mqttServerPort,
75
+ this.adapter.config.mqttServerIPBind,
76
+ () => {
77
+ this.adapter.log.info(
78
+ `Starting MQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
79
+ );
80
+ resolve();
81
+ }
33
82
  );
83
+ this.mqttServer.once('error', reject);
34
84
  });
35
85
  } catch (err) {
36
- this.adapter.log.error(err);
86
+ this.adapter.log.error(`createMQTTServer error: ${err && err.message ? err.message : String(err)}`);
87
+ // Sicherstellen dass kein halboffener Server übrig bleibt
88
+ this.closeServer();
37
89
  }
38
90
  }
39
91
 
40
92
  /**
41
- *
93
+ * Startet einen Dummy-MQTT-Server ohne Persistenz (für WS-Modus).
42
94
  */
43
95
  async createDummyMQTTServer() {
96
+ if (!this._checkConfig()) {
97
+ return;
98
+ }
44
99
  try {
45
- const aedes = Aedes();
46
- mqttServer = net.createServer(aedes.handle);
47
- mqttServer.listen(this.adapter.config.mqttServerPort, this.adapter.config.mqttServerIPBind, () => {
48
- this.adapter.log.info(
49
- `Starting DummyMQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
100
+ this.aedesBroker = await Aedes.createBroker();
101
+ this.mqttServer = net.createServer(this.aedesBroker.handle);
102
+ this._attachServerEvents('DummyMQTT-Server');
103
+
104
+ await new Promise((resolve, reject) => {
105
+ this.mqttServer.listen(
106
+ this.adapter.config.mqttServerPort,
107
+ this.adapter.config.mqttServerIPBind,
108
+ () => {
109
+ this.adapter.log.info(
110
+ `Starting DummyMQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
111
+ );
112
+ resolve();
113
+ }
50
114
  );
115
+ this.mqttServer.once('error', reject);
51
116
  });
52
117
  } catch (err) {
53
- this.adapter.log.error(err);
118
+ this.adapter.log.error(`createDummyMQTTServer error: ${err && err.message ? err.message : String(err)}`);
119
+ this.closeServer();
54
120
  }
55
121
  }
56
122
 
57
123
  /**
58
- *
124
+ * Schließt den MQTT-Server und den Aedes-Broker.
59
125
  */
60
126
  closeServer() {
61
- if (mqttServer && !mqttServer.close()) {
62
- mqttServer.close();
127
+ if (this.mqttServer) {
128
+ const server = this.mqttServer;
129
+ this.mqttServer = null;
130
+ try {
131
+ server.close((err) => {
132
+ if (err) {
133
+ this.adapter.log.debug(`MQTT server close callback error: ${err.message}`);
134
+ }
135
+ });
136
+ } catch (err) {
137
+ this.adapter.log.error(`closeServer mqttServer.close() error: ${err}`);
138
+ }
139
+ }
140
+ if (this.aedesBroker) {
141
+ const broker = this.aedesBroker;
142
+ this.aedesBroker = null;
143
+ try {
144
+ broker.close((err) => {
145
+ if (err) {
146
+ this.adapter.log.debug(`Aedes broker close callback error: ${err}`);
147
+ } else {
148
+ this.adapter.log.debug('Aedes broker closed.');
149
+ }
150
+ });
151
+ } catch (err) {
152
+ this.adapter.log.error(`closeServer aedesBroker.close() error: ${err}`);
153
+ }
63
154
  }
64
155
  }
65
156
  }
package/lib/states.js CHANGED
@@ -3,9 +3,7 @@
3
3
 
4
4
  /*eslint no-unused-vars: ['off']*/
5
5
 
6
- const rgb = require('./rgb.js');
7
6
  const utils = require('./utils.js');
8
- const colors = require('./colors.js');
9
7
 
10
8
  /* states for device:
11
9
  id - sysname of state, id
@@ -58,8 +56,6 @@ const unitLookup = {
58
56
  illuminance_lux: 'lx',
59
57
  };
60
58
 
61
- const timers = {};
62
-
63
59
  const states = {
64
60
  link_quality: {
65
61
  id: 'link_quality',
@@ -98,7 +94,18 @@ const states = {
98
94
  type: 'string',
99
95
  def: '',
100
96
  getter: (payload) => {
101
- return payload.last_seen.replace('T', ' ').split('+')[0];
97
+ const val = payload.last_seen;
98
+ if (val == null) {
99
+ return undefined;
100
+ }
101
+ // Z2M kann last_seen als Unix-Timestamp (number in ms) ODER als ISO-String senden
102
+ if (typeof val === 'number') {
103
+ return new Date(val).toISOString().replace('T', ' ').split('.')[0];
104
+ }
105
+ if (typeof val === 'string') {
106
+ return val.replace('T', ' ').split('+')[0].split('.')[0];
107
+ }
108
+ return String(val);
102
109
  },
103
110
  },
104
111
 
@@ -1,6 +1,4 @@
1
1
  const utils = require('./utils');
2
- const incStatsQueue = [];
3
- const timeOutCache = {};
4
2
 
5
3
  /**
6
4
  *
@@ -20,29 +18,36 @@ class StatesController {
20
18
  this.deviceCache = deviceCache;
21
19
  this.logCustomizations = logCustomizations;
22
20
  this.createCache = createCache;
21
+ this.incStatsQueue = [];
22
+ this.timeOutCache = {};
23
23
  }
24
24
 
25
25
  /**
26
26
  *
27
27
  * @param messageObj
28
28
  */
29
- processDeviceMessage(messageObj) {
30
- // Is payload present?
31
- if (messageObj.payload == ''|| messageObj.payload == undefined || messageObj.payload == null) {
29
+ async processDeviceMessage(messageObj) {
30
+ if (!messageObj || typeof messageObj !== 'object') {
31
+ return;
32
+ }
33
+ if (messageObj.payload === '' || messageObj.payload === undefined || messageObj.payload === null) {
32
34
  return;
33
35
  }
34
36
 
35
- const device = this.groupCache.concat(this.deviceCache).find((x) => x.id == messageObj.topic);
37
+ const device = this.groupCache.concat(this.deviceCache).find((x) => x.id === messageObj.topic);
36
38
  if (device) {
37
39
  try {
38
- this.setDeviceStateSafely(messageObj, device);
40
+ await this.setDeviceStateSafely(messageObj, device);
39
41
  } catch (error) {
40
- this.adapter.log.error(error);
42
+ this.adapter.log.error(`setDeviceStateSafely error for ${messageObj.topic}: ${error}`);
41
43
  }
42
44
  } else {
43
- // incStatsQueue[incStatsQueue.length] = messageObj;
44
- if (!incStatsQueue.some((x) => x && x.topic === messageObj.topic)) {
45
- incStatsQueue.push(messageObj);
45
+ if (!this.incStatsQueue.some((x) => x && x.topic === messageObj.topic)) {
46
+ if (this.incStatsQueue.length < 500) {
47
+ this.incStatsQueue.push(messageObj);
48
+ } else {
49
+ this.adapter.log.warn(`incStatsQueue is full (500), dropping message for ${messageObj.topic}`);
50
+ }
46
51
  }
47
52
  this.adapter.log.debug(`Device: ${messageObj.topic} not found, queue state in incStatsQueue!`);
48
53
  }
@@ -59,6 +64,17 @@ class StatesController {
59
64
  }
60
65
 
61
66
  const actionStates = [];
67
+ // Fix 1: Flag damit messageObj nur EINMAL in die Queue kommt, egal wie viele
68
+ // States noch nicht im createCache sind (verhindert N-faches Requeue)
69
+ let queuedThisRound = false;
70
+
71
+ const pushToQueue = (msg) => {
72
+ if (!queuedThisRound && this.incStatsQueue.length < 500
73
+ && !this.incStatsQueue.some((x) => x && x.topic === msg.topic)) {
74
+ this.incStatsQueue.push(msg);
75
+ queuedThisRound = true;
76
+ }
77
+ };
62
78
 
63
79
  for (let [key, value] of Object.entries(messageObj.payload)) {
64
80
  if (value === undefined || value === null) {
@@ -69,12 +85,12 @@ class StatesController {
69
85
  return state.prop && state.prop === key;
70
86
  });
71
87
 
72
- if (states.length == 0) {
73
- states = device.states.filter((x) => x.id == key);
88
+ if (states.length === 0) {
89
+ states = device.states.filter((x) => x.id === key);
74
90
  }
75
91
 
76
- if (states.length == 0) {
77
- if (key == 'device' || device.ieee_address.includes('group')) {
92
+ if (states.length === 0) {
93
+ if (key === 'device' || device.ieee_address.includes('group')) {
78
94
  // do nothing
79
95
  } else {
80
96
  // some devices has addition information in payload
@@ -92,13 +108,13 @@ class StatesController {
92
108
  common: {
93
109
  name: key,
94
110
  role: 'state',
95
- type: value !== null ? typeof value : 'mixed',
111
+ type: typeof value,
96
112
  write: false,
97
113
  read: true,
98
114
  },
99
115
  native: {},
100
116
  });
101
- if (typeof value == 'object') {
117
+ if (typeof value === 'object') {
102
118
  value = JSON.stringify(value);
103
119
  }
104
120
  this.adapter.setState(`${fullPath}.${key}`, value, true);
@@ -114,26 +130,22 @@ class StatesController {
114
130
  await this.setStateSafelyAsync(`${device.ieee_address}.available`, true);
115
131
  }
116
132
 
117
- // It may be that the state has not yet been created!
133
+ // State noch nicht erstellt? einmal in Queue legen und weiter
118
134
  if (!this.createCache[device.ieee_address]
119
- || !this.createCache[device.ieee_address][state.id]
120
- || !this.createCache[device.ieee_address][state.id].created == true) {
121
- incStatsQueue[incStatsQueue.length] = messageObj;
135
+ || !this.createCache[device.ieee_address][state.id]
136
+ || this.createCache[device.ieee_address][state.id].created !== true) {
137
+ pushToQueue(messageObj);
122
138
  continue;
123
139
  }
124
140
 
125
141
  try {
126
142
  // Is an action
127
- if (state.prop && state.prop == 'action') {
143
+ if (state.prop && state.prop === 'action') {
128
144
  actionStates.push(state);
129
145
  }
130
- // Is not an action
131
- // check if its a motion sensor (occupancy state) and if configuration is set to update state every time
132
- // if yes, use setStateSafelyAsync instead of setStateChangedSafelyAsync
133
- else if (this.adapter.config.allwaysUpdateOccupancyState === true && state.id === 'occupancy' && value === true) {
146
+ else if (this.adapter.config.allwaysUpdateOccupancyState === true && state.id === 'occupancy' && value === true) {
134
147
  await this.setStateSafelyAsync(stateName, value);
135
148
  }
136
- // end section for motion sensor update
137
149
  else {
138
150
  if (state.getter) {
139
151
  await this.setStateChangedSafelyAsync(stateName, state.getter(messageObj.payload));
@@ -142,7 +154,8 @@ class StatesController {
142
154
  }
143
155
  }
144
156
  } catch (err) {
145
- incStatsQueue[incStatsQueue.length] = messageObj;
157
+ // Fix 2: Größenlimit auch hier
158
+ pushToQueue(messageObj);
146
159
  this.adapter.log.debug(`Can not set ${stateName}, queue state in incStatsQueue!`);
147
160
  }
148
161
  }
@@ -153,11 +166,10 @@ class StatesController {
153
166
 
154
167
  try {
155
168
  const getterPayload = state.getter(messageObj.payload);
156
- if (getterPayload != undefined) {
157
- if (state.isEvent && state.isEvent == true) {
158
- if (state.type == 'boolean') {
169
+ if (getterPayload !== undefined) {
170
+ if (state.isEvent && state.isEvent === true) {
171
+ if (state.type === 'boolean') {
159
172
  await this.setStateWithTimeoutAsync(stateName, getterPayload, 250);
160
-
161
173
  } else {
162
174
  await this.setStateSafelyAsync(stateName, getterPayload);
163
175
  }
@@ -166,7 +178,8 @@ class StatesController {
166
178
  }
167
179
  }
168
180
  } catch (err) {
169
- incStatsQueue[incStatsQueue.length] = messageObj;
181
+ // Fix 2: Größenlimit auch hier
182
+ pushToQueue(messageObj);
170
183
  this.adapter.log.debug(`Can not set ${stateName}, queue state in incStatsQueue!`);
171
184
  }
172
185
  }
@@ -208,22 +221,25 @@ class StatesController {
208
221
  }
209
222
 
210
223
  await this.adapter.setStateAsync(stateName, value, true);
211
- if (timeOutCache[stateName]) {
212
- clearTimeout(timeOutCache[stateName]);
224
+ if (this.timeOutCache[stateName]) {
225
+ clearTimeout(this.timeOutCache[stateName]);
213
226
  }
214
- timeOutCache[stateName] = setTimeout(() => {
215
- this.adapter.setStateAsync(stateName, !value, true);
227
+ this.timeOutCache[stateName] = setTimeout(() => {
228
+ this.adapter.setStateAsync(stateName, !value, true).catch((err) => {
229
+ this.adapter.log.debug(`setStateWithTimeout reset error for ${stateName}: ${err}`);
230
+ });
216
231
  }, timeout);
217
232
  }
218
233
 
219
234
  /**
220
235
  *
221
236
  */
222
- processQueue() {
237
+ async processQueue() {
223
238
  const oldIncStatsQueue = [];
224
- utils.moveArray(incStatsQueue, oldIncStatsQueue);
239
+ utils.moveArray(this.incStatsQueue, oldIncStatsQueue);
225
240
  while (oldIncStatsQueue.length > 0) {
226
- this.processDeviceMessage(oldIncStatsQueue.shift());
241
+ // seriell abarbeiten – nicht parallel feuern
242
+ await this.processDeviceMessage(oldIncStatsQueue.shift());
227
243
  }
228
244
  }
229
245
 
@@ -231,10 +247,14 @@ class StatesController {
231
247
  *
232
248
  */
233
249
  async subscribeWritableStates() {
234
- await this.adapter.unsubscribeObjectsAsync('*');
250
+ // Alle bestehenden State-Subscriptions zuerst abmelden
251
+ this.adapter.unsubscribeStates('*');
235
252
  for (const device of this.groupCache.concat(this.deviceCache)) {
253
+ if (!device || !Array.isArray(device.states)) {
254
+ continue;
255
+ }
236
256
  for (const state of device.states) {
237
- if (state.write == true) {
257
+ if (state && state.write === true) {
238
258
  this.adapter.subscribeStates(`${device.ieee_address}.${state.id}`);
239
259
  }
240
260
  }
@@ -249,7 +269,8 @@ class StatesController {
249
269
  */
250
270
  async setAllAvailableToFalse() {
251
271
  const availableStates = await this.adapter.getStatesAsync('*.available');
252
- for (const availableState in availableStates) {
272
+ // Fix: Object.keys() statt for...in verhindert ungewollte Prototype-Properties
273
+ for (const availableState of Object.keys(availableStates)) {
253
274
  await this.adapter.setStateChangedAsync(availableState, false, true);
254
275
  }
255
276
  }
@@ -258,9 +279,10 @@ class StatesController {
258
279
  *
259
280
  */
260
281
  async allTimerClear() {
261
- for (const timer in timeOutCache) {
262
- clearTimeout(timeOutCache[timer]);
282
+ for (const timer of Object.keys(this.timeOutCache)) {
283
+ clearTimeout(this.timeOutCache[timer]);
263
284
  }
285
+ this.timeOutCache = {};
264
286
  }
265
287
  }
266
288