homebridge-plugin-klares4 1.1.1-beta.5 → 1.1.1-beta.6
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/CHANGELOG.md +30 -6
- package/README.md +34 -34
- package/dist/accessories/cover-accessory.d.ts +8 -4
- package/dist/accessories/cover-accessory.d.ts.map +1 -1
- package/dist/accessories/cover-accessory.js +36 -37
- package/dist/accessories/cover-accessory.js.map +1 -1
- package/dist/accessories/light-accessory.d.ts +7 -3
- package/dist/accessories/light-accessory.d.ts.map +1 -1
- package/dist/accessories/light-accessory.js +27 -24
- package/dist/accessories/light-accessory.js.map +1 -1
- package/dist/accessories/scenario-accessory.d.ts +8 -4
- package/dist/accessories/scenario-accessory.d.ts.map +1 -1
- package/dist/accessories/scenario-accessory.js +23 -19
- package/dist/accessories/scenario-accessory.js.map +1 -1
- package/dist/accessories/sensor-accessory.d.ts +5 -1
- package/dist/accessories/sensor-accessory.d.ts.map +1 -1
- package/dist/accessories/sensor-accessory.js +49 -36
- package/dist/accessories/sensor-accessory.js.map +1 -1
- package/dist/accessories/thermostat-accessory.d.ts +5 -1
- package/dist/accessories/thermostat-accessory.d.ts.map +1 -1
- package/dist/accessories/thermostat-accessory.js +68 -55
- package/dist/accessories/thermostat-accessory.js.map +1 -1
- package/dist/accessories/zone-accessory.d.ts +5 -1
- package/dist/accessories/zone-accessory.d.ts.map +1 -1
- package/dist/accessories/zone-accessory.js +32 -32
- package/dist/accessories/zone-accessory.js.map +1 -1
- package/dist/mqtt-bridge.d.ts +6 -12
- package/dist/mqtt-bridge.d.ts.map +1 -1
- package/dist/mqtt-bridge.js +148 -104
- package/dist/mqtt-bridge.js.map +1 -1
- package/dist/platform.d.ts +26 -37
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +177 -132
- package/dist/platform.js.map +1 -1
- package/dist/types.d.ts +203 -21
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +78 -0
- package/dist/types.js.map +1 -1
- package/dist/websocket-client.d.ts +6 -5
- package/dist/websocket-client.d.ts.map +1 -1
- package/dist/websocket-client.js +257 -313
- package/dist/websocket-client.js.map +1 -1
- package/package.json +4 -4
package/dist/websocket-client.js
CHANGED
|
@@ -40,6 +40,9 @@ exports.KseniaWebSocketClient = void 0;
|
|
|
40
40
|
const ws_1 = __importDefault(require("ws"));
|
|
41
41
|
const https = __importStar(require("https"));
|
|
42
42
|
const crypto = __importStar(require("crypto"));
|
|
43
|
+
/**
|
|
44
|
+
* WebSocket client for Ksenia Lares4 communication
|
|
45
|
+
*/
|
|
43
46
|
class KseniaWebSocketClient {
|
|
44
47
|
constructor(ip, port, useHttps, sender, pin, log, options = {}) {
|
|
45
48
|
this.ip = ip;
|
|
@@ -50,13 +53,12 @@ class KseniaWebSocketClient {
|
|
|
50
53
|
this.log = log;
|
|
51
54
|
this.options = options;
|
|
52
55
|
this.isConnected = false;
|
|
53
|
-
// Device storage
|
|
54
56
|
this.devices = new Map();
|
|
55
57
|
this.options = {
|
|
56
58
|
debug: false,
|
|
57
59
|
reconnectInterval: 5000,
|
|
58
60
|
heartbeatInterval: 30000,
|
|
59
|
-
...options
|
|
61
|
+
...options,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
async connect() {
|
|
@@ -64,37 +66,40 @@ class KseniaWebSocketClient {
|
|
|
64
66
|
try {
|
|
65
67
|
const protocol = this.useHttps ? 'wss' : 'ws';
|
|
66
68
|
const wsUrl = `${protocol}://${this.ip}:${this.port}/KseniaWsock/`;
|
|
67
|
-
this.log.info(
|
|
68
|
-
const wsOptions = {
|
|
69
|
+
this.log.info(`Connecting to ${wsUrl}...`);
|
|
70
|
+
const wsOptions = {
|
|
71
|
+
rejectUnauthorized: false,
|
|
72
|
+
};
|
|
69
73
|
if (this.useHttps) {
|
|
70
|
-
wsOptions.rejectUnauthorized = false;
|
|
71
74
|
wsOptions.agent = new https.Agent({
|
|
72
75
|
rejectUnauthorized: false,
|
|
73
76
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
74
77
|
secureProtocol: 'TLS_method',
|
|
75
|
-
ciphers: 'ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA'
|
|
78
|
+
ciphers: 'ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA',
|
|
76
79
|
});
|
|
77
80
|
}
|
|
78
81
|
this.ws = new ws_1.default(wsUrl, ['KS_WSOCK'], wsOptions);
|
|
79
82
|
this.ws.on('open', () => {
|
|
80
|
-
this.log.info('
|
|
83
|
+
this.log.info('WebSocket connected');
|
|
81
84
|
this.isConnected = true;
|
|
82
|
-
this.login()
|
|
85
|
+
this.login()
|
|
86
|
+
.then(() => {
|
|
83
87
|
this.onConnected?.();
|
|
84
88
|
resolve();
|
|
85
|
-
})
|
|
89
|
+
})
|
|
90
|
+
.catch(reject);
|
|
86
91
|
});
|
|
87
92
|
this.ws.on('message', (data) => {
|
|
88
93
|
this.handleMessage(data.toString());
|
|
89
94
|
});
|
|
90
95
|
this.ws.on('close', (code, reason) => {
|
|
91
|
-
this.log.warn(
|
|
96
|
+
this.log.warn(`WebSocket closed: ${code} - ${reason.toString()}`);
|
|
92
97
|
this.isConnected = false;
|
|
93
98
|
this.onDisconnected?.();
|
|
94
99
|
this.scheduleReconnect();
|
|
95
100
|
});
|
|
96
101
|
this.ws.on('error', (error) => {
|
|
97
|
-
this.log.error('
|
|
102
|
+
this.log.error('WebSocket error:', error.message);
|
|
98
103
|
reject(error);
|
|
99
104
|
});
|
|
100
105
|
}
|
|
@@ -111,27 +116,25 @@ class KseniaWebSocketClient {
|
|
|
111
116
|
ID: Math.floor(Math.random() * 65535).toString(),
|
|
112
117
|
PAYLOAD_TYPE: 'UNKNOWN',
|
|
113
118
|
PAYLOAD: {
|
|
114
|
-
PIN: this.pin
|
|
119
|
+
PIN: this.pin,
|
|
115
120
|
},
|
|
116
121
|
TIMESTAMP: Math.floor(Date.now() / 1000).toString(),
|
|
117
|
-
CRC_16: '0x0000'
|
|
122
|
+
CRC_16: '0x0000',
|
|
118
123
|
};
|
|
119
124
|
loginMessage.CRC_16 = this.calculateCRC16(JSON.stringify(loginMessage));
|
|
120
|
-
this.log.info('
|
|
125
|
+
this.log.info('Executing login...');
|
|
121
126
|
await this.sendMessage(loginMessage);
|
|
122
127
|
}
|
|
123
128
|
handleMessage(data) {
|
|
124
129
|
try {
|
|
125
130
|
const message = JSON.parse(data);
|
|
126
|
-
// Filtra i messaggi di debug
|
|
127
131
|
const isHeartbeat = message.CMD === 'PING' || message.PAYLOAD_TYPE === 'HEARTBEAT';
|
|
128
132
|
const isGenericRealtime = message.CMD === 'REALTIME' && message.PAYLOAD_TYPE === 'CHANGES';
|
|
129
|
-
// Mostra messaggi solo se sono importanti o se siamo in debug
|
|
130
133
|
if (this.options.debug || (!isHeartbeat && !isGenericRealtime)) {
|
|
131
|
-
this.log.info(
|
|
134
|
+
this.log.info(`Received: ${data}`);
|
|
132
135
|
}
|
|
133
136
|
else if (isHeartbeat || isGenericRealtime) {
|
|
134
|
-
this.log.debug(
|
|
137
|
+
this.log.debug(`Debug: ${data}`);
|
|
135
138
|
}
|
|
136
139
|
switch (message.CMD) {
|
|
137
140
|
case 'LOGIN_RES':
|
|
@@ -144,7 +147,6 @@ class KseniaWebSocketClient {
|
|
|
144
147
|
this.handleRealtimeResponse(message);
|
|
145
148
|
break;
|
|
146
149
|
case 'REALTIME':
|
|
147
|
-
// I messaggi real-time di aggiornamento stato
|
|
148
150
|
if (message.PAYLOAD_TYPE === 'CHANGES') {
|
|
149
151
|
this.handleStatusUpdate(message);
|
|
150
152
|
}
|
|
@@ -153,61 +155,56 @@ class KseniaWebSocketClient {
|
|
|
153
155
|
this.handleStatusUpdate(message);
|
|
154
156
|
break;
|
|
155
157
|
case 'PING':
|
|
156
|
-
// PING ricevuto - rispondi solo in debug
|
|
157
158
|
if (this.options.debug) {
|
|
158
|
-
this.log.debug(
|
|
159
|
+
this.log.debug('PING received from system');
|
|
159
160
|
}
|
|
160
161
|
break;
|
|
161
162
|
default:
|
|
162
163
|
if (this.options.debug) {
|
|
163
|
-
this.log.debug(
|
|
164
|
+
this.log.debug(`Unhandled message: ${message.CMD}`);
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
catch (error) {
|
|
168
|
-
this.log.error('
|
|
169
|
+
this.log.error('Message parsing error:', error instanceof Error ? error.message : String(error));
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
handleLoginResponse(message) {
|
|
172
173
|
if (message.PAYLOAD?.RESULT === 'OK') {
|
|
173
|
-
this.idLogin = message.PAYLOAD.ID_LOGIN
|
|
174
|
-
this.log.info(
|
|
174
|
+
this.idLogin = String(message.PAYLOAD.ID_LOGIN ?? '1');
|
|
175
|
+
this.log.info(`Login completed, ID_LOGIN: ${this.idLogin}`);
|
|
175
176
|
this.startHeartbeat();
|
|
176
|
-
this.requestSystemData()
|
|
177
|
+
this.requestSystemData().catch((error) => {
|
|
178
|
+
this.log.error('Error requesting system data:', error instanceof Error ? error.message : String(error));
|
|
179
|
+
});
|
|
177
180
|
}
|
|
178
181
|
else {
|
|
179
|
-
this.log.error('
|
|
182
|
+
this.log.error('Login failed:', String(message.PAYLOAD?.RESULT_DETAIL ?? 'Unknown error'));
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
185
|
async requestSystemData() {
|
|
183
186
|
if (!this.idLogin) {
|
|
184
|
-
this.log.error('
|
|
187
|
+
this.log.error('ID_LOGIN not available');
|
|
185
188
|
return;
|
|
186
189
|
}
|
|
187
|
-
this.log.info('
|
|
188
|
-
// Richiedi zone
|
|
190
|
+
this.log.info('Requesting system data...');
|
|
189
191
|
await this.sendKseniaCommand('READ', 'ZONES', {
|
|
190
192
|
ID_LOGIN: this.idLogin,
|
|
191
|
-
ID_ITEMS_RANGE: ['ALL', 'ALL']
|
|
193
|
+
ID_ITEMS_RANGE: ['ALL', 'ALL'],
|
|
192
194
|
});
|
|
193
|
-
// Richiedi output (luci, tapparelle, ecc.)
|
|
194
195
|
await this.sendKseniaCommand('READ', 'MULTI_TYPES', {
|
|
195
196
|
ID_LOGIN: this.idLogin,
|
|
196
|
-
TYPES: ['OUTPUTS', 'BUS_HAS', 'SCENARIOS']
|
|
197
|
+
TYPES: ['OUTPUTS', 'BUS_HAS', 'SCENARIOS'],
|
|
197
198
|
});
|
|
198
|
-
// Richiedi stati attuali degli output (FONDAMENTALE per sincronizzare stati iniziali!)
|
|
199
199
|
await this.sendKseniaCommand('READ', 'STATUS_OUTPUTS', {
|
|
200
|
-
ID_LOGIN: this.idLogin
|
|
200
|
+
ID_LOGIN: this.idLogin,
|
|
201
201
|
});
|
|
202
|
-
// Richiedi stati attuali dei sensori
|
|
203
202
|
await this.sendKseniaCommand('READ', 'STATUS_BUS_HA_SENSORS', {
|
|
204
|
-
ID_LOGIN: this.idLogin
|
|
203
|
+
ID_LOGIN: this.idLogin,
|
|
205
204
|
});
|
|
206
|
-
// Richiedi stati del sistema (AGGIUNTO per termostati!)
|
|
207
205
|
await this.sendKseniaCommand('READ', 'STATUS_SYSTEM', {
|
|
208
|
-
ID_LOGIN: this.idLogin
|
|
206
|
+
ID_LOGIN: this.idLogin,
|
|
209
207
|
});
|
|
210
|
-
// Registra per aggiornamenti real-time
|
|
211
208
|
await this.sendKseniaCommand('REALTIME', 'REGISTER', {
|
|
212
209
|
ID_LOGIN: this.idLogin,
|
|
213
210
|
TYPES: [
|
|
@@ -215,15 +212,15 @@ class KseniaWebSocketClient {
|
|
|
215
212
|
'STATUS_OUTPUTS',
|
|
216
213
|
'STATUS_BUS_HA_SENSORS',
|
|
217
214
|
'STATUS_SYSTEM',
|
|
218
|
-
'SCENARIOS'
|
|
219
|
-
]
|
|
215
|
+
'SCENARIOS',
|
|
216
|
+
],
|
|
220
217
|
});
|
|
221
218
|
}
|
|
222
219
|
handleReadResponse(message) {
|
|
223
220
|
const payload = message.PAYLOAD;
|
|
224
|
-
this.log.info(
|
|
221
|
+
this.log.info(`Response received: ${message.PAYLOAD_TYPE}`);
|
|
225
222
|
if (message.PAYLOAD_TYPE === 'ZONES' && payload.ZONES) {
|
|
226
|
-
this.log.info(
|
|
223
|
+
this.log.info(`Found ${payload.ZONES.length} zones`);
|
|
227
224
|
payload.ZONES.forEach((zone) => {
|
|
228
225
|
const device = this.parseZoneData(zone);
|
|
229
226
|
this.devices.set(device.id, device);
|
|
@@ -232,13 +229,12 @@ class KseniaWebSocketClient {
|
|
|
232
229
|
}
|
|
233
230
|
if (message.PAYLOAD_TYPE === 'MULTI_TYPES') {
|
|
234
231
|
if (payload.OUTPUTS) {
|
|
235
|
-
this.log.info(
|
|
232
|
+
this.log.info(`Found ${payload.OUTPUTS.length} outputs`);
|
|
236
233
|
payload.OUTPUTS.forEach((output) => {
|
|
237
|
-
|
|
238
|
-
const category = output.CAT || output.TYPE || '';
|
|
234
|
+
const category = output.CAT ?? output.TYPE ?? '';
|
|
239
235
|
const type = this.determineOutputType(category);
|
|
240
|
-
if (type === 'thermostat') {
|
|
241
|
-
this.log.
|
|
236
|
+
if (type === 'thermostat' && this.options.debug) {
|
|
237
|
+
this.log.debug(`Thermostat found - ID: ${output.ID}, DES: ${output.DES}, TYPE: ${output.TYPE}, CAT: ${output.CAT}`);
|
|
242
238
|
}
|
|
243
239
|
const device = this.parseOutputData(output);
|
|
244
240
|
if (device) {
|
|
@@ -248,11 +244,10 @@ class KseniaWebSocketClient {
|
|
|
248
244
|
});
|
|
249
245
|
}
|
|
250
246
|
if (payload.SCENARIOS) {
|
|
251
|
-
this.log.info(
|
|
247
|
+
this.log.info(`Found ${payload.SCENARIOS.length} scenarios`);
|
|
252
248
|
payload.SCENARIOS.forEach((scenario) => {
|
|
253
|
-
// Filtra gli scenari ARM/DISARM come fa lares4-ts
|
|
254
249
|
if (scenario.CAT === 'ARM' || scenario.CAT === 'DISARM') {
|
|
255
|
-
this.log.debug(
|
|
250
|
+
this.log.debug(`Scenario ${scenario.DES} ignored (category ${scenario.CAT})`);
|
|
256
251
|
return;
|
|
257
252
|
}
|
|
258
253
|
const device = this.parseScenarioData(scenario);
|
|
@@ -263,11 +258,9 @@ class KseniaWebSocketClient {
|
|
|
263
258
|
});
|
|
264
259
|
}
|
|
265
260
|
if (payload.BUS_HAS) {
|
|
266
|
-
this.log.info(
|
|
261
|
+
this.log.info(`Found ${payload.BUS_HAS.length} sensors`);
|
|
267
262
|
payload.BUS_HAS.forEach((sensor) => {
|
|
268
|
-
|
|
269
|
-
const baseName = sensor.DES || `Sensore ${sensor.ID}`;
|
|
270
|
-
// Sensore temperatura
|
|
263
|
+
const baseName = sensor.DES || `Sensor ${sensor.ID}`;
|
|
271
264
|
const tempDevice = {
|
|
272
265
|
id: `sensor_temp_${sensor.ID}`,
|
|
273
266
|
type: 'sensor',
|
|
@@ -275,109 +268,102 @@ class KseniaWebSocketClient {
|
|
|
275
268
|
description: `${baseName} - Temperatura`,
|
|
276
269
|
status: {
|
|
277
270
|
sensorType: 'temperature',
|
|
278
|
-
value:
|
|
279
|
-
unit: '
|
|
280
|
-
}
|
|
271
|
+
value: 0,
|
|
272
|
+
unit: 'C',
|
|
273
|
+
},
|
|
281
274
|
};
|
|
282
275
|
this.devices.set(tempDevice.id, tempDevice);
|
|
283
276
|
this.onDeviceDiscovered?.(tempDevice);
|
|
284
|
-
// Sensore umidità
|
|
285
277
|
const humDevice = {
|
|
286
278
|
id: `sensor_hum_${sensor.ID}`,
|
|
287
279
|
type: 'sensor',
|
|
288
|
-
name: `${baseName} -
|
|
289
|
-
description: `${baseName} -
|
|
280
|
+
name: `${baseName} - Umidita`,
|
|
281
|
+
description: `${baseName} - Umidita`,
|
|
290
282
|
status: {
|
|
291
283
|
sensorType: 'humidity',
|
|
292
284
|
value: 50,
|
|
293
|
-
unit: '%'
|
|
294
|
-
}
|
|
285
|
+
unit: '%',
|
|
286
|
+
},
|
|
295
287
|
};
|
|
296
288
|
this.devices.set(humDevice.id, humDevice);
|
|
297
289
|
this.onDeviceDiscovered?.(humDevice);
|
|
298
|
-
// Sensore luminosità
|
|
299
290
|
const lightDevice = {
|
|
300
291
|
id: `sensor_light_${sensor.ID}`,
|
|
301
292
|
type: 'sensor',
|
|
302
|
-
name: `${baseName} -
|
|
303
|
-
description: `${baseName} -
|
|
293
|
+
name: `${baseName} - Luminosita`,
|
|
294
|
+
description: `${baseName} - Luminosita`,
|
|
304
295
|
status: {
|
|
305
296
|
sensorType: 'light',
|
|
306
297
|
value: 100,
|
|
307
|
-
unit: 'lux'
|
|
308
|
-
}
|
|
298
|
+
unit: 'lux',
|
|
299
|
+
},
|
|
309
300
|
};
|
|
310
301
|
this.devices.set(lightDevice.id, lightDevice);
|
|
311
302
|
this.onDeviceDiscovered?.(lightDevice);
|
|
312
303
|
});
|
|
313
304
|
}
|
|
314
305
|
}
|
|
315
|
-
// Gestisci stati iniziali degli output
|
|
316
306
|
if (message.PAYLOAD_TYPE === 'STATUS_OUTPUTS' && payload.STATUS_OUTPUTS) {
|
|
317
|
-
this.log.info(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
307
|
+
this.log.info(`Initial states for ${payload.STATUS_OUTPUTS.length} outputs`);
|
|
308
|
+
if (this.options.debug) {
|
|
309
|
+
payload.STATUS_OUTPUTS.forEach((output) => {
|
|
310
|
+
const thermostatDevice = this.devices.get(`thermostat_${output.ID}`);
|
|
311
|
+
if (thermostatDevice) {
|
|
312
|
+
this.log.debug(`Initial thermostat state ${output.ID}: ${JSON.stringify(output)}`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
325
316
|
this.updateOutputStatuses(payload.STATUS_OUTPUTS);
|
|
326
317
|
}
|
|
327
|
-
// Gestisci stati iniziali dei sensori
|
|
328
318
|
if (message.PAYLOAD_TYPE === 'STATUS_BUS_HA_SENSORS' && payload.STATUS_BUS_HA_SENSORS) {
|
|
329
|
-
this.log.info(
|
|
319
|
+
this.log.info(`Initial states for ${payload.STATUS_BUS_HA_SENSORS.length} sensors`);
|
|
330
320
|
this.updateSensorStatuses(payload.STATUS_BUS_HA_SENSORS);
|
|
331
321
|
}
|
|
332
|
-
// Gestisci stati iniziali del sistema
|
|
333
322
|
if (message.PAYLOAD_TYPE === 'STATUS_SYSTEM' && payload.STATUS_SYSTEM) {
|
|
334
|
-
this.log.info(
|
|
323
|
+
this.log.info('Initial system temperatures');
|
|
335
324
|
this.updateSystemTemperatures(payload.STATUS_SYSTEM);
|
|
336
325
|
}
|
|
337
326
|
}
|
|
338
327
|
handleRealtimeResponse(message) {
|
|
339
|
-
this.log.info('
|
|
340
|
-
// Processa gli stati iniziali se presenti
|
|
328
|
+
this.log.info('Real-time registration completed');
|
|
341
329
|
const payload = message.PAYLOAD;
|
|
342
330
|
if (payload.STATUS_OUTPUTS) {
|
|
343
|
-
this.log.info(
|
|
331
|
+
this.log.info(`Updating states for ${payload.STATUS_OUTPUTS.length} outputs`);
|
|
344
332
|
this.updateOutputStatuses(payload.STATUS_OUTPUTS);
|
|
345
333
|
}
|
|
346
334
|
if (payload.STATUS_BUS_HA_SENSORS) {
|
|
347
|
-
this.log.info(
|
|
335
|
+
this.log.info(`Updating states for ${payload.STATUS_BUS_HA_SENSORS.length} sensors`);
|
|
348
336
|
this.updateSensorStatuses(payload.STATUS_BUS_HA_SENSORS);
|
|
349
337
|
}
|
|
350
338
|
if (payload.STATUS_ZONES) {
|
|
351
|
-
this.log.info(
|
|
339
|
+
this.log.info(`Updating states for ${payload.STATUS_ZONES.length} zones`);
|
|
352
340
|
this.updateZoneStatuses(payload.STATUS_ZONES);
|
|
353
341
|
}
|
|
354
342
|
if (payload.STATUS_SYSTEM) {
|
|
355
|
-
this.log.info(
|
|
343
|
+
this.log.info('Updating initial system temperatures');
|
|
356
344
|
this.updateSystemTemperatures(payload.STATUS_SYSTEM);
|
|
357
345
|
}
|
|
358
346
|
}
|
|
359
347
|
handleStatusUpdate(message) {
|
|
360
|
-
// Gestisce gli aggiornamenti di stato in tempo reale
|
|
361
348
|
const payload = message.PAYLOAD;
|
|
362
|
-
this.log.debug(
|
|
363
|
-
|
|
364
|
-
for (const [sender, data] of Object.entries(payload)) {
|
|
349
|
+
this.log.debug(`handleStatusUpdate called, payload keys: ${Object.keys(payload)}`);
|
|
350
|
+
for (const [, data] of Object.entries(payload)) {
|
|
365
351
|
if (data && typeof data === 'object') {
|
|
366
352
|
const statusData = data;
|
|
367
353
|
if (statusData.STATUS_OUTPUTS) {
|
|
368
|
-
this.log.info(
|
|
354
|
+
this.log.info(`Real-time update for ${statusData.STATUS_OUTPUTS.length} outputs`);
|
|
369
355
|
this.updateOutputStatuses(statusData.STATUS_OUTPUTS);
|
|
370
356
|
}
|
|
371
357
|
if (statusData.STATUS_BUS_HA_SENSORS) {
|
|
372
|
-
this.log.info(
|
|
358
|
+
this.log.info(`Real-time update for ${statusData.STATUS_BUS_HA_SENSORS.length} sensors`);
|
|
373
359
|
this.updateSensorStatuses(statusData.STATUS_BUS_HA_SENSORS);
|
|
374
360
|
}
|
|
375
361
|
if (statusData.STATUS_ZONES) {
|
|
376
|
-
this.log.info(
|
|
362
|
+
this.log.info(`Real-time update for ${statusData.STATUS_ZONES.length} zones`);
|
|
377
363
|
this.updateZoneStatuses(statusData.STATUS_ZONES);
|
|
378
364
|
}
|
|
379
365
|
if (statusData.STATUS_SYSTEM) {
|
|
380
|
-
this.log.info(
|
|
366
|
+
this.log.info('System temperature update');
|
|
381
367
|
this.updateSystemTemperatures(statusData.STATUS_SYSTEM);
|
|
382
368
|
}
|
|
383
369
|
}
|
|
@@ -387,78 +373,60 @@ class KseniaWebSocketClient {
|
|
|
387
373
|
return {
|
|
388
374
|
id: `zone_${zoneData.ID}`,
|
|
389
375
|
type: 'zone',
|
|
390
|
-
name: zoneData.DES || `
|
|
376
|
+
name: zoneData.DES || `Zone ${zoneData.ID}`,
|
|
391
377
|
description: zoneData.DES || '',
|
|
392
378
|
status: {
|
|
393
379
|
armed: zoneData.STATUS === '1',
|
|
394
380
|
bypassed: false,
|
|
395
381
|
fault: false,
|
|
396
|
-
open: zoneData.STATUS === '2'
|
|
397
|
-
}
|
|
382
|
+
open: zoneData.STATUS === '2',
|
|
383
|
+
},
|
|
398
384
|
};
|
|
399
385
|
}
|
|
400
386
|
parseOutputData(outputData) {
|
|
401
|
-
|
|
402
|
-
const category = outputData.CAT || outputData.TYPE || '';
|
|
387
|
+
const category = outputData.CAT ?? outputData.TYPE ?? '';
|
|
403
388
|
const categoryUpper = category.toUpperCase();
|
|
404
|
-
// Usa l'ID reale del sistema (non remappato)
|
|
405
389
|
const systemId = outputData.ID;
|
|
406
390
|
if (categoryUpper === 'LIGHT') {
|
|
407
391
|
return {
|
|
408
392
|
id: `light_${systemId}`,
|
|
409
393
|
type: 'light',
|
|
410
|
-
name: outputData.DES || `
|
|
394
|
+
name: outputData.DES || `Light ${systemId}`,
|
|
411
395
|
description: outputData.DES || '',
|
|
412
396
|
status: {
|
|
413
|
-
on: false,
|
|
397
|
+
on: false,
|
|
414
398
|
brightness: undefined,
|
|
415
|
-
dimmable: false
|
|
416
|
-
}
|
|
399
|
+
dimmable: false,
|
|
400
|
+
},
|
|
417
401
|
};
|
|
418
402
|
}
|
|
419
403
|
else if (categoryUpper === 'ROLL') {
|
|
420
404
|
return {
|
|
421
405
|
id: `cover_${systemId}`,
|
|
422
406
|
type: 'cover',
|
|
423
|
-
name: outputData.DES || `
|
|
407
|
+
name: outputData.DES || `Cover ${systemId}`,
|
|
424
408
|
description: outputData.DES || '',
|
|
425
409
|
status: {
|
|
426
|
-
position: 0,
|
|
427
|
-
state: 'stopped'
|
|
428
|
-
}
|
|
410
|
+
position: 0,
|
|
411
|
+
state: 'stopped',
|
|
412
|
+
},
|
|
429
413
|
};
|
|
430
414
|
}
|
|
431
415
|
else if (categoryUpper === 'GATE') {
|
|
432
416
|
return {
|
|
433
|
-
id: `cover_${systemId}`,
|
|
417
|
+
id: `cover_${systemId}`,
|
|
434
418
|
type: 'cover',
|
|
435
|
-
name: outputData.DES || `
|
|
419
|
+
name: outputData.DES || `Gate ${systemId}`,
|
|
436
420
|
description: outputData.DES || '',
|
|
437
421
|
status: {
|
|
438
422
|
position: 0,
|
|
439
|
-
state: 'stopped'
|
|
440
|
-
}
|
|
423
|
+
state: 'stopped',
|
|
424
|
+
},
|
|
441
425
|
};
|
|
442
426
|
}
|
|
443
|
-
|
|
444
|
-
this.log.debug(`📋 Output ignorato: ID ${systemId}, CAT: ${category}, DES: ${outputData.DES}`);
|
|
427
|
+
this.log.debug(`Output ignored: ID ${systemId}, CAT: ${category}, DES: ${outputData.DES}`);
|
|
445
428
|
return null;
|
|
446
429
|
}
|
|
447
|
-
parseSensorData(sensorData) {
|
|
448
|
-
const baseName = sensorData.DES || `Sensore ${sensorData.ID}`;
|
|
449
|
-
// Creiamo solo il sensore temperatura per ora
|
|
450
|
-
return {
|
|
451
|
-
id: `sensor_temp_${sensorData.ID}`,
|
|
452
|
-
type: 'sensor',
|
|
453
|
-
name: `${baseName} - Temperatura`,
|
|
454
|
-
description: `${baseName} - Temperatura`,
|
|
455
|
-
status: {
|
|
456
|
-
sensorType: 'temperature',
|
|
457
|
-
value: undefined, // Sarà aggiornato dai dati real-time
|
|
458
|
-
unit: '°C'
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
430
|
parseScenarioData(scenarioData) {
|
|
463
431
|
return {
|
|
464
432
|
id: `scenario_${scenarioData.ID}`,
|
|
@@ -466,28 +434,25 @@ class KseniaWebSocketClient {
|
|
|
466
434
|
name: scenarioData.DES || `Scenario ${scenarioData.ID}`,
|
|
467
435
|
description: scenarioData.DES || '',
|
|
468
436
|
status: {
|
|
469
|
-
active: false
|
|
470
|
-
}
|
|
437
|
+
active: false,
|
|
438
|
+
},
|
|
471
439
|
};
|
|
472
440
|
}
|
|
473
441
|
determineOutputType(category) {
|
|
474
442
|
const catUpper = category.toUpperCase();
|
|
475
|
-
|
|
476
|
-
this.log.debug(`🔍 Determinazione tipo per categoria: "${category}" (normalizzato: "${catUpper}")`);
|
|
477
|
-
// Usa la logica della libreria lares4-ts basata sul campo CAT
|
|
443
|
+
this.log.debug(`Determining type for category: "${category}" (normalized: "${catUpper}")`);
|
|
478
444
|
if (catUpper === 'ROLL') {
|
|
479
|
-
this.log.debug(
|
|
445
|
+
this.log.debug(`Identified as cover: ${category}`);
|
|
480
446
|
return 'cover';
|
|
481
447
|
}
|
|
482
448
|
if (catUpper === 'LIGHT') {
|
|
483
|
-
this.log.debug(
|
|
449
|
+
this.log.debug(`Identified as light: ${category}`);
|
|
484
450
|
return 'light';
|
|
485
451
|
}
|
|
486
452
|
if (catUpper === 'GATE') {
|
|
487
|
-
this.log.debug(
|
|
453
|
+
this.log.debug(`Identified as gate (treated as cover): ${category}`);
|
|
488
454
|
return 'cover';
|
|
489
455
|
}
|
|
490
|
-
// Termostati - controlla diverse possibili denominazioni per retrocompatibilità
|
|
491
456
|
if (catUpper.includes('THERM') ||
|
|
492
457
|
catUpper.includes('CLIMA') ||
|
|
493
458
|
catUpper.includes('TEMP') ||
|
|
@@ -495,160 +460,145 @@ class KseniaWebSocketClient {
|
|
|
495
460
|
catUpper.includes('RAFFRES') ||
|
|
496
461
|
catUpper.includes('HVAC') ||
|
|
497
462
|
catUpper.includes('TERMOS')) {
|
|
498
|
-
this.log.debug(
|
|
463
|
+
this.log.debug(`Identified as thermostat: ${category}`);
|
|
499
464
|
return 'thermostat';
|
|
500
465
|
}
|
|
501
|
-
|
|
502
|
-
this.log.debug(`✅ Identificato come luce (default): ${category}`);
|
|
466
|
+
this.log.debug(`Identified as light (default): ${category}`);
|
|
503
467
|
return 'light';
|
|
504
468
|
}
|
|
505
|
-
determineSensorType(type) {
|
|
506
|
-
if (type.includes('TEMP'))
|
|
507
|
-
return 'temperature';
|
|
508
|
-
if (type.includes('HUM'))
|
|
509
|
-
return 'humidity';
|
|
510
|
-
if (type.includes('LIGHT') || type.includes('LUX'))
|
|
511
|
-
return 'light';
|
|
512
|
-
if (type.includes('MOTION') || type.includes('PIR'))
|
|
513
|
-
return 'motion';
|
|
514
|
-
if (type.includes('CONTACT') || type.includes('DOOR'))
|
|
515
|
-
return 'contact';
|
|
516
|
-
return 'temperature'; // Default
|
|
517
|
-
}
|
|
518
469
|
updateOutputStatuses(outputs) {
|
|
519
|
-
outputs.forEach(output => {
|
|
520
|
-
this.log.debug(
|
|
521
|
-
// Usa gli ID reali del sistema (non remappati)
|
|
470
|
+
outputs.forEach((output) => {
|
|
471
|
+
this.log.debug(`Output update ${output.ID}: STA=${output.STA}, POS=${output.POS}, TPOS=${output.TPOS}`);
|
|
522
472
|
const lightDevice = this.devices.get(`light_${output.ID}`);
|
|
523
|
-
if (lightDevice) {
|
|
524
|
-
const
|
|
525
|
-
|
|
473
|
+
if (lightDevice && lightDevice.type === 'light') {
|
|
474
|
+
const lightStatus = lightDevice.status;
|
|
475
|
+
const wasOn = lightStatus.on;
|
|
476
|
+
lightStatus.on = output.STA === 'ON';
|
|
526
477
|
if (output.POS !== undefined) {
|
|
527
|
-
|
|
528
|
-
|
|
478
|
+
lightStatus.brightness = parseInt(output.POS, 10);
|
|
479
|
+
lightStatus.dimmable = true;
|
|
529
480
|
}
|
|
530
481
|
if (wasOn !== (output.STA === 'ON')) {
|
|
531
|
-
this.log.info(
|
|
482
|
+
this.log.info(`Light ${lightDevice.name} (Output ${output.ID}): ${output.STA === 'ON' ? 'ON' : 'OFF'}`);
|
|
532
483
|
}
|
|
533
484
|
this.onDeviceStatusUpdate?.(lightDevice);
|
|
534
485
|
}
|
|
535
486
|
const coverDevice = this.devices.get(`cover_${output.ID}`);
|
|
536
|
-
if (coverDevice) {
|
|
537
|
-
const
|
|
487
|
+
if (coverDevice && coverDevice.type === 'cover') {
|
|
488
|
+
const coverStatus = coverDevice.status;
|
|
489
|
+
const oldPos = coverStatus.position;
|
|
538
490
|
const newPosition = this.mapCoverPosition(output.STA, output.POS);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
491
|
+
coverStatus.position = newPosition;
|
|
492
|
+
coverStatus.targetPosition = parseInt(output.TPOS ?? output.POS ?? '0', 10);
|
|
493
|
+
coverStatus.state = this.mapCoverState(output.STA, output.POS, output.TPOS);
|
|
542
494
|
if (oldPos !== newPosition) {
|
|
543
|
-
this.log.info(
|
|
495
|
+
this.log.info(`Cover ${coverDevice.name} (Output ${output.ID}): ${output.STA} position ${newPosition}%`);
|
|
544
496
|
}
|
|
545
497
|
this.onDeviceStatusUpdate?.(coverDevice);
|
|
546
498
|
}
|
|
547
499
|
const thermostatDevice = this.devices.get(`thermostat_${output.ID}`);
|
|
548
|
-
if (thermostatDevice) {
|
|
549
|
-
|
|
550
|
-
// Aggiorna i dati del termostato se disponibili
|
|
500
|
+
if (thermostatDevice && thermostatDevice.type === 'thermostat') {
|
|
501
|
+
const thermostatStatus = thermostatDevice.status;
|
|
551
502
|
let updated = false;
|
|
552
|
-
// Se abbiamo dati di temperatura nel payload (dipende dal protocollo Lares4)
|
|
553
503
|
if (output.TEMP_CURRENT !== undefined) {
|
|
554
|
-
const oldCurrentTemp =
|
|
504
|
+
const oldCurrentTemp = thermostatStatus.currentTemperature;
|
|
555
505
|
const newCurrentTemp = parseFloat(output.TEMP_CURRENT);
|
|
556
|
-
|
|
506
|
+
thermostatStatus.currentTemperature = newCurrentTemp;
|
|
557
507
|
if (oldCurrentTemp !== newCurrentTemp) {
|
|
558
|
-
this.log.info(
|
|
508
|
+
this.log.info(`${thermostatDevice.name}: Current temperature ${newCurrentTemp}C`);
|
|
559
509
|
updated = true;
|
|
560
510
|
}
|
|
561
511
|
}
|
|
562
512
|
if (output.TEMP_TARGET !== undefined) {
|
|
563
|
-
const oldTargetTemp =
|
|
513
|
+
const oldTargetTemp = thermostatStatus.targetTemperature;
|
|
564
514
|
const newTargetTemp = parseFloat(output.TEMP_TARGET);
|
|
565
|
-
|
|
515
|
+
thermostatStatus.targetTemperature = newTargetTemp;
|
|
566
516
|
if (oldTargetTemp !== newTargetTemp) {
|
|
567
|
-
this.log.info(
|
|
517
|
+
this.log.info(`${thermostatDevice.name}: Target temperature ${newTargetTemp}C`);
|
|
568
518
|
updated = true;
|
|
569
519
|
}
|
|
570
520
|
}
|
|
571
521
|
if (output.MODE !== undefined) {
|
|
572
|
-
const oldMode =
|
|
522
|
+
const oldMode = thermostatStatus.mode;
|
|
573
523
|
const newMode = this.mapThermostatMode(output.MODE);
|
|
574
|
-
|
|
524
|
+
thermostatStatus.mode = newMode;
|
|
575
525
|
if (oldMode !== newMode) {
|
|
576
|
-
this.log.info(
|
|
526
|
+
this.log.info(`${thermostatDevice.name}: Mode ${newMode}`);
|
|
577
527
|
updated = true;
|
|
578
528
|
}
|
|
579
529
|
}
|
|
580
|
-
// Se abbiamo aggiornato qualcosa, notifica l'accessorio
|
|
581
530
|
if (updated) {
|
|
582
531
|
this.onDeviceStatusUpdate?.(thermostatDevice);
|
|
583
532
|
}
|
|
584
|
-
else {
|
|
585
|
-
|
|
586
|
-
this.log.debug(`🌡️ Debug termostato ${output.ID}: ${JSON.stringify(output)}`);
|
|
533
|
+
else if (this.options.debug) {
|
|
534
|
+
this.log.debug(`Debug thermostat ${output.ID}: ${JSON.stringify(output)}`);
|
|
587
535
|
}
|
|
588
536
|
}
|
|
589
537
|
});
|
|
590
538
|
}
|
|
591
539
|
updateSensorStatuses(sensors) {
|
|
592
|
-
sensors.forEach(sensor => {
|
|
540
|
+
sensors.forEach((sensor) => {
|
|
593
541
|
if (sensor.DOMUS) {
|
|
594
|
-
this.log.debug(
|
|
542
|
+
this.log.debug(`Sensor update ${sensor.ID}: TEM=${sensor.DOMUS.TEM}C, HUM=${sensor.DOMUS.HUM}%, LHT=${sensor.DOMUS.LHT}lux`);
|
|
595
543
|
const tempDevice = this.devices.get(`sensor_temp_${sensor.ID}`);
|
|
596
|
-
if (tempDevice) {
|
|
597
|
-
const
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
544
|
+
if (tempDevice && tempDevice.type === 'sensor') {
|
|
545
|
+
const tempStatus = tempDevice.status;
|
|
546
|
+
const oldTemp = tempStatus.value;
|
|
547
|
+
const newTemp = parseFloat(sensor.DOMUS.TEM ?? '0');
|
|
548
|
+
tempStatus.value = newTemp;
|
|
549
|
+
if (oldTemp !== newTemp && newTemp > 0) {
|
|
550
|
+
this.log.info(`${tempDevice.name}: ${newTemp}C`);
|
|
602
551
|
}
|
|
603
552
|
this.onDeviceStatusUpdate?.(tempDevice);
|
|
604
553
|
}
|
|
605
554
|
const humDevice = this.devices.get(`sensor_hum_${sensor.ID}`);
|
|
606
|
-
if (humDevice) {
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
|
|
555
|
+
if (humDevice && humDevice.type === 'sensor') {
|
|
556
|
+
const humStatus = humDevice.status;
|
|
557
|
+
const oldHum = humStatus.value;
|
|
558
|
+
const newHum = parseInt(sensor.DOMUS.HUM ?? '50', 10);
|
|
559
|
+
humStatus.value = newHum;
|
|
610
560
|
if (oldHum !== newHum) {
|
|
611
|
-
this.log.info(
|
|
561
|
+
this.log.info(`${humDevice.name}: ${newHum}%`);
|
|
612
562
|
}
|
|
613
563
|
this.onDeviceStatusUpdate?.(humDevice);
|
|
614
564
|
}
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
617
|
-
const
|
|
618
|
-
const
|
|
619
|
-
|
|
565
|
+
const lightSensorDevice = this.devices.get(`sensor_light_${sensor.ID}`);
|
|
566
|
+
if (lightSensorDevice && lightSensorDevice.type === 'sensor') {
|
|
567
|
+
const lightStatus = lightSensorDevice.status;
|
|
568
|
+
const oldLight = lightStatus.value;
|
|
569
|
+
const newLight = parseInt(sensor.DOMUS.LHT ?? '100', 10);
|
|
570
|
+
lightStatus.value = newLight;
|
|
620
571
|
if (oldLight !== newLight) {
|
|
621
|
-
this.log.info(
|
|
572
|
+
this.log.info(`${lightSensorDevice.name}: ${newLight}lux`);
|
|
622
573
|
}
|
|
623
|
-
this.onDeviceStatusUpdate?.(
|
|
574
|
+
this.onDeviceStatusUpdate?.(lightSensorDevice);
|
|
624
575
|
}
|
|
625
576
|
}
|
|
626
577
|
});
|
|
627
578
|
}
|
|
628
579
|
updateZoneStatuses(zones) {
|
|
629
|
-
zones.forEach(zone => {
|
|
630
|
-
this.log.debug(
|
|
580
|
+
zones.forEach((zone) => {
|
|
581
|
+
this.log.debug(`Zone update ${zone.ID}: STA=${zone.STA}, BYP=${zone.BYP}, A=${zone.A}`);
|
|
631
582
|
const zoneDevice = this.devices.get(`zone_${zone.ID}`);
|
|
632
|
-
if (zoneDevice) {
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
583
|
+
if (zoneDevice && zoneDevice.type === 'zone') {
|
|
584
|
+
const zoneStatus = zoneDevice.status;
|
|
585
|
+
const oldOpen = zoneStatus.open;
|
|
586
|
+
const newOpen = zone.STA === 'A';
|
|
587
|
+
zoneStatus.open = newOpen;
|
|
588
|
+
zoneStatus.bypassed = zone.BYP === 'YES';
|
|
589
|
+
zoneStatus.armed = zone.A === 'Y';
|
|
590
|
+
zoneStatus.fault = zone.FM === 'T';
|
|
639
591
|
if (oldOpen !== newOpen) {
|
|
640
|
-
this.log.info(
|
|
592
|
+
this.log.info(`${zoneDevice.name}: ${newOpen ? 'OPEN/ALARM' : 'IDLE'}`);
|
|
641
593
|
}
|
|
642
594
|
this.onDeviceStatusUpdate?.(zoneDevice);
|
|
643
595
|
}
|
|
644
596
|
});
|
|
645
597
|
}
|
|
646
598
|
mapCoverPosition(sta, pos) {
|
|
647
|
-
// Per cancelli e tapparelle, converti lo stato in posizione percentuale
|
|
648
599
|
if (pos !== undefined && pos !== '') {
|
|
649
|
-
return parseInt(pos);
|
|
600
|
+
return parseInt(pos, 10);
|
|
650
601
|
}
|
|
651
|
-
// Fallback basato sullo stato
|
|
652
602
|
switch (sta?.toUpperCase()) {
|
|
653
603
|
case 'OPEN':
|
|
654
604
|
case 'UP':
|
|
@@ -657,94 +607,92 @@ class KseniaWebSocketClient {
|
|
|
657
607
|
case 'DOWN':
|
|
658
608
|
return 0;
|
|
659
609
|
case 'STOP':
|
|
660
|
-
return 50;
|
|
610
|
+
return 50;
|
|
661
611
|
default:
|
|
662
612
|
return 0;
|
|
663
613
|
}
|
|
664
614
|
}
|
|
665
615
|
mapCoverState(sta, pos, tpos) {
|
|
666
|
-
// Se abbiamo posizione e target position, verifichiamo se è in movimento
|
|
667
616
|
if (pos !== undefined && tpos !== undefined) {
|
|
668
|
-
const currentPos = parseInt(pos);
|
|
669
|
-
const targetPos = parseInt(tpos);
|
|
617
|
+
const currentPos = parseInt(pos, 10);
|
|
618
|
+
const targetPos = parseInt(tpos, 10);
|
|
670
619
|
if (currentPos === targetPos) {
|
|
671
|
-
return 'stopped';
|
|
620
|
+
return 'stopped';
|
|
672
621
|
}
|
|
673
622
|
else if (currentPos < targetPos) {
|
|
674
|
-
return 'opening';
|
|
623
|
+
return 'opening';
|
|
675
624
|
}
|
|
676
625
|
else {
|
|
677
|
-
return 'closing';
|
|
626
|
+
return 'closing';
|
|
678
627
|
}
|
|
679
628
|
}
|
|
680
|
-
// Fallback alla logica precedente se non abbiamo posizioni
|
|
681
629
|
switch (sta?.toUpperCase()) {
|
|
682
630
|
case 'UP':
|
|
683
|
-
case 'OPEN':
|
|
631
|
+
case 'OPEN':
|
|
632
|
+
return 'opening';
|
|
684
633
|
case 'DOWN':
|
|
685
|
-
case 'CLOSE':
|
|
634
|
+
case 'CLOSE':
|
|
635
|
+
return 'closing';
|
|
686
636
|
case 'STOP':
|
|
687
|
-
default:
|
|
637
|
+
default:
|
|
638
|
+
return 'stopped';
|
|
688
639
|
}
|
|
689
640
|
}
|
|
690
|
-
// Metodi per controllare i dispositivi
|
|
691
641
|
async switchLight(lightId, on) {
|
|
692
642
|
if (!this.idLogin)
|
|
693
|
-
throw new Error('
|
|
643
|
+
throw new Error('Not connected');
|
|
694
644
|
const systemOutputId = lightId.replace('light_', '');
|
|
695
|
-
// Usa formato corretto con sostituzione automatica di ID_LOGIN e PIN
|
|
696
645
|
await this.sendKseniaCommand('CMD_USR', 'CMD_SET_OUTPUT', {
|
|
697
|
-
ID_LOGIN: 'true',
|
|
698
|
-
PIN: 'true',
|
|
646
|
+
ID_LOGIN: 'true',
|
|
647
|
+
PIN: 'true',
|
|
699
648
|
OUTPUT: {
|
|
700
649
|
ID: systemOutputId,
|
|
701
|
-
STA: on ? 'ON' : 'OFF'
|
|
702
|
-
}
|
|
650
|
+
STA: on ? 'ON' : 'OFF',
|
|
651
|
+
},
|
|
703
652
|
});
|
|
704
|
-
this.log.info(
|
|
653
|
+
this.log.info(`Light command sent: Output ${systemOutputId} -> ${on ? 'ON' : 'OFF'}`);
|
|
705
654
|
}
|
|
706
655
|
async dimLight(lightId, brightness) {
|
|
707
656
|
if (!this.idLogin)
|
|
708
|
-
throw new Error('
|
|
657
|
+
throw new Error('Not connected');
|
|
709
658
|
const systemOutputId = lightId.replace('light_', '');
|
|
710
659
|
await this.sendKseniaCommand('CMD_USR', 'CMD_SET_OUTPUT', {
|
|
711
660
|
ID_LOGIN: 'true',
|
|
712
661
|
PIN: 'true',
|
|
713
662
|
OUTPUT: {
|
|
714
663
|
ID: systemOutputId,
|
|
715
|
-
STA: brightness.toString()
|
|
716
|
-
}
|
|
664
|
+
STA: brightness.toString(),
|
|
665
|
+
},
|
|
717
666
|
});
|
|
718
|
-
this.log.info(
|
|
667
|
+
this.log.info(`Dimmer command sent: Output ${systemOutputId} -> ${brightness}%`);
|
|
719
668
|
}
|
|
720
669
|
async moveCover(coverId, position) {
|
|
721
670
|
if (!this.idLogin)
|
|
722
|
-
throw new Error('
|
|
671
|
+
throw new Error('Not connected');
|
|
723
672
|
const systemOutputId = coverId.replace('cover_', '');
|
|
724
|
-
// Determina il comando basato sulla posizione
|
|
725
673
|
let command;
|
|
726
674
|
if (position === 0) {
|
|
727
|
-
command = 'DOWN';
|
|
675
|
+
command = 'DOWN';
|
|
728
676
|
}
|
|
729
677
|
else if (position === 100) {
|
|
730
|
-
command = 'UP';
|
|
678
|
+
command = 'UP';
|
|
731
679
|
}
|
|
732
680
|
else {
|
|
733
|
-
command = position.toString();
|
|
681
|
+
command = position.toString();
|
|
734
682
|
}
|
|
735
683
|
await this.sendKseniaCommand('CMD_USR', 'CMD_SET_OUTPUT', {
|
|
736
684
|
ID_LOGIN: 'true',
|
|
737
685
|
PIN: 'true',
|
|
738
686
|
OUTPUT: {
|
|
739
687
|
ID: systemOutputId,
|
|
740
|
-
STA: command
|
|
741
|
-
}
|
|
688
|
+
STA: command,
|
|
689
|
+
},
|
|
742
690
|
});
|
|
743
|
-
this.log.info(
|
|
691
|
+
this.log.info(`Cover command sent: Output ${systemOutputId} -> ${command}`);
|
|
744
692
|
}
|
|
745
693
|
async setThermostatMode(thermostatId, mode) {
|
|
746
694
|
if (!this.idLogin)
|
|
747
|
-
throw new Error('
|
|
695
|
+
throw new Error('Not connected');
|
|
748
696
|
let modeValue;
|
|
749
697
|
switch (mode) {
|
|
750
698
|
case 'heat':
|
|
@@ -758,41 +706,40 @@ class KseniaWebSocketClient {
|
|
|
758
706
|
break;
|
|
759
707
|
default:
|
|
760
708
|
modeValue = '0';
|
|
761
|
-
break;
|
|
709
|
+
break;
|
|
762
710
|
}
|
|
763
711
|
await this.sendKseniaCommand('WRITE', 'THERMOSTAT', {
|
|
764
712
|
ID_LOGIN: this.idLogin,
|
|
765
713
|
ID_THERMOSTAT: thermostatId.replace('thermostat_', ''),
|
|
766
|
-
MODE: modeValue
|
|
714
|
+
MODE: modeValue,
|
|
767
715
|
});
|
|
768
716
|
}
|
|
769
717
|
async setThermostatTemperature(thermostatId, temperature) {
|
|
770
718
|
if (!this.idLogin)
|
|
771
|
-
throw new Error('
|
|
719
|
+
throw new Error('Not connected');
|
|
772
720
|
await this.sendKseniaCommand('WRITE', 'THERMOSTAT', {
|
|
773
721
|
ID_LOGIN: this.idLogin,
|
|
774
722
|
ID_THERMOSTAT: thermostatId.replace('thermostat_', ''),
|
|
775
|
-
TARGET_TEMP: temperature.toString()
|
|
723
|
+
TARGET_TEMP: temperature.toString(),
|
|
776
724
|
});
|
|
777
725
|
}
|
|
778
726
|
async triggerScenario(scenarioId) {
|
|
779
727
|
if (!this.idLogin)
|
|
780
|
-
throw new Error('
|
|
728
|
+
throw new Error('Not connected');
|
|
781
729
|
const systemScenarioId = scenarioId.replace('scenario_', '');
|
|
782
730
|
await this.sendKseniaCommand('CMD_USR', 'CMD_EXE_SCENARIO', {
|
|
783
731
|
ID_LOGIN: 'true',
|
|
784
732
|
PIN: 'true',
|
|
785
733
|
SCENARIO: {
|
|
786
|
-
ID: systemScenarioId
|
|
787
|
-
}
|
|
734
|
+
ID: systemScenarioId,
|
|
735
|
+
},
|
|
788
736
|
});
|
|
789
|
-
this.log.info(
|
|
737
|
+
this.log.info(`Scenario ${systemScenarioId} executed`);
|
|
790
738
|
}
|
|
791
739
|
async sendKseniaCommand(cmd, payloadType, payload) {
|
|
792
740
|
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
793
|
-
throw new Error('WebSocket
|
|
741
|
+
throw new Error('WebSocket not connected');
|
|
794
742
|
}
|
|
795
|
-
// Implementa la sostituzione automatica di ID_LOGIN e PIN come fa lares4-ts
|
|
796
743
|
const processedPayload = this.buildPayload(payload);
|
|
797
744
|
const id = Math.floor(Math.random() * 100000).toString();
|
|
798
745
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
@@ -804,27 +751,23 @@ class KseniaWebSocketClient {
|
|
|
804
751
|
PAYLOAD_TYPE: payloadType,
|
|
805
752
|
PAYLOAD: processedPayload,
|
|
806
753
|
TIMESTAMP: timestamp,
|
|
807
|
-
CRC_16: '0x0000'
|
|
754
|
+
CRC_16: '0x0000',
|
|
808
755
|
};
|
|
809
|
-
// Calcola il CRC del messaggio prima di inviarlo
|
|
810
756
|
message.CRC_16 = this.calculateCRC16(JSON.stringify(message));
|
|
811
757
|
const jsonMessage = JSON.stringify(message);
|
|
812
|
-
// Filtra i log di invio per PING/HEARTBEAT
|
|
813
758
|
const isPing = cmd === 'PING' || payloadType === 'HEARTBEAT';
|
|
814
759
|
if (this.options.debug || !isPing) {
|
|
815
|
-
this.log.info(
|
|
760
|
+
this.log.info(`Sending: ${jsonMessage}`);
|
|
816
761
|
}
|
|
817
762
|
else {
|
|
818
|
-
this.log.debug(
|
|
763
|
+
this.log.debug(`Debug: ${jsonMessage}`);
|
|
819
764
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
this.log.info(`🔧 DEBUG - Comando ${payloadType}: ${JSON.stringify(payload, null, 2)}`);
|
|
765
|
+
if (cmd === 'CMD_USR' && this.options.debug) {
|
|
766
|
+
this.log.debug(`DEBUG - Command ${payloadType}: ${JSON.stringify(payload, null, 2)}`);
|
|
823
767
|
}
|
|
824
768
|
this.ws.send(jsonMessage);
|
|
825
769
|
}
|
|
826
770
|
buildPayload(payload) {
|
|
827
|
-
// Implementa la logica di sostituzione della libreria lares4-ts
|
|
828
771
|
return {
|
|
829
772
|
...payload,
|
|
830
773
|
...(payload?.ID_LOGIN === 'true' && { ID_LOGIN: this.idLogin }),
|
|
@@ -833,19 +776,19 @@ class KseniaWebSocketClient {
|
|
|
833
776
|
}
|
|
834
777
|
async sendMessage(message) {
|
|
835
778
|
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
836
|
-
throw new Error('WebSocket
|
|
779
|
+
throw new Error('WebSocket not connected');
|
|
837
780
|
}
|
|
838
781
|
const messageStr = JSON.stringify(message);
|
|
839
|
-
|
|
840
|
-
this.log.info(`📤 Invio: ${messageStr}`);
|
|
782
|
+
this.log.info(`Sending: ${messageStr}`);
|
|
841
783
|
this.ws.send(messageStr);
|
|
842
784
|
}
|
|
843
785
|
calculateCRC16(jsonString) {
|
|
844
786
|
const utf8 = [];
|
|
845
787
|
for (let i = 0; i < jsonString.length; i++) {
|
|
846
788
|
const charcode = jsonString.charCodeAt(i);
|
|
847
|
-
if (charcode < 0x80)
|
|
789
|
+
if (charcode < 0x80) {
|
|
848
790
|
utf8.push(charcode);
|
|
791
|
+
}
|
|
849
792
|
else if (charcode < 0x800) {
|
|
850
793
|
utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
|
|
851
794
|
}
|
|
@@ -858,21 +801,21 @@ class KseniaWebSocketClient {
|
|
|
858
801
|
utf8.push(0xf0 | (surrogate >> 18), 0x80 | ((surrogate >> 12) & 0x3f), 0x80 | ((surrogate >> 6) & 0x3f), 0x80 | (surrogate & 0x3f));
|
|
859
802
|
}
|
|
860
803
|
}
|
|
861
|
-
const SEME_CRC_16_JSON =
|
|
804
|
+
const SEME_CRC_16_JSON = 0xffff;
|
|
862
805
|
const GEN_POLY_JSON = 0x1021;
|
|
863
806
|
const CRC_16 = '"CRC_16"';
|
|
864
807
|
const dataLen = jsonString.lastIndexOf(CRC_16) + CRC_16.length + (utf8.length - jsonString.length);
|
|
865
808
|
let crc = SEME_CRC_16_JSON;
|
|
866
809
|
for (let i = 0; i < dataLen; i++) {
|
|
867
810
|
const charCode = utf8[i];
|
|
868
|
-
for (let
|
|
869
|
-
const
|
|
811
|
+
for (let iCrc = 0x80; iCrc; iCrc >>= 1) {
|
|
812
|
+
const flagCrc = crc & 0x8000 ? 1 : 0;
|
|
870
813
|
crc <<= 1;
|
|
871
|
-
crc =
|
|
872
|
-
if (charCode &
|
|
814
|
+
crc = crc & 0xffff;
|
|
815
|
+
if (charCode & iCrc) {
|
|
873
816
|
crc++;
|
|
874
817
|
}
|
|
875
|
-
if (
|
|
818
|
+
if (flagCrc) {
|
|
876
819
|
crc ^= GEN_POLY_JSON;
|
|
877
820
|
}
|
|
878
821
|
}
|
|
@@ -886,9 +829,9 @@ class KseniaWebSocketClient {
|
|
|
886
829
|
this.heartbeatTimer = setInterval(() => {
|
|
887
830
|
if (this.isConnected && this.idLogin) {
|
|
888
831
|
this.sendKseniaCommand('PING', 'HEARTBEAT', {
|
|
889
|
-
ID_LOGIN: this.idLogin
|
|
890
|
-
}).catch(err => {
|
|
891
|
-
this.log.error('
|
|
832
|
+
ID_LOGIN: this.idLogin,
|
|
833
|
+
}).catch((err) => {
|
|
834
|
+
this.log.error('Heartbeat error:', err instanceof Error ? err.message : String(err));
|
|
892
835
|
});
|
|
893
836
|
}
|
|
894
837
|
}, this.options.heartbeatInterval);
|
|
@@ -898,9 +841,9 @@ class KseniaWebSocketClient {
|
|
|
898
841
|
clearTimeout(this.reconnectTimer);
|
|
899
842
|
}
|
|
900
843
|
this.reconnectTimer = setTimeout(() => {
|
|
901
|
-
this.log.info('
|
|
902
|
-
this.connect().catch(err => {
|
|
903
|
-
this.log.error('
|
|
844
|
+
this.log.info('Attempting reconnection...');
|
|
845
|
+
this.connect().catch((err) => {
|
|
846
|
+
this.log.error('Reconnection failed:', err instanceof Error ? err.message : String(err));
|
|
904
847
|
this.scheduleReconnect();
|
|
905
848
|
});
|
|
906
849
|
}, this.options.reconnectInterval);
|
|
@@ -917,7 +860,6 @@ class KseniaWebSocketClient {
|
|
|
917
860
|
}
|
|
918
861
|
this.isConnected = false;
|
|
919
862
|
}
|
|
920
|
-
// Metodo helper per mappare le modalità del termostato
|
|
921
863
|
mapThermostatMode(mode) {
|
|
922
864
|
switch (mode?.toLowerCase()) {
|
|
923
865
|
case 'heat':
|
|
@@ -938,46 +880,48 @@ class KseniaWebSocketClient {
|
|
|
938
880
|
return 'off';
|
|
939
881
|
}
|
|
940
882
|
}
|
|
941
|
-
// Nuovo metodo per gestire le temperature del sistema
|
|
942
883
|
updateSystemTemperatures(systemData) {
|
|
943
|
-
systemData.forEach(system => {
|
|
884
|
+
systemData.forEach((system) => {
|
|
944
885
|
if (this.options.debug) {
|
|
945
|
-
this.log.debug(
|
|
886
|
+
this.log.debug(`System data ${system.ID}: ${JSON.stringify(system)}`);
|
|
946
887
|
}
|
|
947
888
|
if (system.TEMP) {
|
|
948
|
-
const internalTemp = system.TEMP.IN
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
889
|
+
const internalTemp = system.TEMP.IN
|
|
890
|
+
? parseFloat(system.TEMP.IN.replace('+', ''))
|
|
891
|
+
: undefined;
|
|
892
|
+
const externalTemp = system.TEMP.OUT
|
|
893
|
+
? parseFloat(system.TEMP.OUT.replace('+', ''))
|
|
894
|
+
: undefined;
|
|
895
|
+
let logTemperatures = this.options.debug ?? false;
|
|
953
896
|
if (internalTemp !== undefined) {
|
|
954
|
-
this.devices.forEach((device
|
|
897
|
+
this.devices.forEach((device) => {
|
|
955
898
|
if (device.type === 'thermostat') {
|
|
956
|
-
const
|
|
957
|
-
|
|
899
|
+
const thermostatStatus = device.status;
|
|
900
|
+
const oldCurrentTemp = thermostatStatus.currentTemperature;
|
|
901
|
+
if (oldCurrentTemp === undefined ||
|
|
902
|
+
Math.abs(oldCurrentTemp - internalTemp) >= 0.5) {
|
|
958
903
|
logTemperatures = true;
|
|
959
904
|
}
|
|
960
905
|
}
|
|
961
906
|
});
|
|
962
907
|
}
|
|
963
908
|
if (logTemperatures) {
|
|
964
|
-
this.log.info(
|
|
909
|
+
this.log.info(`System temperatures: Internal=${internalTemp}C, External=${externalTemp}C`);
|
|
965
910
|
}
|
|
966
|
-
// Aggiorna tutti i termostati con la temperatura interna del sistema
|
|
967
|
-
// (assumendo che i termostati utilizzino la temperatura interna come riferimento)
|
|
968
911
|
if (internalTemp !== undefined) {
|
|
969
|
-
this.devices.forEach((device
|
|
912
|
+
this.devices.forEach((device) => {
|
|
970
913
|
if (device.type === 'thermostat') {
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
914
|
+
const thermostatStatus = device.status;
|
|
915
|
+
const oldCurrentTemp = thermostatStatus.currentTemperature;
|
|
916
|
+
thermostatStatus.currentTemperature = internalTemp;
|
|
917
|
+
if (thermostatStatus.targetTemperature === undefined ||
|
|
918
|
+
thermostatStatus.targetTemperature === null) {
|
|
919
|
+
thermostatStatus.targetTemperature = Math.round(internalTemp + 1);
|
|
920
|
+
this.log.info(`${device.name}: Initial target temperature set to ${thermostatStatus.targetTemperature}C`);
|
|
977
921
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
this.log.info(
|
|
922
|
+
if (oldCurrentTemp === undefined ||
|
|
923
|
+
Math.abs(oldCurrentTemp - internalTemp) >= 0.5) {
|
|
924
|
+
this.log.info(`${device.name}: Current temperature updated to ${internalTemp}C`);
|
|
981
925
|
}
|
|
982
926
|
this.onDeviceStatusUpdate?.(device);
|
|
983
927
|
}
|