UNPKG

homebridge-systemair-ventilator

Version:

A Homebridge 2 plugin for controlling Systemair Ventilators.

324 lines (262 loc) 10.1 kB
'use strict'; const axios = require('axios'); let Service; let Characteristic; const PLUGIN_NAME = 'homebridge-systemair-ventilator'; const ACCESSORY_NAME = 'SystemairVentilator'; class SystemairVentilator { constructor(log, config, api) { this.log = log; this.config = config || {}; this.api = api; this.name = this.config.name || 'Systemair Ventilator'; this.ip = this.config.ip; this.timeout = Number(this.config.timeout || 20000); this.lastFanSpeed = 2; this.cachedActive = Characteristic.Active.INACTIVE; this.cachedRotationSpeed = 16; this.cachedTimer = 0; if (!this.ip) { this.log.error('Missing required config value: ip'); throw new Error('Missing required config value: ip'); } this.axiosInstance = axios.create({ baseURL: `http://${this.ip}`, timeout: this.timeout, }); this.informationService = new Service.AccessoryInformation() .setCharacteristic(Characteristic.Manufacturer, 'Systemair') .setCharacteristic(Characteristic.Model, 'Ventilator') .setCharacteristic(Characteristic.Name, this.name) .setCharacteristic(Characteristic.SerialNumber, this.ip); this.fanService = new Service.Fanv2(`${this.name} Fan`); this.refreshService = new Service.Switch(`${this.name} Refresh`); const BatteryService = Service.Battery || Service.BatteryService; if (BatteryService) { this.timerService = new BatteryService(`${this.name} Timer`); } else { this.log.warn('Battery service is not available in this Homebridge/HAP version. Timer service disabled.'); this.timerService = null; } this.setupCharacteristics(); this.log(`SystemairVentilator loaded for ${this.name} at ${this.ip}`); } setupCharacteristics() { this.fanService .getCharacteristic(Characteristic.Active) .onSet(async (value) => { try { await this.setActive(value); } catch (error) { this.log.error(`SetActive failed: ${error.message}`); } }) .onGet(async () => { try { return await this.getActive(); } catch (error) { this.log.error(`GetActive failed: ${error.message}`); return this.cachedActive; } }); this.fanService .getCharacteristic(Characteristic.RotationSpeed) .setProps({ minValue: 0, maxValue: 100, minStep: 1, }) .onSet(async (value) => { try { await this.setRotationSpeed(value); } catch (error) { this.log.error(`SetRotationSpeed failed: ${error.message}`); } }) .onGet(async () => { try { return await this.getRotationSpeed(); } catch (error) { this.log.error(`GetRotationSpeed failed: ${error.message}`); return this.cachedRotationSpeed; } }); this.refreshService .getCharacteristic(Characteristic.On) .onSet(async (value) => { try { await this.setRefresh(value); } catch (error) { this.log.error(`Refresh failed: ${error.message}`); this.refreshService.getCharacteristic(Characteristic.On).updateValue(false); } }) .onGet(async () => false); if (this.timerService) { this.timerService .getCharacteristic(Characteristic.BatteryLevel) .onGet(async () => { try { return await this.getTimer(); } catch (error) { this.log.error(`Timer read failed: ${error.message}`); return this.cachedTimer; } }); this.timerService .getCharacteristic(Characteristic.StatusLowBattery) .onGet(async () => Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); } } async retryRequest(path, retries = 3) { let lastError; for (let attempt = 1; attempt <= retries; attempt += 1) { try { return await this.axiosInstance.get(path); } catch (error) { lastError = error; this.log.warn(`Request failed (${attempt}/${retries}): ${error.message}`); if (attempt < retries) { await new Promise((resolve) => setTimeout(resolve, 1000)); } } } throw lastError; } async mread(address, count = 1) { const response = await this.retryRequest(`/mread?{"${address}":${count}}`); const value = response.data[String(address)]; if (value === undefined || value === null) { throw new Error(`No value returned for address ${address}`); } return value; } async mwrite(values) { const payload = JSON.stringify(values); await this.retryRequest(`/mwrite?${payload}`); } fanSpeedToPercentage(speed) { switch (Number(speed)) { case 2: return 16; case 3: return 50; case 4: return 83; default: return 0; } } percentageToFanSpeed(value) { const percentage = Number(value); if (percentage <= 0) { return 0; } if (percentage <= 16) { return 2; } if (percentage <= 50) { return 3; } return 4; } async setActive(value) { const speed = value ? this.lastFanSpeed || 2 : 0; this.log(`SetActive: ${value ? 'ON' : 'OFF'} using Systemair speed ${speed}`); await this.mwrite({ 1130: speed }); this.cachedActive = speed > 0 ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE; this.cachedRotationSpeed = this.fanSpeedToPercentage(speed); this.fanService.getCharacteristic(Characteristic.RotationSpeed).updateValue(this.cachedRotationSpeed); } async getActive() { const speed = Number(await this.mread(1130, 1)); const isActive = speed > 0; if (isActive) { this.lastFanSpeed = speed; } this.cachedActive = isActive ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE; this.log(`GetActive: ${isActive ? 'ON' : 'OFF'} with Systemair speed ${speed}`); return this.cachedActive; } async setRotationSpeed(value) { const speed = this.percentageToFanSpeed(value); if (speed > 0) { this.lastFanSpeed = speed; } this.log(`SetRotationSpeed: ${value}% -> Systemair speed ${speed}`); await this.mwrite({ 1130: speed }); this.cachedActive = speed > 0 ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE; this.cachedRotationSpeed = this.fanSpeedToPercentage(speed); this.fanService.getCharacteristic(Characteristic.Active).updateValue(this.cachedActive); } async getRotationSpeed() { const speed = Number(await this.mread(1130, 1)); const percentage = this.fanSpeedToPercentage(speed); if (speed > 0) { this.lastFanSpeed = speed; } this.cachedRotationSpeed = percentage; this.cachedActive = speed > 0 ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE; this.log(`GetRotationSpeed: Systemair speed ${speed} -> ${percentage}%`); return percentage; } async setRefresh(value) { if (!value) { return; } const previousSpeed = this.lastFanSpeed || 2; this.log('Refresh: starting refresh mode'); await this.mwrite({ 1130: 2, 1161: 4, 2000: 180, 2504: 0, 16100: 0, }); setTimeout(() => { this.refreshService.getCharacteristic(Characteristic.On).updateValue(false); }, 1000); setTimeout(async () => { if (previousSpeed === 2) { return; } try { this.log(`Refresh: restoring previous Systemair speed ${previousSpeed}`); await this.mwrite({ 1130: previousSpeed }); this.cachedRotationSpeed = this.fanSpeedToPercentage(previousSpeed); this.fanService.getCharacteristic(Characteristic.RotationSpeed).updateValue(this.cachedRotationSpeed); } catch (error) { this.log.error(`Refresh: failed to restore speed: ${error.message}`); } }, 2000); } async getTimer() { let timerValue = Number(await this.mread(1110, 2)); if (Number.isNaN(timerValue)) { timerValue = 0; } timerValue = Math.min(100, Math.max(0, timerValue)); this.cachedTimer = timerValue; this.log(`Timer: ${timerValue} minutes remaining`); return timerValue; } identify(callback) { this.log(`Identify requested for ${this.name}`); if (typeof callback === 'function') { callback(); } } getServices() { return [ this.informationService, this.fanService, this.refreshService, this.timerService, ].filter(Boolean); } } module.exports = (api) => { Service = api.hap.Service; Characteristic = api.hap.Characteristic; api.registerAccessory(PLUGIN_NAME, ACCESSORY_NAME, SystemairVentilator); };