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.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/config.schema.json +206 -0
- package/dist/accessories/cover-accessory.d.ts +21 -0
- package/dist/accessories/cover-accessory.d.ts.map +1 -0
- package/dist/accessories/cover-accessory.js +128 -0
- package/dist/accessories/cover-accessory.js.map +1 -0
- package/dist/accessories/light-accessory.d.ts +16 -0
- package/dist/accessories/light-accessory.d.ts.map +1 -0
- package/dist/accessories/light-accessory.js +78 -0
- package/dist/accessories/light-accessory.js.map +1 -0
- package/dist/accessories/scenario-accessory.d.ts +13 -0
- package/dist/accessories/scenario-accessory.d.ts.map +1 -0
- package/dist/accessories/scenario-accessory.js +55 -0
- package/dist/accessories/scenario-accessory.js.map +1 -0
- package/dist/accessories/sensor-accessory.d.ts +24 -0
- package/dist/accessories/sensor-accessory.d.ts.map +1 -0
- package/dist/accessories/sensor-accessory.js +128 -0
- package/dist/accessories/sensor-accessory.js.map +1 -0
- package/dist/accessories/thermostat-accessory.d.ts +21 -0
- package/dist/accessories/thermostat-accessory.d.ts.map +1 -0
- package/dist/accessories/thermostat-accessory.js +215 -0
- package/dist/accessories/thermostat-accessory.js.map +1 -0
- package/dist/accessories/zone-accessory.d.ts +18 -0
- package/dist/accessories/zone-accessory.d.ts.map +1 -0
- package/dist/accessories/zone-accessory.js +99 -0
- package/dist/accessories/zone-accessory.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +69 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +345 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +6 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket-client.d.ts +57 -0
- package/dist/websocket-client.d.ts.map +1 -0
- package/dist/websocket-client.js +991 -0
- package/dist/websocket-client.js.map +1 -0
- 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
|