homebridge-fibaro-home-center
Version:
Homebridge plugin for Fibaro Home Center (2, 2 Lite, 3, 3 Lite, Yubii Home).
534 lines (462 loc) • 20.2 kB
text/typescript
// getFunctions.ts
import 'reflect-metadata';
import { Utils } from './utils';
import { Characteristics } from './constants';
// Decorator function for mapping getCharacteristicValue to the Characteristic
function characteristicGetter(...characteristics: unknown[]) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
const existingMethods = Reflect.getMetadata('characteristicMethods', target) || [];
existingMethods.push([propertyKey, characteristics]);
Reflect.defineMetadata('characteristicMethods', existingMethods, target);
return descriptor;
};
}
export class GetFunctions {
getFunctionsMapping: Map<unknown, (...args: unknown[]) => unknown>;
CurrentSecuritySystemStateMapping;
TargetSecuritySystemStateMapping;
modeMap;
constructor(private platform) {
this.getFunctionsMapping = new Map();
this.CurrentSecuritySystemStateMapping = new Map([
['AwayArmed', this.platform.Characteristic.SecuritySystemCurrentState.AWAY_ARM],
['Disarmed', this.platform.Characteristic.SecuritySystemCurrentState.DISARMED],
['NightArmed', this.platform.Characteristic.SecuritySystemCurrentState.NIGHT_ARM],
['StayArmed', this.platform.Characteristic.SecuritySystemCurrentState.STAY_ARM],
['AlarmTriggered', this.platform.Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED],
]);
this.TargetSecuritySystemStateMapping = new Map([
['AwayArmed', this.platform.Characteristic.SecuritySystemTargetState.AWAY_ARM],
['Disarmed', this.platform.Characteristic.SecuritySystemTargetState.DISARM],
['NightArmed', this.platform.Characteristic.SecuritySystemTargetState.NIGHT_ARM],
['StayArmed', this.platform.Characteristic.SecuritySystemTargetState.STAY_ARM],
]);
const { CurrentHeatingCoolingState } = this.platform.Characteristic;
this.modeMap = {
'Off': CurrentHeatingCoolingState.OFF,
'Heat': CurrentHeatingCoolingState.HEAT,
'Cool': CurrentHeatingCoolingState.COOL,
};
this.initializeFunctionsMapping();
}
private initializeFunctionsMapping() {
const prototype = Object.getPrototypeOf(this);
const characteristicMethods = Reflect.getMetadata('characteristicMethods', prototype) || [];
for (const [methodName, characteristics] of characteristicMethods) {
for (const characteristic of characteristics) {
this.getFunctionsMapping.set(this.platform.Characteristic[characteristic], this[methodName].bind(this));
}
}
}
getBool(characteristic, service, IDs, properties) {
const v = properties.value ?? properties['ui.startStopActivitySwitch.value'] ?? false;
characteristic.updateValue(Utils.getBoolean(v));
}
getBrightness(characteristic, service, _IDs, { value }) {
if (isNaN(value)) {
return;
}
let r = Math.min(Math.max(parseFloat(value), 0), 100);
if (service && !service.isGlobalVariableDimmer && r === 99) {
r = 100;
}
const onCharacteristic = service?.getCharacteristic(this.platform.Characteristic.On);
if (onCharacteristic?.value === false) {
return;
}
characteristic.updateValue(r);
}
getPositionState(characteristic) {
characteristic.updateValue(this.platform.Characteristic.PositionState.STOPPED);
}
getCurrentPosition(characteristic, _service, _IDs, properties) {
const value = properties.value;
const state = properties.state;
let position;
if (value !== undefined && !isNaN(value)) {
position = Math.min(Math.max(parseInt(value), characteristic.props.minValue), characteristic.props.maxValue);
position = position === 99 ? 100 : position === 1 ? 0 : position;
} else {
position = state === 'Closed' ? 0 : 100;
}
characteristic.updateValue(position);
}
getCurrentTiltAngle(characteristic, _service, _IDs, properties) {
const value2 = parseInt(properties.value2 ?? '');
const angle = (() => {
if (value2 >= 0 && value2 <= 100) {
const adjustedValue = value2 === 99 ? 100 : value2 === 1 ? 0 : value2;
return Utils.scale(adjustedValue, 0, 100, characteristic.props.minValue, characteristic.props.maxValue);
}
return characteristic.props.minValue;
})();
characteristic.updateValue(angle);
}
async getCurrentTemperature(characteristic, service, IDs, properties) {
try {
let temperature;
switch (true) {
case service.isClimateZone || service.isHeatingZone:
temperature = await this.getZoneTemperature(service, IDs[0]);
break;
case service.isRadiatorThermostaticValve:
temperature = properties.heatingThermostatSetpoint;
break;
default:
temperature = properties.value;
}
if (typeof temperature === 'string') {
temperature = parseFloat(temperature);
}
if (isNaN(temperature)) {
throw new Error('No value');
}
if (temperature < characteristic.props.minValue) {
temperature = characteristic.props.minValue;
}
if (temperature > characteristic.props.maxValue) {
temperature = characteristic.props.maxValue;
}
characteristic.updateValue(temperature);
} catch (e) {
this.platform.log(`Error getting Current Temperature: ${service.IDs[0]} - Err: ${e}`);
}
}
async getTargetTemperature(characteristic, service, IDs, properties) {
try {
let temperature;
switch (true) {
case service.isClimateZone || service.isHeatingZone:
temperature = await this.getZoneTemperature(service, IDs[0]);
break;
case service.isRadiatorThermostaticValve:
temperature = properties.heatingThermostatSetpointFuture;
break;
default:
temperature = properties.value;
}
if (typeof temperature === 'string') {
temperature = parseFloat(temperature);
}
if (isNaN(temperature)) {
throw new Error('No value');
}
if (temperature < characteristic.props.minValue) {
temperature = characteristic.props.minValue;
}
if (temperature > characteristic.props.maxValue) {
temperature = characteristic.props.maxValue;
}
characteristic.updateValue(temperature);
} catch (e) {
this.platform.log(`Error getting Target Temperature: ${service.IDs[0]} - Err: ${e}`);
}
}
async getZoneTemperature(service, zoneId) {
const getZoneFunction = service.isClimateZone ?
this.platform.fibaroClient.getClimateZone :
this.platform.fibaroClient.getHeatingZone;
const response = await getZoneFunction.call(this.platform.fibaroClient, zoneId);
if (!response || !response.body || !response.body.properties) {
throw new Error(`No valid response for zone ${zoneId}.`);
}
const { body: { properties: zoneProperties } } = response;
const tempProperty = service.isClimateZone ? 'currentTemperatureHeating' : 'currentTemperature';
const temperature = zoneProperties[tempProperty];
if (temperature === undefined) {
throw new Error(`No value for Temperature in zone: ${zoneId}.`);
}
return temperature;
}
getContactSensorState(characteristic, _service, _IDs, properties) {
const v = Utils.getBoolean(properties.value);
characteristic.updateValue(v === false ?
this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED :
this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);
}
getLeakDetected(characteristic, _service, _IDs, properties) {
const v = Utils.getBoolean(properties.value);
characteristic.updateValue(v === true ?
this.platform.Characteristic.LeakDetected.LEAK_DETECTED :
this.platform.Characteristic.LeakDetected.LEAK_NOT_DETECTED);
}
getSmokeDetected(characteristic, _service, _IDs, properties) {
const v = Utils.getBoolean(properties.value);
characteristic.updateValue(v === true ?
this.platform.Characteristic.SmokeDetected.SMOKE_DETECTED :
this.platform.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED);
}
getCarbonMonoxideDetected(characteristic, _service, _IDs, properties) {
const v = Utils.getBoolean(properties.value);
characteristic.updateValue(v === true ?
this.platform.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL :
this.platform.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL);
}
getCarbonMonoxideLevel(characteristic, _service, _IDs, properties) {
const r = parseFloat(properties.concentration);
characteristic.updateValue(r);
}
getCarbonMonoxidePeakLevel(characteristic, _service, _IDs, properties) {
const r = parseFloat(properties.maxConcentration);
characteristic.updateValue(r);
}
getOutletInUse(characteristic, _service, _IDs, properties) {
const power = properties.power;
if (power !== undefined && !isNaN(power)) {
characteristic.updateValue(parseFloat(power) > 1.0);
}
}
getLockCurrentState(characteristic, service, _IDs, properties) {
const v = Utils.getBoolean(properties.value);
const { LockCurrentState } = this.platform.Characteristic;
const state = service.isLockSwitch
? (v ? LockCurrentState.UNSECURED : LockCurrentState.SECURED)
: (v ? LockCurrentState.SECURED : LockCurrentState.UNSECURED);
characteristic.updateValue(state);
}
async getCurrentHeatingCoolingState(characteristic, service, IDs, properties) {
try {
switch (true) {
case service.isClimateZone: {
const { body: { properties } } = await this.platform.fibaroClient.getClimateZone(IDs[0]);
if (!properties.mode) {
throw new Error('No value for heating cooling state.');
}
const state = this.modeMap[properties.mode];
if (state !== undefined) {
characteristic.updateValue(state);
}
break;
}
case service.isHeatingZone:
characteristic.updateValue(this.platform.Characteristic.TargetHeatingCoolingState.HEAT);
break;
case service.isRadiatorThermostaticValve: {
const state = this.modeMap[properties.thermostatMode] || this.platform.Characteristic.CurrentHeatingCoolingState.HEAT;
characteristic.updateValue(state);
break;
}
}
} catch (e) {
this.platform.log(`Error getting Current Heating Cooling State: ${service.IDs[0]} - Err: ${e}`);
}
}
async getTargetHeatingCoolingState(characteristic, service, IDs, properties) {
try {
switch (true) {
case service.isClimateZone: {
const { body: { properties } } = await this.platform.fibaroClient.getClimateZone(IDs[0]);
if (!properties.mode) {
throw new Error('No value for heating cooling state.');
}
const state = this.modeMap[properties.mode];
if (state !== undefined) {
characteristic.updateValue(state);
}
break;
}
case service.isHeatingZone:
characteristic.updateValue(this.platform.Characteristic.TargetHeatingCoolingState.HEAT);
break;
case service.isRadiatorThermostaticValve: {
const state = this.modeMap[properties.thermostatModeFuture] || this.platform.Characteristic.TargetHeatingCoolingState.HEAT;
characteristic.updateValue(state);
break;
}
}
} catch (e) {
this.platform.log(`Error getting Target Heating Cooling State: ${service.IDs[0]} - Err: ${e}`);
}
}
getTemperatureDisplayUnits(characteristic) {
characteristic.updateValue(this.platform.Characteristic.TemperatureDisplayUnits.CELSIUS);
}
getHue(characteristic, service, _IDs, properties) {
characteristic.updateValue(Math.round(Utils.updateHomeKitColorFromHomeCenter(properties.color).h));
}
getSaturation(characteristic, service, _IDs, properties) {
characteristic.updateValue(Math.round(Utils.updateHomeKitColorFromHomeCenter(properties.color).s));
}
getCurrentDoorState(characteristic, _service, _IDs, properties) {
const { CurrentDoorState } = this.platform.Characteristic;
const stateMap = {
'Opened': CurrentDoorState.OPEN,
'Opening': CurrentDoorState.OPENING,
'Closing': CurrentDoorState.CLOSING,
'Closed': CurrentDoorState.CLOSED,
};
const v = parseInt(properties.value);
if (!isNaN(v)) {
this.platform.log('getCurrentDoorState value:', v);
if (v === 0) {
return characteristic.updateValue(CurrentDoorState.CLOSED);
}
if (v === 99) {
return characteristic.updateValue(CurrentDoorState.OPEN);
}
return characteristic.updateValue(CurrentDoorState.STOPPED);
}
if (properties.state !== undefined) {
this.platform.log('getCurrentDoorState:', properties.state);
const mappedState = stateMap[properties.state];
if (mappedState !== undefined) {
return characteristic.updateValue(mappedState);
}
}
characteristic.updateValue(CurrentDoorState.STOPPED);
}
getTargetDoorState(characteristic, _service, _IDs, properties) {
const { TargetDoorState } = this.platform.Characteristic;
const stateMap = {
'Opened': TargetDoorState.OPEN,
'Opening': TargetDoorState.OPEN,
'Closing': TargetDoorState.CLOSED,
'Closed': TargetDoorState.CLOSED,
};
const v = parseInt(properties.value);
if (!isNaN(v)) {
this.platform.log('getTargetDoorState value:', v);
return characteristic.updateValue(v === 0 ? TargetDoorState.CLOSED : TargetDoorState.OPEN);
}
if (properties.state) {
this.platform.log('getTargetDoorState:', properties.state);
const mappedState = stateMap[properties.state];
if (mappedState !== undefined) {
return characteristic.updateValue(mappedState);
}
}
characteristic.updateValue(TargetDoorState.CLOSED);
}
getObstructionDetected(characteristic) {
characteristic.updateValue(0);
}
getBatteryLevel(characteristic, _service, _IDs, properties) {
const batteryLevel = parseFloat(properties.batteryLevel);
if (isNaN(batteryLevel)) {
this.platform.log('batteryLevel is not a number.');
return;
}
const level = batteryLevel > 100 ? 0 : batteryLevel;
characteristic.updateValue(level);
}
getChargingState(characteristic) {
characteristic.updateValue(0);
}
getStatusLowBattery(characteristic, _service, _IDs, properties) {
if (isNaN(properties.batteryLevel)) {
this.platform.log('batteryLevel is not a number.', '');
return;
}
const batteryLevel = parseFloat(properties.batteryLevel);
const r = (batteryLevel <= 20 || batteryLevel > 100) ? 1 : 0;
characteristic.updateValue(r);
}
getSecuritySystemState(characteristic, _service, _IDs, securitySystemStatus) {
let r = this.platform.Characteristic.SecuritySystemTargetState.DISARM;
if (characteristic.constructor === this.platform.Characteristic.SecuritySystemCurrentState) {
r = this.CurrentSecuritySystemStateMapping.get(securitySystemStatus.value) ?? 0;
} else if (characteristic.constructor === this.platform.Characteristic.SecuritySystemTargetState) {
r = this.TargetSecuritySystemStateMapping.get(securitySystemStatus.value) ?? 0;
}
if (r !== undefined) {
characteristic.updateValue(r);
}
}
getActive(characteristic, _service, _IDs, properties) {
const v = Utils.getBoolean(properties.value);
characteristic.updateValue(v === false ?
this.platform.Characteristic.Active.INACTIVE :
this.platform.Characteristic.Active.ACTIVE);
}
getInUse(characteristic, _service, _IDs, properties) {
const v = Utils.getBoolean(properties.value);
characteristic.updateValue(v === false ?
this.platform.Characteristic.InUse.NOT_IN_USE :
this.platform.Characteristic.InUse.IN_USE);
}
getProgrammableSwitchEvent(characteristic, service, _IDs, properties) {
if (service.isRemoteControllerSceneActivation) {
if (properties.value % 2 === 1) { // 1 for single press, 0 for double press
characteristic.updateValue(this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
} else {
characteristic.updateValue(this.platform.Characteristic.ProgrammableSwitchEvent.LONG_PRESS);
}
} else if (service.isRemoteControllerCentralScene) {
switch (properties.value) {
case 'Pressed':
characteristic.updateValue(this.platform.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
break;
case 'Pressed2':
characteristic.updateValue(this.platform.Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS);
break;
case 'HeldDown':
characteristic.updateValue(this.platform.Characteristic.ProgrammableSwitchEvent.LONG_PRESS);
break;
default:
return;
}
}
}
getServiceLabelIndex(characteristic, service) {
characteristic.updateValue(service.remoteButtonNumber);
}
getAirQuality(characteristic, _service, _IDs, properties) {
const v = parseFloat(properties.value);
// 2024 AQI for PM 2.5
// https://www.epa.gov/system/files/documents/2024-02/pm-naaqs-air-quality-index-fact-sheet.pdf
if (_service.isPM2_5Sensor) {
if (v <= 5) {
characteristic.updateValue(this.platform.Characteristic.AirQuality.EXCELLENT);
} else if (v < 9.1) {
characteristic.updateValue(this.platform.Characteristic.AirQuality.GOOD);
} else if ( v < 35.5) {
characteristic.updateValue(this.platform.Characteristic.AirQuality.FAIR);
} else if ( v < 55.5) {
characteristic.updateValue(this.platform.Characteristic.AirQuality.INFERIOR);
} else {
characteristic.updateValue(this.platform.Characteristic.AirQuality.POOR);
}
}
}
getFloat(characteristic, _service, _IDs, properties) {
const value = properties.value;
if (value !== undefined && !isNaN(value)) {
characteristic.updateValue(parseFloat(value));
}
}
}