Skip to content

Commit

Permalink
feat: implement humidifier accessory (read only)
Browse files Browse the repository at this point in the history
re #75
  • Loading branch information
grivkees committed Jul 19, 2022
1 parent 955893a commit 2771087
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 7 deletions.
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@
"ctype",
"damperposition",
"deadband",
"dehum",
"Fanv",
"filtrlvl",
"featureset",
"Homebridge",
"htsp",
"mutex",
"rclg",
"rclgovercool",
"rh",
"rhtg",
"setpoint",
"setpoints",
"sig",
Expand Down
24 changes: 22 additions & 2 deletions src/accessory_thermostat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import { FilterService } from './characteristics_filter';
import {
convertSystemTemp2CharTemp,
} from './helpers';
import { ThermostatRHService } from './characteristics_humidity';
import { ThermostatRHService, HumidifierService } from './characteristics_humidity';
import { FanService } from './characteristics_fan';
import { ACService } from './characteristics_ac';
import { BaseAccessory } from './accessory_base';

export class ThermostatAccessory extends BaseAccessory {
private service: Service;
private fan_service?: Service;
private hum_service?: Service;
private system_status: InfinityEvolutionSystemStatus;
private system_config: InfinityEvolutionSystemConfig;
private system_profile: InfinityEvolutionSystemProfile;
Expand Down Expand Up @@ -85,11 +86,14 @@ export class ThermostatAccessory extends BaseAccessory {
this.accessory.context,
).wrap(this.service);

// Humidity Control
// Humidity Sensor
new ThermostatRHService(
this.platform,
this.accessory.context,
).wrap(this.service);
// Humidifier/Dehumidifier Control
this.hum_service = this.accessory.getService(this.platform.Service.HumidifierDehumidifier);
this.setupHumidifierService();
}

setupFanService(): void {
Expand All @@ -107,4 +111,20 @@ export class ThermostatAccessory extends BaseAccessory {
this.accessory.context,
).wrap(this.fan_service);
}

setupHumidifierService(): void {
this.hum_service = this.hum_service || this.accessory.addService(this.platform.Service.HumidifierDehumidifier);

this.system_config.fetch().then(async () => {
this.hum_service?.setCharacteristic(
this.platform.Characteristic.Name,
await this.system_config.getZoneName(this.accessory.context.zone) + ' Humidifier',
);
});

new HumidifierService(
this.platform,
this.accessory.context,
).wrap(this.hum_service);
}
}
94 changes: 89 additions & 5 deletions src/characteristics_humidity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { CharacteristicWrapper, MultiWrapper } from './base';
import { CharacteristicWrapper, MultiWrapper, ThermostatCharacteristicWrapper } from './base';
import { convertSystemDehum2CharDehum, convertSystemHum2CharHum } from './helpers';
import { STATUS } from './infinityApi';

class CurrentRH extends CharacteristicWrapper {
ctype = this.Characteristic.CurrentRelativeHumidity;
Expand All @@ -7,12 +9,91 @@ class CurrentRH extends CharacteristicWrapper {
};
}

