UNPKG

homebridge-unifi-access-gate

Version:

PRE-ALPHA (Do not install for stable installation - This is in developpement stage) - Homebridge plugin for Unifi Access with Sensors - Support for DOOR-HUB (G2) with the new Access API provided by Ubiquiti

570 lines (569 loc) 30.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const axios_1 = __importDefault(require("axios")); const https_1 = __importDefault(require("https")); const ws_1 = __importDefault(require("ws")); const settings_1 = require("./settings"); const platformAccessory_1 = require("./platformAccessory"); class UnifiAccessPlatform { logWarning(message) { this.log.warn(message); } constructor(log, config, api) { this.accessories = []; this.lastHelloTime = Date.now(); this.pendingTimeouts = new Map(); this.lastUnlockCall = 0; // Stocke l'heure du dernier appel this.log = log; this.config = config; this.api = api; // Initialize WebSocket and Axios instances this.initAxiosInstance(); this.initWebSocket(); this.apiToken = this.config.apiToken; // Register the existing accessories this.api.on('didFinishLaunching', () => { this.discoverDevices(); }); } initAxiosInstance() { this.axiosInstance = axios_1.default.create({ httpsAgent: new https_1.default.Agent({ rejectUnauthorized: false, }), }); } ////////////////////////////////////////////////////////////////////////////////////////// // Initial connection to access websocket for receive event from access API ///////////////////////////////////////////////////////////////////////////////////////// initWebSocket() { const wsBaseUrl = this.config.baseUrl.replace(/^https:\/\//, 'wss://'); const path = '/api/v1/developer/devices/notifications'; // Ajouter le chemin spécifique const wsUrl = `${wsBaseUrl}${path}`; // Initialize WebSocket connection this.ws = new ws_1.default(wsUrl, { headers: { Authorization: `Bearer ${this.config.apiToken}`, }, rejectUnauthorized: false, }); this.ws.on('open', () => { this.log.info('WebSocket connected'); // Reset lastHelloTime when WebSocket is connected this.lastHelloTime = Date.now(); }); this.ws.on('message', (data) => { try { const eventData = JSON.parse(data.toString()); //this.log.debug(eventData); if (eventData.data === 'Hello') { this.handleHelloEvent(); } else { this.handleEvent(eventData); } // Update lastHelloTime on every 'Hello' message received this.lastHelloTime = Date.now(); } catch (error) { this.log.error('Error parsing WebSocket message:', error); } }); this.ws.on('error', (error) => { this.log.error('WebSocket error:', error); // Handle error and attempt to reconnect this.reconnectWebSocket(); }); this.ws.on('close', () => { //this.log.info('WebSocket connection closed'); // Automatically attempt to reconnect this.reconnectWebSocket(); }); // Handle reconnection if 'Hello' is not received within 1 minute setInterval(() => { const now = Date.now(); const timeDiff = now - this.lastHelloTime; const oneMinute = 60 * 1000; if (timeDiff >= oneMinute) { this.log.warn('No "Hello" received for 1 minute. Reconnecting WebSocket...'); this.reconnectWebSocket(); } }, 60 * 1000); // Check every minute } ////////////////////////////////////////////////////////////////////////////////////////// // Reconnect websocket (Called if ping server is not received for 1 minute) ///////////////////////////////////////////////////////////////////////////////////////// reconnectWebSocket() { // Close existing WebSocket connection if it exists if (this.ws && this.ws.readyState !== ws_1.default.CLOSED) { this.ws.close(); } const wsBaseUrl = this.config.baseUrl.replace(/^https:\/\//, 'wss://'); const path = '/api/v1/developer/devices/notifications'; // Ajouter le chemin spécifique const wsUrl = `${wsBaseUrl}${path}`; // Initialize a new WebSocket connection this.ws = new ws_1.default(wsUrl, { headers: { Authorization: `Bearer ${this.config.apiToken}`, }, rejectUnauthorized: false, }); this.ws.on('open', () => { this.log.info('WebSocket reconnected'); // Reset lastHelloTime when WebSocket is reconnected this.lastHelloTime = Date.now(); }); this.ws.on('message', (data) => { try { const eventData = JSON.parse(data.toString()); if (eventData.data === 'Hello') { this.handleHelloEvent(); } else { this.handleEvent(eventData); } // Update lastHelloTime on every 'Hello' message received this.lastHelloTime = Date.now(); } catch (error) { this.log.error('Error parsing WebSocket message:', error); } }); this.ws.on('error', (error) => { this.log.error('WebSocket error:', error); // Handle error and attempt to reconnect this.reconnectWebSocket(); }); this.ws.on('close', () => { this.log.info('WebSocket connection closed'); // Automatically attempt to reconnect this.reconnectWebSocket(); }); } ////////////////////////////////////////////////////////////////////////////////////////// // Discover devices ///////////////////////////////////////////////////////////////////////////////////////// async discoverDevices() { try { const urlForDiscover = this.config.baseUrl; const discoverUrl = `${urlForDiscover}/api/v1/developer/devices`; const response = await this.axiosInstance.get(discoverUrl, { headers: { 'Authorization': `Bearer ${this.config.apiToken}`, 'Accept': 'application/json', }, }); if (response.status !== 200) { throw new Error(`Failed to fetch devices. Status: ${response.status}`); } if (!response.data || !Array.isArray(response.data.data) || response.data.data.length === 0) { throw new Error('Invalid or empty data array received'); } // Flatten the data array const devices = response.data.data.flat(2); devices.forEach(device => { const uuid = this.api.hap.uuid.generate(device.id); const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); if (existingAccessory) { // Update existing accessory if needed this.log.info(`Updating existing accessory from cache: ${existingAccessory.displayName}`); new platformAccessory_1.UnifiAccessory(this, existingAccessory, this.api, this.log); // Pass this.api as the third argument } else { // Create new accessory this.log.info(`Adding new accessory: ${device.name}`); const accessory = new this.api.platformAccessory(device.name, uuid); accessory.context.device = device; accessory.context.id = device.id; // Check device type and add Contact Sensor service if it's a UAH-DOOR if (device.type === 'UAH-DOOR' || device.type === 'UAH') { this.log.info(`Adding Contact Sensor service for: ${device.name}`); // Définir les variables à suivre pour chaque hub const variablesToTrack = ['isClosed', 'ContactRen', 'ContactRex', 'ContactRel']; // Remplacez par vos variables spécifiques variablesToTrack.forEach(variable => { const contactSensorName = `${device.name} - ${variable}`; this.log.info(`Adding Contact Sensor service for: ${contactSensorName}`); const contactSensorService = accessory.addService(this.api.hap.Service.ContactSensor, contactSensorName, variable); contactSensorService.setCharacteristic(this.api.hap.Characteristic.Name, contactSensorName); // Initialize sensor state for each variable (assuming it's closed initially) accessory.context[`${variable}State`] = false; // Initialisez l'état selon vos besoins contactSensorService.updateCharacteristic(this.api.hap.Characteristic.ContactSensorState, this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED); }); accessory.context.lockState = 'locked'; // Initialize sensor state (assuming it's closed initially) accessory.context.isClosed = false; } else if (device.type === 'UA-G2-MINI' || device.type === 'UA-LITE' || device.type === 'UA-Intercom' || device.type === 'UA-Intercom-viewer') { this.log.info(`Adding ContactSensor service for: ${device.name}`); accessory.addService(this.api.hap.Service.ContactSensor, device.name); // Initialize sensor state (assuming it's closed initially) accessory.context.isClosed = true; } // Register the accessory this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); // Create UnifiAccessory instance for handling updates new platformAccessory_1.UnifiAccessory(this, accessory, this.api, this.log); // Pass this.api as the third argument } }); } catch (error) { this.log.error('Failed to discover devices:', error.message); } } ////////////////////////////////////////////////////////////////////////////////////////// // Fetch door details ///////////////////////////////////////////////////////////////////////////////////////// async fetchDoorDetails() { try { const urlForDoorDetail = this.config.baseUrl; const doorUrl = `${urlForDoorDetail}/api/v1/developer/doors`; const response = await this.axiosInstance.get(doorUrl, { headers: { 'Authorization': `Bearer ${this.config.apiToken}`, 'Accept': 'application/json', }, }); if (response.status !== 200) { throw new Error(`Failed to fetch door details. Status: ${response.status}`); } const doors = response.data; // Supposons que la réponse contient un tableau d'objets représentant les portes // Vous pouvez maintenant utiliser ces détails de porte comme nécessaire dans votre logique return doors; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) { this.log.error('Failed to fetch door details:', error.message); throw error; // Propagez l'erreur pour une gestion supplémentaire si nécessaire } } ////////////////////////////////////////////////////////////////////////////////////////// // Unlock door function (Called when a remote unlock is requested by the user) ///////////////////////////////////////////////////////////////////////////////////////// async unlockDoorById(doorId) { const currentTime = Date.now(); // Vérifiez si 3 secondes se sont écoulées depuis le dernier appel if (currentTime - this.lastUnlockCall < 3500) { //console.log('Please wait before trying to unlock the door again. If occured then unlock, can be ignored safely'); return; } // Mettre à jour le dernier appel this.lastUnlockCall = currentTime; const urlForUnlock = this.config.baseUrl; const unlockUrl = `${urlForUnlock}/api/v1/developer/doors/${doorId}/unlock`; try { const response = await this.axiosInstance.put(unlockUrl, {}, { headers: { Authorization: `Bearer ${this.apiToken}`, 'Content-Type': 'application/json', }, }); if (response.status !== 200) { throw new Error(`Failed to unlock door. Status: ${response.status}`); } console.log('Door unlocked successfully, if not unlocked in real, reboot now and retry.'); } catch (error) { console.error('Error unlocking door:', error.message); throw error; // Propagez l'erreur pour une gestion supplémentaire si nécessaire } } ///////////////////////////////////////////////////////////////////////////////////////// // Configure accessory when called ///////////////////////////////////////////////////////////////////////////////////////// configureAccessory(accessory) { this.log.info(`Loading accessory from cache: ${accessory.displayName}`); this.accessories.push(accessory); new platformAccessory_1.UnifiAccessory(this, accessory, this.api, this.log); } handleEvent(eventData) { if (!this.isValidEventData(eventData)) { return; } // Log the received event //const eventString = JSON.stringify(eventData, null, 2); //this.log.debug(`Received event:\n${eventString}`); // Process the event based on its type if (eventData.event) { switch (eventData.event) { case 'access.remote_view': if (eventData.data) { this.handleRemoteViewEvent(eventData.data); } break; case 'access.remote_view.change': if (eventData.data) { this.handleRemoteViewChangeEvent(eventData.data); } break; case 'access.data.device.remote_unlock': //this.log.warn(eventData.data); if (eventData.data) { this.handleRemoteUnlockEvent(eventData.data); } break; case 'access.data.device.update': this.handleDeviceUpdateEvent(eventData); break; case 'access.logs.add': //this.logWarning(eventData.data); this.handleMessage(eventData); break; case 'access.hw.door_bell': //this.logWarning(eventData.data); break; default: //this.log.debug('Unhandled event:', eventData); break; } } else { //this.log.warn('Unhandled event:', eventData); } // Attempt to find the accessory by deviceId if available const device = this.findAccessoryByDeviceId(eventData.deviceId); if (device) { new platformAccessory_1.UnifiAccessory(this, device, this.api, this.log).updateAccessoryCharacteristics(); } else { //this.log.warn(`No accessory found for deviceId: ${eventData.deviceId}`); } } ////////////////////////////////////////////////////////////////////////////////////////// // Handle specific devices states (Reader, Intercom, ect) ///////////////////////////////////////////////////////////////////////////////////////// checkEvent(message) { // Assurez-vous que le message contient bien les données attendues if (!message || !message._source || !message._source.event) { this.log.warn('Invalid message format or missing event data.'); return false; } const eventType = message._source.event.type; //this.log.debug(`Event type from message: ${eventType}`); // Vérifiez si le type d'événement correspond à l'ouverture de la porte return true; } handleMessage(eventData) { try { // Vérifiez que les données de l'événement sont définies et contiennent les cibles if (!eventData.data || !eventData.data._source || !eventData.data._source.target) { this.log.warn('Event data or target is missing.'); return; } // Cherchez l'objet avec le type "UA-G2-MINI" dans le tableau target const targetItem = eventData.data._source.target.find((item) => item.type === 'UA-G2-MINI' || item.type === 'UA-LITE'); // Vérifiez que targetItem est défini if (!targetItem) { //this.log.warn('No target item with type "UA-G2-MINI" found.'); return; } // Récupérez l'ID correspondant const uniqueId = targetItem.id; // Trouvez l'accessoire avec le ID unique const device = this.accessories.find(acc => acc.context.device.id === uniqueId); if (!device) { this.log.warn(`No accessory found for unique_id: ${uniqueId}. If this is the first configuration, please reboot now for register the devices.`); return; } // Récupérez l'ID du capteur à partir du contexte de l'accessoire const sensorId = device.context.device.id; //this.log.debug('Received message:', JSON.stringify(eventData.data, null, 2)); //this.log.debug('Sensor ID to compare:', sensorId); //this.log.info('Device context:', JSON.stringify(device.context, null, 2)); // Vérifiez si le capteur est ouvert ou fermé en fonction des données de l'événement const isOpened = this.checkEvent(eventData.data); // Utilisez cette méthode pour déterminer si le capteur est ouvert // Log pour l'état actuel du capteur //this.log.debug(`Current state: ${isOpened ? 'Open' : 'Closed'}`); // Mettre à jour l'état du capteur uniquement si nécessaire const contactSensorService = device.getService(this.api.hap.Service.ContactSensor); if (contactSensorService) { const currentState = contactSensorService.getCharacteristic(this.api.hap.Characteristic.ContactSensorState).value; // Si le capteur est ouvert et était précédemment fermé, ou vice versa, mettre à jour l'état if (isOpened && currentState !== this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED) { // Mise à jour pour "ouvert" contactSensorService.getCharacteristic(this.api.hap.Characteristic.ContactSensorState) .updateValue(this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED); this.log.debug(`Sensor state changed for ${device.displayName}: Open`); device.context.isClosed = false; } else if (!isOpened && currentState !== this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED) { // Mise à jour pour "fermé" contactSensorService.getCharacteristic(this.api.hap.Characteristic.ContactSensorState) .updateValue(this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED); this.log.debug(`Sensor state changed for ${device.displayName}: Closed`); device.context.isClosed = true; } // Si un timeout est déjà en cours pour cet appareil, annulez-le if (this.pendingTimeouts.has(sensorId)) { clearTimeout(this.pendingTimeouts.get(sensorId)); } // Lancez un délai de 5 secondes avant de fermer le contact si le capteur est ouvert if (isOpened) { const timeoutId = setTimeout(() => { contactSensorService.getCharacteristic(this.api.hap.Characteristic.ContactSensorState) .updateValue(this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED); this.log.debug(`Sensor state changed for ${device.displayName}: Closed`); device.context.isClosed = true; this.pendingTimeouts.delete(sensorId); // Nettoyez la Map après l'expiration du délai }, 5000); // 5000 millisecondes = 5 secondes // Stockez le timeout en cours this.pendingTimeouts.set(sensorId, timeoutId); } } else { this.log.warn('ContactSensor service not found.'); } } catch (error) { this.log.error('Failed to handle message from devices:', error); } } ////////////////////////////////////////////////////////////////////////////////////////// // Handle device update (Used for get token for door) ///////////////////////////////////////////////////////////////////////////////////////// handleDeviceUpdateEvent(eventData) { var _a, _b; try { const uniqueId = eventData.data.unique_id; const device = this.accessories.find(acc => acc.context.id === uniqueId); if (!device) { this.log.warn(`No accessory found for unique_id: ${uniqueId}. If this is the first configurations, please reboot.`); return; } // Extraction des valeurs de unique_id et door const doorId = (_a = eventData.data.door) === null || _a === void 0 ? void 0 : _a.unique_id; const doorName = (_b = eventData.data.door) === null || _b === void 0 ? void 0 : _b.name; // Stocker ces valeurs dans le contexte de l'accessoire device.context.doorId = doorId; device.context.doorName = doorName; // Création de l'accessoire UnifiAccessory pour gérer les caractéristiques const unifiAccessory = new platformAccessory_1.UnifiAccessory(this, device, this.api, this.log); // Extraction des configurations const configs = eventData.data.configs; // Fonction pour traiter les configurations const handleConfig = (key, contextKey, stateMapping) => { const config = configs.find((config) => config.key === key); if (config) { const state = stateMapping[config.value]; if (state === undefined) { this.log.warn(`Unhandled state value '${config.value}' for ${key}. Defaulting to 'CONTACT_NOT_DETECTED'.`); } const previousState = device.context[contextKey]; const finalState = state !== null && state !== void 0 ? state : this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; // Valeur par défaut if (previousState !== finalState) { device.context[contextKey] = finalState; this.log.info(`${contextKey} state changed for ${device.displayName}: ${finalState}`); // Mise à jour du service ContactSensor const contactSensorService = device.getServiceById(this.api.hap.Service.ContactSensor, contextKey); if (contactSensorService) { contactSensorService.getCharacteristic(this.api.hap.Characteristic.ContactSensorState) .updateValue(finalState); } unifiAccessory.updateAccessoryCharacteristics(); } } }; // Gestion de l'état du capteur principal handleConfig('input_state_dps', 'isClosed', { 'off': this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED, 'on': this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED, 'default': this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED, }); // Gestion de l'état de la serrure handleConfig('input_state_rly-lock_dry', 'lockState', { 'on': 'unlocked', 'off': 'locked', 'default': device.context.lockState, }); // Gestion des variables supplémentaires const variableConfigs = [ { key: 'input_state_rex', contextKey: 'ContactRex' }, { key: 'input_state_ren', contextKey: 'ContactRen' }, { key: 'input_state_rel', contextKey: 'ContactRel' }, ]; variableConfigs.forEach(({ key, contextKey }) => { handleConfig(key, contextKey, { 'on': this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED, 'off': this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED, 'default': this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED, }); }); } catch (error) { this.log.error('Failed to handle device update event:', error); } } ////////////////////////////////////////////////////////////////////////////////////////// // Find accessory in cached devices by "id" ///////////////////////////////////////////////////////////////////////////////////////// findAccessoryByDeviceId(deviceId) { if (!deviceId) { return undefined; } return this.accessories.find(acc => acc.UUID === this.api.hap.uuid.generate(deviceId)); } ////////////////////////////////////////////////////////////////////////////////////////// // Handle Ping websocket server ///////////////////////////////////////////////////////////////////////////////////////// handleHelloEvent() { this.lastHelloTime = Date.now(); this.log.debug('Ping reçu'); } ////////////////////////////////////////////////////////////////////////////////////////// // Verify if eventdata is a valid payload ///////////////////////////////////////////////////////////////////////////////////////// isValidEventData(eventData) { if (!eventData) { //this.log.error('Invalid event data received:', eventData); return false; } if (typeof eventData.event !== 'string') { //this.log.error('Invalid event data received:', eventData); return false; } return true; } ////////////////////////////////////////////////////////////////////////////////////////// // Handle remote call from access API ///////////////////////////////////////////////////////////////////////////////////////// handleRemoteViewEvent(eventData) { if (!eventData.data) { //this.log.error('No data provided for access.remote_view event'); return; } // Handle remote_view event data //this.log.debug('Handling access.remote_view event:', eventData.data); } handleRemoteViewChangeEvent(eventData) { if (!eventData.data) { //this.log.error('No data provided for access.remote_view.change event'); return; } // Handle remote_view.change event data //this.log.debug('Handling access.remote_view.change event:', eventData.data); } handleRemoteUnlockEvent(eventData) { if (!eventData.data) { //this.log.error('No data provided for access.data.device.remote_unlock event'); return; } // Handle remote_unlock event data //this.log.debug('Handling access.data.device.remote_unlock event:', eventData.data); } //////////////////////////////////////////////////////////////////////////////////////// // UNUSED AT THIS TIME /////////////////////////////////////////////////////////////////////////////////////// handleDeviceUpdate(eventData) { if (!eventData.data) { //this.log.error('No data provided for access.data.device.update event'); return; } // Handle device update event data //this.log.debug('Handling access.data.device.update event:', eventData.data); } } exports.default = UnifiAccessPlatform;