Skip to content

Commit

Permalink
Support heating state and current temperature
Browse files Browse the repository at this point in the history
* Added the support of heating state ("Heating to" when the combustation is ON) and the current water temperature (from the outlet temperature).
* Improved the stepped target temperature calculation for controllers using imperial unit.
* Avoid adding recirulation button if the heater is not configured for it yet
  • Loading branch information
zimuliu committed Dec 22, 2023
1 parent 2ec4b63 commit f5fea0b
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 36 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge Rinnai Control-R",
"name": "homebridge-rinnai-controlr",
"version": "1.0.24",
"version": "1.0.27",
"description": "Integrates with Rinnai Control-R for HomeKit control of water heaters",
"license": "Apache-2.0",
"repository": {
Expand Down
19 changes: 17 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,15 @@ export const API_KEY_SET_PRIORITY_STATUS = 'set_priority_status';
export const API_KEY_RECIRCULATION_DURATION = 'recirculation_duration';
export const API_KEY_SET_RECIRCULATION_ENABLED = 'set_recirculation_enabled';
export const API_KEY_SET_TEMPERATURE = 'set_domestic_temperature';
export const API_KEY_DO_MAINTENANCE_RETRIEVAL = 'do_maintenance_retrieval';
export const API_VALUE_TRUE = 'true';
export const API_VALUE_FALSE = 'false';

export const API_POLL_THROTTLE_MILLIS = 1000;
export const SET_STATE_WAIT_TIME_MILLIS = 5000;

export const ACCESSARY_INFO_UPDATE_THROTTLE_MILLIS = 30000;

export const MANUFACTURER = 'Rinnai';
export const UNKNOWN = 'Unknown';

Expand All @@ -273,8 +276,20 @@ export enum TemperatureUnits {
F = 'F',
}

export const THERMOSTAT_STEP_VALUE = 0.5; // in C, as HomeKit uses this unit for accessories
export const WATER_HEATER_STEP_VALUE_IN_F = 2; // Controllers with the imperial unit, use 98/100/102/etc
// All numbers below use metric units (C), as required by HomeKit APIs
export const THERMOSTAT_TARGET_TEMP_STEP_VALUE = 1;

export const THERMOSTAT_CURRENT_TEMP_STEP_VALUE = 0.5;
export const THERMOSTAT_CURRENT_TEMP_MAX_VALUE = 65; // 60C for residential water heater based on the manual, adding 5 as buffer
export const THERMOSTAT_CURRENT_TEMP_MIN_VALUE = 0; // 0C, frozen pipe...

/**
* For water heater controllers with the imperial unit,
* the steps are 98/100/102/.../108/110/105/110/115/120/...
*/
export const WATER_HEATER_SMALL_STEP_VALUE_IN_F = 2;
export const WATER_HEATER_BIG_STEP_START_IN_F = 110;
export const WATER_HEATER_BIG_STEP_VALUE_IN_F = 5;

// Increment only for breaking service changes to remove and re-add devices
export const PREVIOUS_UUID_SUFFICES = ['-1'];
Expand Down
148 changes: 117 additions & 31 deletions src/platformAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ import {
API_KEY_RECIRCULATION_DURATION,
API_KEY_SET_PRIORITY_STATUS, API_KEY_SET_RECIRCULATION_ENABLED,
API_KEY_SET_TEMPERATURE,
API_KEY_DO_MAINTENANCE_RETRIEVAL,
API_VALUE_TRUE,
MANUFACTURER,
SET_STATE_WAIT_TIME_MILLIS,
TemperatureUnits, THERMOSTAT_STEP_VALUE,
WATER_HEATER_STEP_VALUE_IN_F,
TemperatureUnits, THERMOSTAT_TARGET_TEMP_STEP_VALUE,
WATER_HEATER_SMALL_STEP_VALUE_IN_F,
THERMOSTAT_CURRENT_TEMP_MAX_VALUE,
THERMOSTAT_CURRENT_TEMP_MIN_VALUE,
UNKNOWN,
ACCESSARY_INFO_UPDATE_THROTTLE_MILLIS,
API_VALUE_FALSE,
THERMOSTAT_CURRENT_TEMP_STEP_VALUE,
WATER_HEATER_BIG_STEP_START_IN_F,
WATER_HEATER_BIG_STEP_VALUE_IN_F,
} from './constants';
import _ from 'lodash';
import {celsiusToFahrenheit, fahrenheitToCelsius} from './util';

const RECIRC_SERVICE_NAME = 'Recirculation';
Expand All @@ -23,16 +32,19 @@ const RECIRC_SERVICE_NAME = 'Recirculation';
*/
export class RinnaiControlrPlatformAccessory {
private service: Service;
// Controller unit
private readonly isFahrenheit: boolean;
private readonly minValue: number; // in C
private readonly maxValue: number; // in C
private targetTemperature: number; // in C
// All values below are in C as required by HomeKit
private readonly minValue: number;
private readonly maxValue: number;
private targetTemperature!: number;
private outletTemperature!: number;
private isRunning!: boolean;

constructor(
private readonly platform: RinnaiControlrHomebridgePlatform,
private readonly accessory: PlatformAccessory,
) {
this.platform.log.debug(`Setting accessory details for device: ${JSON.stringify(this.accessory.context, null, 2)}`);
this.isFahrenheit = this.platform.getConfig().temperatureUnits === TemperatureUnits.F;

this.minValue = this.platform.getConfig().minimumTemperature;
Expand All @@ -42,15 +54,11 @@ export class RinnaiControlrPlatformAccessory {
this.maxValue = fahrenheitToCelsius(this.maxValue);
}

this.minValue = Math.floor(this.minValue / THERMOSTAT_STEP_VALUE) * THERMOSTAT_STEP_VALUE;
this.maxValue = Math.ceil(this.maxValue / THERMOSTAT_STEP_VALUE) * THERMOSTAT_STEP_VALUE;
this.minValue = Math.floor(this.minValue / THERMOSTAT_TARGET_TEMP_STEP_VALUE) * THERMOSTAT_TARGET_TEMP_STEP_VALUE;
this.maxValue = Math.ceil(this.maxValue / THERMOSTAT_TARGET_TEMP_STEP_VALUE) * THERMOSTAT_TARGET_TEMP_STEP_VALUE;
this.platform.log.debug(`Target Temperature Slider Min: ${this.minValue}, Max: ${this.maxValue}`);

this.targetTemperature = this.isFahrenheit && this.accessory.context.info?.domestic_temperature
? fahrenheitToCelsius(this.accessory.context.info.domestic_temperature)
: this.accessory.context.info.domestic_temperature;

this.platform.log.info(`Temperature Slider Min: ${this.minValue}, Max: ${this.maxValue}, ` +
`target temperature: ${this.targetTemperature}`);
this.extractDeviceInfo();

// set accessory information
this.accessory.getService(this.platform.Service.AccessoryInformation)!
Expand All @@ -67,24 +75,47 @@ export class RinnaiControlrPlatformAccessory {
this.bindStaticValues();
}

extractDeviceInfo() {
this.platform.log.debug(`Setting accessory details from device payload: ${JSON.stringify(this.accessory.context, null, 2)}`);

if (this.accessory.context.info) {
this.targetTemperature = this.isFahrenheit && this.accessory.context.info.domestic_temperature
? fahrenheitToCelsius(this.accessory.context.info.domestic_temperature)
: this.accessory.context.info.domestic_temperature;

this.outletTemperature = this.isFahrenheit && this.accessory.context.info.m02_outlet_temperature
? fahrenheitToCelsius(this.accessory.context.info.m02_outlet_temperature)
: this.accessory.context.info.m02_outlet_temperature;

this.isRunning = this.accessory.context.info.domestic_combustion == API_VALUE_TRUE;
} else {
this.platform.log.error(`Cannot extract details from ${JSON.stringify(this.accessory.context, null, 2)}`);
}

this.platform.log.debug(`Extracted device info: ` +
`target temperature = ${this.targetTemperature}, ` +
`outlet temperature = ${this.outletTemperature}, ` +
`is running = ${this.isRunning}`);
}

bindTemperature() {
this.service.getCharacteristic(this.platform.Characteristic.TargetTemperature)
.onSet(this.setTargetTemperature.bind(this))
.onGet(this.getTargetTemperature.bind(this))
.setProps({
minValue: this.minValue,
maxValue: this.maxValue,
minStep: THERMOSTAT_STEP_VALUE,
minStep: THERMOSTAT_TARGET_TEMP_STEP_VALUE,
})
.updateValue(this.targetTemperature);

this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
.onGet(this.getTargetTemperature.bind(this))
.updateValue(this.targetTemperature)
.onGet(this.getOutletTemperature.bind(this))
.updateValue(this.outletTemperature)
.setProps({
minValue: this.minValue,
maxValue: this.maxValue,
minStep: THERMOSTAT_STEP_VALUE,
minValue: THERMOSTAT_CURRENT_TEMP_MIN_VALUE,
maxValue: THERMOSTAT_CURRENT_TEMP_MAX_VALUE,
minStep: THERMOSTAT_CURRENT_TEMP_STEP_VALUE,
});

this.service.getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
Expand All @@ -94,10 +125,20 @@ export class RinnaiControlrPlatformAccessory {
maxValue: this.platform.Characteristic.TargetHeatingCoolingState.HEAT,
validValues: [this.platform.Characteristic.TargetHeatingCoolingState.HEAT],
});

this.service.getCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState)
.onGet(this.getIsRunning.bind(this))
.updateValue(this.isRunning ? this.platform.Characteristic.CurrentHeatingCoolingState.HEAT : this.platform.Characteristic.CurrentHeatingCoolingState.OFF)
.setProps({
minValue: this.platform.Characteristic.CurrentHeatingCoolingState.OFF,
maxValue: this.platform.Characteristic.CurrentHeatingCoolingState.HEAT,
validValues: [this.platform.Characteristic.CurrentHeatingCoolingState.OFF, this.platform.Characteristic.CurrentHeatingCoolingState.HEAT],
});
}

bindRecirculation() {
if (this.accessory.context.info?.recirculation_capable === API_VALUE_TRUE) {
if (this.accessory.context.info?.recirculation_capable === API_VALUE_TRUE &&
this.accessory.context.shadow?.recirculation_not_configured === API_VALUE_FALSE) {
this.platform.log.debug(`Device ${this.accessory.context.id} has recirculation capabilities. Adding service.`);
const recircService = this.accessory.getService(RECIRC_SERVICE_NAME) ||
this.accessory.addService(this.platform.Service.Switch, RECIRC_SERVICE_NAME, `${this.accessory.context.id}-Recirculation`);
Expand All @@ -106,7 +147,7 @@ export class RinnaiControlrPlatformAccessory {
recircService.updateCharacteristic(this.platform.Characteristic.On,
this.accessory.context.shadow.recirculation_enabled);
} else {
this.platform.log.debug(`Device ${this.accessory.context.id} does not support recirculation.`);
this.platform.log.debug(`Device ${this.accessory.context.id} does not support recirculation or has not be configured for recirculation.`);
}
}

Expand All @@ -115,8 +156,6 @@ export class RinnaiControlrPlatformAccessory {
this.platform.getConfig().temperatureUnits === TemperatureUnits.F
? this.platform.Characteristic.TemperatureDisplayUnits.FAHRENHEIT
: this.platform.Characteristic.TemperatureDisplayUnits.CELSIUS);
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState,
this.platform.Characteristic.CurrentHeatingCoolingState.HEAT);
}

async setRecirculateActive(value: CharacteristicValue) {
Expand All @@ -132,29 +171,76 @@ export class RinnaiControlrPlatformAccessory {
await this.platform.setState(this.accessory, state);
}

async setTargetTemperature(value: CharacteristicValue) {
this.platform.log.info(`setTemperature to ${value} for device ${this.accessory.context.id}`);
async retrieveMaintenanceInfo() {
const state: Record<string, string | number | boolean> = {
[API_KEY_DO_MAINTENANCE_RETRIEVAL]: true,
};

await this.platform.setState(this.accessory, state);
}

public throttledRetrieveMaintenanceInfo = _.throttle(async () => {
await this.retrieveMaintenanceInfo();
}, ACCESSARY_INFO_UPDATE_THROTTLE_MILLIS);

accessoryToControllerTemperature(value: number): number {
let convertedValue: number = this.isFahrenheit ? celsiusToFahrenheit(value) : value;
if (this.isFahrenheit) {
if (convertedValue >= WATER_HEATER_BIG_STEP_START_IN_F) {
convertedValue = Math.round(celsiusToFahrenheit(convertedValue) / WATER_HEATER_SMALL_STEP_VALUE_IN_F) * WATER_HEATER_SMALL_STEP_VALUE_IN_F
} else {
convertedValue = Math.round(celsiusToFahrenheit(convertedValue) / WATER_HEATER_BIG_STEP_VALUE_IN_F) * WATER_HEATER_BIG_STEP_VALUE_IN_F
}
} else {
convertedValue = Math.round(convertedValue);
}

convertedValue = Math.max(this.platform.getConfig().minimumTemperature as number, convertedValue);
convertedValue = Math.min(this.platform.getConfig().maximumTemperature as number, convertedValue);

const convertedValue: number = this.isFahrenheit
? Math.round(celsiusToFahrenheit(value as number) / WATER_HEATER_STEP_VALUE_IN_F) * WATER_HEATER_STEP_VALUE_IN_F
: value as number;
return convertedValue;
}

async setTargetTemperature(value: CharacteristicValue) {
this.platform.log.info(`setTemperature to ${value} C for device ${this.accessory.context.id}`);

this.platform.log.info(`Sending converted/rounded temperature: ${convertedValue}`);
const convertedValue = this.accessoryToControllerTemperature(value as number);
this.platform.log.info(`Sending converted/rounded temperature: ${convertedValue} ${this.platform.getConfig().temperatureUnits}`);

const state: Record<string, string | number | boolean> = {
[API_KEY_SET_PRIORITY_STATUS]: true,
[API_KEY_SET_TEMPERATURE]: convertedValue,
};

await this.platform.setState(this.accessory, state);

setTimeout(() => {
this.platform.throttledPoll();
}, SET_STATE_WAIT_TIME_MILLIS);

this.targetTemperature = this.isFahrenheit ? fahrenheitToCelsius(convertedValue) : convertedValue;
}

async getTargetTemperature(): Promise<Nullable<CharacteristicValue>> {
this.platform.throttledPoll();
await this.platform.throttledPoll();
this.extractDeviceInfo();

return this.targetTemperature;
}

async getOutletTemperature(): Promise<Nullable<CharacteristicValue>> {
await this.throttledRetrieveMaintenanceInfo()

await this.platform.throttledPoll();
this.extractDeviceInfo();

return this.outletTemperature;
}

async getIsRunning(): Promise<Nullable<CharacteristicValue>> {
await this.platform.throttledPoll();
this.extractDeviceInfo();

return this.isRunning;
}
}

0 comments on commit f5fea0b

Please sign in to comment.