class TargetDehumidify extends CharacteristicWrapper {
class HumidifierActive extends ThermostatCharacteristicWrapper {
ctype = this.Characteristic.Active;
get = async () => {
// I'd rather use 'status' here instead of 'config', but if the accessory
// is inactive HomeKit always shows the target state as off.
const activity_name = await this.getActivity();
const [c_humidifier, c_dehumidifier] = await Promise.all([
this.system.config.getActivityHumidifierState(activity_name),
this.system.config.getActivityDehumidifierState(activity_name),
]);
if (c_humidifier === STATUS.OFF && c_dehumidifier === STATUS.OFF) {
return this.Characteristic.Active.INACTIVE;
} else {
return this.Characteristic.Active.ACTIVE;
}
};
}

class HumidifierCurrentState extends ThermostatCharacteristicWrapper {
ctype = this.Characteristic.CurrentHumidifierDehumidifierState;
get = async () => {
const activity_name = await this.getActivity();
const [s_humidifier, s_dehumidifier, c_humidifier, c_dehumidifier] = await Promise.all([
this.system.status.getHumidifier(),
this.system.status.getDehumidifier(),
this.system.config.getActivityHumidifierState(activity_name),
this.system.config.getActivityDehumidifierState(activity_name),
]);
if (s_humidifier === STATUS.ON) {
return this.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING;
} else if (s_dehumidifier === STATUS.ON) {
return this.Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING;
} else if (c_humidifier === STATUS.ON || c_dehumidifier === STATUS.ON) {
return this.Characteristic.CurrentHumidifierDehumidifierState.IDLE;
} else {
return this.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE;
}
};
}

class HumidifierTargetState extends ThermostatCharacteristicWrapper {
ctype = this.Characteristic.TargetHumidifierDehumidifierState;
get = async () => {
const activity_name = await this.getActivity();
const [humidifier, dehumidifier] = await Promise.all([
this.system.config.getActivityHumidifierState(activity_name),
this.system.config.getActivityDehumidifierState(activity_name),
]);
if (humidifier === STATUS.ON && dehumidifier === STATUS.OFF) {
return this.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER;
} else if (humidifier === STATUS.OFF && dehumidifier === STATUS.ON) {
return this.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER;
} else {
// both off also returns here, but HumidifierActive handles that.
return this.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER;
}
};
}

class TargetDehumidifyPoint extends ThermostatCharacteristicWrapper {
ctype = this.Characteristic.RelativeHumidityDehumidifierThreshold;
props = {minValue: 46, maxValue: 58, minStep: 2};
default_value = 58;

get = async () => {
return convertSystemDehum2CharDehum(
await this.system.config.getActivityDehumidifierTarget(
await this.getActivity(),
),
);
};
}

class TargetHumidify extends CharacteristicWrapper {
class TargetHumidifyPoint extends ThermostatCharacteristicWrapper {
ctype = this.Characteristic.RelativeHumidityHumidifierThreshold;
props = {minValue: 5, maxValue: 45, minStep: 5};
default_value = 5;

get = async () => {
return convertSystemHum2CharHum(
await this.system.config.getActivityHumidifierTarget(
await this.getActivity(),
),
);
};
}


Expand All @@ -25,7 +106,10 @@ export class ThermostatRHService extends MultiWrapper {
export class HumidifierService extends MultiWrapper {
WRAPPERS = [
CurrentRH,
TargetDehumidify,
TargetHumidify,
HumidifierActive,
HumidifierCurrentState,
HumidifierTargetState,
TargetHumidifyPoint,
TargetDehumidifyPoint,
];
}
44 changes: 44 additions & 0 deletions src/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {
convertCharDehum2SystemDehum,
convertCharHum2SystemHum,
convertCharTemp2SystemTemp,
convertSystemDehum2CharDehum,
convertSystemHum2CharHum,
processSetpointDeadband,
} from './helpers';

Expand Down Expand Up @@ -45,3 +49,43 @@ describe('processSetpointDeadband', () => {
expect(processSetpointDeadband(72, 70, 'F', false)).toEqual([72, 74]);
});
});

describe('humidity helpers', () => {
test('convertSystemHum2CharHum', () => {
expect(convertSystemHum2CharHum(1)).toEqual(5);
expect(convertSystemHum2CharHum(5)).toEqual(25);
expect(convertSystemHum2CharHum(8)).toEqual(40);
});

test('convertCharHum2SystemHum', () => {
expect(convertCharHum2SystemHum(40)).toEqual(8);
expect(convertCharHum2SystemHum(10)).toEqual(2);
});

test('there and back again', () => {
expect(convertCharHum2SystemHum(convertSystemHum2CharHum(1))).toEqual(1);
expect(convertCharHum2SystemHum(convertSystemHum2CharHum(8))).toEqual(8);
expect(convertSystemHum2CharHum(convertCharHum2SystemHum(50))).toEqual(50);
expect(convertSystemHum2CharHum(convertCharHum2SystemHum(60))).toEqual(60);
});
});

describe('dehumidity helpers', () => {
test('convertSystemDehum2CharDehum', () => {
expect(convertSystemDehum2CharDehum(1)).toEqual(46);
expect(convertSystemDehum2CharDehum(5)).toEqual(54);
expect(convertSystemDehum2CharDehum(8)).toEqual(60);
});

test('convertCharHum2SystemHum', () => {
expect(convertCharDehum2SystemDehum(60)).toEqual(8);
expect(convertCharDehum2SystemDehum(80)).toEqual(18);
});

test('there and back again', () => {
expect(convertCharDehum2SystemDehum(convertSystemDehum2CharDehum(1))).toEqual(1);
expect(convertCharDehum2SystemDehum(convertSystemDehum2CharDehum(8))).toEqual(8);
expect(convertSystemDehum2CharDehum(convertCharDehum2SystemDehum(50))).toEqual(50);
expect(convertSystemDehum2CharDehum(convertCharDehum2SystemDehum(60))).toEqual(60);
});
});
16 changes: 16 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,20 @@ export function processSetpointDeadband(
}
}
return [htsp, clsp];
}

