homebridge-eightsleepthermostat
Version:
Homebridge thermostat accessory for the Eight Sleep Pod smart bed.
130 lines • 5.87 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tempMapper = exports.TwoWayTempMapper = void 0;
/**
* Maps between 'real temps' (degrees °C/°F), and 'levels' used by client API.
*
* Locally temps range from 10°C - 45°C, and 50°F - 113°F, but the Eight Sleep
* API uses a value between -100 (max cooling) to +100 (max heating), independent
* of the temp units.
*
* Eight Sleep app gives users the choice between displaying 'real' temps
* or displaying a value between -10 & +10. If user selects to display real
* temps, there's a further specification between °F/°C. Regardless of what
* the user decides to use, levels (-10/+10) or temps (°F/°C), all of the
* logic to set the bed temperature is handled on the frontend app and is
* sent to the client API as a value between -100 & +100. Thus, regardless
* of user's preferred unit setting, the API always processes the values as
* if there wasn't any units option at all.
*/
class TwoWayTempMapper {
constructor() {
this.tempsToLvlMap = {};
this.lvlsToTempMap = {};
this.celsiusToF = {};
this.fahrenheitToC = {};
/**
* Actual min. on Homekit thermostat is 50°, but client API applies a different
* weight to 'real' temps when between 50°F-61°F. For this initial lower bound
* each 1°F displayed in 8slp app corresponds to a 'level' increase of +1.
* - i.e. between 55°F/61°F, the level ranges from -96 to -90 (each 1°F is
* equivalent to +1 level), whereas above 62°F, each 1°F corresponds to an
* increase of approx. +3 on the client API's 'level' scale.
*
* Due to this, we need to apply some trickery to adjust our local thermostat
* temperature to match the API. Although using 'real' temps in Eight Sleep app
* doesn't allow going to the full min and max of -100/+100, this plugin has been
* developed to enable users to do so. This plugin is also designed to keep the
* current bed temp between Eight Sleep app and homekit as consistent as possible
* to ensure cooling/heating temps aren't miasligned between the two.
*
* See {@linkcode calculateTempF()} method below to understand how this works
*/
// Min & max cooling temps on thermostat
this.cooling_tempF_start = 61;
this.cooling_tempF_end = 80;
// Range of cooling levels for client
this.cooling_level_start = -89;
this.cooling_level_end = -1;
// Min & max heating temps on thermostat
this.heating_tempF_start = 81;
this.heating_tempF_end = 113;
// Range of heating levels for client
this.heating_level_start = 2;
this.heating_level_end = 101;
this.generateMaps();
}
levelToFahrenheit(level) {
return this.lvlsToTempMap[level];
}
fahrenheitToLevel(degF) {
return this.tempsToLvlMap[degF];
}
levelToCelsius(level) {
const tempF = this.lvlsToTempMap[level];
return this.fahrenheitToC[tempF];
}
celsiusToLevel(degC) {
const formattedC = this.formatCelsius(degC);
const tempF = this.celsiusToF[formattedC];
return this.tempsToLvlMap[tempF];
}
fahrenheitToCelsius(degF) {
return this.fahrenheitToC[degF];
}
celsiusToFahrenheit(degC) {
const formattedC = this.formatCelsius(degC);
return this.celsiusToF[formattedC];
}
formatCelsius(degC) {
return Math.trunc(100 * degC) / 100;
}
// Convert client api levels to 'real' temps for thermostat
generateMaps() {
for (let lvl = -100; lvl <= 100; lvl++) {
const tempF = this.calculateTempF(lvl);
this.updateRecords(tempF, lvl);
}
}
// Set temp/level records with inverse keys/values
updateRecords(tempF, level) {
this.lvlsToTempMap[level] = tempF;
const tempC = this.formatCelsius((tempF - 32) * 5 / 9);
// Only add to records if we haven't previously added it
if (!this.tempsToLvlMap[tempF]) {
this.celsiusToF[tempC] = tempF;
this.fahrenheitToC[tempF] = tempC;
this.tempsToLvlMap[tempF] = level;
}
}
calculateTempF(level) {
switch (true) {
case (level <= -89):
// Adjust temp according to (seemingly) arbitrary client api values.
// Cooling levels between -100 & -89 from the API correspond to a
// single degree difference locally on thermostat
return 50 - (-100 - level);
case (level <= this.cooling_level_end):
// Cooling level range -88 <= level <= 0
// Each 1°F corresponds to approx. +3 on 'level' scale
return this.getCoolingTempF(level);
default:
// Heating level range 0 <= level <= 100
// Each 1°F corresponds to approx. +3 on 'level' scale
return this.getHeatingTempF(level);
}
}
getCoolingTempF(level) {
const slope = (this.cooling_tempF_end - this.cooling_tempF_start) / (this.cooling_level_end - this.cooling_level_start);
const temp = this.cooling_tempF_start + Math.round(slope * (level - this.cooling_level_start));
return temp;
}
getHeatingTempF(level) {
const slope = (this.heating_tempF_end - this.heating_tempF_start) / (this.heating_level_end - this.heating_level_start);
const temp = this.heating_tempF_start + Math.round(slope * (level - this.heating_level_start));
return temp;
}
}
exports.TwoWayTempMapper = TwoWayTempMapper;
exports.tempMapper = new TwoWayTempMapper();
//# sourceMappingURL=twoWayTempMapper.js.map