homebridge-plugin-klares4 1.1.0

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/config.schema.json +206 -0
  4. package/dist/accessories/cover-accessory.d.ts +21 -0
  5. package/dist/accessories/cover-accessory.d.ts.map +1 -0
  6. package/dist/accessories/cover-accessory.js +128 -0
  7. package/dist/accessories/cover-accessory.js.map +1 -0
  8. package/dist/accessories/light-accessory.d.ts +16 -0
  9. package/dist/accessories/light-accessory.d.ts.map +1 -0
  10. package/dist/accessories/light-accessory.js +78 -0
  11. package/dist/accessories/light-accessory.js.map +1 -0
  12. package/dist/accessories/scenario-accessory.d.ts +13 -0
  13. package/dist/accessories/scenario-accessory.d.ts.map +1 -0
  14. package/dist/accessories/scenario-accessory.js +55 -0
  15. package/dist/accessories/scenario-accessory.js.map +1 -0
  16. package/dist/accessories/sensor-accessory.d.ts +24 -0
  17. package/dist/accessories/sensor-accessory.d.ts.map +1 -0
  18. package/dist/accessories/sensor-accessory.js +128 -0
  19. package/dist/accessories/sensor-accessory.js.map +1 -0
  20. package/dist/accessories/thermostat-accessory.d.ts +21 -0
  21. package/dist/accessories/thermostat-accessory.d.ts.map +1 -0
  22. package/dist/accessories/thermostat-accessory.js +215 -0
  23. package/dist/accessories/thermostat-accessory.js.map +1 -0
  24. package/dist/accessories/zone-accessory.d.ts +18 -0
  25. package/dist/accessories/zone-accessory.d.ts.map +1 -0
  26. package/dist/accessories/zone-accessory.js +99 -0
  27. package/dist/accessories/zone-accessory.js.map +1 -0
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +7 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/platform.d.ts +69 -0
  33. package/dist/platform.d.ts.map +1 -0
  34. package/dist/platform.js +345 -0
  35. package/dist/platform.js.map +1 -0
  36. package/dist/settings.d.ts +3 -0
  37. package/dist/settings.d.ts.map +1 -0
  38. package/dist/settings.js +6 -0
  39. package/dist/settings.js.map +1 -0
  40. package/dist/types.d.ts +82 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +3 -0
  43. package/dist/types.js.map +1 -0
  44. package/dist/websocket-client.d.ts +57 -0
  45. package/dist/websocket-client.d.ts.map +1 -0
  46. package/dist/websocket-client.js +991 -0
  47. package/dist/websocket-client.js.map +1 -0
  48. package/package.json +64 -0
