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.
Files changed (43) hide show
  1. package/CHANGELOG.md +30 -6
  2. package/README.md +34 -34
  3. package/dist/accessories/cover-accessory.d.ts +8 -4
  4. package/dist/accessories/cover-accessory.d.ts.map +1 -1
  5. package/dist/accessories/cover-accessory.js +36 -37
  6. package/dist/accessories/cover-accessory.js.map +1 -1
  7. package/dist/accessories/light-accessory.d.ts +7 -3
  8. package/dist/accessories/light-accessory.d.ts.map +1 -1
  9. package/dist/accessories/light-accessory.js +27 -24
  10. package/dist/accessories/light-accessory.js.map +1 -1
  11. package/dist/accessories/scenario-accessory.d.ts +8 -4
  12. package/dist/accessories/scenario-accessory.d.ts.map +1 -1
  13. package/dist/accessories/scenario-accessory.js +23 -19
  14. package/dist/accessories/scenario-accessory.js.map +1 -1
  15. package/dist/accessories/sensor-accessory.d.ts +5 -1
  16. package/dist/accessories/sensor-accessory.d.ts.map +1 -1
  17. package/dist/accessories/sensor-accessory.js +49 -36
  18. package/dist/accessories/sensor-accessory.js.map +1 -1
  19. package/dist/accessories/thermostat-accessory.d.ts +5 -1
  20. package/dist/accessories/thermostat-accessory.d.ts.map +1 -1
  21. package/dist/accessories/thermostat-accessory.js +68 -55
  22. package/dist/accessories/thermostat-accessory.js.map +1 -1
  23. package/dist/accessories/zone-accessory.d.ts +5 -1
  24. package/dist/accessories/zone-accessory.d.ts.map +1 -1
  25. package/dist/accessories/zone-accessory.js +32 -32
  26. package/dist/accessories/zone-accessory.js.map +1 -1
  27. package/dist/mqtt-bridge.d.ts +6 -12
  28. package/dist/mqtt-bridge.d.ts.map +1 -1
  29. package/dist/mqtt-bridge.js +148 -104
  30. package/dist/mqtt-bridge.js.map +1 -1
  31. package/dist/platform.d.ts +26 -37
  32. package/dist/platform.d.ts.map +1 -1
  33. package/dist/platform.js +177 -132
  34. package/dist/platform.js.map +1 -1
  35. package/dist/types.d.ts +203 -21
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +78 -0
  38. package/dist/types.js.map +1 -1
  39. package/dist/websocket-client.d.ts +6 -5
  40. package/dist/websocket-client.d.ts.map +1 -1
  41. package/dist/websocket-client.js +257 -313
  42. package/dist/websocket-client.js.map +1 -1
  43. package/package.json +4 -4
@@ -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(`🔗 Connessione a ${wsUrl}...`);
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('WebSocket connesso');
83
+ this.log.info('WebSocket connected');
81
84
  this.isConnected = true;
82
- this.login().then(() => {
85
+ this.login()
86
+ .then(() => {
83
87
  this.onConnected?.();
84
88
  resolve();
85
- }).catch(reject);
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(`🔌 WebSocket chiuso: ${code} - ${reason.toString()}`);
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(' Errore WebSocket:', error.message);
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('🔐 Esecuzione login...');
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(`📨 Ricevuto: ${data}`);
134
+ this.log.info(`Received: ${data}`);
132
135
  }
133
136
  else if (isHeartbeat || isGenericRealtime) {
134
- this.log.debug(`📨 Debug: ${data}`);
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(`🏓 PING ricevuto dal sistema`);
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(`📋 Messaggio non gestito: ${message.CMD}`);
164
+ this.log.debug(`Unhandled message: ${message.CMD}`);
164
165
  }
165
166
  }
166
167
  }
