From c8f4a55873533eb01f27a88bd8a3f7a6346ff5bc Mon Sep 17 00:00:00 2001 From: Jon Ziebell Date: Sat, 20 Feb 2021 21:47:10 -0500 Subject: [PATCH] Time to temperature for Patrons --- api/ecobee_thermostat.php | 26 +- js/beestat.js | 73 ------ js/beestat/thermostat.js | 102 ++++++++ js/component/card/system.js | 307 ++++++++++++++---------- js/component/modal/change_thermostat.js | 2 +- js/layer/detail.js | 4 +- 6 files changed, 306 insertions(+), 208 deletions(-) diff --git a/api/ecobee_thermostat.php b/api/ecobee_thermostat.php index 8c53d32..613dce0 100644 --- a/api/ecobee_thermostat.php +++ b/api/ecobee_thermostat.php @@ -182,10 +182,10 @@ class ecobee_thermostat extends cora\crud { $attributes['name'] = $api_thermostat['name']; $attributes['inactive'] = 0; - // There are some instances where ecobee gives invalid temperature values. + // Temperature. Ignore values outside possible ranges available. if( - ($api_thermostat['runtime']['actualTemperature'] / 10) > 999.9 || - ($api_thermostat['runtime']['actualTemperature'] / 10) < -999.9 + ($api_thermostat['runtime']['actualTemperature'] / 10) > 120 || + ($api_thermostat['runtime']['actualTemperature'] / 10) < -10 ) { $attributes['temperature'] = null; } else { @@ -204,6 +204,26 @@ class ecobee_thermostat extends cora\crud { $attributes['humidity'] = $api_thermostat['runtime']['actualHumidity']; } + // Heat setpoint. Ignore values outside possible ranges available. + if( + ($api_thermostat['runtime']['desiredHeat'] / 10) > 120 || + ($api_thermostat['runtime']['desiredHeat'] / 10) < 45 + ) { + $attributes['setpoint_heat'] = null; + } else { + $attributes['setpoint_heat'] = ($api_thermostat['runtime']['desiredHeat'] / 10); + } + + // Cool setpoint. Ignore values outside possible ranges available. + if( + ($api_thermostat['runtime']['desiredCool'] / 10) > 120 || + ($api_thermostat['runtime']['desiredCool'] / 10) < -10 + ) { + $attributes['setpoint_cool'] = null; + } else { + $attributes['setpoint_cool'] = ($api_thermostat['runtime']['desiredCool'] / 10); + } + $attributes['first_connected'] = $api_thermostat['runtime']['firstConnected']; $address = $this->get_address($thermostat, $ecobee_thermostat); diff --git a/js/beestat.js b/js/beestat.js index f1cf3f3..7c5da8e 100644 --- a/js/beestat.js +++ b/js/beestat.js @@ -30,79 +30,6 @@ beestat.default_value = function(argument, default_value) { return (argument === undefined) ? default_value : argument; }; -/** - * Get the climate for whatever climate_ref is specified. - * - * @param {string} climate_ref The ecobee climateRef - * - * @return {object} The climate - */ -beestat.get_climate = function(climate_ref) { - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; - - var ecobee_thermostat = beestat.cache.ecobee_thermostat[ - thermostat.ecobee_thermostat_id - ]; - - var climates = ecobee_thermostat.program.climates; - - for (var i = 0; i < climates.length; i++) { - if (climates[i].climateRef === climate_ref) { - return climates[i]; - } - } -}; - -/** - * Get the color a thermostat should be based on what equipment is running. - * - * @return {string} The color string. - */ -beestat.get_thermostat_color = function(thermostat_id) { - var thermostat = beestat.cache.thermostat[thermostat_id]; - var ecobee_thermostat = beestat.cache.ecobee_thermostat[ - thermostat.ecobee_thermostat_id - ]; - - if ( - ecobee_thermostat.equipment_status.indexOf('compCool2') !== -1 || - ecobee_thermostat.equipment_status.indexOf('compCool1') !== -1 - ) { - return beestat.style.color.blue.light; - } else if ( - ecobee_thermostat.settings.hasHeatPump === true && - ( - ecobee_thermostat.equipment_status.indexOf('auxHeat3') !== -1 || - ecobee_thermostat.equipment_status.indexOf('auxHeat2') !== -1 || - ecobee_thermostat.equipment_status.indexOf('auxHeat1') !== -1 || - ecobee_thermostat.equipment_status.indexOf('auxHotWater') !== -1 - ) - ) { - return beestat.style.color.red.base; - } else if ( - ( - ecobee_thermostat.settings.hasHeatPump === false && - ( - ecobee_thermostat.equipment_status.indexOf('auxHeat3') !== -1 || - ecobee_thermostat.equipment_status.indexOf('auxHeat2') !== -1 || - ecobee_thermostat.equipment_status.indexOf('auxHeat1') !== -1 || - ecobee_thermostat.equipment_status.indexOf('compHotWater') !== -1 || - ecobee_thermostat.equipment_status.indexOf('auxHotWater') !== -1 - ) - ) || - ( - ecobee_thermostat.settings.hasHeatPump === true && - ( - ecobee_thermostat.equipment_status.indexOf('heatPump1') !== -1 || - ecobee_thermostat.equipment_status.indexOf('heatPump2') !== -1 - ) - ) - ) { - return beestat.style.color.orange.base; - } - return beestat.style.color.bluegray.dark; -}; - // Register service worker if ('serviceWorker' in navigator) { window.addEventListener('load', function() { diff --git a/js/beestat/thermostat.js b/js/beestat/thermostat.js index 37f1a33..7fd5695 100644 --- a/js/beestat/thermostat.js +++ b/js/beestat/thermostat.js @@ -97,3 +97,105 @@ beestat.thermostat.get_stages = function(thermostat_id, mode) { return 'unknown'; }; + +/** + * Get the color a thermostat should be based on what equipment is running. + * + * @param {number} thermostat_id + * + * @return {string} The color string. + */ +beestat.thermostat.get_color = function(thermostat_id) { + var thermostat = beestat.cache.thermostat[thermostat_id]; + var ecobee_thermostat = beestat.cache.ecobee_thermostat[ + thermostat.ecobee_thermostat_id + ]; + + if ( + ecobee_thermostat.equipment_status.indexOf('compCool2') !== -1 || + ecobee_thermostat.equipment_status.indexOf('compCool1') !== -1 + ) { + return beestat.style.color.blue.light; + } else if ( + ecobee_thermostat.settings.hasHeatPump === true && + ( + ecobee_thermostat.equipment_status.indexOf('auxHeat3') !== -1 || + ecobee_thermostat.equipment_status.indexOf('auxHeat2') !== -1 || + ecobee_thermostat.equipment_status.indexOf('auxHeat1') !== -1 || + ecobee_thermostat.equipment_status.indexOf('auxHotWater') !== -1 + ) + ) { + return beestat.style.color.red.base; + } else if ( + ( + ecobee_thermostat.settings.hasHeatPump === false && + ( + ecobee_thermostat.equipment_status.indexOf('auxHeat3') !== -1 || + ecobee_thermostat.equipment_status.indexOf('auxHeat2') !== -1 || + ecobee_thermostat.equipment_status.indexOf('auxHeat1') !== -1 || + ecobee_thermostat.equipment_status.indexOf('compHotWater') !== -1 || + ecobee_thermostat.equipment_status.indexOf('auxHotWater') !== -1 + ) + ) || + ( + ecobee_thermostat.settings.hasHeatPump === true && + ( + ecobee_thermostat.equipment_status.indexOf('heatPump1') !== -1 || + ecobee_thermostat.equipment_status.indexOf('heatPump2') !== -1 + ) + ) + ) { + return beestat.style.color.orange.base; + } + return beestat.style.color.bluegray.dark; +}; + +/** + * Get the overall operating mode. This is meant for casual display of what my + * system is doing right now. + * + * @param {number} thermostat_id + * + * @return {string} One of heat_1, heat_2, auxiliary_heat_1, auxiliary_heat_2, + * cool_1, cool_2, or off. + */ +beestat.thermostat.get_operating_mode = function(thermostat_id) { + const thermostat = beestat.cache.thermostat[thermostat_id]; + + const operating_modes = [ + 'auxiliary_heat_2', + 'auxiliary_heat_1', + 'heat_2', + 'heat_1', + 'cool_2', + 'cool_1' + ]; + + for (let i = 0; i < operating_modes.length; i++) { + if (thermostat.running_equipment.includes(operating_modes[i]) === true) { + return operating_modes[i]; + } + } + + return 'off'; +}; + +/** + * Get the currently in-use climate. + * + * @param {number} thermostat_id + * @param {string} climate_ref The ecobee climateRef + * + * @return {object} The climate + */ +beestat.thermostat.get_current_climate = function(thermostat_id) { + const thermostat = beestat.cache.thermostat[thermostat_id]; + const climates = thermostat.program.climates; + const climate_ref = thermostat.program.currentClimateRef; + + for (var i = 0; i < climates.length; i++) { + if (climates[i].climateRef === climate_ref) { + return climates[i]; + } + } +}; diff --git a/js/component/card/system.js b/js/component/card/system.js index dfad10a..b74bc2a 100644 --- a/js/component/card/system.js +++ b/js/component/card/system.js @@ -1,10 +1,14 @@ /** * System card. Shows a big picture of your thermostat, it's sensors, and lets * you switch between thermostats. + * + * @param {number} thermostat_id */ -beestat.component.card.system = function() { +beestat.component.card.system = function(thermostat_id) { var self = this; + this.thermostat_id_ = thermostat_id; + var change_function = beestat.debounce(function() { self.rerender(); }, 10); @@ -26,6 +30,10 @@ beestat.component.card.system.prototype.decorate_contents_ = function(parent) { this.decorate_weather_(parent); this.decorate_equipment_(parent); this.decorate_climate_(parent); + + if (beestat.user.has_early_access() === true) { + this.decorate_time_to_temperature_(parent); + } }; /** @@ -34,7 +42,7 @@ beestat.component.card.system.prototype.decorate_contents_ = function(parent) { * @param {rocket.Elements} parent */ beestat.component.card.system.prototype.decorate_circle_ = function(parent) { - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + var thermostat = beestat.cache.thermostat[this.thermostat_id_]; var temperature = beestat.temperature(thermostat.temperature); var temperature_whole = Math.floor(temperature); @@ -44,7 +52,7 @@ beestat.component.card.system.prototype.decorate_circle_ = function(parent) { .style({ 'padding': (beestat.style.size.gutter * 3), 'border-radius': '50%', - 'background': beestat.get_thermostat_color(beestat.setting('thermostat_id')), + 'background': beestat.thermostat.get_color(this.thermostat_id_), 'height': '180px', 'width': '180px', 'margin': beestat.style.size.gutter + 'px auto ' + beestat.style.size.gutter + 'px auto', @@ -93,7 +101,7 @@ beestat.component.card.system.prototype.decorate_circle_ = function(parent) { * @param {rocket.Elements} parent Parent */ beestat.component.card.system.prototype.decorate_weather_ = function(parent) { - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + var thermostat = beestat.cache.thermostat[this.thermostat_id_]; var circle = $.createElement('div') .style({ @@ -165,66 +173,7 @@ beestat.component.card.system.prototype.decorate_weather_ = function(parent) { * @param {rocket.Elements} parent */ beestat.component.card.system.prototype.decorate_equipment_ = function(parent) { - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; - - var ecobee_thermostat = beestat.cache.ecobee_thermostat[ - thermostat.ecobee_thermostat_id - ]; - - var running_equipment = []; - - if (ecobee_thermostat.equipment_status.indexOf('fan') !== -1) { - running_equipment.push('fan'); - } - - if (ecobee_thermostat.equipment_status.indexOf('ventilator') !== -1) { - running_equipment.push('ventilator'); - } - if (ecobee_thermostat.equipment_status.indexOf('humidifier') !== -1) { - running_equipment.push('humidifier'); - } - if (ecobee_thermostat.equipment_status.indexOf('dehumidifier') !== -1) { - running_equipment.push('dehumidifier'); - } - if (ecobee_thermostat.equipment_status.indexOf('economizer') !== -1) { - running_equipment.push('economizer'); - } - - if (ecobee_thermostat.equipment_status.indexOf('compCool2') !== -1) { - running_equipment.push('cool_2'); - } else if (ecobee_thermostat.equipment_status.indexOf('compCool1') !== -1) { - running_equipment.push('cool_1'); - } - - if (ecobee_thermostat.settings.hasHeatPump === true) { - if (ecobee_thermostat.equipment_status.indexOf('heatPump3') !== -1) { - running_equipment.push('heat_3'); - } else if (ecobee_thermostat.equipment_status.indexOf('heatPump2') !== -1) { - running_equipment.push('heat_2'); - } else if (ecobee_thermostat.equipment_status.indexOf('heatPump') !== -1) { - running_equipment.push('heat_1'); - } - if (ecobee_thermostat.equipment_status.indexOf('auxHeat3') !== -1) { - running_equipment.push('aux_3'); - } else if (ecobee_thermostat.equipment_status.indexOf('auxHeat2') !== -1) { - running_equipment.push('aux_2'); - } else if (ecobee_thermostat.equipment_status.indexOf('auxHeat1') !== -1) { - running_equipment.push('aux_1'); - } - } else if (ecobee_thermostat.equipment_status.indexOf('auxHeat3') !== -1) { - running_equipment.push('heat_3'); - } else if (ecobee_thermostat.equipment_status.indexOf('auxHeat2') !== -1) { - running_equipment.push('heat_2'); - } else if (ecobee_thermostat.equipment_status.indexOf('auxHeat1') !== -1) { - running_equipment.push('heat_1'); - } - - if (ecobee_thermostat.equipment_status.indexOf('compHotWater') !== -1) { - running_equipment.push('heat_1'); - } - if (ecobee_thermostat.equipment_status.indexOf('auxHotWater') !== -1) { - running_equipment.push('aux_1'); - } + var thermostat = beestat.cache.thermostat[this.thermostat_id_]; var render_icon = function(icon_parent, icon, color, text) { (new beestat.component.icon(icon) @@ -248,56 +197,69 @@ beestat.component.card.system.prototype.decorate_equipment_ = function(parent) { } }; - if (running_equipment.length === 0) { - running_equipment.push('nothing'); + if (thermostat.running_equipment.length === 0) { + render_icon(parent, 'cancel', beestat.style.color.gray.base, 'none'); + } else { + thermostat.running_equipment.forEach(function(equipment) { + let subscript; + switch (equipment) { + case 'fan': + render_icon(parent, 'fan', beestat.style.color.gray.light); + break; + case 'cool_1': + if (thermostat.system_type2.detected.cool.stages > 1) { + subscript = '1'; + } else { + subscript = undefined; + } + render_icon(parent, 'snowflake', beestat.style.color.blue.light, subscript); + break; + case 'cool_2': + render_icon(parent, 'snowflake', beestat.style.color.blue.light, '2'); + break; + case 'heat_1': + if (thermostat.system_type2.detected.heat.stages > 1) { + subscript = '1'; + } else { + subscript = undefined; + } + render_icon(parent, 'fire', beestat.style.color.orange.base, subscript); + break; + case 'heat_2': + render_icon(parent, 'fire', beestat.style.color.orange.base, '2'); + break; + case 'heat_3': + render_icon(parent, 'fire', beestat.style.color.orange.base, '3'); + break; + case 'auxiliary_heat_1': + if (thermostat.system_type2.detected.auxiliary_heat.stages > 1) { + subscript = '1'; + } else { + subscript = undefined; + } + render_icon(parent, 'fire', beestat.style.color.red.base, subscript); + break; + case 'auxiliary_heat_2': + render_icon(parent, 'fire', beestat.style.color.red.base, '2'); + break; + case 'auxiliary_heat_3': + render_icon(parent, 'fire', beestat.style.color.red.base, '3'); + break; + case 'humidifier': + render_icon(parent, 'water_percent', beestat.style.color.gray.base, ''); + break; + case 'dehumidifier': + render_icon(parent, 'water_off', beestat.style.color.gray.base, ''); + break; + case 'ventilator': + render_icon(parent, 'air_purifier', beestat.style.color.gray.base, 'v'); + break; + case 'economizer': + render_icon(parent, 'cash', beestat.style.color.gray.base, ''); + break; + } + }); } - - running_equipment.forEach(function(equipment) { - switch (equipment) { - case 'nothing': - render_icon(parent, 'cancel', beestat.style.color.gray.base, 'none'); - break; - case 'fan': - render_icon(parent, 'fan', beestat.style.color.gray.light); - break; - case 'cool_1': - render_icon(parent, 'snowflake', beestat.style.color.blue.light, '1'); - break; - case 'cool_2': - render_icon(parent, 'snowflake', beestat.style.color.blue.light, '2'); - break; - case 'heat_1': - render_icon(parent, 'fire', beestat.style.color.orange.base, '1'); - break; - case 'heat_2': - render_icon(parent, 'fire', beestat.style.color.orange.base, '2'); - break; - case 'heat_3': - render_icon(parent, 'fire', beestat.style.color.orange.base, '3'); - break; - case 'aux_1': - render_icon(parent, 'fire', beestat.style.color.red.base, '1'); - break; - case 'aux_2': - render_icon(parent, 'fire', beestat.style.color.red.base, '2'); - break; - case 'aux_3': - render_icon(parent, 'fire', beestat.style.color.red.base, '3'); - break; - case 'humidifier': - render_icon(parent, 'water_percent', beestat.style.color.gray.base, ''); - break; - case 'dehumidifier': - render_icon(parent, 'water_off', beestat.style.color.gray.base, ''); - break; - case 'ventilator': - render_icon(parent, 'air_purifier', beestat.style.color.gray.base, 'v'); - break; - case 'economizer': - render_icon(parent, 'cash', beestat.style.color.gray.base, ''); - break; - } - }); }; /** @@ -306,14 +268,10 @@ beestat.component.card.system.prototype.decorate_equipment_ = function(parent) { * @param {rocket.Elements} parent */ beestat.component.card.system.prototype.decorate_climate_ = function(parent) { - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + var thermostat = beestat.cache.thermostat[this.thermostat_id_]; - var ecobee_thermostat = beestat.cache.ecobee_thermostat[ - thermostat.ecobee_thermostat_id - ]; - - var climate = beestat.get_climate( - ecobee_thermostat.program.currentClimateRef + var climate = beestat.thermostat.get_current_climate( + thermostat.thermostat_id ); var climate_container = $.createElement('div') @@ -344,13 +302,102 @@ beestat.component.card.system.prototype.decorate_climate_ = function(parent) { .style('margin-left', beestat.style.size.gutter / 4)); }; +/** + * Decorate time to heat/cool. This is how long it will take your home to heat + * or cool to the desired setpoint. + * + * @param {rocket.Elements} parent + */ +beestat.component.card.system.prototype.decorate_time_to_temperature_ = function(parent) { + const thermostat = beestat.cache.thermostat[this.thermostat_id_]; + + const container = $.createElement('div').style({ + 'background': beestat.style.color.bluegray.dark, + 'padding': beestat.style.size.gutter / 2, + 'text-align': 'center', + 'margin-top': beestat.style.size.gutter + }); + parent.appendChild(container); + + const operating_mode = beestat.thermostat.get_operating_mode( + thermostat.thermostat_id + ); + + // System off; don't display anything. + if (operating_mode === 'off') { + return; + } + + // Convert "heat_1" etc to "heat" + const simplified_operating_mode = operating_mode.replace(/_\d/, ''); + + let header_text = 'Time to ' + simplified_operating_mode; + let text; + if (thermostat.profile.temperature[operating_mode] === null) { + // If there is no profile data; TTT is unknown. + text = 'Unknown'; + } else { + const linear_trendline = thermostat.profile.temperature[operating_mode].linear_trendline; + const outdoor_temperature = thermostat.weather.temperature; + const degrees_per_hour = (linear_trendline.slope * outdoor_temperature) + linear_trendline.intercept; + + header_text += ' (' + + beestat.temperature({ + 'temperature': degrees_per_hour, + 'delta': true, + 'units': true + }) + + ' / h)'; + + if (degrees_per_hour < 0.05) { + // If the degrees would display as 0.0/h, go for "never" as the time. + text = 'Never'; + } else { + const indoor_temperature = thermostat.temperature; + let degrees_to_go; + let hours_to_go; + switch (simplified_operating_mode) { + case 'heat': + degrees_to_go = thermostat.setpoint_heat - indoor_temperature; + hours_to_go = degrees_to_go / degrees_per_hour; + text = beestat.time(hours_to_go * 60 * 60); + break; + case 'cool': + degrees_to_go = indoor_temperature - thermostat.setpoint_cool; + hours_to_go = degrees_to_go / degrees_per_hour; + text = beestat.time(hours_to_go * 60 * 60); + break; + } + + /** + * Show the actual time the temperature will be reached if there are + * less than 12 hours to go. Otherwise it's mostly irrelevant. + */ + if (hours_to_go <= 12) { + text += ' (' + + moment() + .add(hours_to_go, 'hour') + .format('h:mm a') + + ')'; + } + } + } + + container.appendChild( + $.createElement('div') + .style('font-weight', 'bold') + .innerText(header_text) + ); + container.appendChild($.createElement('div').innerText(text)); +}; + /** * Decorate the menu * * @param {rocket.Elements} parent */ beestat.component.card.system.prototype.decorate_top_right_ = function(parent) { - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + var thermostat = beestat.cache.thermostat[this.thermostat_id_]; var menu = (new beestat.component.menu()).render(parent); @@ -384,7 +431,7 @@ beestat.component.card.system.prototype.decorate_top_right_ = function(parent) { * @return {string} The title of the card. */ beestat.component.card.system.prototype.get_title_ = function() { - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + var thermostat = beestat.cache.thermostat[this.thermostat_id_]; return 'System - ' + thermostat.name; }; @@ -401,29 +448,29 @@ beestat.component.card.system.prototype.get_subtitle_ = function() { thermostat.ecobee_thermostat_id ]; - var climate = beestat.get_climate( - ecobee_thermostat.program.currentClimateRef + var climate = beestat.thermostat.get_current_climate( + thermostat.thermostat_id ); // Is the temperature overridden? var override = ( - ecobee_thermostat.runtime.desiredHeat !== climate.heatTemp || - ecobee_thermostat.runtime.desiredCool !== climate.coolTemp + thermostat.setpoint_heat !== climate.heatTemp || + thermostat.setpoint_cool !== climate.coolTemp ); // Get the heat/cool values to display. var heat; if (override === true) { - heat = ecobee_thermostat.runtime.desiredHeat / 10; + heat = thermostat.setpoint_heat; } else { - heat = climate.heatTemp / 10; + heat = climate.heatTemp; } var cool; if (override === true) { - cool = ecobee_thermostat.runtime.desiredCool / 10; + cool = thermostat.setpoint_cool; } else { - cool = climate.coolTemp / 10; + cool = climate.coolTemp; } // Translate ecobee strings to GUI strings. diff --git a/js/component/modal/change_thermostat.js b/js/component/modal/change_thermostat.js index 6587e41..34d968d 100644 --- a/js/component/modal/change_thermostat.js +++ b/js/component/modal/change_thermostat.js @@ -83,7 +83,7 @@ beestat.component.modal.change_thermostat.prototype.decorate_thermostat_ = funct var left = $.createElement('div') .style({ - 'background': beestat.get_thermostat_color(thermostat_id), + 'background': beestat.thermostat.get_color(thermostat_id), 'font-weight': beestat.style.font_weight.light, 'border-radius': '50%', 'width': thermostat_height, diff --git a/js/layer/detail.js b/js/layer/detail.js index 87d7592..275c937 100644 --- a/js/layer/detail.js +++ b/js/layer/detail.js @@ -7,6 +7,8 @@ beestat.layer.detail = function() { beestat.extend(beestat.layer.detail, beestat.layer); beestat.layer.detail.prototype.decorate_ = function(parent) { + const thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + /* * Set the overflow on the body so the scrollbar is always present so * highcharts graphs render properly. @@ -33,7 +35,7 @@ beestat.layer.detail.prototype.decorate_ = function(parent) { cards.push([ { - 'card': new beestat.component.card.system(), + 'card': new beestat.component.card.system(thermostat.thermostat_id), 'size': 4 }, {