UNPKG

homebridge-http-sensors-switches

Version:

This plugin communicates with your devices over HTTP or MQTT. Currently it supports Light Bulb, Switches, Outlets, Fan, Garage Door, Shades / Blinds, Temperature/Humidity, Motion, Contact and Occupancy sensor, Door, Sprinkler, Valve, Air Quality, Smoke, C

421 lines 19.6 kB
import { SharedPolling } from './lib/SharedPolling.js'; // Include shared polling library import { getNestedValue, hasNestedKey } from './lib/utilities.js'; // Include utility function for nested value retrieval import { discordWebHooks } from './lib/discordWebHooks.js'; // Include Discord webhook library import { HttpsAgentManager } from './lib/HttpsAgentManager.js'; import axios from 'axios'; import mqtt from 'mqtt'; export class platformOutlet { platform; accessory; service; mqttClient; sharedPollingInstance; isReachable = true; // Track if the device is reachable // Device and configuration properties enableLogging = true; // Security, Self Signed Certificates rules ignoreHttpsCertErrors = false; trustedCert; // Ensure backward compatibility for shared polling sharedPolling = false; // Default to false sharedPollingId = ''; // Default to empty sharedPollingInterval = 5000; deviceId = ''; deviceType = ''; deviceName = ''; deviceManufacturer = ''; deviceModel = ''; deviceSerialNumber = ''; deviceFirmwareVersion = ''; urlON = ''; urlOFF = ''; url = ''; urlStatus = ''; statusStateParam = ''; statusOnCheck = ''; statusOffCheck = ''; inUseStateParam = ''; inUseOnCheck = ''; inUseOffCheck = ''; mqttReconnectInterval = ''; mqttBroker = ''; mqttPort = ''; mqttSwitch = ''; mqttInUse = ''; mqttUsername = ''; mqttPassword = ''; discordWebhook = ''; discordUsername = ''; discordAvatar = ''; discordMessage = ''; outletStates = { On: false, OutletInUse: false, }; httpsAgentManager; constructor(platform, accessory) { this.platform = platform; this.accessory = accessory; const device = this.accessory.context.device; this.deviceType = device.deviceType; this.deviceName = device.deviceName || 'NoName'; this.deviceManufacturer = device.deviceManufacturer || 'Stergo'; this.deviceModel = device.deviceModel || 'Outlet'; this.deviceSerialNumber = device.deviceSerialNumber || accessory.UUID; this.deviceFirmwareVersion = device.deviceFirmwareVersion || '0.0'; // From config this.enableLogging = device.enableLogging; // Security, Self Signed Certificates rules this.ignoreHttpsCertErrors = device.ignoreHttpsCertErrors || false; this.trustedCert = device.trustedCert || undefined; this.urlStatus = device.urlStatus; this.statusStateParam = device.stateName; this.statusOnCheck = device.onStatusValue; this.statusOffCheck = device.offStatusValue; this.urlON = device.urlON; this.urlOFF = device.urlOFF; this.inUseStateParam = device.inUseStateName; this.inUseOnCheck = device.inUseOnStatusValue; this.inUseOffCheck = device.inUseOffStatusValue; this.mqttReconnectInterval = device.mqttReconnectInterval || 60; // 60 sec default this.mqttBroker = device.mqttBroker; this.mqttPort = device.mqttPort; this.mqttSwitch = device.mqttSwitch; this.mqttInUse = device.mqttInUse; this.mqttUsername = device.mqttUsername; this.mqttPassword = device.mqttPassword; this.discordWebhook = device.discordWebhook; this.discordUsername = device.discordUsername || 'StergoSmart'; this.discordAvatar = device.discordAvatar || 'https://raw.githubusercontent.com/homebridge/branding/latest/logos/homebridge-color-round-stylized.png'; this.discordMessage = device.discordMessage; this.httpsAgentManager = new HttpsAgentManager(this.trustedCert, this.ignoreHttpsCertErrors, this.urlStatus); // Ensure backward compatibility for shared polling this.sharedPolling = device.sharedPolling ?? false; // Default shared polling to false this.sharedPollingId = device.sharedPollingId ?? ''; // Default shared polling group ID to an empty string this.sharedPollingInterval = device.sharedPollingInterval ?? 5000; // Set the polling interval to 5 sec or from config value if (this.sharedPolling && this.sharedPollingId) { const sharedPollingInstance = SharedPolling.registerPolling(this.sharedPollingId, this.urlStatus, this.platform, this.sharedPollingInterval, // Set the polling interval to 60 sec or from config value this.httpsAgentManager); // Subscribe to data updates sharedPollingInstance.on('dataUpdated', (data) => { this.isReachable = true; // ✅ Mark as reachable this.updateOutletStatusFromSharedData(data); }); sharedPollingInstance.on('dataError', () => { this.isReachable = false; // ❌ Mark as unreachable }); } else if (this.urlStatus) { this.getOn(); setInterval(this.getOn.bind(this), 5000); } if (!this.deviceType) { this.platform.log.warn(this.deviceName, ': Ignoring accessory; No deviceType defined.'); return; } if (this.deviceType === 'Outlet' && (this.urlON || this.mqttBroker)) { this.accessory.getService(this.platform.Service.AccessoryInformation) .setCharacteristic(this.platform.Characteristic.Manufacturer, this.deviceManufacturer) .setCharacteristic(this.platform.Characteristic.Model, this.deviceModel) .setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.deviceFirmwareVersion) .setCharacteristic(this.platform.Characteristic.SerialNumber, this.deviceSerialNumber); if (this.urlON || this.mqttBroker) { this.service = this.accessory.getService(this.platform.Service.Outlet) || this.accessory.addService(this.platform.Service.Outlet); this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.deviceName); if (this.urlON) { this.service.getCharacteristic(this.platform.Characteristic.On) .on('set', this.wrapSetHandler()) // Reuses setOn .on('get', this.wrapGetHandler('On')); } if (this.inUseOnCheck) { this.service.getCharacteristic(this.platform.Characteristic.OutletInUse) .on('get', this.wrapGetHandler('OutletInUse')); } if (this.mqttBroker) { this.initMQTT(); this.service.getCharacteristic(this.platform.Characteristic.On) .on('set', this.publishMQTTmessage.bind(this)); } } } } wrapGetHandler(state) { return (callback) => { if (!this.isReachable) { callback(new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */)); return; } callback(null, this.outletStates[state]); }; } wrapSetHandler() { return (value, callback) => { if (!this.isReachable) { callback(new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */)); return; } this.setOn(value, callback); // Preserves your original logic }; } getStatus(isOn) { return isOn ? 'ON' : 'OFF'; } updateOutletState(isOn, deviceName) { if (this.outletStates.On !== isOn) { this.outletStates.On = isOn; if (this.enableLogging) { this.platform.log.info(deviceName, `: Outlet is ${isOn ? 'ON' : 'OFF'}`); } this.service.updateCharacteristic(this.platform.Characteristic.On, isOn); } } updateOutletStatusFromSharedData(data) { this.processOutletGetData(data, true); } async getOn() { if (!this.urlStatus) { this.platform.log.warn(this.deviceName, ': Ignoring request; No status url defined.'); return; } try { this.isReachable = true; // ✅ Mark as reachable const httpsAgent = this.httpsAgentManager?.getAgent(); // Use HTTPS agent if applicable const response = await axios.get(this.urlStatus, { timeout: 8000, httpsAgent }); const data = response.data; this.platform.log.debug(`${this.deviceName}: Fetched JSON data:`, data); this.processOutletGetData(data, false); } catch (error) { this.isReachable = false; // ❌ Mark as unreachable const axiosError = error; if (axios.isAxiosError(axiosError)) { this.platform.log.warn(`${this.deviceName}: Axios error while fetching JSON:`, axiosError.message); } else { this.platform.log.warn(`${this.deviceName}: Unknown error occurred while fetching JSON.`); } } } processOutletGetData(data, isSharedData) { if (!data) { this.platform.log.warn(`${this.deviceName}: No data available for ${isSharedData ? 'shared data update' : 'fetching Outlet state'}.`); return; } if (this.statusStateParam && hasNestedKey(data, this.statusStateParam)) { const value = getNestedValue(data, this.statusStateParam, 'string'); // Adjust returnType as needed const valueType = typeof value; let statusOnCheck; let statusOffCheck; if (valueType === 'boolean') { statusOnCheck = true; statusOffCheck = false; } else if (valueType === 'number') { statusOnCheck = parseFloat(this.statusOnCheck); statusOffCheck = parseFloat(this.statusOffCheck); } else { statusOnCheck = this.statusOnCheck; statusOffCheck = this.statusOffCheck; } if (value === statusOnCheck) { this.updateOutletState(true, this.deviceName); } else if (value === statusOffCheck) { this.updateOutletState(false, this.deviceName); } else { this.platform.log.warn(this.deviceName, `: The value of ${this.statusStateParam} does not match statusOnCheck or statusOffCheck.`); } } if (this.inUseStateParam && hasNestedKey(data, this.inUseStateParam)) { const value = getNestedValue(data, this.inUseStateParam, 'string'); // Adjust returnType as needed const valueType = typeof value; let inUseOnCheck; let inUseOffCheck; if (valueType === 'boolean') { inUseOnCheck = true; inUseOffCheck = false; } else if (valueType === 'number') { inUseOnCheck = parseFloat(this.inUseOnCheck); inUseOffCheck = parseFloat(this.inUseOffCheck); } else { inUseOnCheck = this.inUseOnCheck; inUseOffCheck = this.inUseOffCheck; } // Update OutletInUse characteristic if (value === inUseOnCheck) { if (this.enableLogging && this.outletStates.OutletInUse !== true) { this.platform.log.info(this.deviceName, ': inUse set to: ', this.getStatus(true)); } this.outletStates.OutletInUse = true; } else if (value === inUseOffCheck) { if (this.enableLogging && this.outletStates.OutletInUse !== false) { this.platform.log.info(this.deviceName, ': inUse set to: ', this.getStatus(false)); } this.outletStates.OutletInUse = false; } else { this.platform.log.warn(this.deviceName, `: The value of ${this.inUseStateParam} does not match inUseOnCheck or inUseOffCheck.`); } this.service.updateCharacteristic(this.platform.Characteristic.OutletInUse, this.outletStates.OutletInUse); } else { if (this.inUseStateParam) { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.statusStateParam, 'in JSON'); } } } async setOn(value, callback) { this.outletStates.On = value; if (!this.urlON || !this.urlOFF) { this.platform.log.warn(this.deviceName, ': Ignoring request; No Outlet trigger url defined.'); callback(new Error('No Outlet trigger url defined.')); return; } if (this.outletStates.On) { this.url = this.urlON; this.platform.log.debug(this.deviceName, ': Setting power state to ON'); this.service.updateCharacteristic(this.platform.Characteristic.On, true); } else { this.url = this.urlOFF; this.platform.log.debug(this.deviceName, ': Setting power state to OFF'); this.service.updateCharacteristic(this.platform.Characteristic.On, false); } axios.get(this.url) .then(() => { this.isReachable = true; // ✅ Mark as reachable this.outletStates.OutletInUse = this.outletStates.On; this.service.updateCharacteristic(this.platform.Characteristic.OutletInUse, this.outletStates.OutletInUse); if (this.enableLogging) { this.platform.log.info('Success: Outlet ', this.deviceName, ' is: ', this.getStatus(this.outletStates.On)); } }) .catch((error) => { this.isReachable = false; // ❌ Mark as unreachable this.outletStates.On = !value; this.service.updateCharacteristic(this.platform.Characteristic.On, this.outletStates.On); this.platform.log.warn(this.deviceName, ': Setting power state to :', this.outletStates.On); this.platform.log.warn(this.deviceName, ': Error: ', error.message); }); if (this.discordWebhook) { this.initDiscordWebhooks(); } callback(null); } // // Connect to MQTT and update Outlets initMQTT() { const mqttSubscribedTopics = []; const mqttOptions = { keepalive: 10, protocol: 'mqtt', host: this.mqttBroker, port: Number(this.mqttPort), clientId: this.deviceName, clean: true, username: this.mqttUsername, password: this.mqttPassword, rejectUnauthorized: false, reconnectPeriod: Number(this.mqttReconnectInterval) * 1000, }; if (this.mqttSwitch) { mqttSubscribedTopics.push(this.mqttSwitch); } if (this.mqttInUse) { mqttSubscribedTopics.push(this.mqttInUse); } this.mqttClient = mqtt.connect(mqttOptions); this.mqttClient.on('connect', () => { this.isReachable = true; // ✅ Mark as reachable if (this.enableLogging) { this.platform.log.info(this.deviceName, ': MQTT Connected'); } this.mqttClient.subscribe(mqttSubscribedTopics, (err) => { if (!err) { if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Subscribed to: ', mqttSubscribedTopics.toString()); } } else { // Need to insert error handler this.platform.log.warn(this.deviceName, err.toString()); } }); }); this.mqttClient.on('message', (topic, message) => { if (topic === this.mqttSwitch) { if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Status set to: ', this.getStatus(Boolean(Number(message)))); } if (message.toString() === '1' || message.toString() === 'true') { this.outletStates.On = true; } if (message.toString() === '0' || message.toString() === 'false') { this.outletStates.On = false; } this.service.updateCharacteristic(this.platform.Characteristic.On, this.outletStates.On); // If discordWebhook is set if (this.discordWebhook) { this.initDiscordWebhooks(); } } if (topic === this.mqttInUse) { this.platform.log.info(this.deviceName, ': inUse set to: ', this.getStatus(Boolean(Number(message)))); if (message.toString() === '1' || message.toString() === 'true') { this.outletStates.OutletInUse = true; } if (message.toString() === '0' || message.toString() === 'false') { this.outletStates.OutletInUse = false; } this.service.updateCharacteristic(this.platform.Characteristic.OutletInUse, this.outletStates.OutletInUse); } }); this.mqttClient.on('offline', () => { this.isReachable = false; // ❌ Mark as unreachable this.platform.log.debug(this.deviceName, ': Client is offline'); }); this.mqttClient.on('reconnect', () => { this.platform.log.debug(this.deviceName, ': Reconnecting...'); }); this.mqttClient.on('close', () => { this.isReachable = false; // ❌ Mark as unreachable this.platform.log.debug(this.deviceName, ': Connection closed'); }); // Handle errors this.mqttClient.on('error', (err) => { this.isReachable = false; // ❌ Mark as unreachable this.platform.log.warn(this.deviceName, ': Connection error:', err); this.platform.log.warn(this.deviceName, ': Reconnecting in: ', this.mqttReconnectInterval, ' seconds.'); }); } // Function to publish a message publishMQTTmessage(value, callback) { this.platform.log.debug(this.deviceName, ': Setting power state to:', this.getStatus(!this.outletStates.On)); this.mqttClient.publish(this.mqttSwitch, String(Number(!this.outletStates.On)), { qos: 1, retain: true }, (err) => { if (err) { this.platform.log.debug(this.deviceName, ': Failed to publish message: ', err); } else { this.service.updateCharacteristic(this.platform.Characteristic.On, this.outletStates.On); this.platform.log.debug(this.deviceName, ': Message published successfully'); } }); callback(null); } initDiscordWebhooks() { // Prepare message just to send On Off status const message = this.deviceName + ': ' + this.discordMessage + this.getStatus(this.outletStates.On); const discord = new discordWebHooks(this.discordWebhook, this.discordUsername, this.discordAvatar, message); discord.discordSimpleSend().then((result) => { if (this.enableLogging) { this.platform.log.info(this.deviceName, ': ', result); } }); } } //# sourceMappingURL=platformOutletServices.js.map