// fibaroAccessory.ts import { PlatformAccessory, CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicGetCallback, CharacteristicValue, } from 'homebridge'; import { FibaroHC } from './platform'; import * as constants from './constants'; import { manualDeviceConfigs } from './deviceManual'; import { autoDeviceConfigs } from './deviceAuto'; export class FibaroAccessory { isValid; lastServiceChecked; constructor( private readonly platform: FibaroHC, private readonly accessory: PlatformAccessory, private readonly device, ) { this.isValid = true; // set accessory information const properties = this.device.properties || {}; const manufacturer = (properties.zwaveCompany || 'IlCato').replace('Fibargroup', 'Fibar Group'); const roomName = this.platform.getRoomNameById(this.device.roomID); // Set accessory information const serialNumber = properties.serialNumber ? properties.serialNumber.startsWith('h\'') ? properties.serialNumber.slice(2).replace(/^0+/, '') : properties.serialNumber.replace(/^0+/, '') : undefined; const accessoryInfo = this.accessory.getService(this.platform.Service.AccessoryInformation)!; accessoryInfo .setCharacteristic(this.platform.Characteristic.Manufacturer, manufacturer) .setCharacteristic(this.platform.Characteristic.Model, `${this.device.type.length > 1 ? this.device.type : 'HomeCenter Bridged Accessory'}`); const serialNumberValue = this.constructSerialNumber(this.device.id, roomName, serialNumber); accessoryInfo.setCharacteristic(this.platform.Characteristic.SerialNumber, serialNumberValue); // Check for device-specific configuration const devConfig = this.platform.config.devices?.find(item => item.id === this.device.id); // Configure accessory based on config or device type let serviceAndCharacteristics; if (devConfig) { serviceAndCharacteristics = this.configureAccessoryFromConfig(devConfig); } else { serviceAndCharacteristics = this.configureAccessoryFromType(); } if (!serviceAndCharacteristics || !(serviceAndCharacteristics instanceof Array) || serviceAndCharacteristics.length === 0) { this.isValid = false; return; } // Add battery service if the device supports it if (this.device.interfaces && this.device.interfaces.includes('battery')) { serviceAndCharacteristics.push({ service: this.platform.Service.Battery, characteristics: [this.platform.Characteristic.BatteryLevel, this.platform.Characteristic.ChargingState, this.platform.Characteristic.StatusLowBattery], subtype: this.device.id + '----', }); } // loop through all services, create it if necessary and bind characteristics serviceAndCharacteristics.forEach(({ service, characteristics, subtype }) => { let s = this.accessory.getServiceById(service, subtype); if (!s) { const serviceName = this.buildServiceName(service, subtype); s = this.accessory.addService(service, serviceName, subtype); } this.bindCharacterstics(s, characteristics); }); // Remove no more existing services for (let t = 0; t < this.accessory.services.length; t++) { const s = this.accessory.services[t]; if (s.constructor === this.platform.Service.AccessoryInformation) { continue; } if (!serviceAndCharacteristics.some(sc => sc.service === s.constructor)) { this.accessory.removeService(s); } } } buildServiceName(service, subtype) { if (service === this.platform.Service.Battery) { return this.device.name + ' Battery'; } else if (service === this.platform.Service.StatelessProgrammableSwitch) { return this.device.name + ' Button ' + subtype.split('-')[4]; } else { return this.device.name; } } bindCharacterstics(service, characteristics) { if (!characteristics) { return; } const IDs = service.subtype.split('-'); this.setServiceProperties(service, IDs); for (let i = 0; i < characteristics.length; i++) { const characteristic = service.getCharacteristic(characteristics[i]); // Set the range for light level measurements if (characteristic.constructor === this.platform.Characteristic.CurrentAmbientLightLevel) { characteristic.props.maxValue = 100000; characteristic.props.minStep = 1; characteristic.props.minValue = 0; } // Set the minimum temperature that can be reported if (characteristic.constructor === this.platform.Characteristic.CurrentTemperature) { characteristic.props.minValue = -50; } // Set the maximum target temperature based on configuration if (characteristic.constructor === this.platform.Characteristic.TargetTemperature) { characteristic.props.maxValue = this.platform.config.thermostatmaxtemperature; } // Set the default valve type to generic if (characteristic.constructor === this.platform.Characteristic.ValveType) { characteristic.value = this.platform.Characteristic.ValveType.GENERIC_VALVE; } // Set the initial air quality status to unknown if (characteristic.constructor === this.platform.Characteristic.AirQuality) { characteristic.value = this.platform.Characteristic.AirQuality.UNKNOWN; } // Set the remote controller scene activationbutton options if (service.isRemoteControllerSceneActivation && characteristic.constructor === this.platform.Characteristic.ProgrammableSwitchEvent) { characteristic.props.validValues = [this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, this.platform.Characteristic.ProgrammableSwitchEvent.LONG_PRESS]; } // Set the remote controller central scene button options if (service.isRemoteControllerCentralScene && characteristic.constructor === this.platform.Characteristic.ProgrammableSwitchEvent) { characteristic.props.validValues = [this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, this.platform.Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS, this.platform.Characteristic.ProgrammableSwitchEvent.LONG_PRESS]; } // Set the thermosta radiator options if (service.isRadiatorThermostaticValve && (characteristic.constructor === this.platform.Characteristic.CurrentHeatingCoolingState || characteristic.constructor === this.platform.Characteristic.TargetHeatingCoolingState)) { characteristic.props.validValues =[ this.platform.Characteristic.CurrentHeatingCoolingState.OFF, this.platform.Characteristic.CurrentHeatingCoolingState.HEAT, ]; } // Bind the characteristic to the service this.bindCharacteristic(characteristic, service, IDs); } } bindCharacteristic(characteristic, service, IDs) { if (!characteristic || !service || !service.subtype) { return; } if (this.shouldSubscribeToUpdates(service, characteristic)) { this.subscribeUpdate(service, characteristic, this.getPropertyToSubscribe(service, characteristic)); } this.bindSetEvent(characteristic, service, IDs); this.bindGetEvent(characteristic, service, IDs); } private setServiceProperties(service, IDs) { // IDs[0] is always device ID, "0" for security system and "G" for global variables switches // IDs[1] is reserved for the button ID for virtual devices, or the global variable name for global variable devices, otherwise is "" // IDs[2] is a subdevice type: // "LOCK" for locks, // "SC" for Scenes, // "CZ" for Climate zones, // "HZ" for heating zones, // "G" for global variables, // "D" for dimmer global variables, // "PM2.5" for PM2.5 sensor // IDs[3] is for remote controllers and remote scene controllers and represent the remote controller type: // "CS" for remote controller central scene, // "SA" for remote controller scene activation // IDs[4] is for remote controllers and remote scene controllers and represent the remote controller button number service.isVirtual = IDs[1] !== ''; service.isSecuritySystem = IDs[0] === '0'; service.isGlobalVariableSwitch = IDs[0] === 'G'; service.isGlobalVariableDimmer = IDs[0] === 'D'; service.isLockSwitch = IDs.length >= 3 && IDs[2] === constants.SUBTYPE_LOCK; service.isScene = IDs.length >= 3 && IDs[2] === constants.SUBTYPE_SCENE; service.isClimateZone = IDs.length >= 3 && IDs[2] === constants.SUBTYPE_CLIMATE_ZONE; service.isHeatingZone = IDs.length >= 3 && IDs[2] === constants.SUBTYPE_HEATING_ZONE; service.isRadiatorThermostaticValve = IDs.length >= 3 && IDs[2] === constants.SUBTYPE_RADIATOR_THERMOSTATIC_VALVE; service.isOpenCloseOnly = IDs.length >= 3 && IDs[2] === constants.SUBTYPE_OPEN_CLOSE_ONLY; service.isPM2_5Sensor = IDs.length >= 3 && IDs[2] === constants.SUBTYPE_PM2_5; service.isRemoteControllerCentralScene = IDs.length >= 5 && IDs[3] === constants.SUBTYPE_REMOTE_CONTROLLER_CENTRAL_SCENE; service.isRemoteControllerSceneActivation = IDs.length >= 5 && IDs[3] === constants.SUBTYPE_REMOTE_CONTROLLER_SCENE_ACTIVATION; service.remoteButtonNumber = IDs.length >= 5 ? parseInt(IDs[4]) : -1; } private shouldSubscribeToUpdates(service, characteristic) { return !service.isVirtual && !service.isScene && characteristic.constructor !== this.platform.Characteristic.ValveType; } private getPropertyToSubscribe(service, characteristic) { if (characteristic.constructor === this.platform.Characteristic.Hue || characteristic.constructor === this.platform.Characteristic.Saturation) { return 'color'; } if (characteristic.constructor === this.platform.Characteristic.CurrentHeatingCoolingState || characteristic.constructor === this.platform.Characteristic.TargetHeatingCoolingState) { return 'mode'; } if (characteristic.constructor === this.platform.Characteristic.TargetTemperature) { return 'targettemperature'; } if (service.UUID === this.platform.Service.WindowCovering.UUID && (characteristic.constructor === this.platform.Characteristic.CurrentHorizontalTiltAngle || characteristic.constructor === this.platform.Characteristic.TargetHorizontalTiltAngle)) { return 'value2'; } return 'value'; } private bindSetEvent(characteristic, service, IDs) { characteristic.on(CharacteristicEventTypes.SET, async (value: CharacteristicValue, callback: CharacteristicSetCallback, context) => { this.setCharacteristicValue(value, context, characteristic, service, IDs); callback(); }); } private bindGetEvent(characteristic, service, IDs) { characteristic.on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => { if (this.isStaticCharacteristic(characteristic)) { callback(undefined, characteristic.value); } else if (this.isVirtualButtonOrScene(service)) { callback(undefined, false); } else { this.getCharacteristicValue(callback, characteristic, service, this.accessory, IDs); } }); } private isStaticCharacteristic(characteristic) { return characteristic.constructor === this.platform.Characteristic.Name || characteristic.constructor === this.platform.Characteristic.ValveType; } private isVirtualButtonOrScene(service) { return (service.isVirtual && !service.isGlobalVariableSwitch && !service.isGlobalVariableDimmer) || service.isScene; } async setCharacteristicValue(value, context, characteristic, service, IDs) { if (context !== 'fromFibaro' && context !== 'fromSetValue') { if (this.platform.setFunctions) { const setFunction = this.platform.setFunctions.setFunctionsMapping.get(characteristic.constructor); const poller = this.platform.poller; if (poller) { poller.cancelPoll(); } if (setFunction) { setFunction.call(this.platform.setFunctions, value, context, characteristic, service, IDs); } if (poller) { poller.restartPoll(constants.POLLER_RESTART_AFTER_SET_VALUE); } } } } async getCharacteristicValue(callback, characteristic, service, accessory, IDs) { if (this.platform.config.logsLevel === constants.CONFIG_LOGS_LEVEL_VERBOSE) { this.platform.log.info(`${this.device.name} [${IDs[0]}]:`, 'getting', `${characteristic.displayName}`); } if (!this.platform.fibaroClient) { this.platform.log.error('No Fibaro client available.'); callback(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); return; } else { // communicate to HomeKit the current value of the characteristic. // the logic that follows may update the value or return error in case it is not able to get the updated value callback(undefined, characteristic.value); } try { if (service.isSecuritySystem) { await this.handleSecuritySystem(characteristic, service, IDs); } else if (service.isGlobalVariableSwitch || service.isGlobalVariableDimmer) { await this.handleGlobalVariable(characteristic, service, IDs); } else { await this.handleDefaultCase(characteristic, service, IDs); } } catch (e) { characteristic.updateValue(new Error(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE.toString())); this.platform.log.error('G1 - There was a problem getting value from: ', `${IDs[0]} - Err: ${e}`); } } private async handleSecuritySystem(characteristic, service, IDs) { const securitySystemStatus = (await this.platform.fibaroClient!.getGlobalVariable(constants.SECURITY_SYSTEM_GLOBAL_VARIABLE)).body; this.platform.getFunctions!.getSecuritySystemState(characteristic, service, IDs, securitySystemStatus); } private async handleGlobalVariable(characteristic, service, IDs) { const value = (await this.platform.fibaroClient!.getGlobalVariable(IDs[1])).body; const getFunction = this.platform.getFunctions!.getFunctionsMapping.get(characteristic.constructor); if (getFunction) { getFunction.call(this.platform.getFunctions, characteristic, service, IDs, value); } } private async handleDefaultCase(characteristic, service, IDs) { const getFunction = this.platform.getFunctions!.getFunctionsMapping.get(characteristic.constructor); if (getFunction) { const properties = await this.getDeviceProperties(service, IDs[0]); if (this.shouldConvertTemperature(characteristic)) { properties.value = this.convertFahrenheitToCelsius(properties.value); } this.handleDeadDeviceStatus(service, properties, IDs[0]); this.callGetFunctionAndHandleResult(getFunction, characteristic, service, IDs, properties); } else { characteristic.updateValue(new Error(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE.toString())); this.platform.log.error('No get function defined for: ', `${characteristic.displayName}`); } } private async getDeviceProperties(service, deviceId) { if (!service.isClimateZone && !service.isHeatingZone && this.platform.fibaroClient) { return (await this.platform.fibaroClient.getDeviceProperties(deviceId)).body.properties; } return {}; } private shouldConvertTemperature(characteristic) { return this.platform.config.FibaroTemperatureUnit === constants.CONFIG_FIBARO_TEMPERATURE_UNIT_FAHRENHEIT && characteristic.displayName === 'Current Temperature'; } private convertFahrenheitToCelsius(value) { return (value - 32) * 5 / 9; } private handleDeadDeviceStatus(service, properties, deviceId) { // Reset deadLogged flag if this is a different service if (this.lastServiceChecked !== service) { this.lastServiceChecked = service; service.deadLogged = false; } // Log dead status once per service if ((properties.dead === true || properties.dead === 'true') && !service.deadLogged) { this.platform.log.warn('Device dead: ', `${deviceId} service: ${service.displayName}, reason: ${properties.deadReason}`); service.deadLogged = true; } } private callGetFunctionAndHandleResult(getFunction, characteristic, service, IDs, properties) { if (properties.dead === true || properties.dead === 'true') { if (this.platform.config.markDeadDevices) { // Report dead status to HomeKit characteristic.updateValue(new Error(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE.toString())); } else { // Do nothing, we already set the last known value before return; } } else { getFunction.call(this.platform.getFunctions, characteristic, service, IDs, properties); } } subscribeUpdate(service, characteristic, propertyChanged) { const IDs = service.subtype.split('-'); this.platform.updateSubscriptions.push( { 'id': IDs[0], 'service': service, 'characteristic': characteristic, 'property': propertyChanged }, ); } configureAccessoryFromConfig(devConfig): unknown { if (!devConfig) { return; } const Service = this.platform.Service; const Characteristic = this.platform.Characteristic; if (this.platform.config.logsLevel === constants.CONFIG_LOGS_LEVEL_VERBOSE) { this.platform.log.info(`${this.device.name} [id: ${this.device.id}, type: ${this.device.type}]: device found in config`); } // If the device is excluded in config if (devConfig.displayAs === 'exclude') { if (this.platform.config.logsLevel > 0) { this.platform.log.info(`${this.device.name} [id: ${this.device.id}, type: ${this.device.type}]: device excluded in config`); } return; } // Find a matching function based on the name const manualConfigFunc = manualDeviceConfigs.get(devConfig.displayAs); // If a matching manualConfigFunc was found if (manualConfigFunc) { // Set the configuration for this device by calling manualConfigFunc return manualConfigFunc.call(null, Service, Characteristic, this.device); } else { // Default to switch if no matching configuration is found return [{ service: Service.Switch, characteristics: [Characteristic.On], subtype: this.device.id + '----', }]; } } configureAccessoryFromType() { const Service = this.platform.Service; const Characteristic = this.platform.Characteristic; const type = this.device.type; // Find the matching device configuration function const deviceConfigFunction = [...autoDeviceConfigs.entries()].find(([key]) => (key instanceof RegExp && key.test(type)) || key === type, )?.[1]; // If a matching deviceConfigFunction was found if (deviceConfigFunction) { // Set the configuration for this device by calling deviceConfigFunction return deviceConfigFunction.call(null, Service, Characteristic, this.device, this.platform.config, this.platform.log); } else { // If no matching DeviceClass was found, log that the device is not supported if (this.platform.config.logsLevel > 0) { this.platform.log.debug(`${this.device.name} [id: ${this.device.id}, type: ${type}]: device not supported`); } return; } } private constructSerialNumber(deviceId: string, roomName?: string, serialNumber?: string): string { let serialNumberValue = `ID:${deviceId}`; let remainingLength = 64 - serialNumberValue.length; if (roomName && remainingLength > 0) { const roomString = `,Room:${roomName}`; if (roomString.length <= remainingLength) { serialNumberValue += roomString; remainingLength -= roomString.length; } } if (serialNumber && remainingLength > 0) { const serialString = `,${serialNumber}`; if (serialString.length <= remainingLength) { serialNumberValue += serialString; } } return serialNumberValue; } }