homebridge-proflame
Version:
Homebridge plugin for Proflame fireplaces (Single Accessory) with all features including lamp control
421 lines (390 loc) • 14 kB
JavaScript
/**
* 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;
}
}