167
168
  catch (error) {
168
- this.log.error(' Errore parsing messaggio:', 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 || '1';
174
- this.log.info(`✅ Login completato, ID_LOGIN: ${this.idLogin}`);
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('Login fallito:', message.PAYLOAD?.RESULT_DETAIL);
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('ID_LOGIN non disponibile');
187
+ this.log.error('ID_LOGIN not available');
185
188
  return;
186
189
  }
187
- this.log.info('📥 Richiesta dati sistema...');
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(`📥 Risposta ricevuta: ${message.PAYLOAD_TYPE}`);
221
+ this.log.info(`Response received: ${message.PAYLOAD_TYPE}`);
225
222
  if (message.PAYLOAD_TYPE === 'ZONES' && payload.ZONES) {
226
- this.log.info(`🏠 Trovate ${payload.ZONES.length} zone`);
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(`💡 Trovati ${payload.OUTPUTS.length} output`);
232
+ this.log.info(`Found ${payload.OUTPUTS.length} outputs`);
236
233
  payload.OUTPUTS.forEach((output) => {
237
- // Log dettagliato per debugging termostati
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.info(`🌡️ DEBUG Termostato trovato - ID: ${output.ID}, DES: ${output.DES}, TYPE: ${output.TYPE}, CAT: ${output.CAT}, RAW: ${JSON.stringify(output)}`);
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(`🎬 Trovati ${payload.SCENARIOS.length} scenari`);
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(`⏭️ Scenario ${scenario.DES} ignorato (categoria ${scenario.CAT})`);
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(`🌡️ Trovati ${payload.BUS_HAS.length} sensori`);
261
+ this.log.info(`Found ${payload.BUS_HAS.length} sensors`);
267
262
  payload.BUS_HAS.forEach((sensor) => {
268
- // Crea sensori multipli per ogni DOMUS
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: undefined, // Sarà aggiornato dai dati real-time
279
- unit: '°C'
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} - Umidità`,
289
- description: `${baseName} - Umidità`,
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} - Luminosità`,
303
- description: `${baseName} - Luminosità`,
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(`📊 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
- });
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(`🌡️ Stati iniziali ${payload.STATUS_BUS_HA_SENSORS.length} sensori`);
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(`🌡️ Temperature sistema iniziali`);
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('🔄 Registrazione real-time completata');
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(`📊 Aggiornamento stati ${payload.STATUS_OUTPUTS.length} output`);
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(`🌡️ Aggiornamento stati ${payload.STATUS_BUS_HA_SENSORS.length} sensori`);
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(`🚪 Aggiornamento stati ${payload.STATUS_ZONES.length} zone`);
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(`🌡️ Aggiornamento temperature sistema iniziali`);
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(`🔄 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)) {
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(`📊 Aggiornamento real-time ${statusData.STATUS_OUTPUTS.length} output`);
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(`🌡️ Aggiornamento real-time ${statusData.STATUS_BUS_HA_SENSORS.length} sensori`);
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(`🚪 Aggiornamento real-time ${statusData.STATUS_ZONES.length} zone`);
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(`🌡️ Aggiornamento temperature sistema`);
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 || `Zona ${zoneData.ID}`,
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
- // Usa CAT (categoria) dal payload reale - implementazione diretta senza libreria
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 || `Luce ${systemId}`,
394
+ name: outputData.DES || `Light ${systemId}`,
411
395
  description: outputData.DES || '',
412
396
  status: {
413
- on: false, // Sarà aggiornato dai dati real-time
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 || `Tapparella ${systemId}`,
407
+ name: outputData.DES || `Cover ${systemId}`,
424
408
  description: outputData.DES || '',
425
409
  status: {
426
- position: 0, // Sarà aggiornato dai dati real-time
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}`, // I cancelli sono trattati come cover
417
+ id: `cover_${systemId}`,
434
418
  type: 'cover',
435
- name: outputData.DES || `Cancello ${systemId}`,
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
- // Ignora altri tipi di output per ora
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 // Gli scenari non hanno uno stato persistente
470
- }
437
+ active: false,
438
+ },
471
439
  };
472
440
  }
473
441
  determineOutputType(category) {
474
442
  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
443
+ this.log.debug(`Determining type for category: "${category}" (normalized: "${catUpper}")`);
478
444
  if (catUpper === 'ROLL') {
479
- this.log.debug(`✅ Identificato come tapparella: ${category}`);
445
+ this.log.debug(`Identified as cover: ${category}`);
480
446
  return 'cover';
481
447
  }
482
448
  if (catUpper === 'LIGHT') {
483
- this.log.debug(`✅ Identificato come luce: ${category}`);
449
+ this.log.debug(`Identified as light: ${category}`);
484
450
  return 'light';
485
451
  }
486
452
  if (catUpper === 'GATE') {
487
- this.log.debug(`✅ Identificato come cancello (trattato come copertura): ${category}`);
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(`✅ Identificato come termostato: ${category}`);
463
+ this.log.debug(`Identified as thermostat: ${category}`);
499
464
  return 'thermostat';
500
465
  }
501
- // Default: luce (per compatibilità con sistemi più vecchi)
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(`📊 Aggiornamento output ${output.ID}: STA=${output.STA}, POS=${output.POS}, TPOS=${output.TPOS}`);
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 wasOn = lightDevice.status.on;
525
- lightDevice.status.on = output.STA === 'ON';
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
- lightDevice.status.brightness = parseInt(output.POS);
528
- lightDevice.status.dimmable = true;
478
+ lightStatus.brightness = parseInt(output.POS, 10);
479
+ lightStatus.dimmable = true;
529
480
  }
530
481
  if (wasOn !== (output.STA === 'ON')) {
531
- this.log.info(`💡 Luce ${lightDevice.name} (Output ${output.ID}): ${output.STA === 'ON' ? 'ACCESA' : 'SPENTA'}`);
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 oldPos = coverDevice.status.position;
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
- 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);
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(`🪟 Cover ${coverDevice.name} (Output ${output.ID}): ${output.STA} posizione ${newPosition}%`);
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
- // IMPLEMENTAZIONE REALE PER TERMOSTATI
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 = thermostatDevice.status.currentTemperature;
504
+ const oldCurrentTemp = thermostatStatus.currentTemperature;
555
505
  const newCurrentTemp = parseFloat(output.TEMP_CURRENT);
556
- thermostatDevice.status.currentTemperature = newCurrentTemp;
506
+ thermostatStatus.currentTemperature = newCurrentTemp;
557
507
  if (oldCurrentTemp !== newCurrentTemp) {
558
- this.log.info(`🌡️ ${thermostatDevice.name}: Temperatura corrente ${newCurrentTemp}°C`);
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 = thermostatDevice.status.targetTemperature;
513
+ const oldTargetTemp = thermostatStatus.targetTemperature;
564
514
  const newTargetTemp = parseFloat(output.TEMP_TARGET);
565
- thermostatDevice.status.targetTemperature = newTargetTemp;
515
+ thermostatStatus.targetTemperature = newTargetTemp;
566
516
  if (oldTargetTemp !== newTargetTemp) {
567
- this.log.info(`🌡️ ${thermostatDevice.name}: Temperatura target ${newTargetTemp}°C`);
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 = thermostatDevice.status.mode;
522
+ const oldMode = thermostatStatus.mode;
573
523
  const newMode = this.mapThermostatMode(output.MODE);
574
- thermostatDevice.status.mode = newMode;
524
+ thermostatStatus.mode = newMode;
575
525
  if (oldMode !== newMode) {
576
- this.log.info(`🌡️ ${thermostatDevice.name}: Modalità ${newMode}`);
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
- // Log di debug per capire che dati stanno arrivando per i termostati
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(`🌡️ Aggiornamento sensore ${sensor.ID}: TEM=${sensor.DOMUS.TEM}°C, HUM=${sensor.DOMUS.HUM}%, LHT=${sensor.DOMUS.LHT}lux`);
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 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`);
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 oldHum = humDevice.status.value;
608
- const newHum = parseInt(sensor.DOMUS.HUM || '50');
609
- humDevice.status.value = newHum;
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(`💧 ${humDevice.name}: ${newHum}%`);
561
+ this.log.info(`${humDevice.name}: ${newHum}%`);
612
562
  }
613
563
  this.onDeviceStatusUpdate?.(humDevice);
614
564
  }
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;
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(`☀️ ${lightDevice.name}: ${newLight}lux`);
572
+ this.log.info(`${lightSensorDevice.name}: ${newLight}lux`);
622
573
  }
623
- this.onDeviceStatusUpdate?.(lightDevice);
574
+ this.onDeviceStatusUpdate?.(lightSensorDevice);
624
575
  }
625
576
  }
