homebridge-elmo
Version:
Homebridge plugin per integrare i sistemi di antifurto Elmo con HomeKit
1,106 lines (945 loc) • 39.8 kB
JavaScript
;
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);
};