UNPKG

homebridge-proflame

Version:

Homebridge plugin for Proflame fireplaces (Single Accessory) with all features including lamp control

421 lines (390 loc) 14 kB
/** * Homebridge Proflame (Single Accessory) - All Features with Updated Thermostat Fields * * Features: * - Main On/Off (main_mode) * - Pilot Mode * - Fan with 6 speeds (0..6) * - Split Flow * - Auxiliary Output * - Flame Control with 6 levels (0..6) * - Thermostat: uses "temperature_set" for target and "room_temperature" for current * - Lamp/Light with 6 levels (0..6) * * The plugin sends a keep-alive ping ("PROFLAMEPING") every 30 seconds and reconnects after 10 seconds. * Connection open/close events are logged only on first connection (subsequent events at debug level). */ const WebSocket = require('ws'); let Service, Characteristic; module.exports = (homebridge) => { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; homebridge.registerAccessory('homebridge-proflame', 'ProflameFireplace', ProflameFireplaceAccessory); }; class ProflameFireplaceAccessory { constructor(log, config) { this.log = log; this.config = config || {}; this.log.info('[Proflame] Constructor called'); // Required fields this.name = this.config.name || 'My Fireplace'; this.ip = this.config.ip || '192.168.1.252'; // Optional features this.pilotEnabled = !!this.config.pilotEnabled; this.fanEnabled = !!this.config.fanEnabled; this.splitFlowEnabled = !!this.config.splitFlowEnabled; this.auxEnabled = !!this.config.auxEnabled; this.flameControlEnabled = !!this.config.flameControlEnabled; this.thermostatEnabled = !!this.config.thermostatEnabled; this.lampEnabled = !!this.config.lampEnabled; // Device state this.isOn = false; // main_mode: 0 = off, otherwise on this.flameLevel = 0; // 0 to 6 this.fanSpeed = 0; // 0 to 6 // Thermostat: Device uses Fahrenheit tenths. // Target temperature is sent via "temperature_set" this.targetTemperature = 770; // e.g. 770 => 77°F, ~25°C // Current room temperature is received via "room_temperature" this.currentTemp = 710; // e.g. 710 => 71°F, ~21.7°C this.lampLevel = 0; // 0 to 6 this.services = []; this.initialConnectionLogged = false; this.setupServices(); this.setupWebSocket(); } setupServices() { this.log.info('[Proflame] Setting up services'); const infoService = new Service.AccessoryInformation() .setCharacteristic(Characteristic.Manufacturer, 'Proflame') .setCharacteristic(Characteristic.Model, 'Fireplace') .setCharacteristic(Characteristic.SerialNumber, 'SingleAccessory'); this.services.push(infoService); // Main On/Off Switch const mainSwitch = new Service.Switch(this.name); mainSwitch .getCharacteristic(Characteristic.On) .on('get', cb => cb(null, this.isOn)) .on('set', (val, cb) => { this.isOn = val; this.log.info(`[Proflame] main_mode => ${val ? 1 : 0}`); this.sendMainMode(val ? 1 : 0); cb(null); }); this.services.push(mainSwitch); // Pilot Switch if (this.pilotEnabled) { const pilotSwitch = new Service.Switch(`${this.name} Pilot`); pilotSwitch .getCharacteristic(Characteristic.On) .on('get', cb => cb(null, false)) .on('set', (val, cb) => { this.log.info(`[Proflame] pilot_mode => ${val ? 1 : 0}`); this.sendPilotMode(val ? 1 : 0); cb(null); }); this.services.push(pilotSwitch); } // Fan Service (6 speeds: 0..6) if (this.fanEnabled) { const fanService = new Service.Fan(`${this.name} Fan`); fanService .getCharacteristic(Characteristic.On) .on('get', cb => cb(null, this.fanSpeed > 0)) .on('set', (val, cb) => { if (!val) { this.fanSpeed = 0; this.log.info('[Proflame] Fan => 0'); this.sendFanControl(0); } else { if (this.fanSpeed === 0) { this.fanSpeed = 1; this.log.info('[Proflame] Fan => 1'); this.sendFanControl(1); } else { this.log.info(`[Proflame] Fan => ${this.fanSpeed}`); this.sendFanControl(this.fanSpeed); } } cb(null); }); fanService .getCharacteristic(Characteristic.RotationSpeed) .setProps({ minValue: 0, maxValue: 100, minStep: 1 }) .on('get', cb => { const rotation = Math.round((this.fanSpeed / 6) * 100); cb(null, rotation); }) .on('set', (val, cb) => { const speed = Math.round((val / 100) * 6); this.fanSpeed = speed; this.log.info(`[Proflame] Fan speed set => ${speed}`); this.sendFanControl(speed); cb(null); }); this.services.push(fanService); } // Split Flow Switch if (this.splitFlowEnabled) { const splitFlowSwitch = new Service.Switch(`${this.name} SplitFlow`); splitFlowSwitch .getCharacteristic(Characteristic.On) .on('get', cb => cb(null, false)) .on('set', (val, cb) => { this.log.info(`[Proflame] split_flow => ${val ? 1 : 0}`); this.sendSplitFlow(val ? 1 : 0); cb(null); }); this.services.push(splitFlowSwitch); } // Auxiliary Output Switch if (this.auxEnabled) { const auxSwitch = new Service.Switch(`${this.name} Auxiliary`); auxSwitch .getCharacteristic(Characteristic.On) .on('get', cb => cb(null, false)) .on('set', (val, cb) => { this.log.info(`[Proflame] auxiliary_out => ${val ? 1 : 0}`); this.sendAux(val ? 1 : 0); cb(null); }); this.services.push(auxSwitch); } // Flame Control as Lightbulb (6 levels: 0..6) if (this.flameControlEnabled) { const flameService = new Service.Lightbulb(`${this.name} Flame`); flameService .getCharacteristic(Characteristic.On) .on('get', cb => cb(null, this.flameLevel > 0)) .on('set', (val, cb) => { if (val && !this.isOn && this.flameLevel === 0) { this.isOn = true; this.log.info('[Proflame] Flame turned on, main_mode=1'); this.sendMainMode(1); } else if (!val) { this.flameLevel = 0; this.log.info('[Proflame] Flame => 0'); this.sendFlameControl(0); } cb(null); }); flameService .getCharacteristic(Characteristic.Brightness) .on('get', cb => { const brightness = Math.round((this.flameLevel / 6) * 100); cb(null, brightness); }) .on('set', (val, cb) => { const level = Math.round((val / 100) * 6); this.flameLevel = level; this.log.info(`[Proflame] Flame level set => ${level}`); this.sendFlameControl(level); cb(null); }); this.services.push(flameService); } // Thermostat as HeaterCooler (Device uses Fahrenheit tenths; convert to/from Celsius) if (this.thermostatEnabled) { const thermoService = new Service.HeaterCooler(`${this.name} Thermostat`); thermoService .getCharacteristic(Characteristic.Active) .on('get', cb => cb(null, this.isOn ? 1 : 0)) .on('set', (val, cb) => { if (val === 1) { this.isOn = true; this.log.info('[Proflame] Thermostat activated, main_mode=2'); this.sendMainMode(2); } else { this.isOn = false; this.log.info('[Proflame] Thermostat deactivated, main_mode=0'); this.sendMainMode(0); } cb(null); }); thermoService .getCharacteristic(Characteristic.TargetTemperature) .setProps({ minValue: 10, maxValue: 30, minStep: 0.5 }) .on('get', cb => { // Convert device targetTemperature (Fahrenheit tenths) to Celsius. // f = targetTemperature/10, c = (f - 32) * 5/9. const f = this.targetTemperature / 10; const c = ((f - 32) * 5) / 9; cb(null, c); }) .on('set', (val, cb) => { // Convert Celsius (val) to Fahrenheit tenths. const f = (val * 9) / 5 + 32; const setVal = Math.round(f * 10); this.targetTemperature = setVal; this.log.info(`[Proflame] Target temperature set (device value) => ${setVal}`); this.sendTemperatureSet(setVal); cb(null); }); thermoService .getCharacteristic(Characteristic.CurrentTemperature) .on('get', cb => { // Convert device currentTemp (Fahrenheit tenths) to Celsius. const f = this.currentTemp / 10; const c = ((f - 32) * 5) / 9; cb(null, c); }); this.services.push(thermoService); } // Lamp as separate Lightbulb (6 levels: 0..6) if (this.lampEnabled) { const lampService = new Service.Lightbulb(`${this.name} Lamp`); lampService .getCharacteristic(Characteristic.On) .on('get', cb => cb(null, this.lampLevel > 0)) .on('set', (val, cb) => { if (val && this.lampLevel === 0) { this.lampLevel = 1; this.log.info('[Proflame] Lamp turned on, lampLevel=1'); this.sendLampControl(1); } else if (!val) { this.lampLevel = 0; this.log.info('[Proflame] Lamp => 0'); this.sendLampControl(0); } cb(null); }); lampService .getCharacteristic(Characteristic.Brightness) .on('get', cb => { const brightness = Math.round((this.lampLevel / 6) * 100); cb(null, brightness); }) .on('set', (val, cb) => { const level = Math.round((val / 100) * 6); this.lampLevel = level; this.log.info(`[Proflame] Lamp level set => ${level}`); this.sendLampControl(level); cb(null); }); this.services.push(lampService); } } setupWebSocket() { if (!this.ip) { this.log.warn('[Proflame] No IP specified, skipping WebSocket'); return; } const wsUrl = `ws://${this.ip}:88/`; if (!this.initialConnectionLogged) { this.log.info(`[Proflame] Connecting to ${wsUrl}`); this.initialConnectionLogged = true; } else { this.log.debug(`[Proflame] Reconnecting to ${wsUrl}`); } this.ws = new WebSocket(wsUrl); this.ws.on('open', () => { if (!this.initialConnectionLogged) { this.log.info('[Proflame] WebSocket open'); this.initialConnectionLogged = true; } else { this.log.debug('[Proflame] WebSocket open (reconnected)'); } this.ws.send('PROFLAMEPING'); this.ws.send('PROFLAMECONNECTION'); this.pingInterval = setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send('PROFLAMEPING'); } }, 30000); }); this.ws.on('message', packet => { const data = Buffer.isBuffer(packet) ? packet.toString('utf8') : packet; if (data === 'PROFLAMEPONG') { this.log.debug('[Proflame] Received PONG'); return; } try { const msg = JSON.parse(data); this.log.info(`[Proflame] Received: ${data}`); if (msg.main_mode !== undefined) { const mode = parseInt(msg.main_mode, 10); this.isOn = (mode !== 0); } if (msg.fan_control !== undefined && this.fanEnabled) { this.fanSpeed = parseInt(msg.fan_control, 10); } if (msg.flame_control !== undefined && this.flameControlEnabled) { this.flameLevel = parseInt(msg.flame_control, 10); } if (msg.temperature_set !== undefined && this.thermostatEnabled) { this.targetTemperature = parseInt(msg.temperature_set, 10); } if (msg.room_temperature !== undefined && this.thermostatEnabled) { this.currentTemp = parseInt(msg.room_temperature, 10); } if (msg.lamp_control !== undefined && this.lampEnabled) { this.lampLevel = parseInt(msg.lamp_control, 10); } } catch (err) { this.log.debug(`[Proflame] Non-JSON or invalid data: ${data}`); } }); this.ws.on('close', () => { this.log.debug('[Proflame] WebSocket closed, reconnect in 10s'); if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } setTimeout(() => this.setupWebSocket(), 10000); }); this.ws.on('error', error => { this.log.error(`[Proflame] WebSocket error: ${error.message}`); }); } shutdown() { this.log.info('[Proflame] Shutdown called'); if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } if (this.ws) { this.ws.close(); this.ws = null; } } // Command methods sendMainMode(mode) { this.sendJSON({ main_mode: mode }); } sendPilotMode(val) { this.sendJSON({ pilot_mode: val }); } sendFanControl(val) { this.sendJSON({ fan_control: val }); } sendSplitFlow(val) { this.sendJSON({ split_flow: val }); } sendAux(val) { this.sendJSON({ auxiliary_out: val }); } sendFlameControl(level) { this.sendJSON({ flame_control: level }); } sendTemperatureSet(value) { this.sendJSON({ temperature_set: value }); } sendLampControl(val) { this.sendJSON({ lamp_control: val }); } sendJSON(payload) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { this.log.debug('[Proflame] WebSocket not open; cannot send'); return; } const msg = JSON.stringify(payload); this.log.info(`[Proflame] Sending: ${msg}`); this.ws.send(msg); } identify(callback) { this.log.info('[Proflame] Identify called (not implemented)'); callback(); } getServices() { return this.services; } }