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.
- package/README.md +9 -0
- package/admin/jsonConfig.json +37 -0
- package/io-package.json +16 -42
- package/lib/check.js +19 -11
- package/lib/deviceController.js +167 -133
- package/lib/exposes.js +1350 -1315
- package/lib/imageController.js +49 -47
- package/lib/messages.js +14 -6
- package/lib/mqttServerController.js +113 -22
- package/lib/states.js +12 -5
- package/lib/statesController.js +67 -45
- package/lib/utils.js +1 -50
- package/lib/websocketController.js +151 -46
- package/lib/z2mController.js +38 -29
- package/main.js +390 -194
- package/package.json +13 -12
package/lib/imageController.js
CHANGED
|
@@ -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
|
|
35
|
+
return deviceNameString.replace(/\u0000/g, '');
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
39
|
* @param device
|
|
40
|
+
* @param ext
|
|
39
41
|
*/
|
|
40
|
-
|
|
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)}
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
if (!device || !device.definition || !device.definition.model) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
83
89
|
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
46
|
-
mqttServer
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
62
|
-
mqttServer
|
|
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
|
-
|
|
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
|
|
package/lib/statesController.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
73
|
-
states = device.states.filter((x) => x.id
|
|
88
|
+
if (states.length === 0) {
|
|
89
|
+
states = device.states.filter((x) => x.id === key);
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
if (states.length
|
|
77
|
-
if (key
|
|
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:
|
|
111
|
+
type: typeof value,
|
|
96
112
|
write: false,
|
|
97
113
|
read: true,
|
|
98
114
|
},
|
|
99
115
|
native: {},
|
|
100
116
|
});
|
|
101
|
-
if (typeof value
|
|
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
|
-
//
|
|
133
|
+
// State noch nicht erstellt? → einmal in Queue legen und weiter
|
|
118
134
|
if (!this.createCache[device.ieee_address]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
143
|
+
if (state.prop && state.prop === 'action') {
|
|
128
144
|
actionStates.push(state);
|
|
129
145
|
}
|
|
130
|
-
|
|
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
|
-
|
|
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
|
|
157
|
-
if (state.isEvent && state.isEvent
|
|
158
|
-
if (state.type
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|