626
577
  });
627
578
  }
628
579
  updateZoneStatuses(zones) {
629
- zones.forEach(zone => {
630
- this.log.debug(`🚪 Aggiornamento zona ${zone.ID}: STA=${zone.STA}, BYP=${zone.BYP}, A=${zone.A}`);
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 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';
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(`🚪 ${zoneDevice.name}: ${newOpen ? 'APERTA/ALLARME' : 'RIPOSO'}`);
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; // Posizione intermedia se ferma
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'; // Ferma nella posizione target
620
+ return 'stopped';
672
621
  }
673
622
  else if (currentPos < targetPos) {
674
- return 'opening'; // Si sta aprendo (verso 100)
623
+ return 'opening';
675
624
  }
676
625
  else {
677
- return 'closing'; // Si sta chiudendo (verso 0)
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': return 'opening';
631
+ case 'OPEN':
632
+ return 'opening';
684
633
  case 'DOWN':
685
- case 'CLOSE': return 'closing';
634
+ case 'CLOSE':
635
+ return 'closing';
686
636
  case 'STOP':
687
- default: return 'stopped';
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('Non connesso');
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', // Sarà sostituito con this.idLogin
698
- PIN: 'true', // Sarà sostituito con this.pin
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(`💡 Comando luce inviato: Output ${systemOutputId} -> ${on ? 'ON' : 'OFF'}`);
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('Non connesso');
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(`💡 Comando dimmer inviato: Output ${systemOutputId} -> ${brightness}%`);
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('Non connesso');
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'; // Chiudi
675
+ command = 'DOWN';
728
676
  }
729
677
  else if (position === 100) {
730
- command = 'UP'; // Apri
678
+ command = 'UP';
731
679
  }
732
680
  else {
733
- command = position.toString(); // Posizione specifica per tapparelle
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(`🪟 Comando cover inviato: Output ${systemOutputId} -> ${command}`);
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('Non connesso');
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; // off
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('Non connesso');
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('Non connesso');
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(`🎬 Scenario ${systemScenarioId} eseguito`);
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 non connesso');
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(`📤 Invio: ${jsonMessage}`);
760
+ this.log.info(`Sending: ${jsonMessage}`);
816
761
  }
817
762
  else {
818
- this.log.debug(`📤 Debug: ${jsonMessage}`);
763
+ this.log.debug(`Debug: ${jsonMessage}`);
819
764
  }
820
- // Log esteso per debugging comandi critici
821
- if (cmd === 'CMD_USR') {
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 non connesso');
779
+ throw new Error('WebSocket not connected');
837
780
  }
838
781
  const messageStr = JSON.stringify(message);
839
- // Mostra sempre i messaggi inviati per debug
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 = 0xFFFF;
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 i_CRC = 0x80; i_CRC; i_CRC >>= 1) {
869
- const flag_CRC = (crc & 0x8000) ? 1 : 0;
811
+ for (let iCrc = 0x80; iCrc; iCrc >>= 1) {
812
+ const flagCrc = crc & 0x8000 ? 1 : 0;
870
813
  crc <<= 1;
871
- crc = (crc & 0xFFFF);
872
- if (charCode & i_CRC) {
814
+ crc = crc & 0xffff;
815
+ if (charCode & iCrc) {
873
816
  crc++;
874
817
  }
875
- if (flag_CRC) {
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(' Errore heartbeat:', err);
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('🔄 Tentativo riconnessione...');
902
- this.connect().catch(err => {
903
- this.log.error(' Riconnessione fallita:', err);
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(`🌡️ Dati sistema ${system.ID}: ${JSON.stringify(system)}`);
886
+ this.log.debug(`System data ${system.ID}: ${JSON.stringify(system)}`);
946
887
  }
947
888
  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
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, deviceId) => {
897
+ this.devices.forEach((device) => {
955
898
  if (device.type === 'thermostat') {
956
- const oldCurrentTemp = device.status.currentTemperature;
957
- if (oldCurrentTemp === undefined || Math.abs(oldCurrentTemp - internalTemp) >= 0.5) {
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(`🌡️ Temperature sistema: Interna=${internalTemp}°C, Esterna=${externalTemp}°C`);
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, deviceId) => {
912
+ this.devices.forEach((device) => {
970
913
  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`);
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
- // 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`);
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
  }