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.
- package/README.md +12 -0
- package/admin/jsonConfig.json +37 -0
- package/io-package.json +42 -55
- 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 +68 -43
- package/lib/utils.js +1 -50
- package/lib/websocketController.js +151 -46
- package/lib/z2mController.js +38 -29
- package/main.js +398 -207
- 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,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
|
-
|
|
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
|
-
incStatsQueue
|
|
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
|
|
70
|
-
states = device.states.filter((x) => x.id
|
|
88
|
+
if (states.length === 0) {
|
|
89
|
+
states = device.states.filter((x) => x.id === key);
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
if (states.length
|
|
74
|
-
if (key
|
|
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:
|
|
111
|
+
type: typeof value,
|
|
93
112
|
write: false,
|
|
94
113
|
read: true,
|
|
95
114
|
},
|
|
96
115
|
native: {},
|
|
97
116
|
});
|
|
98
|
-
if (typeof value
|
|
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
|
-
//
|
|
133
|
+
// State noch nicht erstellt? → einmal in Queue legen und weiter
|
|
115
134
|
if (!this.createCache[device.ieee_address]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
143
|
+
if (state.prop && state.prop === 'action') {
|
|
125
144
|
actionStates.push(state);
|
|
126
145
|
}
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
154
|
-
if (state.isEvent && state.isEvent
|
|
155
|
-
if (state.type
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|