@@ -0,0 +1,991 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.KseniaWebSocketClient = void 0;
40
+ const ws_1 = __importDefault(require("ws"));
41
+ const https = __importStar(require("https"));
42
+ const crypto = __importStar(require("crypto"));
43
+ class KseniaWebSocketClient {
44
+ constructor(ip, port, useHttps, sender, pin, log, options = {}) {
45
+ this.ip = ip;
46
+ this.port = port;
47
+ this.useHttps = useHttps;
48
+ this.sender = sender;
49
+ this.pin = pin;
50
+ this.log = log;
51
+ this.options = options;
52
+ this.isConnected = false;
53
+ // Device storage
54
+ this.devices = new Map();
55
+ this.options = {
56
+ debug: false,
57
+ reconnectInterval: 5000,
58
+ heartbeatInterval: 30000,
59
+ ...options
60
+ };
61
+ }
62
+ async connect() {
63
+ return new Promise((resolve, reject) => {
64
+ try {
65
+ const protocol = this.useHttps ? 'wss' : 'ws';
66
+ const wsUrl = `${protocol}://${this.ip}:${this.port}/KseniaWsock/`;
67
+ this.log.info(`🔗 Connessione a ${wsUrl}...`);
68
+ const wsOptions = {};
69
+ if (this.useHttps) {
70
+ wsOptions.rejectUnauthorized = false;
71
+ wsOptions.agent = new https.Agent({
72
+ rejectUnauthorized: false,
73
+ secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
74
+ secureProtocol: 'TLS_method',
75
+ ciphers: 'ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA'
76
+ });
77
+ }
78
+ this.ws = new ws_1.default(wsUrl, ['KS_WSOCK'], wsOptions);
79
+ this.ws.on('open', () => {
80
+ this.log.info('✅ WebSocket connesso');
81
+ this.isConnected = true;
82
+ this.login().then(() => {
83
+ this.onConnected?.();
84
+ resolve();
85
+ }).catch(reject);
86
+ });
87
+ this.ws.on('message', (data) => {
88
+ this.handleMessage(data.toString());
89
+ });
90
+ this.ws.on('close', (code, reason) => {
91
+ this.log.warn(`🔌 WebSocket chiuso: ${code} - ${reason.toString()}`);
92
+ this.isConnected = false;
93
+ this.onDisconnected?.();
94
+ this.scheduleReconnect();
95
+ });
96
+ this.ws.on('error', (error) => {
97
+ this.log.error('❌ Errore WebSocket:', error.message);
98
+ reject(error);
99
+ });
100
+ }
101
+ catch (error) {
102
+ reject(error);
103
+ }
104
+ });
105
+ }
106
+ async login() {
107
+ const loginMessage = {
108
+ SENDER: this.sender,
109
+ RECEIVER: '',
110
+ CMD: 'LOGIN',
111
+ ID: Math.floor(Math.random() * 65535).toString(),
112
+ PAYLOAD_TYPE: 'UNKNOWN',
113
+ PAYLOAD: {
114
+ PIN: this.pin
115
+ },
116
+ TIMESTAMP: Math.floor(Date.now() / 1000).toString(),
117
+ CRC_16: '0x0000'
118
+ };
119
+ loginMessage.CRC_16 = this.calculateCRC16(JSON.stringify(loginMessage));
120
+ this.log.info('🔐 Esecuzione login...');
121
+ await this.sendMessage(loginMessage);
122
+ }
123
+ handleMessage(data) {
124
+ try {
125
+ const message = JSON.parse(data);
126
+ // Filtra i messaggi di debug
127
+ const isHeartbeat = message.CMD === 'PING' || message.PAYLOAD_TYPE === 'HEARTBEAT';
128
+ const isGenericRealtime = message.CMD === 'REALTIME' && message.PAYLOAD_TYPE === 'CHANGES';
129
+ // Mostra messaggi solo se sono importanti o se siamo in debug
130
+ if (this.options.debug || (!isHeartbeat && !isGenericRealtime)) {
131
+ this.log.info(`📨 Ricevuto: ${data}`);
132
+ }
133
+ else if (isHeartbeat || isGenericRealtime) {
134
+ this.log.debug(`📨 Debug: ${data}`);
135
+ }
136
+ switch (message.CMD) {
137
+ case 'LOGIN_RES':
138
+ this.handleLoginResponse(message);
139
+ break;
140
+ case 'READ_RES':
141
+ this.handleReadResponse(message);
142
+ break;
143
+ case 'REALTIME_RES':
144
+ this.handleRealtimeResponse(message);
145
+ break;
146
+ case 'REALTIME':
147
+ // I messaggi real-time di aggiornamento stato
148
+ if (message.PAYLOAD_TYPE === 'CHANGES') {
149
+ this.handleStatusUpdate(message);
150
+ }
151
+ break;
152
+ case 'STATUS_UPDATE':
153
+ this.handleStatusUpdate(message);
154
+ break;
155
+ case 'PING':
156
+ // PING ricevuto - rispondi solo in debug
157
+ if (this.options.debug) {
158
+ this.log.debug(`🏓 PING ricevuto dal sistema`);
159
+ }
160
+ break;
161
+ default:
162
+ if (this.options.debug) {
163
+ this.log.debug(`📋 Messaggio non gestito: ${message.CMD}`);
164
+ }
165
+ }
166
+ }
167
+ catch (error) {
168
+ this.log.error('❌ Errore parsing messaggio:', error);
169
+ }
170
+ }
171
+ handleLoginResponse(message) {
172
+ if (message.PAYLOAD?.RESULT === 'OK') {
173
+ this.idLogin = message.PAYLOAD.ID_LOGIN || '1';
174
+ this.log.info(`✅ Login completato, ID_LOGIN: ${this.idLogin}`);
175
+ this.startHeartbeat();
176
+ this.requestSystemData();
177
+ }
178
+ else {
179
+ this.log.error('❌ Login fallito:', message.PAYLOAD?.RESULT_DETAIL);
180
+ }
181
+ }
182
+ async requestSystemData() {
183
+ if (!this.idLogin) {
184
+ this.log.error('❌ ID_LOGIN non disponibile');
185
+ return;
186
+ }
187
+ this.log.info('📥 Richiesta dati sistema...');
188
+ // Richiedi zone
189
+ await this.sendKseniaCommand('READ', 'ZONES', {
190
+ ID_LOGIN: this.idLogin,
191
+ ID_ITEMS_RANGE: ['ALL', 'ALL']
192
+ });
193
+ // Richiedi output (luci, tapparelle, ecc.)
194
+ await this.sendKseniaCommand('READ', 'MULTI_TYPES', {
195
+ ID_LOGIN: this.idLogin,
196
+ TYPES: ['OUTPUTS', 'BUS_HAS', 'SCENARIOS']
197
+ });
198
+ // Richiedi stati attuali degli output (FONDAMENTALE per sincronizzare stati iniziali!)
199
+ await this.sendKseniaCommand('READ', 'STATUS_OUTPUTS', {
200
+ ID_LOGIN: this.idLogin
201
+ });
202
+ // Richiedi stati attuali dei sensori
203
+ await this.sendKseniaCommand('READ', 'STATUS_BUS_HA_SENSORS', {
204
+ ID_LOGIN: this.idLogin
205
+ });
206
+ // Richiedi stati del sistema (AGGIUNTO per termostati!)
207
+ await this.sendKseniaCommand('READ', 'STATUS_SYSTEM', {
208
+ ID_LOGIN: this.idLogin
209
+ });
210
+ // Registra per aggiornamenti real-time
211
+ await this.sendKseniaCommand('REALTIME', 'REGISTER', {
212
+ ID_LOGIN: this.idLogin,
213
+ TYPES: [
214
+ 'STATUS_ZONES',
215
+ 'STATUS_OUTPUTS',
216
+ 'STATUS_BUS_HA_SENSORS',
217
+ 'STATUS_SYSTEM',
218
+ 'SCENARIOS'
219
+ ]
220
+ });
221
+ }
222
+ handleReadResponse(message) {
223
+ const payload = message.PAYLOAD;
224
+ this.log.info(`📥 Risposta ricevuta: ${message.PAYLOAD_TYPE}`);
225
+ if (message.PAYLOAD_TYPE === 'ZONES' && payload.ZONES) {
226
+ this.log.info(`🏠 Trovate ${payload.ZONES.length} zone`);
227
+ payload.ZONES.forEach((zone) => {
228
+ const device = this.parseZoneData(zone);
229
+ this.devices.set(device.id, device);
230
+ this.onDeviceDiscovered?.(device);
231
+ });
232
+ }
233
+ if (message.PAYLOAD_TYPE === 'MULTI_TYPES') {
234
+ if (payload.OUTPUTS) {
235
+ this.log.info(`💡 Trovati ${payload.OUTPUTS.length} output`);
236
+ payload.OUTPUTS.forEach((output) => {
237
+ // Log dettagliato per debugging termostati
238
+ const category = output.CAT || output.TYPE || '';
239
+ const type = this.determineOutputType(category);
240
+ if (type === 'thermostat') {
241
+ this.log.info(`🌡️ DEBUG Termostato trovato - ID: ${output.ID}, DES: ${output.DES}, TYPE: ${output.TYPE}, CAT: ${output.CAT}, RAW: ${JSON.stringify(output)}`);
242
+ }
243
+ const device = this.parseOutputData(output);
244
+ if (device) {
245
+ this.devices.set(device.id, device);
246
+ this.onDeviceDiscovered?.(device);
247
+ }
248
+ });
249
+ }
250
+ if (payload.SCENARIOS) {
251
+ this.log.info(`🎬 Trovati ${payload.SCENARIOS.length} scenari`);
252
+ payload.SCENARIOS.forEach((scenario) => {
253
+ // Filtra gli scenari ARM/DISARM come fa lares4-ts
254
+ if (scenario.CAT === 'ARM' || scenario.CAT === 'DISARM') {
255
+ this.log.debug(`⏭️ Scenario ${scenario.DES} ignorato (categoria ${scenario.CAT})`);
256
+ return;
257
+ }
258
+ const device = this.parseScenarioData(scenario);
259
+ if (device) {
260
+ this.devices.set(device.id, device);
261
+ this.onDeviceDiscovered?.(device);
262
+ }
263
+ });
264
+ }
265
+ if (payload.BUS_HAS) {
266
+ this.log.info(`🌡️ Trovati ${payload.BUS_HAS.length} sensori`);
267
+ payload.BUS_HAS.forEach((sensor) => {
268
+ // Crea sensori multipli per ogni DOMUS
269
+ const baseName = sensor.DES || `Sensore ${sensor.ID}`;
270
+ // Sensore temperatura
271
+ const tempDevice = {
272
+ id: `sensor_temp_${sensor.ID}`,
273
+ type: 'sensor',
274
+ name: `${baseName} - Temperatura`,
275
+ description: `${baseName} - Temperatura`,
276
+ status: {
277
+ sensorType: 'temperature',
278
+ value: undefined, // Sarà aggiornato dai dati real-time
279
+ unit: '°C'
280
+ }
281
+ };
282
+ this.devices.set(tempDevice.id, tempDevice);
283
+ this.onDeviceDiscovered?.(tempDevice);
284
+ // Sensore umidità
285
+ const humDevice = {
286
+ id: `sensor_hum_${sensor.ID}`,
287
+ type: 'sensor',
288
+ name: `${baseName} - Umidità`,
289
+ description: `${baseName} - Umidità`,
290
+ status: {
291
+ sensorType: 'humidity',
292
+ value: 50,
293
+ unit: '%'
294
+ }
295
+ };
296
+ this.devices.set(humDevice.id, humDevice);
297
+ this.onDeviceDiscovered?.(humDevice);
298
+ // Sensore luminosità
299
+ const lightDevice = {
300
+ id: `sensor_light_${sensor.ID}`,
301
+ type: 'sensor',
302
+ name: `${baseName} - Luminosità`,
303
+ description: `${baseName} - Luminosità`,
304
+ status: {
305
+ sensorType: 'light',
306
+ value: 100,
307
+ unit: 'lux'
308
+ }
309
+ };
310
+ this.devices.set(lightDevice.id, lightDevice);
311
+ this.onDeviceDiscovered?.(lightDevice);
312
+ });
313
+ }
314
+ }
315
+ // Gestisci stati iniziali degli output
316
+ if (message.PAYLOAD_TYPE === 'STATUS_OUTPUTS' && payload.STATUS_OUTPUTS) {
317
+ this.log.info(`📊 Stati iniziali ${payload.STATUS_OUTPUTS.length} output`);
318
+ // Log dettagliato per debugging termostati
319
+ payload.STATUS_OUTPUTS.forEach((output) => {
320
+ const thermostatDevice = this.devices.get(`thermostat_${output.ID}`);
321
+ if (thermostatDevice) {
322
+ this.log.info(`🌡️ DEBUG Stato iniziale termostato ${output.ID}: ${JSON.stringify(output)}`);
323
+ }
324
+ });
325
+ this.updateOutputStatuses(payload.STATUS_OUTPUTS);
326
+ }
327
+ // Gestisci stati iniziali dei sensori
328
+ if (message.PAYLOAD_TYPE === 'STATUS_BUS_HA_SENSORS' && payload.STATUS_BUS_HA_SENSORS) {
329
+ this.log.info(`🌡️ Stati iniziali ${payload.STATUS_BUS_HA_SENSORS.length} sensori`);
330
+ this.updateSensorStatuses(payload.STATUS_BUS_HA_SENSORS);
331
+ }
332
+ // Gestisci stati iniziali del sistema
333
+ if (message.PAYLOAD_TYPE === 'STATUS_SYSTEM' && payload.STATUS_SYSTEM) {
334
+ this.log.info(`🌡️ Temperature sistema iniziali`);
335
+ this.updateSystemTemperatures(payload.STATUS_SYSTEM);
336
+ }
337
+ }
338
+ handleRealtimeResponse(message) {
339
+ this.log.info('🔄 Registrazione real-time completata');
340
+ // Processa gli stati iniziali se presenti
341
+ const payload = message.PAYLOAD;
342
+ if (payload.STATUS_OUTPUTS) {
343
+ this.log.info(`📊 Aggiornamento stati ${payload.STATUS_OUTPUTS.length} output`);
344
+ this.updateOutputStatuses(payload.STATUS_OUTPUTS);
345
+ }
346
+ if (payload.STATUS_BUS_HA_SENSORS) {
347
+ this.log.info(`🌡️ Aggiornamento stati ${payload.STATUS_BUS_HA_SENSORS.length} sensori`);
348
+ this.updateSensorStatuses(payload.STATUS_BUS_HA_SENSORS);
349
+ }
350
+ if (payload.STATUS_ZONES) {
351
+ this.log.info(`🚪 Aggiornamento stati ${payload.STATUS_ZONES.length} zone`);
352
+ this.updateZoneStatuses(payload.STATUS_ZONES);
353
+ }
354
+ if (payload.STATUS_SYSTEM) {
355
+ this.log.info(`🌡️ Aggiornamento temperature sistema iniziali`);
356
+ this.updateSystemTemperatures(payload.STATUS_SYSTEM);
357
+ }
358
+ }
359
+ handleStatusUpdate(message) {
360
+ // Gestisce gli aggiornamenti di stato in tempo reale
361
+ const payload = message.PAYLOAD;
362
+ this.log.debug(`🔄 handleStatusUpdate chiamato, payload keys: ${Object.keys(payload)}`);
363
+ // I messaggi real-time hanno un formato diverso
364
+ for (const [sender, data] of Object.entries(payload)) {
365
+ if (data && typeof data === 'object') {
366
+ const statusData = data;
367
+ if (statusData.STATUS_OUTPUTS) {
368
+ this.log.info(`📊 Aggiornamento real-time ${statusData.STATUS_OUTPUTS.length} output`);
369
+ this.updateOutputStatuses(statusData.STATUS_OUTPUTS);
370
+ }
371
+ if (statusData.STATUS_BUS_HA_SENSORS) {
372
+ this.log.info(`🌡️ Aggiornamento real-time ${statusData.STATUS_BUS_HA_SENSORS.length} sensori`);
373
+ this.updateSensorStatuses(statusData.STATUS_BUS_HA_SENSORS);
374
+ }
375
+ if (statusData.STATUS_ZONES) {
376
+ this.log.info(`🚪 Aggiornamento real-time ${statusData.STATUS_ZONES.length} zone`);
377
+ this.updateZoneStatuses(statusData.STATUS_ZONES);
378
+ }
379
+ if (statusData.STATUS_SYSTEM) {
380
+ this.log.info(`🌡️ Aggiornamento temperature sistema`);
381
+ this.updateSystemTemperatures(statusData.STATUS_SYSTEM);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ parseZoneData(zoneData) {
387
+ return {
388
+ id: `zone_${zoneData.ID}`,
389
+ type: 'zone',
390
+ name: zoneData.DES || `Zona ${zoneData.ID}`,
391
+ description: zoneData.DES || '',
392
+ status: {
393
+ armed: zoneData.STATUS === '1',
394
+ bypassed: false,
395
+ fault: false,
396
+ open: zoneData.STATUS === '2'
397
+ }
398
+ };
399
+ }
400
+ parseOutputData(outputData) {
401
+ // Usa CAT (categoria) dal payload reale - implementazione diretta senza libreria
402
+ const category = outputData.CAT || outputData.TYPE || '';
403
+ const categoryUpper = category.toUpperCase();
404
+ // Usa l'ID reale del sistema (non remappato)
405
+ const systemId = outputData.ID;
406
+ if (categoryUpper === 'LIGHT') {
407
+ return {
408
+ id: `light_${systemId}`,
409
+ type: 'light',
410
+ name: outputData.DES || `Luce ${systemId}`,
411
+ description: outputData.DES || '',
412
+ status: {
413
+ on: false, // Sarà aggiornato dai dati real-time
414
+ brightness: undefined,
415
+ dimmable: false
416
+ }
417
+ };
418
+ }
419
+ else if (categoryUpper === 'ROLL') {
420
+ return {
421
+ id: `cover_${systemId}`,
422
+ type: 'cover',
423
+ name: outputData.DES || `Tapparella ${systemId}`,
424
+ description: outputData.DES || '',
425
+ status: {
426
+ position: 0, // Sarà aggiornato dai dati real-time
427
+ state: 'stopped'
428
+ }
429
+ };
430
+ }
431
+ else if (categoryUpper === 'GATE') {
432
+ return {
433
+ id: `cover_${systemId}`, // I cancelli sono trattati come cover
434
+ type: 'cover',
435
+ name: outputData.DES || `Cancello ${systemId}`,
436
+ description: outputData.DES || '',
437
+ status: {
438
+ position: 0,
439
+ state: 'stopped'
440
+ }
441
+ };
442
+ }
443
+ // Ignora altri tipi di output per ora
444
+ this.log.debug(`📋 Output ignorato: ID ${systemId}, CAT: ${category}, DES: ${outputData.DES}`);
445
+ return null;
446
+ }
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
+ parseScenarioData(scenarioData) {
463
+ return {
464
+ id: `scenario_${scenarioData.ID}`,
465
+ type: 'scenario',
466
+ name: scenarioData.DES || `Scenario ${scenarioData.ID}`,
467
+ description: scenarioData.DES || '',
468
+ status: {
469
+ active: false // Gli scenari non hanno uno stato persistente
470
+ }
471
+ };
472
+ }
473
+ determineOutputType(category) {
474
+ const catUpper = category.toUpperCase();
475
+ // Log per debugging
476
+ this.log.debug(`🔍 Determinazione tipo per categoria: "${category}" (normalizzato: "${catUpper}")`);
477
+ // Usa la logica della libreria lares4-ts basata sul campo CAT
478
+ if (catUpper === 'ROLL') {
479
+ this.log.debug(`✅ Identificato come tapparella: ${category}`);
480
+ return 'cover';
481
+ }
482
+ if (catUpper === 'LIGHT') {
483
+ this.log.debug(`✅ Identificato come luce: ${category}`);
484
+ return 'light';
485
+ }
486
+ if (catUpper === 'GATE') {
487
+ this.log.debug(`✅ Identificato come cancello (trattato come copertura): ${category}`);
488
+ return 'cover';
489
+ }
490
+ // Termostati - controlla diverse possibili denominazioni per retrocompatibilità
491
+ if (catUpper.includes('THERM') ||
492
+ catUpper.includes('CLIMA') ||
493
+ catUpper.includes('TEMP') ||
494
+ catUpper.includes('RISCALD') ||
495
+ catUpper.includes('RAFFRES') ||
496
+ catUpper.includes('HVAC') ||
497
+ catUpper.includes('TERMOS')) {
498
+ this.log.debug(`✅ Identificato come termostato: ${category}`);
499
+ return 'thermostat';
500
+ }
501
+ // Default: luce (per compatibilità con sistemi più vecchi)
502
+ this.log.debug(`✅ Identificato come luce (default): ${category}`);
503
+ return 'light';
504
+ }
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
+ updateOutputStatuses(outputs) {
519
+ outputs.forEach(output => {
520
+ this.log.debug(`📊 Aggiornamento output ${output.ID}: STA=${output.STA}, POS=${output.POS}, TPOS=${output.TPOS}`);
521
+ // Usa gli ID reali del sistema (non remappati)
522
+ const lightDevice = this.devices.get(`light_${output.ID}`);
523
+ if (lightDevice) {
524
+ const wasOn = lightDevice.status.on;
525
+ lightDevice.status.on = output.STA === 'ON';
526
+ if (output.POS !== undefined) {
527
+ lightDevice.status.brightness = parseInt(output.POS);
528
+ lightDevice.status.dimmable = true;
529
+ }
530
+ if (wasOn !== (output.STA === 'ON')) {
531
+ this.log.info(`💡 Luce ${lightDevice.name} (Output ${output.ID}): ${output.STA === 'ON' ? 'ACCESA' : 'SPENTA'}`);
532
+ }
533
+ this.onDeviceStatusUpdate?.(lightDevice);
534
+ }
535
+ const coverDevice = this.devices.get(`cover_${output.ID}`);
536
+ if (coverDevice) {
537
+ const oldPos = coverDevice.status.position;
538
+ const newPosition = this.mapCoverPosition(output.STA, output.POS);
539
+ coverDevice.status.position = newPosition;
540
+ coverDevice.status.targetPosition = parseInt(output.TPOS || output.POS || '0');
541
+ coverDevice.status.state = this.mapCoverState(output.STA, output.POS, output.TPOS);
542
+ if (oldPos !== newPosition) {
543
+ this.log.info(`🪟 Cover ${coverDevice.name} (Output ${output.ID}): ${output.STA} posizione ${newPosition}%`);
544
+ }
545
+ this.onDeviceStatusUpdate?.(coverDevice);
546
+ }
547
+ const thermostatDevice = this.devices.get(`thermostat_${output.ID}`);
548
+ if (thermostatDevice) {
549
+ // IMPLEMENTAZIONE REALE PER TERMOSTATI
550
+ // Aggiorna i dati del termostato se disponibili
551
+ let updated = false;
552
+ // Se abbiamo dati di temperatura nel payload (dipende dal protocollo Lares4)
553
+ if (output.TEMP_CURRENT !== undefined) {
554
+ const oldCurrentTemp = thermostatDevice.status.currentTemperature;
555
+ const newCurrentTemp = parseFloat(output.TEMP_CURRENT);
556
+ thermostatDevice.status.currentTemperature = newCurrentTemp;
557
+ if (oldCurrentTemp !== newCurrentTemp) {
558
+ this.log.info(`🌡️ ${thermostatDevice.name}: Temperatura corrente ${newCurrentTemp}°C`);
559
+ updated = true;
560
+ }
561
+ }
562
+ if (output.TEMP_TARGET !== undefined) {
563
+ const oldTargetTemp = thermostatDevice.status.targetTemperature;
564
+ const newTargetTemp = parseFloat(output.TEMP_TARGET);
565
+ thermostatDevice.status.targetTemperature = newTargetTemp;
566
+ if (oldTargetTemp !== newTargetTemp) {
567
+ this.log.info(`🌡️ ${thermostatDevice.name}: Temperatura target ${newTargetTemp}°C`);
568
+ updated = true;
569
+ }
570
+ }
571
+ if (output.MODE !== undefined) {
572
+ const oldMode = thermostatDevice.status.mode;
573
+ const newMode = this.mapThermostatMode(output.MODE);
574
+ thermostatDevice.status.mode = newMode;
575
+ if (oldMode !== newMode) {
576
+ this.log.info(`🌡️ ${thermostatDevice.name}: Modalità ${newMode}`);
577
+ updated = true;
578
+ }
579
+ }
580
+ // Se abbiamo aggiornato qualcosa, notifica l'accessorio
581
+ if (updated) {
582
+ this.onDeviceStatusUpdate?.(thermostatDevice);
583
+ }
584
+ else {
585
+ // Log di debug per capire che dati stanno arrivando per i termostati
586
+ this.log.debug(`🌡️ Debug termostato ${output.ID}: ${JSON.stringify(output)}`);
587
+ }
588
+ }
589
+ });
590
+ }
591
+ updateSensorStatuses(sensors) {
592
+ sensors.forEach(sensor => {
593
+ if (sensor.DOMUS) {
594
+ this.log.debug(`🌡️ Aggiornamento sensore ${sensor.ID}: TEM=${sensor.DOMUS.TEM}°C, HUM=${sensor.DOMUS.HUM}%, LHT=${sensor.DOMUS.LHT}lux`);
595
+ const tempDevice = this.devices.get(`sensor_temp_${sensor.ID}`);
596
+ if (tempDevice) {
597
+ const oldTemp = tempDevice.status.value;
598
+ const newTemp = parseFloat(sensor.DOMUS.TEM || '0'); // Usa 0 invece di 20 come fallback
599
+ tempDevice.status.value = newTemp;
600
+ if (oldTemp !== newTemp && newTemp > 0) { // Log solo se abbiamo una temperatura valida
601
+ this.log.info(`🌡️ ${tempDevice.name}: ${newTemp}°C`);
602
+ }
603
+ this.onDeviceStatusUpdate?.(tempDevice);
604
+ }
605
+ const humDevice = this.devices.get(`sensor_hum_${sensor.ID}`);
606
+ if (humDevice) {
607
+ const oldHum = humDevice.status.value;
608
+ const newHum = parseInt(sensor.DOMUS.HUM || '50');
609
+ humDevice.status.value = newHum;
610
+ if (oldHum !== newHum) {
611
+ this.log.info(`💧 ${humDevice.name}: ${newHum}%`);
612
+ }
613
+ this.onDeviceStatusUpdate?.(humDevice);
614
+ }
615
+ const lightDevice = this.devices.get(`sensor_light_${sensor.ID}`);
616
+ if (lightDevice) {
617
+ const oldLight = lightDevice.status.value;
618
+ const newLight = parseInt(sensor.DOMUS.LHT || '100');
619
+ lightDevice.status.value = newLight;
620
+ if (oldLight !== newLight) {
621
+ this.log.info(`☀️ ${lightDevice.name}: ${newLight}lux`);
622
+ }
623
+ this.onDeviceStatusUpdate?.(lightDevice);
624
+ }
625
+ }
626
+ });
627
+ }
628
+ updateZoneStatuses(zones) {
629
+ zones.forEach(zone => {
630
+ this.log.debug(`🚪 Aggiornamento zona ${zone.ID}: STA=${zone.STA}, BYP=${zone.BYP}, A=${zone.A}`);
631
+ const zoneDevice = this.devices.get(`zone_${zone.ID}`);
632
+ if (zoneDevice) {
633
+ const oldOpen = zoneDevice.status.open;
634
+ const newOpen = zone.STA === 'A'; // A = Aperta/Allarme, R = Riposo
635
+ zoneDevice.status.open = newOpen;
636
+ zoneDevice.status.bypassed = zone.BYP === 'YES';
637
+ zoneDevice.status.armed = zone.A === 'Y';
638
+ zoneDevice.status.fault = zone.FM === 'T';
639
+ if (oldOpen !== newOpen) {
640
+ this.log.info(`🚪 ${zoneDevice.name}: ${newOpen ? 'APERTA/ALLARME' : 'RIPOSO'}`);
641
+ }
642
+ this.onDeviceStatusUpdate?.(zoneDevice);
643
+ }
644
+ });
645
+ }
646
+ mapCoverPosition(sta, pos) {
647
+ // Per cancelli e tapparelle, converti lo stato in posizione percentuale
648
+ if (pos !== undefined && pos !== '') {
649
+ return parseInt(pos);
650
+ }
651
+ // Fallback basato sullo stato
652
+ switch (sta?.toUpperCase()) {
653
+ case 'OPEN':
654
+ case 'UP':
655
+ return 100;
656
+ case 'CLOSE':
657
+ case 'DOWN':
658
+ return 0;
659
+ case 'STOP':
660
+ return 50; // Posizione intermedia se ferma
661
+ default:
662
+ return 0;
663
+ }
664
+ }
665
+ mapCoverState(sta, pos, tpos) {
666
+ // Se abbiamo posizione e target position, verifichiamo se è in movimento
667
+ if (pos !== undefined && tpos !== undefined) {
668
+ const currentPos = parseInt(pos);
669
+ const targetPos = parseInt(tpos);
670
+ if (currentPos === targetPos) {
671
+ return 'stopped'; // Ferma nella posizione target
672
+ }
673
+ else if (currentPos < targetPos) {
674
+ return 'opening'; // Si sta aprendo (verso 100)
675
+ }
676
+ else {
677
+ return 'closing'; // Si sta chiudendo (verso 0)
678
+ }
679
+ }
680
+ // Fallback alla logica precedente se non abbiamo posizioni
681
+ switch (sta?.toUpperCase()) {
682
+ case 'UP':
683
+ case 'OPEN': return 'opening';
684
+ case 'DOWN':
685
+ case 'CLOSE': return 'closing';
686
+ case 'STOP':
687
+ default: return 'stopped';
688
+ }
689
+ }
690
+ // Metodi per controllare i dispositivi
691
+ async switchLight(lightId, on) {
692
+ if (!this.idLogin)
693
+ throw new Error('Non connesso');
694
+ const systemOutputId = lightId.replace('light_', '');
695
+ // Usa formato corretto con sostituzione automatica di ID_LOGIN e PIN
696
+ await this.sendKseniaCommand('CMD_USR', 'CMD_SET_OUTPUT', {
697
+ ID_LOGIN: 'true', // Sarà sostituito con this.idLogin
698
+ PIN: 'true', // Sarà sostituito con this.pin
699
+ OUTPUT: {
700
+ ID: systemOutputId,
701
+ STA: on ? 'ON' : 'OFF'
702
+ }
703
+ });
704
+ this.log.info(`💡 Comando luce inviato: Output ${systemOutputId} -> ${on ? 'ON' : 'OFF'}`);
705
+ }
706
+ async dimLight(lightId, brightness) {
707
+ if (!this.idLogin)
708
+ throw new Error('Non connesso');
709
+ const systemOutputId = lightId.replace('light_', '');
710
+ await this.sendKseniaCommand('CMD_USR', 'CMD_SET_OUTPUT', {
711
+ ID_LOGIN: 'true',
712
+ PIN: 'true',
713
+ OUTPUT: {
714
+ ID: systemOutputId,
715
+ STA: brightness.toString()
716
+ }
717
+ });
718
+ this.log.info(`💡 Comando dimmer inviato: Output ${systemOutputId} -> ${brightness}%`);
719
+ }
720
+ async moveCover(coverId, position) {
721
+ if (!this.idLogin)
722
+ throw new Error('Non connesso');
723
+ const systemOutputId = coverId.replace('cover_', '');
724
+ // Determina il comando basato sulla posizione
725
+ let command;
726
+ if (position === 0) {
727
+ command = 'DOWN'; // Chiudi
728
+ }
729
+ else if (position === 100) {
730
+ command = 'UP'; // Apri
731
+ }
732
+ else {
733
+ command = position.toString(); // Posizione specifica per tapparelle
734
+ }
735
+ await this.sendKseniaCommand('CMD_USR', 'CMD_SET_OUTPUT', {
736
+ ID_LOGIN: 'true',
737
+ PIN: 'true',
738
+ OUTPUT: {
739
+ ID: systemOutputId,
740
+ STA: command
741
+ }
742
+ });
743
+ this.log.info(`🪟 Comando cover inviato: Output ${systemOutputId} -> ${command}`);
744
+ }
745
+ async setThermostatMode(thermostatId, mode) {
746
+ if (!this.idLogin)
747
+ throw new Error('Non connesso');
748
+ let modeValue;
749
+ switch (mode) {
750
+ case 'heat':
751
+ modeValue = '1';
752
+ break;
753
+ case 'cool':
754
+ modeValue = '2';
755
+ break;
756
+ case 'auto':
757
+ modeValue = '3';
758
+ break;
759
+ default:
760
+ modeValue = '0';
761
+ break; // off
762
+ }
763
+ await this.sendKseniaCommand('WRITE', 'THERMOSTAT', {
764
+ ID_LOGIN: this.idLogin,
765
+ ID_THERMOSTAT: thermostatId.replace('thermostat_', ''),
766
+ MODE: modeValue
767
+ });
768
+ }
769
+ async setThermostatTemperature(thermostatId, temperature) {
770
+ if (!this.idLogin)
771
+ throw new Error('Non connesso');
772
+ await this.sendKseniaCommand('WRITE', 'THERMOSTAT', {
773
+ ID_LOGIN: this.idLogin,
774
+ ID_THERMOSTAT: thermostatId.replace('thermostat_', ''),
775
+ TARGET_TEMP: temperature.toString()
776
+ });
777
+ }
778
+ async triggerScenario(scenarioId) {
779
+ if (!this.idLogin)
780
+ throw new Error('Non connesso');
781
+ const systemScenarioId = scenarioId.replace('scenario_', '');
782
+ await this.sendKseniaCommand('CMD_USR', 'CMD_EXE_SCENARIO', {
783
+ ID_LOGIN: 'true',
784
+ PIN: 'true',
785
+ SCENARIO: {
786
+ ID: systemScenarioId
787
+ }
788
+ });
789
+ this.log.info(`🎬 Scenario ${systemScenarioId} eseguito`);
790
+ }
791
+ async sendKseniaCommand(cmd, payloadType, payload) {
792
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
793
+ throw new Error('WebSocket non connesso');
794
+ }
795
+ // Implementa la sostituzione automatica di ID_LOGIN e PIN come fa lares4-ts
796
+ const processedPayload = this.buildPayload(payload);
797
+ const id = Math.floor(Math.random() * 100000).toString();
798
+ const timestamp = Math.floor(Date.now() / 1000).toString();
799
+ const message = {
800
+ SENDER: this.sender,
801
+ RECEIVER: '',
802
+ CMD: cmd,
803
+ ID: id,
804
+ PAYLOAD_TYPE: payloadType,
805
+ PAYLOAD: processedPayload,
806
+ TIMESTAMP: timestamp,
807
+ CRC_16: '0x0000'
808
+ };
809
+ // Calcola il CRC del messaggio prima di inviarlo
810
+ message.CRC_16 = this.calculateCRC16(JSON.stringify(message));
811
+ const jsonMessage = JSON.stringify(message);
812
+ // Filtra i log di invio per PING/HEARTBEAT
813
+ const isPing = cmd === 'PING' || payloadType === 'HEARTBEAT';
814
+ if (this.options.debug || !isPing) {
815
+ this.log.info(`📤 Invio: ${jsonMessage}`);
816
+ }
817
+ else {
818
+ this.log.debug(`📤 Debug: ${jsonMessage}`);
819
+ }
820
+ // Log esteso per debugging comandi critici
821
+ if (cmd === 'CMD_USR') {
822
+ this.log.info(`🔧 DEBUG - Comando ${payloadType}: ${JSON.stringify(payload, null, 2)}`);
823
+ }
824
+ this.ws.send(jsonMessage);
825
+ }
826
+ buildPayload(payload) {
827
+ // Implementa la logica di sostituzione della libreria lares4-ts
828
+ return {
829
+ ...payload,
830
+ ...(payload?.ID_LOGIN === 'true' && { ID_LOGIN: this.idLogin }),
831
+ ...(payload?.PIN === 'true' && { PIN: this.pin }),
832
+ };
833
+ }
834
+ async sendMessage(message) {
835
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
836
+ throw new Error('WebSocket non connesso');
837
+ }
838
+ const messageStr = JSON.stringify(message);
839
+ // Mostra sempre i messaggi inviati per debug
840
+ this.log.info(`📤 Invio: ${messageStr}`);
841
+ this.ws.send(messageStr);
842
+ }
843
+ calculateCRC16(jsonString) {
844
+ const utf8 = [];
845
+ for (let i = 0; i < jsonString.length; i++) {
846
+ const charcode = jsonString.charCodeAt(i);
847
+ if (charcode < 0x80)
848
+ utf8.push(charcode);
849
+ else if (charcode < 0x800) {
850
+ utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
851
+ }
852
+ else if (charcode < 0xd800 || charcode >= 0xe000) {
853
+ utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f));
854
+ }
855
+ else {
856
+ i++;
857
+ const surrogate = 0x10000 + (((charcode & 0x3ff) << 10) | (jsonString.charCodeAt(i) & 0x3f));
858
+ utf8.push(0xf0 | (surrogate >> 18), 0x80 | ((surrogate >> 12) & 0x3f), 0x80 | ((surrogate >> 6) & 0x3f), 0x80 | (surrogate & 0x3f));
859
+ }
860
+ }
861
+ const SEME_CRC_16_JSON = 0xFFFF;
862
+ const GEN_POLY_JSON = 0x1021;
863
+ const CRC_16 = '"CRC_16"';
864
+ const dataLen = jsonString.lastIndexOf(CRC_16) + CRC_16.length + (utf8.length - jsonString.length);
865
+ let crc = SEME_CRC_16_JSON;
866
+ for (let i = 0; i < dataLen; i++) {
867
+ const charCode = utf8[i];
868
+ for (let i_CRC = 0x80; i_CRC; i_CRC >>= 1) {
869
+ const flag_CRC = (crc & 0x8000) ? 1 : 0;
870
+ crc <<= 1;
871
+ crc = (crc & 0xFFFF);
872
+ if (charCode & i_CRC) {
873
+ crc++;
874
+ }
875
+ if (flag_CRC) {
876
+ crc ^= GEN_POLY_JSON;
877
+ }
878
+ }
879
+ }
880
+ return '0x' + crc.toString(16).padStart(4, '0');
881
+ }
882
+ startHeartbeat() {
883
+ if (this.heartbeatTimer) {
884
+ clearInterval(this.heartbeatTimer);
885
+ }
886
+ this.heartbeatTimer = setInterval(() => {
887
+ if (this.isConnected && this.idLogin) {
888
+ this.sendKseniaCommand('PING', 'HEARTBEAT', {
889
+ ID_LOGIN: this.idLogin
890
+ }).catch(err => {
891
+ this.log.error('❌ Errore heartbeat:', err);
892
+ });
893
+ }
894
+ }, this.options.heartbeatInterval);
895
+ }
896
+ scheduleReconnect() {
897
+ if (this.reconnectTimer) {
898
+ clearTimeout(this.reconnectTimer);
899
+ }
900
+ this.reconnectTimer = setTimeout(() => {
901
+ this.log.info('🔄 Tentativo riconnessione...');
902
+ this.connect().catch(err => {
903
+ this.log.error('❌ Riconnessione fallita:', err);
904
+ this.scheduleReconnect();
905
+ });
906
+ }, this.options.reconnectInterval);
907
+ }
908
+ disconnect() {
909
+ if (this.heartbeatTimer) {
910
+ clearInterval(this.heartbeatTimer);
911
+ }
912
+ if (this.reconnectTimer) {
913
+ clearTimeout(this.reconnectTimer);
914
+ }
915
+ if (this.ws) {
916
+ this.ws.close();
917
+ }
918
+ this.isConnected = false;
919
+ }
920
+ // Metodo helper per mappare le modalità del termostato
921
+ mapThermostatMode(mode) {
922
+ switch (mode?.toLowerCase()) {
923
+ case 'heat':
924
+ case 'heating':
925
+ case 'riscaldamento':
926
+ return 'heat';
927
+ case 'cool':
928
+ case 'cooling':
929
+ case 'raffreddamento':
930
+ return 'cool';
931
+ case 'auto':
932
+ case 'automatic':
933
+ case 'automatico':
934
+ return 'auto';
935
+ case 'off':
936
+ case 'spento':
937
+ default:
938
+ return 'off';
939
+ }
940
+ }
941
+ // Nuovo metodo per gestire le temperature del sistema
942
+ updateSystemTemperatures(systemData) {
943
+ systemData.forEach(system => {
944
+ if (this.options.debug) {
945
+ this.log.debug(`🌡️ Dati sistema ${system.ID}: ${JSON.stringify(system)}`);
946
+ }
947
+ if (system.TEMP) {
948
+ const internalTemp = system.TEMP.IN ? parseFloat(system.TEMP.IN.replace('+', '')) : undefined;
949
+ const externalTemp = system.TEMP.OUT ? parseFloat(system.TEMP.OUT.replace('+', '')) : undefined;
950
+ // Log delle temperature solo se sono cambiate significativamente o in debug
951
+ let logTemperatures = this.options.debug;
952
+ // Controlla se ci sono cambiamenti significativi (>= 0.5°C) nei termostati
953
+ if (internalTemp !== undefined) {
954
+ this.devices.forEach((device, deviceId) => {
955
+ if (device.type === 'thermostat') {
956
+ const oldCurrentTemp = device.status.currentTemperature;
957
+ if (oldCurrentTemp === undefined || Math.abs(oldCurrentTemp - internalTemp) >= 0.5) {
958
+ logTemperatures = true;
959
+ }
960
+ }
961
+ });
962
+ }
963
+ if (logTemperatures) {
964
+ this.log.info(`🌡️ Temperature sistema: Interna=${internalTemp}°C, Esterna=${externalTemp}°C`);
965
+ }
966
+ // Aggiorna tutti i termostati con la temperatura interna del sistema
967
+ // (assumendo che i termostati utilizzino la temperatura interna come riferimento)
968
+ if (internalTemp !== undefined) {
969
+ this.devices.forEach((device, deviceId) => {
970
+ if (device.type === 'thermostat') {
971
+ const oldCurrentTemp = device.status.currentTemperature;
972
+ device.status.currentTemperature = internalTemp;
973
+ // Se non abbiamo ancora una temperatura target, usiamo quella corrente + 1°C come ragionevole default
974
+ if (device.status.targetTemperature === undefined || device.status.targetTemperature === null) {
975
+ device.status.targetTemperature = Math.round(internalTemp + 1);
976
+ this.log.info(`🌡️ ${device.name}: Impostata temperatura target iniziale a ${device.status.targetTemperature}°C`);
977
+ }
978
+ // Log solo per cambiamenti significativi
979
+ if (oldCurrentTemp === undefined || Math.abs(oldCurrentTemp - internalTemp) >= 0.5) {
980
+ this.log.info(`🌡️ ${device.name}: Temperatura corrente aggiornata a ${internalTemp}°C`);
981
+ }
982
+ this.onDeviceStatusUpdate?.(device);
983
+ }
984
+ });
985
+ }
986
+ }
987
+ });
988
+ }
989
+ }
990
+ exports.KseniaWebSocketClient = KseniaWebSocketClient;
991
+ //# sourceMappingURL=websocket-client.js.map