iobroker.zigbee2mqtt 3.0.20 → 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,27 +18,37 @@ 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;
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
+ }
51
+ }
44
52
  this.adapter.log.debug(`Device: ${messageObj.topic} not found, queue state in incStatsQueue!`);
45
53
  }
46
54
  }
@@ -56,6 +64,17 @@ class StatesController {
56
64
  }
57
65
 
58
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
+ };
59
78
 
60
79
  for (let [key, value] of Object.entries(messageObj.payload)) {
61
80
  if (value === undefined || value === null) {
@@ -66,12 +85,12 @@ class StatesController {
66
85
  return state.prop && state.prop === key;
67
86
  });
68
87
 
69
- if (states.length == 0) {
70
- states = device.states.filter((x) => x.id == key);
88
+ if (states.length === 0) {
89
+ states = device.states.filter((x) => x.id === key);
71
90
  }
72
91
 
73
- if (states.length == 0) {
74
- if (key == 'device' || device.ieee_address.includes('group')) {
92
+ if (states.length === 0) {
93
+ if (key === 'device' || device.ieee_address.includes('group')) {
75
94
  // do nothing
76
95
  } else {
77
96
  // some devices has addition information in payload
@@ -89,13 +108,13 @@ class StatesController {
89
108
  common: {
90
109
  name: key,
91
110
  role: 'state',
92
- type: value !== null ? typeof value : 'mixed',
111
+ type: typeof value,
93
112
  write: false,
94
113
  read: true,
95
114
  },
96
115
  native: {},
97
116
  });
98
- if (typeof value == 'object') {
117
+ if (typeof value === 'object') {
99
118
  value = JSON.stringify(value);
100
119
  }
101
120
  this.adapter.setState(`${fullPath}.${key}`, value, true);
@@ -111,26 +130,22 @@ class StatesController {
111
130
  await this.setStateSafelyAsync(`${device.ieee_address}.available`, true);
112
131
  }
113
132
 
114
- // It may be that the state has not yet been created!
133
+ // State noch nicht erstellt? einmal in Queue legen und weiter
115
134
  if (!this.createCache[device.ieee_address]
116
- || !this.createCache[device.ieee_address][state.id]
117
- || !this.createCache[device.ieee_address][state.id].created == true) {
118
- 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);
119
138
  continue;
120
139
  }
121
140
 
122
141
  try {
123
142
  // Is an action
124
- if (state.prop && state.prop == 'action') {
143
+ if (state.prop && state.prop === 'action') {
125
144
  actionStates.push(state);
126
145
  }
127
- // Is not an action
128
- // check if its a motion sensor (occupancy state) and if configuration is set to update state every time
129
- // if yes, use setStateSafelyAsync instead of setStateChangedSafelyAsync
130
- 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) {
131
147
  await this.setStateSafelyAsync(stateName, value);
132
148
  }
133
- // end section for motion sensor update
134
149
  else {
135
150
  if (state.getter) {
136
151
  await this.setStateChangedSafelyAsync(stateName, state.getter(messageObj.payload));
@@ -139,7 +154,8 @@ class StatesController {
139
154
  }
140
155
  }
141
156
  } catch (err) {
142
- incStatsQueue[incStatsQueue.length] = messageObj;
157
+ // Fix 2: Größenlimit auch hier
158
+ pushToQueue(messageObj);
143
159
  this.adapter.log.debug(`Can not set ${stateName}, queue state in incStatsQueue!`);
144
160
  }
145
161
  }
@@ -150,11 +166,10 @@ class StatesController {
150
166
 
151
167
  try {
152
168
  const getterPayload = state.getter(messageObj.payload);
153
- if (getterPayload != undefined) {
154
- if (state.isEvent && state.isEvent == true) {
155
- if (state.type == 'boolean') {
169
+ if (getterPayload !== undefined) {
170
+ if (state.isEvent && state.isEvent === true) {
171
+ if (state.type === 'boolean') {
156
172
  await this.setStateWithTimeoutAsync(stateName, getterPayload, 250);
157
-
158
173
  } else {
159
174
  await this.setStateSafelyAsync(stateName, getterPayload);
160
175
  }
@@ -163,7 +178,8 @@ class StatesController {
163
178
  }
164
179
  }
165
180
  } catch (err) {
166
- incStatsQueue[incStatsQueue.length] = messageObj;
181
+ // Fix 2: Größenlimit auch hier
182
+ pushToQueue(messageObj);
167
183
  this.adapter.log.debug(`Can not set ${stateName}, queue state in incStatsQueue!`);
168
184
  }
169
185
  }
@@ -205,22 +221,25 @@ class StatesController {
205
221
  }
206
222
 
207
223
  await this.adapter.setStateAsync(stateName, value, true);
208
- if (timeOutCache[stateName]) {
209
- clearTimeout(timeOutCache[stateName]);
224
+ if (this.timeOutCache[stateName]) {
225
+ clearTimeout(this.timeOutCache[stateName]);
210
226
  }
211
- timeOutCache[stateName] = setTimeout(() => {
212
- 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
+ });
213
231
  }, timeout);
214
232
  }
215
233
 
216
234
  /**
217
235
  *
218
236
  */
219
- processQueue() {
237
+ async processQueue() {
220
238
  const oldIncStatsQueue = [];
221
- utils.moveArray(incStatsQueue, oldIncStatsQueue);
239
+ utils.moveArray(this.incStatsQueue, oldIncStatsQueue);
222
240
  while (oldIncStatsQueue.length > 0) {
223
- this.processDeviceMessage(oldIncStatsQueue.shift());
241
+ // seriell abarbeiten – nicht parallel feuern
242
+ await this.processDeviceMessage(oldIncStatsQueue.shift());
224
243
  }
225
244
  }
226
245
 
@@ -228,10 +247,14 @@ class StatesController {
228
247
  *
229
248
  */
230
249
  async subscribeWritableStates() {
231
- await this.adapter.unsubscribeObjectsAsync('*');
250
+ // Alle bestehenden State-Subscriptions zuerst abmelden
251
+ this.adapter.unsubscribeStates('*');
232
252
  for (const device of this.groupCache.concat(this.deviceCache)) {
253
+ if (!device || !Array.isArray(device.states)) {
254
+ continue;
255
+ }
233
256
  for (const state of device.states) {
234
- if (state.write == true) {
257
+ if (state && state.write === true) {
235
258
  this.adapter.subscribeStates(`${device.ieee_address}.${state.id}`);
236
259
  }
237
260
  }
@@ -246,7 +269,8 @@ class StatesController {
246
269
  */
247
270
  async setAllAvailableToFalse() {
248
271
  const availableStates = await this.adapter.getStatesAsync('*.available');
249
- for (const availableState in availableStates) {
272
+ // Fix: Object.keys() statt for...in verhindert ungewollte Prototype-Properties
273
+ for (const availableState of Object.keys(availableStates)) {
250
274
  await this.adapter.setStateChangedAsync(availableState, false, true);
251
275
  }
252
276
  }
@@ -255,9 +279,10 @@ class StatesController {
255
279
  *
256
280
  */
257
281
  async allTimerClear() {
258
- for (const timer in timeOutCache) {
259
- clearTimeout(timeOutCache[timer]);
282
+ for (const timer of Object.keys(this.timeOutCache)) {
283
+ clearTimeout(this.timeOutCache[timer]);
260
284
  }
285
+ this.timeOutCache = {};
261
286
  }
262
287
  }
263
288