homebridge-eightsleepthermostat
Version:
Homebridge thermostat accessory for the Eight Sleep Pod smart bed.
243 lines • 11.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EightSleepThermostatAccessory = void 0;
const twoWayTempMapper_1 = require("./twoWayTempMapper");
const clientAdapter_1 = require("./clientAdapter");
class EightSleepThermostatAccessory {
constructor(platform, accessory,
// PlatformClientAdapter used to fetch device info, shared between accessories
// since the device info for both sides is returned from single call to API
platformClient) {
this.platform = platform;
this.accessory = accessory;
this.platformClient = platformClient;
this.log = this.platform.log;
// Minstep calculated based on temp mapping of °C & °F locally,
// and to ensure precision when converting between degrees/levels
// when updating and fetching from client API.
//
// Since minstep slightly greater than 0.5, max temp allowed needs
// to be greater than 45 (i.e. 45.1) to ensure we can set the temp
// to the max on accessory in Home app (displayed as 113°F & 45°C).
this.minStep = 0.55556;
this.minTempC = 10;
this.maxTempC = 45.1;
this.temperatureDisplayUnits = 1;
this.tempMapper = twoWayTempMapper_1.tempMapper;
this.userIdForSide = this.accessory.context.device.userId;
this.deviceSide = this.accessory.context.device.side;
this.refreshState = () => {
this.publishLatestChanges();
this.clearRefreshIfNotActive();
};
// Updates values every 5 seconds using the latest data that was
// downloaded by platform & accessory client adapters. These updates
// are published without directly initiating new requests to client
// API, thus limiting unnecessary network requests.
this.publishLatestChanges = async () => {
const [targetState, targetLevel] = await this.accessoryClient.loadMostRecentSettings();
const targetTemp = this.tempMapper.levelToCelsius(targetLevel);
this.service.updateCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, targetState);
this.service.updateCharacteristic(this.platform.Characteristic.TargetTemperature, targetTemp);
const currentLevel = await this.platformClient.loadMostRecentSettings(this.deviceSide);
const currentTemp = this.tempMapper.levelToCelsius(currentLevel);
this.service.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, currentTemp);
const currentState = this.characteristicValueForCurrentState(currentTemp, targetTemp, targetState);
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, currentState);
};
this.log.debug('Accessory Context:', this.accessory.context);
this.accessoryClient = new clientAdapter_1.AccessoryClientAdapter(this.userIdForSide, this.log);
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Eight Sleep')
.setCharacteristic(this.platform.Characteristic.Model, 'Pod Pro')
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.accessory.context.device.pluginSerial);
this.service = this.accessory.getService(this.platform.Service.Thermostat) ||
this.accessory.addService(this.platform.Service.Thermostat);
this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.displayName);
this.service.getCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState)
.onGet(this.handleCurrentHeatingCoolingStateGet.bind(this))
.setProps({ validValues: [
this.platform.Characteristic.CurrentHeatingCoolingState.OFF,
this.platform.Characteristic.CurrentHeatingCoolingState.HEAT,
this.platform.Characteristic.CurrentHeatingCoolingState.COOL
] });
this.service.getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
.onSet(this.handleTargetHeatingCoolingStateSet.bind(this))
.onGet(this.handleTargetHeatingCoolingStateGet.bind(this))
.setProps({ validValues: [
this.platform.Characteristic.TargetHeatingCoolingState.OFF,
this.platform.Characteristic.TargetHeatingCoolingState.AUTO
] });
this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
.onGet(this.handleCurrentTemperatureGet.bind(this))
.setProps({ minStep: this.minStep, minValue: this.minTempC, maxValue: this.maxTempC });
this.service.getCharacteristic(this.platform.Characteristic.TargetTemperature)
.onSet(this.handleTargetTemperatureSet.bind(this))
.onGet(this.handleTargetTemperatureGet.bind(this))
.setProps({ minStep: this.minStep, minValue: this.minTempC, maxValue: this.maxTempC });
this.service.getCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits)
.onSet(this.handleTemperatureDisplayUnitsSet.bind(this))
.onGet(this.handleTemperatureDisplayUnitsGet.bind(this));
this.lastActive = Date.now();
this.refreshInterval = this.startRefreshing();
}
setPluginAsActive() {
this.lastActive = Date.now();
if (!this.refreshInterval) {
// Fetch new state & start refreshing every 5 seconds for next 2 minutes
this.refreshInterval = this.startRefreshing();
}
}
startRefreshing() {
// Fetch updated info from adapters every 5 seconds (while active)
// NOTE: this logic loads the last fetched values, it does not initiate
// a new fetch. Since each adapter sets its own fetch interval, we just
// need to load from the previous values already retrieved
return setInterval(this.refreshState, 1000 * 5);
}
clearRefreshIfNotActive() {
if (this.refreshInterval && this.lastActive < Date.now() - 1000 * 60 * 2) {
// Go into standby until next time there is controller activity
global.clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
/**
* Gets the *measured* current temperature of each side of bed from
* client API. This metric is returned from client for both sides
* of bed from the same endpoint. In order to prevent multiple
* unecessary requests to the same endpoint for each side of bed,
* we query API once and parse the data using the 'side' property
*/
async fetchCurrentTemp() {
const currentMeasuredLevel = await this.platformClient.getCurrentLevel(this.deviceSide);
const currentC = this.tempMapper.levelToCelsius(currentMeasuredLevel);
return currentC;
}
async fetchTargetState() {
const accessoryIsOn = await this.accessoryClient.getAccessoryIsOn();
const targetState = accessoryIsOn ? 3 : 0;
return targetState;
}
async fetchTargetTemp() {
const targetLevel = await this.accessoryClient.getUserTargetLevel();
const targetC = this.tempMapper.levelToCelsius(targetLevel);
return targetC;
}
async fetchCurrentState() {
const [targetState, currTemp, targetTemp] = [
await this.fetchTargetState(),
await this.fetchCurrentTemp(),
await this.fetchTargetTemp()
];
return this.characteristicValueForCurrentState(currTemp, targetTemp, targetState);
}
characteristicValueForCurrentState(currentTemp, targetTemp, targetState) {
if (targetState === 0) {
return this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
}
if (this.tempsAreEqual(currentTemp, targetTemp)) {
return this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
}
else if (currentTemp < targetTemp) {
return this.platform.Characteristic.CurrentHeatingCoolingState.HEAT;
}
else {
return this.platform.Characteristic.CurrentHeatingCoolingState.COOL;
}
}
async updateCurrentHCState() {
const currState = await this.fetchCurrentState();
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, currState);
}
async updateTargetTemperature(tempC) {
const targetLevel = this.tempMapper.celsiusToLevel(tempC);
if (!targetLevel || targetLevel > 100 || targetLevel < -100) {
this.log.error('Something went wrong calculating new bed temp:', targetLevel);
return;
}
const receivedLevel = await this.accessoryClient.updateUserTargetLevel(targetLevel);
this.verifyInSyncTemps(tempC, targetLevel, receivedLevel);
this.updateCurrentHCState();
}
async updateDeviceState(newValue) {
const side = this.deviceSide;
if (newValue === 3) {
await this.accessoryClient.turnOnAccessory();
}
else if (newValue === 0) {
await this.accessoryClient.turnOffAccessory();
}
this.log.debug(`Toggled device state -> ${newValue} for device:`, side);
this.updateCurrentHCState();
}
/**
* Current Temperature & State Handlers
*/
async handleCurrentHeatingCoolingStateGet() {
this.setPluginAsActive();
return this.fetchCurrentState();
}
async handleCurrentTemperatureGet() {
this.setPluginAsActive();
return this.fetchCurrentTemp();
}
/**
* Target Temperature Handlers
*/
async handleTargetTemperatureGet() {
this.setPluginAsActive();
return this.fetchTargetTemp();
}
async handleTargetTemperatureSet(value) {
this.setPluginAsActive();
const targetTemp = value;
return this.updateTargetTemperature(targetTemp);
}
/**
* Target State Handlers
*/
async handleTargetHeatingCoolingStateGet() {
this.setPluginAsActive();
return this.fetchTargetState();
}
async handleTargetHeatingCoolingStateSet(value) {
this.setPluginAsActive();
const newTargetState = value;
return this.updateDeviceState(newTargetState);
}
/**
* Temperature Display Units Handlers
*/
async handleTemperatureDisplayUnitsGet() {
this.setPluginAsActive();
const tempUnits = this.temperatureDisplayUnits;
return tempUnits;
}
async handleTemperatureDisplayUnitsSet(value) {
this.setPluginAsActive();
this.temperatureDisplayUnits = value;
}
/**
* Adjust equality comparison to account for the `minStep` property
* of 0.5 on Target temp. Ensures that display temps are actually
* equal when determining the current state. If target state is set
* to `on` (`Auto`, `Cool`, `Heat`), then current state will display
* `Idle` in home status when temps are equal.
*/
tempsAreEqual(current, target) {
const diff = Math.abs(target - current);
return (diff <= 0.55);
}
verifyInSyncTemps(targetC, targetLevel, receivedLevel) {
const formattedC = this.tempMapper.formatCelsius(targetC);
const clientTargetC = this.tempMapper.levelToCelsius(receivedLevel);
if (formattedC !== clientTargetC || targetLevel !== receivedLevel) {
const expectation = `${formattedC}°C / ${targetLevel} level`;
const received = `${clientTargetC}°C / ${receivedLevel} level`;
this.log.error(`Local/remote temp mismatch. Expected: ${expectation}, but got: ${received}`);
}
}
}
exports.EightSleepThermostatAccessory = EightSleepThermostatAccessory;
//# sourceMappingURL=platformAccessory.js.map