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

1,047 lines 52.4 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'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ export class platformLightBulb { 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 = ''; useRGB = false; useBrightness255 = false; useColorTKelvin = false; rgbParamName = ''; urlLightBulbControl = ''; brightnessParamName = ''; saturationParamName = ''; hueParamName = ''; colorTemperatureParamName = ''; mqttReconnectInterval = 60; // Default to 60 seconds mqttBroker = ''; mqttPort = ''; mqttUsername = ''; mqttPassword = ''; mqttSwitch = ''; mqttRGB = ''; mqttBrightness = ''; mqttHue = ''; mqttSaturation = ''; mqttColorTemperature = ''; discordWebhook = ''; discordUsername = ''; discordAvatar = ''; discordMessage = ''; lightBulbStates = { On: false, Brightness: 100, Hue: 360, Saturation: 100, ColorTemperature: 500, RGB: 'FFFFFF', }; lightBulbRanges = new Map([ ['brightness', { min: 0, max: 100 }], ['hue', { min: 0, max: 360 }], ['saturation', { min: 0, max: 100 }], ['colorTemperature', { min: 153, max: 500 }], // HomeKit range in mired ]); 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 || 'LightBulb'; 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.useRGB = device.useRGB; this.useBrightness255 = device.useBrightness255; this.useColorTKelvin = device.useColorTKelvin; this.rgbParamName = device.rgbParamName; this.urlLightBulbControl = device.urlLightBulbControl; this.brightnessParamName = device.brightnessParamName; this.saturationParamName = device.saturationParamName; this.hueParamName = device.hueParamName; this.colorTemperatureParamName = device.colorTemperatureParamName; this.mqttReconnectInterval = device.mqttReconnectInterval || 60; // 60 sec default this.mqttBroker = device.mqttBroker; this.mqttPort = device.mqttPort; this.mqttUsername = device.mqttUsername; this.mqttPassword = device.mqttPassword; this.mqttSwitch = device.mqttSwitch; this.mqttRGB = device.mqttRGB; this.mqttBrightness = device.mqttBrightness; this.mqttHue = device.mqttHue; this.mqttSaturation = device.mqttSaturation; this.mqttColorTemperature = device.mqttColorTemperature; 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.updateLightBulbStatusFromSharedData(data); }); } else if (this.urlStatus) { this.getData(); setInterval(this.getData.bind(this), 5000); } if (!this.deviceType) { this.platform.log.warn(this.deviceName, ': Ignoring accessory; No deviceType defined.'); return; } if (this.deviceType === 'LightBulb' && (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); if (this.urlON || this.mqttBroker || this.urlLightBulbControl) { // get the Lightbulb service if it exists, otherwise create a new Lightbulb service this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb); // set the service name, this is what is displayed as the default name on the Home app 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('On', this.setOn)) .on('get', this.wrapGetHandler('On')); } if (this.urlLightBulbControl) { if (this.useRGB || this.brightnessParamName) { this.service.getCharacteristic(this.platform.Characteristic.Brightness) .on('set', this.wrapSetHandler('Brightness', this.setBrightness, this.useRGB ? this.updateAndSendRGB : undefined)) .on('get', this.wrapGetHandler('Brightness')); } if (this.useRGB || this.hueParamName) { this.service.getCharacteristic(this.platform.Characteristic.Hue) .on('set', this.wrapSetHandler('Hue', this.setHue, this.useRGB ? this.updateAndSendRGB : undefined)) .on('get', this.wrapGetHandler('Hue')); } if (this.useRGB || this.saturationParamName) { this.service.getCharacteristic(this.platform.Characteristic.Saturation) .on('set', this.wrapSetHandler('Saturation', this.setSaturation, this.useRGB ? this.updateAndSendRGB : undefined)) .on('get', this.wrapGetHandler('Saturation')); } if (this.colorTemperatureParamName) { this.service.getCharacteristic(this.platform.Characteristic.ColorTemperature) .on('set', this.wrapSetHandler('ColorTemperature', this.setColorTemperature)) .on('get', this.wrapGetHandler('ColorTemperature')); } } // We can now use MQTT if (this.mqttBroker) { this.initMQTT(); if (this.mqttSwitch) { this.service.getCharacteristic(this.platform.Characteristic.On) .on('set', this.publishMQTTmessage.bind(this, 'On')); } if (this.useRGB || this.mqttBrightness) { this.service.getCharacteristic(this.platform.Characteristic.Brightness) .on('set', this.publishMQTTmessage.bind(this, 'Brightness')); } if (this.useRGB || this.mqttHue) { this.service.getCharacteristic(this.platform.Characteristic.Hue) .on('set', this.publishMQTTmessage.bind(this, 'Hue')); } if (this.useRGB || this.mqttSaturation) { this.service.getCharacteristic(this.platform.Characteristic.Saturation) .on('set', this.publishMQTTmessage.bind(this, 'Saturation')); } if (this.mqttColorTemperature) { this.service.getCharacteristic(this.platform.Characteristic.ColorTemperature) .on('set', this.publishMQTTmessage.bind(this, 'ColorTemperature')); } } } } } 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.lightBulbStates[state]); }; } wrapSetHandler(state, setFn, postSetFn) { return (value, callback) => { if (!this.isReachable) { callback(new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */)); return; } setFn.call(this, value, callback); if (postSetFn) { postSetFn.call(this); } }; } // Silly function :) getStatus(isOn) { return isOn ? 'ON' : 'OFF'; } updateSwitchState(isOn, deviceName) { if (this.lightBulbStates.On !== isOn) { this.lightBulbStates.On = isOn; if (this.enableLogging) { this.platform.log.info(deviceName, `: Light is ${isOn ? 'ON' : 'OFF'}`); } this.service.updateCharacteristic(this.platform.Characteristic.On, isOn); } } updateLightBulbStatusFromSharedData(data) { this.processLightBulbStatusData(data, true); } async getData() { // Check if we have Status URL setup 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.processLightBulbStatusData(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.`); } } } processLightBulbStatusData(data, isSharedData) { if (!data) { this.platform.log.warn(`${this.deviceName}: No data available for ${isSharedData ? 'shared data update' : 'fetching Light Bulb state'}.`); return; } // Check if provided For On/Off KEY EXIST in JSON if (this.statusStateParam && hasNestedKey(data, this.statusStateParam)) { 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 if (this.statusStateParam && !hasNestedKey(data, this.statusStateParam)) { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.statusStateParam, 'in JSON'); } if (this.useRGB && this.rgbParamName && hasNestedKey(data, this.rgbParamName)) { // Update RGB and remove # if present let value = getNestedValue(data, this.rgbParamName, 'string'); // Adjust returnType as needed if (typeof value === 'string' && value.startsWith('#')) { value = value.slice(1); } this.lightBulbStates.RGB = value; //this.platform.log.debug(this.deviceName, ': RGB: ', value); this.convertToHSV(); // Update all needed characteristics if (!this.brightnessParamName) { this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.lightBulbStates.Brightness); } this.service.updateCharacteristic(this.platform.Characteristic.Hue, this.lightBulbStates.Hue); this.service.updateCharacteristic(this.platform.Characteristic.Saturation, this.lightBulbStates.Saturation); } else if (this.rgbParamName && !hasNestedKey(data, this.rgbParamName)) { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.rgbParamName, 'in JSON'); } if (this.brightnessParamName && hasNestedKey(data, this.brightnessParamName)) { // Update Brightness let value = getNestedValue(data, this.brightnessParamName, 'string'); // Adjust returnType as needed if (this.useBrightness255) { const convertedValue = this.convertBrightness(Number(value), 1); // convert back to 0-100 if (convertedValue !== undefined) { value = convertedValue; } else { this.platform.log.warn(this.deviceName, ': Error: Invalid brightness value'); } } const fixedBrightnessValue = this.checkAndFixValue('brightness', Number(value)); if (this.lightBulbStates.Brightness !== fixedBrightnessValue) { this.lightBulbStates.Brightness = fixedBrightnessValue; this.service.updateCharacteristic(this.platform.Characteristic.Brightness, fixedBrightnessValue); if (this.enableLogging) { this.platform.log.info(this.deviceName, `: Brightness SET to: ${fixedBrightnessValue}`); } } } else if (this.brightnessParamName && !hasNestedKey(data, this.brightnessParamName)) { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.brightnessParamName, 'in JSON'); } if (!this.useRGB && this.saturationParamName && hasNestedKey(data, this.saturationParamName)) { // Update Saturation const value = getNestedValue(data, this.saturationParamName, 'string'); // Adjust returnType as needed const fixedSaturationValue = this.checkAndFixValue('saturation', Number(value)); if (this.lightBulbStates.Saturation !== fixedSaturationValue) { this.lightBulbStates.Saturation = fixedSaturationValue; this.service.updateCharacteristic(this.platform.Characteristic.Saturation, fixedSaturationValue); if (this.enableLogging) { this.platform.log.info(this.deviceName, `: Saturation SET to: ${fixedSaturationValue}`); } } } else if (this.saturationParamName && !hasNestedKey(data, this.saturationParamName)) { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.saturationParamName, 'in JSON'); } if (!this.useRGB && this.hueParamName && hasNestedKey(data, this.hueParamName)) { // Update Hue const value = getNestedValue(data, this.hueParamName, 'string'); // Adjust returnType as needed const fixedHueValue = this.checkAndFixValue('hue', Number(value)); if (this.lightBulbStates.Hue !== fixedHueValue) { this.lightBulbStates.Hue = fixedHueValue; this.service.updateCharacteristic(this.platform.Characteristic.Hue, fixedHueValue); if (this.enableLogging) { this.platform.log.info(this.deviceName, `: Hue SET to: ${fixedHueValue}`); } } } else if (this.hueParamName && !hasNestedKey(data, this.hueParamName)) { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.hueParamName, 'in JSON'); } if (this.colorTemperatureParamName && hasNestedKey(data, this.colorTemperatureParamName)) { // Update Color Temperature let value = getNestedValue(data, this.colorTemperatureParamName, 'string'); // Adjust returnType as needed if (this.useColorTKelvin) { value = this.convertColorTemperature(Number(value), 0); // Convert from Kelvin to mired } const fixedColorTemperatureValue = this.checkAndFixValue('colorTemperature', Number(value)); if (this.lightBulbStates.ColorTemperature !== fixedColorTemperatureValue) { this.lightBulbStates.ColorTemperature = fixedColorTemperatureValue; this.service.updateCharacteristic(this.platform.Characteristic.ColorTemperature, fixedColorTemperatureValue); if (this.enableLogging) { this.platform.log.info(this.deviceName, `: Light Color Temperature SET to: ${fixedColorTemperatureValue}`); } } } else if (this.colorTemperatureParamName && !hasNestedKey(data, this.colorTemperatureParamName)) { this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.colorTemperatureParamName, 'in JSON'); } } /** * Handle "SET" requests from HomeKit * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. */ async setOn(value, callback) { // this.lightBulbStates.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; } if (this.lightBulbStates.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); } this.isReachable = true; // ✅ Mark as reachable axios.get(this.url) // We are not going to wait, we will presume everything is OK and if it's not Error handler will handle it // .then((response) => { // handle success // callback(response.data); // this.platform.log.debug('Success: ', error); // }) .catch((error) => { this.isReachable = false; // ❌ Mark as unreachable // handle error // Let's reverse On value since we couldn't reach URL this.lightBulbStates.On = !value; this.service.updateCharacteristic(this.platform.Characteristic.On, this.lightBulbStates.On); this.platform.log.warn(this.deviceName, ': Setting power state to :', this.lightBulbStates.On); this.platform.log.warn(this.deviceName, ': Error: ', error.message); //callback(error); }); // If is set dicordWebhook address if (this.discordWebhook) { this.initDiscordWebhooks(); } callback(null); if (this.enableLogging) { this.platform.log.info('Success: Switch ', this.deviceName, ' is: ', this.getStatus(this.lightBulbStates.On)); } } async setBrightness(value, callback) { try { this.isReachable = true; // ✅ Mark as reachable let brightnessValue = value; // Check if useBrightness255 is true and convert the brightness value if (this.useBrightness255) { const convertedValue = this.convertBrightness(value, 0); if (convertedValue !== undefined) { brightnessValue = convertedValue; } else { this.platform.log.warn(this.deviceName, ': Error: Invalid brightness value'); } } // Construct the URL without the brightness parameter const url = `${this.urlLightBulbControl}`; const response = await axios({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json', }, data: { [this.brightnessParamName]: brightnessValue, }, }); if (response.status === 200) { this.service.updateCharacteristic(this.platform.Characteristic.Brightness, value); this.lightBulbStates.Brightness = value; if (this.enableLogging) { this.platform.log.info('Success: Brightness', this.deviceName, 'is set to:', value); } callback(null); } else { callback(new Error('Failed to set brightness')); } } catch (e) { this.isReachable = false; // ❌ Mark as unreachable const error = e; if (axios.isAxiosError(error)) { this.platform.log.warn(this.deviceName, ': Error: URL Status check:', error.message); } callback(e); } } async setHue(value, callback) { if (!this.useRGB && this.hueParamName) { try { this.isReachable = true; // ✅ Mark as reachable // Construct the URL with the hue parameter const url = `${this.urlLightBulbControl}`; const response = await axios({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json', }, data: { [this.hueParamName]: value, }, }); if (response.status === 200) { this.service.updateCharacteristic(this.platform.Characteristic.Hue, value); this.lightBulbStates.Hue = value; if (this.enableLogging) { this.platform.log.info('Success: Hue', this.deviceName, 'is set to:', value); } callback(null); } else { callback(new Error('Failed to set hue')); } } catch (e) { this.isReachable = false; // ❌ Mark as unreachable const error = e; if (axios.isAxiosError(error)) { this.platform.log.warn(this.deviceName, ': Error: URL Status check:', error.message); } callback(e); } } if ((this.useRGB && !this.hueParamName) || (this.useRGB && this.hueParamName)) { this.service.updateCharacteristic(this.platform.Characteristic.Hue, value); this.lightBulbStates.Hue = value; if (this.enableLogging) { this.platform.log.info('Success but not sent: Hue', this.deviceName, 'is SET to:', value); } } } async setSaturation(value, callback) { if (!this.useRGB && this.saturationParamName) { try { this.isReachable = true; // ✅ Mark as reachable // Construct the URL with the saturation parameter const url = `${this.urlLightBulbControl}`; const response = await axios({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json', }, data: { [this.saturationParamName]: value, }, }); if (response.status === 200) { this.service.updateCharacteristic(this.platform.Characteristic.Saturation, value); this.lightBulbStates.Saturation = value; if (this.enableLogging) { this.platform.log.info('Success: Saturation', this.deviceName, 'is set to:', value); } callback(null); } else { callback(new Error('Failed to set saturation')); } } catch (e) { this.isReachable = false; // ❌ Mark as unreachable const error = e; if (axios.isAxiosError(error)) { this.platform.log.warn(this.deviceName, ': Error: URL Status check:', error.message); } callback(e); } } if ((this.useRGB && !this.saturationParamName) || (this.useRGB && this.saturationParamName)) { this.service.updateCharacteristic(this.platform.Characteristic.Saturation, value); this.lightBulbStates.Saturation = value; if (this.enableLogging) { this.platform.log.info('Success but not sent: Saturation', this.deviceName, 'is SET to:', value); } callback(null); } } async setColorTemperature(value, callback) { try { this.isReachable = true; // ✅ Mark as reachable // Convert from mired to Kelvin if useColorTKelvin is set let colorTemperature = value; if (this.useColorTKelvin) { colorTemperature = this.convertColorTemperature(colorTemperature, 1); // Convert from mired to Kelvin } // Construct the URL with the color temperature parameter const url = `${this.urlLightBulbControl}`; const response = await axios({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json', }, data: { [this.colorTemperatureParamName]: colorTemperature, }, }); if (response.status === 200) { this.service.updateCharacteristic(this.platform.Characteristic.ColorTemperature, value); this.lightBulbStates.ColorTemperature = value; if (this.enableLogging) { this.platform.log.info('Success: Color Temperature', this.deviceName, 'is SET to:', value); } callback(null); } else { callback(new Error('Failed to set Color Temperature')); } } catch (e) { this.isReachable = false; // ❌ Mark as unreachable const error = e; if (axios.isAxiosError(error)) { this.platform.log.warn(this.deviceName, ': Error: URL Status check:', error.message); } callback(e); } } // Connect to MQTT and update Lights 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, }; // Subscribe to all MQTT Topics if (this.mqttSwitch) { mqttSubscribedTopics.push(this.mqttSwitch); } if (this.mqttBrightness) { mqttSubscribedTopics.push(this.mqttBrightness); } if (this.mqttColorTemperature) { mqttSubscribedTopics.push(this.mqttColorTemperature); } if (!this.useRGB) { if (this.mqttBrightness) { mqttSubscribedTopics.push(this.mqttBrightness); } if (this.mqttHue) { mqttSubscribedTopics.push(this.mqttHue); } if (this.mqttSaturation) { mqttSubscribedTopics.push(this.mqttSaturation); } } else { if (this.mqttRGB) { mqttSubscribedTopics.push(this.mqttRGB); } } 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) => { //this.platform.log(this.deviceName,': Received message: ', Number(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.lightBulbStates.On = true; } if (message.toString() === '0' || message.toString() === 'false') { this.lightBulbStates.On = false; } this.service.updateCharacteristic(this.platform.Characteristic.On, this.lightBulbStates.On); // If is set dicordWebhook address if (this.discordWebhook) { this.initDiscordWebhooks(); } } if (topic === this.mqttBrightness) { let brightness = Number(message); if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Brightness before convert is : ', brightness); } if (this.useBrightness255) { const convertedValue = this.convertBrightness(brightness, 1); if (convertedValue !== undefined) { brightness = convertedValue; } else { this.platform.log.warn(this.deviceName, ': Error: Invalid brightness value'); } } const fixedBrightness = this.checkAndFixValue('brightness', brightness); this.lightBulbStates.Brightness = fixedBrightness; if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Brightness after convert is : ', fixedBrightness); } if (this.useRGB) { this.convertToHSV(); } this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.lightBulbStates.Brightness); if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Brightness SET to: ', this.lightBulbStates.Brightness); } } if (topic === this.mqttHue) { const hue = Number(message); const fixedHue = this.checkAndFixValue('hue', hue); this.lightBulbStates.Hue = fixedHue; if (this.useRGB) { this.convertToHSV(); } this.service.updateCharacteristic(this.platform.Characteristic.Hue, this.lightBulbStates.Hue); if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Hue SET to: ', this.lightBulbStates.Hue); } } if (topic === this.mqttSaturation) { const saturation = Number(message); const fixedSaturation = this.checkAndFixValue('saturation', saturation); this.lightBulbStates.Saturation = fixedSaturation; if (this.useRGB) { this.convertToHSV(); } this.service.updateCharacteristic(this.platform.Characteristic.Saturation, this.lightBulbStates.Saturation); if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Saturation SET to: ', this.lightBulbStates.Saturation); } } if (topic === this.mqttColorTemperature) { let colorTemperature = Number(message); if (this.useColorTKelvin) { colorTemperature = this.convertColorTemperature(colorTemperature, 0); // Convert from Kelvin to mired } const fixedColorTemperature = this.checkAndFixValue('colorTemperature', colorTemperature); this.lightBulbStates.ColorTemperature = fixedColorTemperature; this.service.updateCharacteristic(this.platform.Characteristic.ColorTemperature, this.lightBulbStates.ColorTemperature); if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Color Temperature SET to: ', this.lightBulbStates.ColorTemperature); } } if (topic === this.mqttRGB) { let rgb = String(message); if (rgb.startsWith('#')) { rgb = rgb.slice(1); } this.lightBulbStates.RGB = rgb; // Here should be a check if value is in proper range if (this.enableLogging) { this.platform.log.info(this.deviceName, ': RGB SET to: ', rgb); } if (this.useRGB) { this.convertToHSV(); } if (this.enableLogging) { this.platform.log.info(this.deviceName, ': RGB IS: ', this.lightBulbStates.RGB); } this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.lightBulbStates.Brightness); this.service.updateCharacteristic(this.platform.Characteristic.Hue, this.lightBulbStates.Hue); this.service.updateCharacteristic(this.platform.Characteristic.Saturation, this.lightBulbStates.Saturation); if (this.enableLogging) { this.platform.log.info(this.deviceName, ': Brightness SET to: ', this.lightBulbStates.Brightness); this.platform.log.info(this.deviceName, ': Hue SET to: ', this.lightBulbStates.Hue); this.platform.log.info(this.deviceName, ': Saturation SET to: ', this.lightBulbStates.Saturation); } } }); 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.'); //this.mqttClient.end(); }); } // Function to publish a message publishMQTTmessage(what, value, callback) { if (this.enableLogging) { this.platform.log.info(this.deviceName, `: Setting ${what} to:`, value); } let topic; let message; // Always publish the On state if (what === 'On') { message = String(Number(!this.lightBulbStates.On)); topic = this.mqttSwitch; } // Always publish the ColorTemperature if mqttColorTemperature is set if (what === 'ColorTemperature' && this.mqttColorTemperature) { const colorTemperature = this.checkAndFixValue('colorTemperature', Number(value)); this.lightBulbStates.ColorTemperature = colorTemperature; message = String(colorTemperature); topic = this.mqttColorTemperature; } // Always publish the Brightness if mqttBrightness is set if (what === 'Brightness') { let brightness = this.checkAndFixValue('brightness', Number(value)); this.lightBulbStates.Brightness = brightness; if (this.useBrightness255) { const convertedValue = this.convertBrightness(brightness, 0); if (convertedValue !== undefined) { brightness = convertedValue; } else { this.platform.log.warn(this.deviceName, ': Error: Invalid brightness value'); } } // This needs better Logic also i have redundant code in Brightnes, Hue and Saturation regarding RGB if (this.mqttBrightness) { message = String(brightness); topic = this.mqttBrightness; } else { const { Hue, Saturation, Brightness } = this.lightBulbStates; const { red, green, blue } = this.convertToRGB(Hue, Saturation, Brightness); const rgbValue = `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`; this.lightBulbStates.RGB = rgbValue; if (this.enableLogging) { this.platform.log.info(this.deviceName, 'Updated lightBulbStates.RGB:', this.lightBulbStates.RGB); } message = String(rgbValue); topic = this.mqttRGB; } if (this.enableLogging) { this.platform.log.info(this.deviceName, 'Updated lightBulbStates.Brightness:', brightness); } } if (what === 'Hue') { const hue = this.checkAndFixValue('hue', Number(value)); this.lightBulbStates.Hue = hue; if (this.useRGB) { const { Hue, Saturation, Brightness } = this.lightBulbStates; const { red, green, blue } = this.convertToRGB(Hue, Saturation, Brightness); const rgbValue = `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`; this.lightBulbStates.RGB = rgbValue; this.platform.log.info(this.deviceName, 'Updated lightBulbStates.RGB:', this.lightBulbStates.RGB); message = String(rgbValue); topic = this.mqttRGB; } else { message = String(hue); topic = this.mqttHue; } if (this.enableLogging) { this.platform.log.info(this.deviceName, 'Updated lightBulbStates.Hue:', hue); } } if (what === 'Saturation') { const saturation = this.checkAndFixValue('saturation', Number(value)); this.lightBulbStates.Saturation = saturation; if (this.useRGB) { const { Hue, Saturation, Brightness } = this.lightBulbStates; const { red, green, blue } = this.convertToRGB(Hue, Saturation, Brightness); const rgbValue = `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`; this.lightBulbStates.RGB = rgbValue; this.platform.log.info(this.deviceName, 'Updated lightBulbStates.RGB:', this.lightBulbStates.RGB); message = String(rgbValue); topic = this.mqttRGB; } else { message = String(saturation); topic = this.mqttSaturation; } if (this.enableLogging) { this.platform.log.info(this.deviceName, 'Updated lightBulbStates.Saturation:', saturation); } } if (topic && message) { this.mqttClient.publish(topic, message, { qos: 1, retain: true }, (err) => { if (err) { this.platform.log.debug(this.deviceName, ': Failed to publish message: ', err); } else { this.platform.log.debug(this.deviceName, ': Message published successfully'); } }); } callback(null); } // Helper function to convert RGB to HSV and update lightBulbStates convertToHSV() { // Parse the RGB values const r = parseInt(this.lightBulbStates.RGB.substring(0, 2), 16); const g = parseInt(this.lightBulbStates.RGB.substring(2, 4), 16); const b = parseInt(this.lightBulbStates.RGB.substring(4, 6), 16); // Convert RGB to HSV const rNorm = r / 255; const gNorm = g / 255; const bNorm = b / 255; const max = Math.max(rNorm, gNorm, bNorm); const min = Math.min(rNorm, gNorm, bNorm); const delta = max - min; let hue = 0; let saturation = 0; const value = max; if (delta !== 0) { saturation = delta / max; switch (max) { case rNorm: hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0); break; case gNorm: hue = (bNorm - rNorm) / delta + 2; break; case bNorm: hue = (rNorm - gNorm) / delta + 4; break; } hue /= 6; } // Update lightBulbStates this.lightBulbStates.Hue = this.checkAndFixValue('hue', hue * 360); this.lightBulbStates.Saturation = this.checkAndFixValue('saturation', saturation * 100); if (!this.brightnessParamName) { this.lightBulbStates.Brightness = this.checkAndFixValue('brightness', value * 100); } } // Helper function to convert HSV to RGB convertToRGB(h, s, v) { s /= 100; v /= 100; const c = v * s; const x = c * (1 - Math.abs((h / 60) % 2 - 1)); const m = v - c; let r = 0, g = 0, b = 0; if (0 <= h && h < 60) { r = c; g = x; b = 0; } else if (60 <= h && h < 120) { r = x; g = c; b = 0; } else if (120 <= h && h < 180) { r = 0; g = c; b = x; } else if (180 <= h && h < 240) { r = 0; g = x; b = c; } else if (240 <= h && h < 300) { r = x; g = 0; b = c; } else if (300 <= h && h < 360) { r = c; g = 0; b = x; } return { red: Math.round((r + m) * 255), green: Math.round((g + m) * 255), blue: Math.round((b + m) * 255), }; } // Function to update RGB value and send it via HTTP or MQTT async updateAndSendRGB() { const { Hue, Saturation, Brightness } = this.lightBulbStates; //this.platform.log.debug('Current lightBulbStates - Hue:', Hue, ', Saturation:', Saturation, ', Brightness:', Brightness); const { red, green, blue } = this.convertToRGB(Hue, Saturation, Brightness); //this.platform.log.debug('Converted RGB values - Red:', red, ', Green:', green, ', Blue:', blue); const rgbValue = `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`; //this.platform.log.debug('Formatted RGB value:', rgbValue); this.lightBulbStates.RGB = rgbValue; // Send the RGB value via HTTP const url = `${this.urlLightBulbControl}`; const postData = new URLSearchParams(); postData.append(this.rgbParamName, rgbValue); //this.platform.log.debug('Sending RGB value to URL:', url); //this.platform.log.debug('POST data:', postData.toString()); try { const response = await axios({ method: 'POST', url: url, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, data: postData, }); //this.platform.log.debug('HTTP POST response status:', response.status); //this.platform.log.debug('HTTP POST response data:', JSON.stringify(response.data)); if (response.status === 200) { if (this.enableLogging) { this.platform.log.info('Success: RGB value sent:', rgbValue); } } else { this.platform.log.error('Failed to send RGB value'); } } catch (e) { const error = e; if (axios.isAxiosError(error)) { this.platform.log.error('Error sending RGB value:', error.message); } } //this.platform.log.debug('Finished updateAndSendRGB function'); } // Helper function to check and fix the value when converting from RGB to HSV // also to ensure the value is with