UNPKG

homebridge-elmo

Version:

Homebridge plugin per integrare i sistemi di antifurto Elmo con HomeKit

1,106 lines (945 loc) 39.8 kB
'use strict'; const { PythonShell } = require('python-shell'); const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); let Service, Characteristic; /** * Platform "ElmoSecuritySystem" */ class ElmoPlatform { constructor(log, config, api) { this.log = log; this.config = config; this.api = api; // Percorso della venv this.venvPath = path.join(__dirname, '.venv'); this.pythonPath = process.platform === 'win32' ? path.join(this.venvPath, 'Scripts', 'python.exe') : path.join(this.venvPath, 'bin', 'python3'); // Crea la venv se necessario this.ensureVenv(); // Installa le dipendenze Python se necessario this.ensurePythonDependencies(); // Verifica che la configurazione sia valida if (!this.config) { this.log.error('Configurazione non valida!'); return; } // Verifica che i parametri obbligatori siano presenti if (!this.config.username || !this.config.password || !this.config.code) { this.log.error('Configurazione incompleta! Sono richiesti username, password e codice.'); return; } this.name = this.config.name || 'Elmo Security System'; this.username = this.config.username; this.password = this.config.password; this.code = this.config.code; this.system = this.config.system || 'e-connect'; this.domain = this.config.domain || 'default'; this.pollInterval = this.config.pollInterval || 30; this.devicePollInterval = this.config.devicePollInterval || 10; // Polling più veloce per dispositivi this.enableDevices = this.config.enableDevices !== false; // Abilita dispositivi per default this.debug = this.config.debug || false; // Configurazione settori this.homeSectors = this.parseSectors(this.config.homeSectors); this.awaySectors = this.parseSectors(this.config.awaySectors); this.nightSectors = this.parseSectors(this.config.nightSectors); this.log.debug('Configurazione settori:'); this.log.debug('- Casa:', this.homeSectors); this.log.debug('- Via:', this.awaySectors); this.log.debug('- Notte:', this.nightSectors); // Stato del sistema this.currentState = 3; // Disarmato this.targetState = 3; // Disarmato this.devices = new Map(); // Mappa dei dispositivi // Accessori - inizializza come array vuoto this.cachedAccessories = []; // Registrazione per il completamento dell'avvio di Homebridge this.api.on('didFinishLaunching', () => { this.log.debug('didFinishLaunching'); this.createAccessories(); this.setupPolling(); if (this.enableDevices) { this.discoverDevices().then(() => { this.setupDevicePolling(); }); } }); } /** * Metodo chiamato da Homebridge per configurare accessori dalla cache * @param {object} accessory - Accessorio da configurare */ configureAccessory(accessory) { this.log.info('Caricamento accessorio dalla cache:', accessory.displayName); // Aggiungi l'accessorio alla lista cache this.cachedAccessories.push(accessory); // Configura l'accessorio in base al tipo if (accessory.context.deviceInfo) { // È un dispositivo individuale this.configureDeviceAccessory(accessory, accessory.context.deviceInfo); // Aggiungi il dispositivo alla mappa this.devices.set(accessory.context.deviceInfo.id, accessory.context.deviceInfo); } else { // È il sistema di sicurezza principale this.configureSecuritySystemAccessory(accessory); } } /** * Configura il polling per aggiornare lo stato del sistema */ setupPolling() { this.log.debug('Configurazione del polling'); // Avvia il polling this.pollElmoStatus(); // Imposta il polling periodico setInterval(() => { this.pollElmoStatus(); }, this.pollInterval * 1000); } /** * Esegue il polling dello stato del sistema Elmo */ pollElmoStatus() { this.log.debug('Polling dello stato del sistema Elmo'); const pythonScript = path.join(__dirname, 'scripts', 'elmo_client.py'); const options = { mode: 'json', pythonPath: this.pythonPath, // Usa il python del venv args: [ 'status', this.username, this.password, this.system, this.domain ], timeout: 15000 // Timeout più breve per il polling }; if (this.debug) { options.args.push('--debug'); } PythonShell.run(pythonScript, options, (err, results) => { if (err) { // Log diverso per errori di polling vs errori critici if (err.message && err.message.includes('timeout')) { this.log.debug('Timeout durante il polling dello stato - probabile conflitto di lock'); } else { this.log.warn('Errore durante il polling dello stato:', err.message); } return; } if (!results || results.length === 0) { this.log.debug('Nessun risultato dal polling dello stato'); return; } const result = results[0]; if (!result.success) { // Log specifico per errori di lock if (result.message && result.message.includes('occupato')) { this.log.debug('Sistema occupato durante il polling - saltato questo ciclo'); } else { this.log.warn('Errore nel polling dello stato:', result.message); } return; } this.log.debug('Stato del sistema ottenuto con successo'); // Aggiorna lo stato del sistema this.updateSystemState(result.status); }); } /** * Analizza la stringa dei settori e restituisce un array di numeri * @param {string} sectorsString - Stringa dei settori separati da virgola * @returns {Array|null} Array di numeri o null se vuoto */ parseSectors(sectorsString) { if (!sectorsString || sectorsString.trim() === '') { return null; // null significa tutti i settori } return sectorsString.split(',') .map(s => parseInt(s.trim())) .filter(n => !isNaN(n) && n > 0); } /** * Aggiorna lo stato del sistema in base ai dati ricevuti * @param {Object} status - Stato del sistema */ updateSystemState(status) { if (!status || !status.sectors) { this.log.error('Dati di stato non validi'); return; } this.log.debug('Aggiornamento dello stato del sistema'); this.log.debug('Dati settori ricevuti:', JSON.stringify(status.sectors)); // Converti i settori in un formato standardizzato let sectorsArray = []; if (Array.isArray(status.sectors)) { sectorsArray = status.sectors; } else if (typeof status.sectors === 'object' && status.sectors !== null) { // Converti oggetto in array sectorsArray = Object.values(status.sectors.sectors); this.log.debug(sectorsArray); this.log.debug(status.sectors.sectors); } else { this.log.error('Formato settori non riconosciuto'); return; } this.log.debug('Settori normalizzati:', JSON.stringify(sectorsArray)); // Determina lo stato del sistema in base ai settori configurati const armedSectors = sectorsArray.filter(sector => sector && sector.status).map(sector => sector.element); this.lastArmedSectors = armedSectors; // <--- aggiungi questa riga this.log.debug('Settori armati:', armedSectors); let newState = 3; // Disarmato per default if (armedSectors.length > 0) { // Se l'utente ha richiesto una modalità e i settori armati corrispondono, usa quella if ( typeof this.lastRequestedState === 'number' && ( (this.lastRequestedState === 0 && this.sectorsMatch(armedSectors, this.homeSectors)) || (this.lastRequestedState === 1 && this.sectorsMatch(armedSectors, this.awaySectors)) || (this.lastRequestedState === 2 && this.sectorsMatch(armedSectors, this.nightSectors)) ) ) { newState = this.lastRequestedState; this.log.debug('Modalità rilevata in base all\'ultima richiesta utente:', this.getStateDescription(newState)); } else if (this.sectorsMatch(armedSectors, this.homeSectors)) { newState = 0; // Casa this.log.debug('Rilevata modalità Casa'); } else if (this.sectorsMatch(armedSectors, this.nightSectors)) { newState = 2; // Notte this.log.debug('Rilevata modalità Notte'); } else if (this.sectorsMatch(armedSectors, this.awaySectors)) { newState = 1; // Via this.log.debug('Rilevata modalità Via'); } else { // Se i settori armati non corrispondono a nessuna modalità configurata, // determina la modalità in base al numero di settori armati if (armedSectors.length === sectorsArray.length) { newState = 1; // Tutti armati = Via this.log.debug('Tutti i settori armati, impostato su Via'); } else { newState = 0; // Parzialmente armato = Casa this.log.debug('Settori parzialmente armati, impostato su Casa'); } } } // Aggiorna lo stato solo se è cambiato if (this.currentState !== newState) { this.log.info(`Stato del sistema cambiato: ${this.getStateDescription(this.currentState)} -> ${this.getStateDescription(newState)}`); this.currentState = newState; this.targetState = newState; // Aggiorna lo stato dell'accessorio this.updateAccessoryState(); } } /** * Verifica se i settori armati corrispondono alla configurazione di una modalità * @param {Array} armedSectors - Array dei settori attualmente armati * @param {Array|null} configuredSectors - Array dei settori configurati per una modalità * @returns {boolean} True se corrispondono */ sectorsMatch(armedSectors, configuredSectors) { if (!configuredSectors) { return false; // Se non configurato, non può corrispondere } // Ordina entrambi gli array per il confronto const sortedArmed = [...armedSectors].sort((a, b) => a - b); const sortedConfigured = [...configuredSectors].sort((a, b) => a - b); if (sortedArmed.length !== sortedConfigured.length) { return false; } return sortedArmed.every((sector, index) => sector === sortedConfigured[index]); } /** * Aggiorna lo stato dell'accessorio del sistema di sicurezza */ updateAccessoryState() { for (const accessory of this.cachedAccessories) { const service = accessory.getService(Service.SecuritySystem); if (service) { service.updateCharacteristic(Characteristic.SecuritySystemCurrentState, this.currentState); service.updateCharacteristic(Characteristic.SecuritySystemTargetState, this.targetState); } } } /** * Ottiene la descrizione testuale dello stato * @param {number} state - Stato numerico * @returns {string} Descrizione dello stato */ getStateDescription(state) { switch (state) { case 0: return 'Armato (Casa)'; case 1: return 'Armato (Via)'; case 2: return 'Armato (Notte)'; case 3: return 'Disarmato'; default: return 'Sconosciuto'; } } /** * Crea gli accessori del sistema di sicurezza */ createAccessories() { this.log.info('Creazione degli accessori...'); // Genera un UUID univoco per l'accessorio basato sul nome const uuid = this.api.hap.uuid.generate(this.name); // Verifica se l'accessorio esiste già nella cache let existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid); if (existingAccessory) { this.log.info('Accessorio esistente trovato:', existingAccessory.displayName); this.configureSecuritySystemAccessory(existingAccessory); } else { this.log.info('Creazione di un nuovo accessorio:', this.name); // Crea un nuovo accessorio const accessory = new this.api.platformAccessory(this.name, uuid); // Configura l'accessorio this.configureSecuritySystemAccessory(accessory); // Registra l'accessorio con Homebridge this.api.registerPlatformAccessories('homebridge-elmo', 'ElmoSecuritySystem', [accessory]); // Aggiungi l'accessorio alla lista cache this.cachedAccessories.push(accessory); } } /** * Configura un accessorio del sistema di sicurezza * @param {object} accessory - Accessorio da configurare */ configureSecuritySystemAccessory(accessory) { this.log.info('Configurazione dell\'accessorio del sistema di sicurezza:', accessory.displayName); // Configura le informazioni dell'accessorio const informationService = accessory.getService(Service.AccessoryInformation) || accessory.addService(Service.AccessoryInformation); informationService .setCharacteristic(Characteristic.Manufacturer, 'Elmo') .setCharacteristic(Characteristic.Model, this.system) .setCharacteristic(Characteristic.SerialNumber, 'ELMO-001') .setCharacteristic(Characteristic.FirmwareRevision, '1.0.0'); // Configura il servizio del sistema di sicurezza const securityService = accessory.getService(Service.SecuritySystem) || accessory.addService(Service.SecuritySystem, this.name); securityService.setCharacteristic(Characteristic.Name, this.name); // Configura le caratteristiche securityService.getCharacteristic(Characteristic.SecuritySystemCurrentState) .onGet(this.getCurrentState.bind(this)); securityService.getCharacteristic(Characteristic.SecuritySystemTargetState) .onGet(this.getTargetState.bind(this)) .onSet(this.setTargetState.bind(this)); // Imposta i valori iniziali securityService.updateCharacteristic(Characteristic.SecuritySystemCurrentState, this.currentState); securityService.updateCharacteristic(Characteristic.SecuritySystemTargetState, this.targetState); // Gestisci l'evento di identificazione accessory.on('identify', () => { this.log.info('Identificazione dell\'accessorio:', accessory.displayName); }); } /** * Scopre e crea gli accessori per i dispositivi individuali */ async discoverDevices() { this.log.info('Scoperta dei dispositivi...'); const pythonScript = path.join(__dirname, 'scripts', 'elmo_client.py'); const options = { mode: 'json', pythonPath: this.pythonPath, args: [ 'devices', this.username, this.password, this.system, this.domain ] }; if (this.debug) { options.args.push('--debug'); } return new Promise((resolve, reject) => { PythonShell.run(pythonScript, options, (err, results) => { if (err) { this.log.error('Errore durante la scoperta dei dispositivi:', err); this.log.error('Questo potrebbe essere dovuto a:'); this.log.error('1. Problemi di connessione al sistema Elmo'); this.log.error('2. Credenziali non valide'); this.log.error('3. Sistema Elmo senza dispositivi configurati'); resolve(); return; } if (!results || results.length === 0) { this.log.error('Nessun risultato dalla scoperta dei dispositivi'); this.log.error('Lo script Python potrebbe non aver restituito dati validi'); resolve(); return; } const result = results[0]; this.log.debug('Risultato scoperta dispositivi:', JSON.stringify(result, null, 2)); if (!result.success) { this.log.error('Errore nella scoperta dei dispositivi:', result.message); this.log.error('Possibili cause:'); this.log.error('1. Sistema Elmo occupato da altra sessione'); this.log.error('2. Timeout durante la comunicazione'); this.log.error('3. Permessi insufficienti per accedere ai dispositivi'); resolve(); return; } if (!result.devices || !Array.isArray(result.devices)) { this.log.error('Formato dati dispositivi non valido:', result.devices); resolve(); return; } this.log.info(`Trovati ${result.devices.length} dispositivi nel sistema Elmo`); if (result.devices.length === 0) { this.log.warn('NESSUN DISPOSITIVO TROVATO nel sistema Elmo'); this.log.warn('Possibili motivi:'); this.log.warn('1. Il sistema Elmo non ha dispositivi configurati'); this.log.warn('2. I dispositivi non sono abilitati o non funzionanti'); this.log.warn('3. L\'utente non ha i permessi per vedere i dispositivi'); this.log.warn('4. Il tipo di sistema Elmo non supporta la query dei dispositivi'); this.log.warn('Verifica la configurazione del tuo sistema Elmo'); resolve(); return; } // Mostra dettagli dei dispositivi trovati this.log.info('Dispositivi trovati:'); result.devices.forEach((device, index) => { this.log.info(` ${index + 1}. ${device.name} (ID: ${device.id}, Element: ${device.element}, Stato: ${device.status})`); }); // Crea gli accessori per i dispositivi let createdDevices = 0; for (const device of result.devices) { try { this.createDeviceAccessory(device); createdDevices++; } catch (error) { this.log.error(`Errore nella creazione dell'accessorio per ${device.name}:`, error); } } this.log.info(`Creati ${createdDevices} accessori per i dispositivi`); resolve(); }); }); } /** * Crea un accessorio per un dispositivo specifico * @param {Object} device - Informazioni del dispositivo */ createDeviceAccessory(device) { if (!device.id) { this.log.warn('Dispositivo senza ID, saltato:', device.name); return; } // Genera un UUID univoco per il dispositivo const uuid = this.api.hap.uuid.generate(`${this.name}-device-${device.id}`); // Verifica se l'accessorio esiste già nella cache let existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid); if (existingAccessory) { this.log.debug('Accessorio dispositivo esistente trovato:', device.name); this.configureDeviceAccessory(existingAccessory, device); } else { this.log.info('Creazione nuovo accessorio dispositivo:', device.name); // Crea un nuovo accessorio const accessory = new this.api.platformAccessory(device.name, uuid); // Salva le informazioni del dispositivo nel context accessory.context.deviceInfo = device; // Configura l'accessorio this.configureDeviceAccessory(accessory, device); // Registra l'accessorio con Homebridge this.api.registerPlatformAccessories('homebridge-elmo', 'ElmoSecuritySystem', [accessory]); // Aggiungi l'accessorio alla lista cache this.cachedAccessories.push(accessory); } // Salva il dispositivo nella mappa this.devices.set(device.id, device); } /** * Configura un accessorio del dispositivo * @param {object} accessory - Accessorio da configurare * @param {Object} device - Informazioni del dispositivo */ configureDeviceAccessory(accessory, device) { this.log.debug('Configurazione accessorio dispositivo:', device.name); // Aggiorna le informazioni del dispositivo nel context accessory.context.deviceInfo = device; // Configura le informazioni dell'accessorio const informationService = accessory.getService(Service.AccessoryInformation) || accessory.addService(Service.AccessoryInformation); informationService .setCharacteristic(Characteristic.Manufacturer, 'Elmo') .setCharacteristic(Characteristic.Model, device.type || 'Sensor') .setCharacteristic(Characteristic.SerialNumber, `ELMO-DEV-${device.id}`) .setCharacteristic(Characteristic.FirmwareRevision, '1.0.0'); // Ottieni il tipo di dispositivo dalla configurazione o usa il default const deviceConfig = this.getDeviceConfig(device.id); const deviceType = deviceConfig ? deviceConfig.type : this.config.defaultDeviceType || 'contact'; // Determina il tipo di servizio in base al tipo configurato let serviceType; let characteristicType; switch (deviceType.toLowerCase()) { case 'contact': case 'door': case 'window': case 'magnetic': serviceType = Service.ContactSensor; characteristicType = Characteristic.ContactSensorState; break; case 'motion': case 'pir': case 'movement': serviceType = Service.MotionSensor; characteristicType = Characteristic.MotionDetected; break; case 'smoke': case 'fire': serviceType = Service.SmokeSensor; characteristicType = Characteristic.SmokeDetected; break; default: // Usa ContactSensor come default serviceType = Service.ContactSensor; characteristicType = Characteristic.ContactSensorState; break; } // Configura il servizio del sensore const sensorService = accessory.getService(serviceType) || accessory.addService(serviceType, device.name); sensorService.setCharacteristic(Characteristic.Name, device.name); // Configura la caratteristica principale sensorService.getCharacteristic(characteristicType) .onGet(() => this.getDeviceState(device.id, characteristicType)); // Imposta il valore iniziale const initialValue = this.convertDeviceStatus(device.status, characteristicType); sensorService.updateCharacteristic(characteristicType, initialValue); // Gestisci l'evento di identificazione accessory.on('identify', () => { this.log.info('Identificazione dispositivo:', device.name); }); } /** * Ottiene la configurazione di un dispositivo specifico * @param {string|number} deviceId - ID del dispositivo * @returns {Object|null} Configurazione del dispositivo o null */ getDeviceConfig(deviceId) { if (!this.config.devices || !Array.isArray(this.config.devices)) { return null; } return this.config.devices.find(device => device.id == deviceId); } /** * Aggiorna lo stato di un accessorio dispositivo * @param {object} accessory - Accessorio da aggiornare * @param {Object} device - Nuovi dati del dispositivo */ updateDeviceAccessoryState(accessory, device) { const deviceInfo = accessory.context.deviceInfo; if (!deviceInfo) return; // Ottieni il tipo di dispositivo dalla configurazione const deviceConfig = this.getDeviceConfig(device.id); const deviceType = deviceConfig ? deviceConfig.type : this.config.defaultDeviceType || 'contact'; // Determina il tipo di servizio e caratteristica let serviceType; let characteristicType; switch (deviceType.toLowerCase()) { case 'contact': case 'door': case 'window': case 'magnetic': serviceType = Service.ContactSensor; characteristicType = Characteristic.ContactSensorState; break; case 'motion': case 'pir': case 'movement': serviceType = Service.MotionSensor; characteristicType = Characteristic.MotionDetected; break; case 'smoke': case 'fire': serviceType = Service.SmokeSensor; characteristicType = Characteristic.SmokeDetected; break; default: serviceType = Service.ContactSensor; characteristicType = Characteristic.ContactSensorState; break; } const service = accessory.getService(serviceType); if (service) { const newValue = this.convertDeviceStatus(device.status, characteristicType); service.updateCharacteristic(characteristicType, newValue); this.log.debug(`Aggiornato ${device.name} -> ${newValue}`); } } /** * Ottiene lo stato di un dispositivo specifico * @param {string|number} deviceId - ID del dispositivo * @param {Object} characteristicType - Tipo di caratteristica HomeKit * @returns {boolean|number} Stato del dispositivo */ getDeviceState(deviceId, characteristicType) { const device = this.devices.get(deviceId); if (!device) { this.log.warn('Dispositivo non trovato:', deviceId); return false; } return this.convertDeviceStatus(device.status, characteristicType); } /** * Converte lo stato del dispositivo nel formato HomeKit appropriato * @param {boolean} deviceStatus - Stato del dispositivo dal sistema Elmo * @param {Object} characteristicType - Tipo di caratteristica HomeKit * @returns {boolean|number} Valore convertito per HomeKit */ convertDeviceStatus(deviceStatus, characteristicType) { if (characteristicType === Characteristic.ContactSensorState) { // Per i sensori di contatto: true = aperto, false = chiuso return deviceStatus ? 1 : 0; // 1 = CONTACT_NOT_DETECTED (aperto), 0 = CONTACT_DETECTED (chiuso) } else if (characteristicType === Characteristic.MotionDetected) { // Per i sensori di movimento: true = movimento rilevato return Boolean(deviceStatus); } else if (characteristicType === Characteristic.SmokeDetected) { // Per i sensori di fumo: true = fumo rilevato return deviceStatus ? 1 : 0; // 1 = SMOKE_DETECTED, 0 = SMOKE_NOT_DETECTED } return Boolean(deviceStatus); } /** * Configura il polling per i dispositivi (più veloce) */ setupDevicePolling() { if (!this.enableDevices) { return; } this.log.debug(`Configurazione polling dispositivi (ogni ${this.devicePollInterval} secondi)`); // Avvia il polling dei dispositivi this.pollDevicesStatus(); // Imposta il polling periodico per i dispositivi setInterval(() => { this.pollDevicesStatus(); }, this.devicePollInterval * 1000); } /** * Esegue il polling dello stato dei dispositivi */ pollDevicesStatus() { this.log.debug('Polling dispositivi'); const pythonScript = path.join(__dirname, 'scripts', 'elmo_client.py'); const options = { mode: 'json', pythonPath: this.pythonPath, args: [ 'devices', this.username, this.password, this.system, this.domain ], timeout: 15000 // Timeout più breve per il polling }; if (this.debug) { options.args.push('--debug'); } PythonShell.run(pythonScript, options, (err, results) => { if (err) { // Log più silenzioso per errori di polling dispositivi if (err.message && err.message.includes('timeout')) { this.log.debug('Timeout durante il polling dispositivi - probabile conflitto di lock'); } else { this.log.debug('Errore durante il polling dei dispositivi:', err.message); } return; } if (!results || results.length === 0) { this.log.debug('Nessun risultato dal polling dei dispositivi'); return; } const result = results[0]; if (!result.success) { if (result.message && result.message.includes('occupato')) { this.log.debug('Sistema occupato durante il polling dispositivi - saltato questo ciclo'); } else { this.log.debug('Errore nel polling dei dispositivi:', result.message); } return; } this.log.debug(`Aggiornamento stato di ${result.devices.length} dispositivi`); // Aggiorna lo stato dei dispositivi this.updateDevicesState(result.devices); }); } /** * Aggiorna lo stato dei dispositivi * @param {Array} devices - Array dei dispositivi con i nuovi stati */ updateDevicesState(devices) { for (const device of devices) { if (!device.id) continue; const existingDevice = this.devices.get(device.id); if (!existingDevice) continue; // Verifica se lo stato è cambiato if (existingDevice.status !== device.status) { this.log.debug(`Dispositivo ${device.name}: ${existingDevice.status} -> ${device.status}`); // Aggiorna il dispositivo nella mappa this.devices.set(device.id, device); // Trova l'accessorio corrispondente e aggiorna la caratteristica const uuid = this.api.hap.uuid.generate(`${this.name}-device-${device.id}`); const accessory = this.cachedAccessories.find(acc => acc.UUID === uuid); if (accessory) { this.updateDeviceAccessoryState(accessory, device); } } else { // Aggiorna comunque i dati del dispositivo this.devices.set(device.id, device); } } } /** * Ottiene lo stato corrente del sistema */ getCurrentState() { this.log.debug('getCurrentState:', this.getStateDescription(this.currentState)); return this.currentState; } /** * Ottiene lo stato target del sistema */ getTargetState() { this.log.debug('getTargetState:', this.getStateDescription(this.targetState)); return this.targetState; } /** * Imposta lo stato target del sistema * @param {number} state - Stato target */ async setTargetState(state) { this.log.info('setTargetState:', this.getStateDescription(state)); this.lastRequestedState = state; return new Promise((resolve, reject) => { this.targetState = state; if (this.operationInProgress) { const error = new Error('Operazione già in corso, attendere il completamento'); this.log.warn(error.message); reject(error); return; } this.operationInProgress = true; const pythonScript = path.join(__dirname, 'scripts', 'elmo_client.py'); let command; let sectors = null; // Determina i settori richiesti per la modalità let requestedSectors = null; if (state === 0) requestedSectors = this.homeSectors; if (state === 1) requestedSectors = this.awaySectors; if (state === 2) requestedSectors = this.nightSectors; // Se non ci sono settori configurati per questa modalità, esci senza fare nulla if ((state === 0 || state === 1 || state === 2) && (!Array.isArray(requestedSectors) || requestedSectors.length === 0)) { this.log.warn(`Nessun settore configurato per la modalità ${this.getStateDescription(state)}. Nessuna azione eseguita.`); this.currentState = state; this.updateAccessoryState(); this.operationInProgress = false; resolve(); return; } // Se disarmo, mando il comando disarm su tutti i settori armati if (state === 3) { command = 'disarm'; sectors = this.lastArmedSectors && this.lastArmedSectors.length > 0 ? this.lastArmedSectors : null; } else { // Seleziona i settori da armare e da disarmare const alreadyArmed = Array.isArray(this.lastArmedSectors) ? this.lastArmedSectors : []; const toArm = requestedSectors.filter(s => !alreadyArmed.includes(s)); const toDisarm = alreadyArmed.filter(s => !requestedSectors.includes(s)); if (toDisarm.length > 0) { // Prima disarma i settori non richiesti this.log.info(`Disarmo settori non richiesti dalla modalità: ${toDisarm.join(',')}`); const disarmOptions = { mode: 'json', pythonPath: this.pythonPath, args: [ 'disarm', this.username, this.password, this.system, this.domain, this.code, toDisarm.join(',') ], timeout: 60000 // Timeout più lungo per operazioni di controllo }; PythonShell.run(pythonScript, disarmOptions, (err, results) => { if (err || !results || !results[0].success) { this.operationInProgress = false; const errorMsg = err ? err.message : (results && results[0].message); // Messaggio specifico per errori di lock if (errorMsg && errorMsg.includes('occupato')) { this.log.error('Sistema occupato da un\'altra sessione. Chiudi l\'app Elmo e riprova tra qualche minuto.'); reject(new Error('Sistema occupato - riprova più tardi')); } else { this.log.error('Errore durante il disarmo dei settori:', errorMsg); reject(new Error('Errore durante il disarmo dei settori')); } this.targetState = this.currentState; this.updateAccessoryState(); return; } this.log.info('Settori disarmati con successo:', toDisarm.join(',')); // Dopo il disarmo, procedi con l'armamento se necessario if (toArm.length > 0) { this.log.info(`Armo i settori richiesti: ${toArm.join(',')}`); const armOptions = { mode: 'json', pythonPath: this.pythonPath, args: [ 'arm', this.username, this.password, this.system, this.domain, this.code, toArm.join(',') ], timeout: 60000 }; PythonShell.run(pythonScript, armOptions, (err2, results2) => { this.operationInProgress = false; if (err2 || !results2 || !results2[0].success) { const errorMsg2 = err2 ? err2.message : (results2 && results2[0].message); if (errorMsg2 && errorMsg2.includes('occupato')) { this.log.error('Sistema occupato durante l\'armamento. Chiudi l\'app Elmo e riprova.'); reject(new Error('Sistema occupato - riprova più tardi')); } else { this.log.error('Errore durante l\'armamento dei settori:', errorMsg2); reject(new Error('Errore durante l\'armamento dei settori')); } this.targetState = this.currentState; this.updateAccessoryState(); return; } this.log.info('Settori armati con successo:', toArm.join(',')); this.currentState = state; this.updateAccessoryState(); setTimeout(() => this.pollElmoStatus(), 5000); resolve(); }); } else { // Solo disarmo, mando il comando di aggiornamento stato this.operationInProgress = false; this.currentState = state; this.updateAccessoryState(); setTimeout(() => this.pollElmoStatus(), 5000); resolve(); } }); return; } else if (toArm.length > 0) { // Solo armamento necessario command = 'arm'; sectors = toArm; } else { // Tutto già a posto this.log.info('Tutti i settori richiesti sono già nello stato corretto, nessun comando inviato.'); this.currentState = state; this.updateAccessoryState(); this.operationInProgress = false; resolve(); return; } } // Esegue solo arm o disarm se necessario const options = { mode: 'json', pythonPath: this.pythonPath, args: [ command, this.username, this.password, this.system, this.domain, this.code ], timeout: 60000 // Timeout più lungo per operazioni di controllo }; if (sectors && sectors.length > 0) { options.args.push(sectors.join(',')); this.log.debug(`Comando ${command} con settori: ${sectors.join(',')}`); } else { this.log.debug(`Comando ${command} senza settori specifici`); } if (this.debug) { options.args.push('--debug'); } this.log.debug(`Esecuzione comando ${command} con timeout di 60 secondi`); PythonShell.run(pythonScript, options, (err, results) => { this.operationInProgress = false; if (err || !results || !results[0].success) { const errorMsg = err ? err.message : (results && results[0].message); // Messaggio specifico per errori di lock if (errorMsg && errorMsg.includes('occupato')) { this.log.error(`Sistema occupato durante ${command}. Chiudi l'app Elmo e riprova tra qualche minuto.`); reject(new Error('Sistema occupato - riprova più tardi')); } else { this.log.error(`Errore durante l'esecuzione del comando ${command}:`, errorMsg); reject(new Error(`Errore del comando ${command}`)); } this.targetState = this.currentState; this.updateAccessoryState(); return; } this.log.info(`Comando ${command} eseguito con successo:`, results[0].message); this.currentState = state; this.updateAccessoryState(); setTimeout(() => this.pollElmoStatus(), 5000); resolve(); }); }); } /** * Crea un venv Python se non esiste */ ensureVenv() { if (!fs.existsSync(this.venvPath)) { this.log.info('Creazione di un ambiente virtuale Python per il plugin...'); const result = spawnSync('python3', ['-m', 'venv', this.venvPath], { encoding: 'utf-8' }); if (result.error) { this.log.error('Errore durante la creazione del venv:', result.error.message); } else if (result.status !== 0) { this.log.error('Creazione del venv fallita:\n', result.stdout, result.stderr); } else { this.log.info('Ambiente virtuale Python creato con successo.'); } } else { this.log.debug('Ambiente virtuale Python già presente.'); } } /** * Verifica e installa le dipendenze Python richieste nel venv */ ensurePythonDependencies() { const scriptPath = path.join(__dirname, 'scripts', 'install_dependencies.py'); this.log.info('Verifica delle dipendenze Python nel venv...'); try { const result = spawnSync(this.pythonPath, [scriptPath], { encoding: 'utf-8' }); if (result.error) { this.log.error('Errore durante il controllo delle dipendenze Python:', result.error.message); } else if (result.status !== 0) { this.log.error('Installazione delle dipendenze Python fallita:\n', result.stdout, result.stderr); } else { this.log.info('Dipendenze Python verificate/installate correttamente.'); } } catch (err) { this.log.error('Eccezione durante il controllo delle dipendenze Python:', err.message); } } } /** * Funzione di inizializzazione del plugin */ module.exports = (homebridge) => { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; homebridge.registerPlatform('homebridge-elmo', 'ElmoSecuritySystem', ElmoPlatform); };