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

382 lines 18.3 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 platformSwitch { 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 = ''; mqttReconnectInterval = ''; mqttBroker = ''; mqttPort = ''; mqttSwitch = ''; mqttUsername = ''; mqttPassword = ''; discordWebhook = ''; discordUsername = ''; discordAvatar = ''; discordMessage = ''; switchStates = { On: false }; individualPollingInterval; // Individual polling interval httpsAgentManager; constructor(platform, accessory) { this.platform = platform; this.accessory = accessory; const device = this.accessory.context.device; // Initialize device properties from the accessory context this.deviceType = device.deviceType; this.deviceName = device.deviceName || 'NoName'; this.deviceManufacturer = device.deviceManufacturer || 'Stergo'; this.deviceModel = device.deviceModel || 'Switch'; this.deviceSerialNumber = device.deviceSerialNumber || accessory.UUID; this.deviceFirmwareVersion = device.deviceFirmwareVersion || '0.0'; 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.mqttReconnectInterval = device.mqttReconnectInterval || '60'; this.mqttBroker = device.mqttBroker; this.mqttPort = device.mqttPort; this.mqttSwitch = device.mqttSwitch; 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 5 sec or from config value this.httpsAgentManager); // Subscribe to data updates sharedPollingInstance.on('dataUpdated', (data) => { this.isReachable = true; // ✅ Mark as reachable this.updateSwitchStatusFromSharedData(data); }); sharedPollingInstance.on('dataError', () => { this.isReachable = false; // ❌ Mark as unreachable }); } else if (this.urlStatus) { this.startIndividualPolling(); setInterval(this.startIndividualPolling.bind(this), 5000); } // Initialize the device accessory with HomeKit services and characteristics this.initializeAccessory(); } initializeAccessory() { if (!this.deviceType) { this.platform.log.warn(`${this.deviceName}: Ignoring accessory; No deviceType defined.`); this.cleanup(); // Stop active polling if accessory is invalid return; } if (this.deviceType === 'Switch' && !(this.urlON || this.mqttBroker)) { this.platform.log.warn(`${this.deviceName}: Ignoring accessory; Missing required configuration.`); this.cleanup(); // Stop active polling if configuration is invalid - needs better handling return; } // Configure Accessory Information and Services for valid configurations if (this.deviceType === 'Switch' && (this.urlON || this.mqttBroker)) { // Set accessory information 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); // Add or get the Switch service this.service = this.accessory.getService(this.platform.Service.Switch) || this.accessory.addService(this.platform.Service.Switch); this.service.setCharacteristic(this.platform.Characteristic.Name, this.deviceName); // Configure HTTP-based state changes if (this.urlON) { this.service.getCharacteristic(this.platform.Characteristic.On) .on('set', this.wrapSetHandler()) .on('get', this.wrapGetHandler('On')); } // Configure MQTT if provided if (this.mqttBroker) { this.initMQTT(); // Initialize MQTT functionality this.service.getCharacteristic(this.platform.Characteristic.On) .on('set', this.publishMQTTmessage.bind(this)); // Publish MQTT messages when state changes } } } 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.switchStates[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); }; } updateSwitchState(isOn, deviceName) { if (this.switchStates.On !== isOn) { this.switchStates.On = isOn; if (this.enableLogging) { this.platform.log.info(`${deviceName}: Switch is ${isOn ? 'ON' : 'OFF'}`); } this.service.updateCharacteristic(this.platform.Characteristic.On, isOn); } } updateSwitchStatusFromSharedData(data) { this.processSwitchGetData(data, true); } async startIndividualPolling() { 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 centralized 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.processSwitchGetData(data, false); } catch (error) { this.isReachable = false; // ❌ Mark as unreachable if (this.enableLogging) { 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.`); } } } } processSwitchGetData(data, isSharedData) { if (!data) { this.platform.log.warn(`${this.deviceName}: No data available for ${isSharedData ? 'shared data update' : 'fetching Switch state'}.`); return; } // Check if we have value if (this.statusStateParam && hasNestedKey(data, this.statusStateParam)) { // Proceed with processing the data const value = getNestedValue(data, this.statusStateParam, 'string'); // Adjust returnType as needed const valueType = typeof value; // Convert statusOnCheck and statusOffCheck to the appropriate type 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; } // Check and update switch state if (value === statusOnCheck) { this.updateSwitchState(true, this.deviceName); } else if (value === statusOffCheck) { this.updateSwitchState(false, this.deviceName); } else { this.platform.log.warn(this.deviceName, `: The value of ${this.statusStateParam} does not match statusOnCheck or statusOffCheck.`); } } else { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.statusStateParam, 'in JSON'); } } cleanup() { const device = this.accessory.context.device; this.platform.log.info(`Cleaning up device: ${device.deviceName}`); // Example usage of 'device' // Cleanup shared polling if (this.sharedPolling && this.sharedPollingId) { if (this.sharedPollingInstance) { SharedPolling.unregisterPolling(this.sharedPollingId); this.sharedPollingInstance = undefined; // Ensure this instance doesn't retain a reference this.platform.log.info(`${device.deviceName}: Unregistered shared polling.`); } else { this.platform.log.info(`${device.deviceName}: No shared polling instance to cleanup.`); } } // Cleanup individual polling if (this.individualPollingInterval) { clearInterval(this.individualPollingInterval); this.individualPollingInterval = undefined; if (this.enableLogging) { this.platform.log.info(`${device.deviceName}: Stopped individual polling.`); } } } /** * Handles "SET" requests from HomeKit. * This method is triggered when the user changes the state of a switch. */ async setOn(value, callback) { this.switchStates.On = value; if (!this.urlON || !this.urlOFF) { this.platform.log.warn(`${this.deviceName}: Ignoring request; No Switch trigger URL defined.`); callback(new Error('No Switch trigger URL defined.')); return; } // Determine the URL to use based on the state this.url = this.switchStates.On ? this.urlON : this.urlOFF; this.platform.log.debug(`${this.deviceName}: Setting power state to ${this.switchStates.On ? 'ON' : 'OFF'}`); // Update characteristic this.service.updateCharacteristic(this.platform.Characteristic.On, this.switchStates.On); try { this.isReachable = true; // ✅ Mark as reachable // Send the HTTP request to trigger the switch state await axios.get(this.url); // If Discord Webhook is enabled, send status update if (this.discordWebhook) { this.initDiscordWebhooks(); } // Log the success if logging is enabled if (this.enableLogging) { this.platform.log.info(`Success: Switch ${this.deviceName} is ${this.switchStates.On ? 'ON' : 'OFF'}`); } callback(null); // Indicate success to HomeKit } catch (error) { this.isReachable = false; // ❌ Mark as unreachable // Handle errors and revert the switch state const errorMessage = error.message; this.switchStates.On = !this.switchStates.On; // Revert state this.service.updateCharacteristic(this.platform.Characteristic.On, this.switchStates.On); this.platform.log.warn(`${this.deviceName}: Error setting power state: ${errorMessage}`); callback(new Error(errorMessage)); // Indicate error to HomeKit } } 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); } 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) { this.platform.log.info(`${this.deviceName}: Subscribed to topics - ${mqttSubscribedTopics.toString()}`); } else { this.platform.log.warn(`${this.deviceName}: MQTT subscription error - ${err.message}`); } }); }); this.mqttClient.on('message', (topic, message) => { if (topic === this.mqttSwitch) { this.platform.log.info(`${this.deviceName}: MQTT message received - ${message.toString()}`); this.switchStates.On = message.toString() === '1' || message.toString() === 'true'; this.service.updateCharacteristic(this.platform.Characteristic.On, this.switchStates.On); if (this.discordWebhook) { this.initDiscordWebhooks(); } } }); this.mqttClient.on('error', (err) => { this.isReachable = false; // ❌ Mark as unreachable this.platform.log.warn(`${this.deviceName}: MQTT connection error - ${err.message}`); this.platform.log.warn(`${this.deviceName}: Attempting reconnection in ${this.mqttReconnectInterval} seconds`); }); // Additional event handlers for connection state this.mqttClient.on('offline', () => { this.isReachable = false; 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; this.platform.log.debug(this.deviceName, ': Connection closed'); }); } publishMQTTmessage(value, callback) { const message = String(Number(value)); this.mqttClient.publish(this.mqttSwitch, message, { qos: 1, retain: true }, (err) => { if (err) { this.platform.log.warn(`${this.deviceName}: Failed to publish MQTT message - ${err.message}`); } else { this.platform.log.info(`${this.deviceName}: MQTT message published successfully - ${message}`); } callback(null); }); } initDiscordWebhooks() { const message = `${this.deviceName}: ${this.discordMessage} ${this.switchStates.On ? 'ON' : 'OFF'}`; const discord = new discordWebHooks(this.discordWebhook, this.discordUsername, this.discordAvatar, message); discord.discordSimpleSend().then((result) => { this.platform.log.info(`${this.deviceName}: Discord Webhook result - ${result}`); }).catch((error) => { this.platform.log.warn(`${this.deviceName}: Discord Webhook error - ${error.message}`); }); } } //# sourceMappingURL=platformSwitchServices.js.map