export function convertSystemHum2CharHum(level: number): CharacteristicValue {
return level * 5;
}

export function convertCharHum2SystemHum(level: CharacteristicValue): number {
return Math.round(Number(level) / 5);
}

export function convertSystemDehum2CharDehum(level: number): CharacteristicValue {
return level * 2 + 44;
}

export function convertCharDehum2SystemDehum(level: CharacteristicValue): number {
return Math.round((Number(level) - 44) / 2);
}
58 changes: 58 additions & 0 deletions src/infinityApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ interface BaseElement {
'$': {id: string};
}

interface HumidityActivity extends BaseElement {
rhtg: string[];
rclg: string[];
rclgovercool: string[];
humidifier: string[];
}

interface ZoneActivity extends BaseElement {
clsp: string[];
htsp: string[];
Expand Down Expand Up @@ -351,6 +358,17 @@ export class InfinityEvolutionSystemStatus extends BaseInfinityEvolutionSystemAp
}
}

async getHumidifier(): Promise<string> {
await this.fetch();
return this.data_object.status.humid[0];
}

async getDehumidifier(): Promise<string> {
await this.fetch();
// TODO: include cool also in dehumidifier being on?
return this.data_object.status.mode[0] === 'dehumidify' ? STATUS.ON : STATUS.OFF;
}

private async getZone(zone: string): Promise<Zone> {
await this.fetch();
return this.data_object.status.zones[0].zone.find(
Expand Down Expand Up @@ -469,6 +487,46 @@ export class InfinityEvolutionSystemConfig extends BaseInfinityEvolutionSystemAp
}
}

private async getActivityHumidityConfig(activity_name: string): Promise<HumidityActivity> {
await this.fetch();
switch(activity_name) {
case ACTIVITY.VACATION:
return this.data_object.config.humidityVacation[0];
case ACTIVITY.AWAY:
return this.data_object.config.humidityAway[0];
case ACTIVITY.HOME:
case ACTIVITY.SLEEP:
case ACTIVITY.WAKE:
case ACTIVITY.MANUAL:
return this.data_object.config.humidityHome[0];
default:
this.api_connection.log.error(
`Unknown activity '${activity_name}'. Report bug: https://bit.ly/3igbU7D`,
);
return this.data_object.config.humidityHome[0];
}
}

async getActivityHumidifierState(activity_name: string): Promise<string> {
const activity_obj = await this.getActivityHumidityConfig(activity_name);
return activity_obj.humidifier[0];
}

async getActivityHumidifierTarget(activity_name: string): Promise<number> {
const activity_obj = await this.getActivityHumidityConfig(activity_name);
return Number(activity_obj.rhtg[0]);
}

async getActivityDehumidifierState(activity_name: string): Promise<string> {
const activity_obj = await this.getActivityHumidityConfig(activity_name);
return activity_obj.rclgovercool[0];
}

async getActivityDehumidifierTarget(activity_name: string): Promise<number> {
const activity_obj = await this.getActivityHumidityConfig(activity_name);
return Number(activity_obj.rclg[0]);
}

private async getZoneActivityConfig(zone: string, activity_name: string): Promise<ZoneActivity> {
await this.fetch();
// Vacation is stored somewhere else...
Expand Down

0 comments on commit 2771087

Please sign in to comment.