diff --git a/api/thermostat_group.php b/api/thermostat_group.php index fe91530..bbd8cbd 100644 --- a/api/thermostat_group.php +++ b/api/thermostat_group.php @@ -29,7 +29,7 @@ class thermostat_group extends cora\crud { 'generate_profile' => 604800, // 7 Days 'generate_profiles' => 604800, // 7 Days 'get_scores' => 604800, // 7 Days - 'get_metrics' => 604800 // 7 Days + // 'get_metrics' => 604800 // 7 Days ]; /** @@ -565,7 +565,8 @@ class thermostat_group extends cora\crud { $metric_codes = [ 'setpoint_heat', - 'setpoint_cool' + 'setpoint_cool', + 'runtime_per_heating_degree_day' ]; $metrics = []; @@ -636,6 +637,25 @@ class thermostat_group extends cora\crud { $metrics['setpoint_cool']['histogram'][$setpoint_cool]++; $metrics['setpoint_cool']['values'][] = $setpoint_cool; } + + // runtime_per_heating_degree_day + if( + isset($other_thermostat_group['profile']) === true && + isset($other_thermostat_group['profile']['runtime']) == true && + $other_thermostat_group['profile']['runtime']['heat_1'] !== null && + isset($other_thermostat_group['profile']['degree_days']) === true && + $other_thermostat_group['profile']['degree_days']['heat'] !== null + ) { + $runtime_per_heating_degree_day = round( + $other_thermostat_group['profile']['runtime']['heat_1'] / $other_thermostat_group['profile']['degree_days']['heat'], + 1 + ); + if(isset($metrics['runtime_per_heating_degree_day']['histogram'][(string)$runtime_per_heating_degree_day]) === false) { + $metrics['runtime_per_heating_degree_day']['histogram'][(string)$runtime_per_heating_degree_day] = 0; + } + $metrics['runtime_per_heating_degree_day']['histogram'][(string)$runtime_per_heating_degree_day]++; + $metrics['runtime_per_heating_degree_day']['values'][] = $runtime_per_heating_degree_day; + } } } @@ -656,6 +676,13 @@ class thermostat_group extends cora\crud { $metrics['setpoint_cool']['median'] = array_median($metrics['setpoint_cool']['values']); unset($metrics['setpoint_cool']['values']); + // runtime_per_heating_degree_day + $metrics['runtime_per_heating_degree_day']['standard_deviation'] = round($this->standard_deviation( + $metrics['runtime_per_heating_degree_day']['values'] + ), 2); + $metrics['runtime_per_heating_degree_day']['median'] = array_median($metrics['runtime_per_heating_degree_day']['values']); + unset($metrics['runtime_per_heating_degree_day']['values']); + return $metrics; } diff --git a/js/beestat/home_comparisons.js b/js/beestat/comparisons.js similarity index 88% rename from js/beestat/home_comparisons.js rename to js/beestat/comparisons.js index 2dbc9ab..5ecbd57 100644 --- a/js/beestat/home_comparisons.js +++ b/js/beestat/comparisons.js @@ -1,4 +1,4 @@ -beestat.home_comparisons = {}; +beestat.comparisons = {}; /** * Fire off an API call to get the comparison scores using the currently @@ -14,7 +14,7 @@ beestat.home_comparisons = {}; * @param {Function} callback Optional callback to fire when the API call * completes. */ -beestat.home_comparisons.get_comparison_scores = function(callback) { +beestat.comparisons.get_comparison_scores = function(callback) { var types = [ 'heat', 'cool', @@ -29,15 +29,26 @@ beestat.home_comparisons.get_comparison_scores = function(callback) { 'get_scores', { 'type': type, - 'attributes': beestat.home_comparisons.get_comparison_attributes(type) + 'attributes': beestat.comparisons.get_comparison_attributes(type) }, - type + 'score_' + type ); }); + api.add_call( + 'thermostat_group', + 'get_metrics', + { + 'attributes': beestat.comparisons.get_comparison_attributes('resist') // todo + }, + 'metrics' + ); + api.set_callback(function(data) { + beestat.cache.set('data.metrics', data.metrics); + types.forEach(function(type) { - beestat.cache.set('data.comparison_scores_' + type, data[type]); + beestat.cache.set('data.comparison_scores_' + type, data['score_' + type]); }); if (callback !== undefined) { @@ -56,7 +67,7 @@ beestat.home_comparisons.get_comparison_scores = function(callback) { * * @return {Object} The comparison attributes. */ -beestat.home_comparisons.get_comparison_attributes = function(type) { +beestat.comparisons.get_comparison_attributes = function(type) { var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; var thermostat_group = beestat.cache.thermostat_group[thermostat.thermostat_group_id]; diff --git a/js/beestat/style.js b/js/beestat/style.js index 9e00f5c..c04b752 100644 --- a/js/beestat/style.js +++ b/js/beestat/style.js @@ -298,3 +298,34 @@ beestat.series.indoor_resist_delta = { 'color': beestat.style.color.gray.dark }; beestat.series.indoor_resist_delta_raw = beestat.series.indoor_resist_delta; + +// Temperature Profiles New +beestat.series.indoor_heat_1_delta = { + 'name': 'Indoor Heat 1 Δ', + 'color': beestat.series.compressor_heat_1.color +}; +beestat.series.indoor_heat_1_delta_raw = beestat.series.indoor_heat_1_delta; + +beestat.series.indoor_heat_2_delta = { + 'name': 'Indoor Heat 2 Δ', + 'color': beestat.series.compressor_heat_2.color +}; +beestat.series.indoor_heat_2_delta_raw = beestat.series.indoor_heat_2_delta; + +beestat.series.indoor_cool_1_delta = { + 'name': 'Indoor Cool 1 Δ', + 'color': beestat.series.compressor_cool_1.color +}; +beestat.series.indoor_cool_1_delta_raw = beestat.series.indoor_cool_1_delta; + +beestat.series.indoor_cool_2_delta = { + 'name': 'Indoor Cool 2 Δ', + 'color': beestat.series.compressor_cool_2.color +}; +beestat.series.indoor_cool_2_delta_raw = beestat.series.indoor_cool_2_delta; + +beestat.series.indoor_resist_delta = { + 'name': 'Indoor Δ', + 'color': beestat.style.color.gray.dark +}; +beestat.series.indoor_resist_delta_raw = beestat.series.indoor_resist_delta; diff --git a/js/beestat/user.js b/js/beestat/user.js index b47c3d7..877ea73 100644 --- a/js/beestat/user.js +++ b/js/beestat/user.js @@ -32,8 +32,7 @@ beestat.user.has_early_access = function() { return user.user_id === 1 || ( user.patreon_status !== null && - user.patreon_status.patron_status === 'active_patron' && - user.patreon_status.currently_entitled_amount_cents >= 500 + user.patreon_status.patron_status === 'active_patron' ); }; diff --git a/js/component/card/comparison_issue.js b/js/component/card/comparison_issue.js deleted file mode 100644 index cf3ebff..0000000 --- a/js/component/card/comparison_issue.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Possible issue with your comparison. - */ -beestat.component.card.comparison_issue = function() { - beestat.component.card.apply(this, arguments); -}; -beestat.extend(beestat.component.card.comparison_issue, beestat.component.card); - -/** - * Decorate - * - * @param {rocket.Elements} parent - */ -beestat.component.card.comparison_issue.prototype.decorate_contents_ = function(parent) { - parent.style('background', beestat.style.color.red.dark); - parent.appendChild($.createElement('p').innerText('Notice how one or more of the lines below slopes down or is very flat? The expectation is that these slope upwards. This may affect the accuracy of your scores.')); - parent.appendChild($.createElement('p').innerText('I\'ll be investigating these situations and improving the algorithm as much as possible to provide as accurate results as I can. Thank you!')); -}; - -/** - * Get the title of the card. - * - * @return {string} The title of the card. - */ -beestat.component.card.comparison_issue.prototype.get_title_ = function() { - return 'Possible issue with your temperature profiles!'; -}; - diff --git a/js/component/card/comparison_settings.js b/js/component/card/comparison_settings.js index 10abe27..5ac122e 100644 --- a/js/component/card/comparison_settings.js +++ b/js/component/card/comparison_settings.js @@ -51,24 +51,24 @@ beestat.component.card.comparison_settings.prototype.decorate_contents_ = functi * If the data is available, then get the data if we don't already have it * loaded. If the data is not available, poll until it becomes available. */ - if (thermostat_group.temperature_profile === null) { + if (thermostat_group.profile === null) { // This will show the loading screen. self.data_available_(); var poll_interval = 10000; beestat.add_poll_interval(poll_interval); - beestat.dispatcher.addEventListener('poll.home_comparisons_load', function() { + beestat.dispatcher.addEventListener('poll.comparisons_load', function() { if (self.data_available_() === true) { beestat.remove_poll_interval(poll_interval); - beestat.dispatcher.removeEventListener('poll.home_comparisons_load'); + beestat.dispatcher.removeEventListener('poll.comparisons_load'); new beestat.api() .add_call( 'thermostat_group', - 'generate_temperature_profiles', + 'generate_profiles', {}, - 'generate_temperature_profiles' + 'generate_profiles' ) .add_call( 'thermostat_group', @@ -78,7 +78,7 @@ beestat.component.card.comparison_settings.prototype.decorate_contents_ = functi ) .set_callback(function(response) { beestat.cache.set('thermostat_group', response.thermostat_group); - (new beestat.layer.home_comparisons()).render(); + (new beestat.layer.comparisons()).render(); }) .send(); } @@ -127,7 +127,7 @@ beestat.component.card.comparison_settings.prototype.decorate_region_ = function // Open up the loading window. self.show_loading_('Calculating Score for ' + region + ' region'); - beestat.home_comparisons.get_comparison_scores(function() { + beestat.comparisons.get_comparison_scores(function() { // Rerender to get rid of the loader. self.rerender(); }); @@ -200,7 +200,7 @@ beestat.component.card.comparison_settings.prototype.decorate_property_ = functi // Open up the loading window. self.show_loading_('Calculating Score for ' + property_type.text); - beestat.home_comparisons.get_comparison_scores(function() { + beestat.comparisons.get_comparison_scores(function() { // Rerender to get rid of the loader. self.rerender(); }); diff --git a/js/component/card/early_access.js b/js/component/card/early_access.js new file mode 100644 index 0000000..80c2e85 --- /dev/null +++ b/js/component/card/early_access.js @@ -0,0 +1,27 @@ +/** + * Early access + */ +beestat.component.card.early_access = function() { + beestat.component.card.apply(this, arguments); +}; +beestat.extend(beestat.component.card.early_access, beestat.component.card); + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.card.early_access.prototype.decorate_contents_ = function(parent) { + parent.style('background', beestat.style.color.green.base); + parent.appendChild($.createElement('p').innerText('Experimental early access features below! ⤵')); +}; + +/** + * Get the title of the card. + * + * @return {string} The title of the card. + */ +// beestat.component.card.early_access.prototype.get_title_ = function() { +// return 'Possible issue with your temperature profiles!'; +// }; + diff --git a/js/component/card/metrics.js b/js/component/card/metrics.js new file mode 100644 index 0000000..0db362f --- /dev/null +++ b/js/component/card/metrics.js @@ -0,0 +1,84 @@ +/** + * Metrics card. + */ +beestat.component.card.metrics = function(thermostat_group_id) { + this.thermostat_group_id_ = thermostat_group_id; + + var self = this; + + /* + * Debounce so that multiple setting changes don't re-trigger the same + * event. This fires on the trailing edge so that all changes are accounted + * for when rerendering. + */ + var data_change_function = beestat.debounce(function() { + self.rerender(); + }, 10); + + beestat.dispatcher.addEventListener( + 'cache.data.metrics', + data_change_function + ); + + beestat.component.card.apply(this, arguments); + + // this.layer_.register_loader(beestat.comparisons.get_comparison_metricss); +}; +beestat.extend(beestat.component.card.metrics, beestat.component.card); + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.card.metrics.prototype.decorate_contents_ = function(parent) { + var self = this; + + var metrics = [ + 'setpoint_heat', + 'setpoint_cool', + // 'runtime_per_heating_degree_day' + ]; + + // Decorate the metrics + var metric_container = $.createElement('div') + .style({ + 'display': 'grid', + // 'grid-template-columns': 'repeat(auto-fit, minmax(160px, 1fr))', + 'grid-template-columns': '1fr 1fr 1fr', + 'margin': '0 0 ' + beestat.style.size.gutter + 'px -' + beestat.style.size.gutter + 'px' + }); + parent.appendChild(metric_container); + + metrics.forEach(function(metric) { + var div = $.createElement('div') + .style({ + 'padding': beestat.style.size.gutter + 'px 0 0 ' + beestat.style.size.gutter + 'px' + }); + metric_container.appendChild(div); + + (new beestat.component.metric[metric](self.thermostat_group_id_)).render(div); + }); + + + + +}; + +/** + * Get the title of the card. + * + * @return {string} The title of the card. + */ +beestat.component.card.metrics.prototype.get_title_ = function() { + return 'Metrics'; +}; + +/** + * Decorate the menu. + * + * @param {rocket.Elements} parent + */ +/*beestat.component.card.my_home.prototype.decorate_top_right_ = function(parent) { + +};*/ diff --git a/js/component/card/score.js b/js/component/card/score.js index c225b5c..9ed45ca 100644 --- a/js/component/card/score.js +++ b/js/component/card/score.js @@ -20,7 +20,7 @@ beestat.component.card.score = function() { beestat.component.card.apply(this, arguments); - this.layer_.register_loader(beestat.home_comparisons.get_comparison_scores); + this.layer_.register_loader(beestat.comparisons.get_comparison_scores); }; beestat.extend(beestat.component.card.score, beestat.component.card); diff --git a/js/component/card/temperature_profiles_new.js b/js/component/card/temperature_profiles_new.js new file mode 100644 index 0000000..66c44df --- /dev/null +++ b/js/component/card/temperature_profiles_new.js @@ -0,0 +1,237 @@ +/** + * Temperature profiles. + * + * @param {number} thermostat_group_id The thermostat_group_id this card is + * displaying data for. + */ +beestat.component.card.temperature_profiles_new = function(thermostat_group_id) { + this.thermostat_group_id_ = thermostat_group_id; + + beestat.component.card.apply(this, arguments); +}; +beestat.extend(beestat.component.card.temperature_profiles_new, beestat.component.card); + +/** + * Decorate card. + * + * @param {rocket.Elements} parent + */ +beestat.component.card.temperature_profiles_new.prototype.decorate_contents_ = function(parent) { + var data = this.get_data_(); + this.chart_ = new beestat.component.chart.temperature_profiles_new(data); + this.chart_.render(parent); +}; + +/** + * Get all of the series data. + * + * @return {object} The series data. + */ +beestat.component.card.temperature_profiles_new.prototype.get_data_ = function() { + var thermostat_group = beestat.cache.thermostat_group[ + this.thermostat_group_id_ + ]; + + var data = { + 'x': [], + 'series': {}, + 'metadata': { + 'series': {}, + 'chart': { + 'title': this.get_title_(), + 'subtitle': this.get_subtitle_(), + 'outdoor_temperature': beestat.temperature({ + 'temperature': (thermostat_group.weather.temperature / 10), + 'round': 0 + }) + } + } + }; + + if ( + thermostat_group.profile === null + ) { + this.chart_.render(parent); + this.show_loading_('Calculating'); + } else { + // Global x range. + var x_min = Infinity; + var x_max = -Infinity; + + var y_min = Infinity; + var y_max = -Infinity; + for (var type in thermostat_group.profile.temperature) { + // Cloned because I mutate this data for temperature conversions. + var profile = beestat.clone( + thermostat_group.profile.temperature[type] + ); + + if (profile !== null) { + // Convert the data to Celsius if necessary + var deltas_converted = {}; + for (var key in profile.deltas) { + deltas_converted[beestat.temperature({'temperature': key})] = + beestat.temperature({ + 'temperature': (profile.deltas[key]), + 'delta': true, + 'round': 3 + }); + } + + profile.deltas = deltas_converted; + var linear_trendline = this.get_linear_trendline_(profile.deltas); + + var min_max_keys = Object.keys(profile.deltas); + + // This specific trendline x range. + var this_x_min = Math.min.apply(null, min_max_keys); + var this_x_max = Math.max.apply(null, min_max_keys); + + // Global x range. + x_min = Math.min(x_min, this_x_min); + x_max = Math.max(x_max, this_x_max); + + data.series['trendline_' + type] = []; + data.series['raw_' + type] = []; + + /** + * Data is stored internally as °F with 1 value per degree. That data + * gets converted to °C which then requires additional precision + * (increment). + * + * The additional precision introduces floating point error, so + * convert the x value to a fixed string. + * + * The string then needs converted to a number for highcharts, so + * later on use parseFloat to get back to that. + * + * Stupid Celsius. + */ + var increment; + var fixed; + if (beestat.setting('temperature_unit') === '°F') { + increment = 1; + fixed = 0; + } else { + increment = 0.1; + fixed = 1; + } + for (var x = this_x_min; x <= this_x_max; x += increment) { + var x_fixed = x.toFixed(fixed); + var y = (linear_trendline.slope * x_fixed) + + linear_trendline.intercept; + + data.series['trendline_' + type].push([ + parseFloat(x_fixed), + y + ]); + if (profile.deltas[x_fixed] !== undefined) { + data.series['raw_' + type].push([ + parseFloat(x_fixed), + profile.deltas[x_fixed] + ]); + y_min = Math.min(y_min, profile.deltas[x_fixed]); + y_max = Math.max(y_max, profile.deltas[x_fixed]); + } + + data.metadata.chart.y_min = y_min; + data.metadata.chart.y_max = y_max; + } + } + } + } + + return data; +}; + +/** + * Get a linear trendline from a set of data. + * + * @param {Object} data The data; at least two points required. + * + * @return {Object} The slope and intercept of the trendline. + */ +beestat.component.card.temperature_profiles_new.prototype.get_linear_trendline_ = function(data) { + // Requires at least two points. + if (Object.keys(data).length < 2) { + return null; + } + + var sum_x = 0; + var sum_y = 0; + var sum_xy = 0; + var sum_x_squared = 0; + var n = 0; + + for (var x in data) { + x = parseFloat(x); + var y = parseFloat(data[x]); + + sum_x += x; + sum_y += y; + sum_xy += (x * y); + sum_x_squared += Math.pow(x, 2); + n++; + } + + var slope = ((n * sum_xy) - (sum_x * sum_y)) / + ((n * sum_x_squared) - (Math.pow(sum_x, 2))); + var intercept = ((sum_y) - (slope * sum_x)) / (n); + + return { + 'slope': slope, + 'intercept': intercept + }; +}; + +/** + * Get the title of the card. + * + * @return {string} The title. + */ +beestat.component.card.temperature_profiles_new.prototype.get_title_ = function() { + return 'Temperature Profiles'; +}; + +/** + * Get the subtitle of the card. + * + * @return {string} The subtitle. + */ +beestat.component.card.temperature_profiles_new.prototype.get_subtitle_ = function() { + var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + var thermostat_group = beestat.cache.thermostat_group[ + thermostat.thermostat_group_id + ]; + + var generated_at_m = moment( + thermostat_group.profile.metadata.generated_at + ); + + return 'Generated ' + generated_at_m.format('MMM Do @ h a') + ' (updated weekly)'; +}; + +/** + * Decorate the menu. + * + * @param {rocket.Elements} parent + */ +beestat.component.card.temperature_profiles_new.prototype.decorate_top_right_ = function(parent) { + var self = this; + + var menu = (new beestat.component.menu()).render(parent); + + menu.add_menu_item(new beestat.component.menu_item() + .set_text('Download Chart') + .set_icon('download') + .set_callback(function() { + self.chart_.export(); + })); + + menu.add_menu_item(new beestat.component.menu_item() + .set_text('Help') + .set_icon('help_circle') + .set_callback(function() { + window.open('https://doc.beestat.io/9c0fba6793dd4bc68f798c1516f0ea25'); + })); +}; diff --git a/js/component/chart/temperature_profiles_new.js b/js/component/chart/temperature_profiles_new.js new file mode 100644 index 0000000..5adb56e --- /dev/null +++ b/js/component/chart/temperature_profiles_new.js @@ -0,0 +1,346 @@ +/** + * Temperature profiles chart. + * + * @param {object} data The chart data. + */ +beestat.component.chart.temperature_profiles_new = function(data) { + this.data_ = data; + + beestat.component.chart.apply(this, arguments); +}; +beestat.extend(beestat.component.chart.temperature_profiles_new, beestat.component.chart); + +/** + * Override for get_options_xAxis_labels_formatter_. + * + * @return {Function} xAxis labels formatter. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_xAxis_labels_formatter_ = function() { + return function() { + return this.value + beestat.setting('temperature_unit'); + }; +}; + +/** + * Override for get_options_series_. + * + * @return {Array} All of the series to display on the chart. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_series_ = function() { + var series = []; + + // Trendline data + series.push({ + 'data': this.data_.series.trendline_heat_1, + 'name': 'indoor_heat_1_delta', + 'color': beestat.series.indoor_heat_1_delta.color, + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'line', + 'lineWidth': 2, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Trendline data + series.push({ + 'data': this.data_.series.trendline_heat_2, + 'name': 'indoor_heat_2_delta', + 'color': beestat.series.indoor_heat_2_delta.color, + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'line', + 'lineWidth': 2, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Trendline data + series.push({ + 'data': this.data_.series.trendline_cool_1, + 'name': 'indoor_cool_1_delta', + 'color': beestat.series.indoor_cool_1_delta.color, + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'line', + 'lineWidth': 2, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Trendline data + series.push({ + 'data': this.data_.series.trendline_cool_2, + 'name': 'indoor_cool_2_delta', + 'color': beestat.series.indoor_cool_2_delta.color, + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'line', + 'lineWidth': 2, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Trendline data + series.push({ + 'data': this.data_.series.trendline_resist, + 'name': 'indoor_resist_delta', + 'color': beestat.series.indoor_resist_delta.color, + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'line', + 'lineWidth': 2, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Raw data + series.push({ + 'data': this.data_.series.raw_heat_1, + 'name': 'indoor_heat_1_delta_raw', + 'color': beestat.series.indoor_heat_1_delta_raw.color, + 'dashStyle': 'ShortDot', + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'spline', + 'lineWidth': 1, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Raw data + series.push({ + 'data': this.data_.series.raw_heat_2, + 'name': 'indoor_heat_2_delta_raw', + 'color': beestat.series.indoor_heat_2_delta_raw.color, + 'dashStyle': 'ShortDot', + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'spline', + 'lineWidth': 1, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Raw data + series.push({ + 'data': this.data_.series.raw_cool_1, + 'name': 'indoor_cool_1_delta_raw', + 'color': beestat.series.indoor_cool_1_delta_raw.color, + 'dashStyle': 'ShortDot', + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'spline', + 'lineWidth': 1, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Raw data + series.push({ + 'data': this.data_.series.raw_cool_2, + 'name': 'indoor_cool_2_delta_raw', + 'color': beestat.series.indoor_cool_2_delta_raw.color, + 'dashStyle': 'ShortDot', + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'spline', + 'lineWidth': 1, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + // Raw data + series.push({ + 'data': this.data_.series.raw_resist, + 'name': 'indoor_resist_delta_raw', + 'color': beestat.series.indoor_resist_delta_raw.color, + 'dashStyle': 'ShortDot', + 'marker': { + 'enabled': false, + 'states': {'hover': {'enabled': false}} + }, + 'type': 'spline', + 'lineWidth': 1, + 'states': {'hover': {'lineWidthPlus': 0}} + }); + + return series; +}; + +/** + * Override for get_options_yAxis_. + * + * @return {Array} The y-axis options. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_yAxis_ = function() { + var absolute_y_max = Math.max( + Math.abs(this.data_.metadata.chart.y_min), + Math.abs(this.data_.metadata.chart.y_max) + ); + + var y_min = absolute_y_max * -1; + var y_max = absolute_y_max; + + return [ + { + 'alignTicks': false, + 'gridLineColor': beestat.style.color.bluegray.light, + 'gridLineDashStyle': 'longdash', + 'title': {'text': null}, + 'labels': { + 'style': {'color': beestat.style.color.gray.base}, + 'formatter': function() { + return this.value + beestat.setting('temperature_unit'); + } + }, + 'min': y_min, + 'max': y_max, + 'plotLines': [ + { + 'color': beestat.style.color.bluegray.light, + 'dashStyle': 'solid', + 'width': 3, + 'value': 0, + 'zIndex': 1 + } + ] + } + ]; +}; + +/** + * Override for get_options_tooltip_formatter_. + * + * @return {Function} The tooltip formatter. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_tooltip_formatter_ = function() { + var self = this; + + return function() { + var sections = []; + var section = []; + this.points.forEach(function(point) { + var series = point.series; + + var value = beestat.temperature({ + 'temperature': point.y, + 'units': true, + 'convert': false, + 'delta': true, + 'type': 'string' + }) + ' / h'; + + if (series.name.indexOf('raw') === -1) { + section.push({ + 'label': beestat.series[series.name].name, + 'value': value, + 'color': series.color + }); + } + }); + sections.push(section); + + return self.tooltip_formatter_helper_( + 'Outdoor Temp: ' + + beestat.temperature({ + 'temperature': this.x, + 'round': 0, + 'units': true, + 'convert': false + }), + sections + ); + }; +}; + +/** + * Override for get_options_chart_zoomType_. + * + * @return {string} The zoom type. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_chart_zoomType_ = function() { + return null; +}; + +/** + * Override for get_options_legend_. + * + * @return {object} The legend options. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_legend_ = function() { + return { + 'enabled': false + }; +}; + +/** + * Override for get_options_xAxis_. + * + * @return {object} The xAxis options. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_xAxis_ = function() { + return { + 'lineWidth': 0, + 'tickLength': 0, + 'tickInterval': 5, + 'gridLineWidth': 1, + 'gridLineColor': beestat.style.color.bluegray.light, + 'gridLineDashStyle': 'longdash', + 'labels': { + 'style': { + 'color': beestat.style.color.gray.base + }, + 'formatter': this.get_options_xAxis_labels_formatter_() + }, + 'crosshair': this.get_options_xAxis_crosshair_(), + 'plotLines': [ + { + 'color': beestat.series.outdoor_temperature.color, + 'dashStyle': 'ShortDash', + 'width': 1, + 'label': { + 'style': { + 'color': beestat.series.outdoor_temperature.color + }, + 'useHTML': true, + 'text': 'Now: ' + beestat.temperature({ + 'temperature': this.data_.metadata.chart.outdoor_temperature, + 'convert': false, + 'units': true, + 'round': 0 + }) + }, + 'value': this.data_.metadata.chart.outdoor_temperature, + 'zIndex': 2 + } + ] + }; +}; + +/** + * Override for get_options_chart_height_. + * + * @return {number} The height of the chart. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_chart_height_ = function() { + return 300; +}; + +/** + * Override for get_options_plotOptions_series_connectNulls_. + * + * @return {boolean} Whether or not to connect nulls. + */ +beestat.component.chart.temperature_profiles_new.prototype.get_options_plotOptions_series_connectNulls_ = function() { + return true; +}; diff --git a/js/component/header.js b/js/component/header.js index 46b1c20..dd54ec8 100644 --- a/js/component/header.js +++ b/js/component/header.js @@ -38,7 +38,7 @@ beestat.component.header.prototype.decorate_ = function(parent) { 'icon': 'signal_variant' }, { - 'layer': 'home_comparisons', + 'layer': 'comparisons', 'text': 'Comparisons', 'icon': 'home_group' } diff --git a/js/component/metric.js b/js/component/metric.js new file mode 100644 index 0000000..ca24ce2 --- /dev/null +++ b/js/component/metric.js @@ -0,0 +1,137 @@ +/** + * Generic customizable metric. + */ +beestat.component.metric = function() { + beestat.component.apply(this, arguments); +}; +beestat.extend(beestat.component.metric, beestat.component); + +beestat.component.metric.prototype.rerender_on_breakpoint_ = false; + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.metric.prototype.decorate_ = function(parent) { + if (beestat.cache.data.metrics === undefined) { // todo + parent.appendChild($.createElement('div').innerText('Loading...')); + return; + } + + var outer_container = $.createElement('div').style({ + 'background': beestat.style.color.bluegray.dark, + 'padding': (beestat.style.size.gutter / 2) + }); + + outer_container.appendChild( + $.createElement('div').innerText(this.get_title_()) + ); + + var inner_container = $.createElement('div').style({ + 'position': 'relative', + 'margin-top': '50px', + 'margin-bottom': '20px', + 'margin-left': '25px' + }); + + var icon = $.createElement('div').style({ + 'position': 'absolute', + 'top': '-12px', + 'left': '-28px' + }); + + (new beestat.component.icon(this.get_icon_())) + .set_color(this.get_color_()) + .render(icon); + + var line = $.createElement('div').style({ + 'background': this.get_color_(), + 'height': '5px', + 'border-radius': '5px' + }); + + var min = $.createElement('div') + .innerText(this.get_min_(true)) + .style({ + 'position': 'absolute', + 'top': '10px', + 'left': '0px' + }); + + var max = $.createElement('div') + .innerText(this.get_max_(true)) + .style({ + 'position': 'absolute', + 'top': '10px', + 'right': '0px' + }); + + var label = $.createElement('div') + .innerText(this.get_value_()) + .style({ + 'position': 'absolute', + 'top': '-25px', + 'left': this.get_marker_position_() + '%', + 'width': '100px', + 'text-align': 'center', + 'margin-left': '-50px', + 'font-weight': beestat.style.font_weight.bold + }); + + var circle = $.createElement('div').style({ + 'background': this.get_color_(), + 'position': 'absolute', + 'top': '-4px', + 'left': this.get_marker_position_() + '%', + 'margin-left': '-7px', + 'width': '14px', + 'height': '14px', + 'border-radius': '50%' + }); + + var chart = $.createElement('div').style({ + 'position': 'absolute', + 'top': '-40px', + 'left': '0px', + 'width': '100%', + 'height': '40px' + }); + + var histogram = this.get_histogram_(); + var histogram_max = this.get_histogram_max_(); + var column_width = (100 / histogram.length) + '%'; + histogram.forEach(function(data) { + var column = $.createElement('div').style({ + 'display': 'inline-block', + 'background': 'rgba(255, 255, 255, 0.1)', + 'width': column_width, + 'height': (data.count / histogram_max * 100) + '%' + }); + chart.appendChild(column); + }); + + inner_container.appendChild(icon); + inner_container.appendChild(line); + inner_container.appendChild(min); + inner_container.appendChild(max); + inner_container.appendChild(label); + inner_container.appendChild(circle); + inner_container.appendChild(chart); + + outer_container.appendChild(inner_container); + + parent.appendChild(outer_container); +}; + +beestat.component.metric.prototype.get_marker_position_ = function() { + return 100 * (this.get_value_() - this.get_min_()) / (this.get_max_() - this.get_min_()); +}; + +beestat.component.metric.prototype.get_histogram_max_ = function() { + var max = -Infinity; + this.get_histogram_().forEach(function(data) { + max = Math.max(max, data.count); + }); + return max; +}; diff --git a/js/component/metric/runtime_per_heating_degree_day.js b/js/component/metric/runtime_per_heating_degree_day.js new file mode 100644 index 0000000..09635e1 --- /dev/null +++ b/js/component/metric/runtime_per_heating_degree_day.js @@ -0,0 +1,98 @@ +/** + * Runtime per heating degree day metric. + * + * @param {number} thermostat_group_id The thermostat group. + */ +beestat.component.metric.runtime_per_heating_degree_day = function(thermostat_group_id) { + this.thermostat_group_id_ = thermostat_group_id; + + beestat.component.metric.apply(this, arguments); +}; +beestat.extend(beestat.component.metric.runtime_per_heating_degree_day, beestat.component.metric); + +beestat.component.metric.runtime_per_heating_degree_day.prototype.rerender_on_breakpoint_ = false; + +/** + * Get the title of this metric. + * + * @return {string} The title of this metric. + */ +beestat.component.metric.runtime_per_heating_degree_day.prototype.get_title_ = function() { + return 'Runtime / HDD'; +}; + +/** + * Get the icon of this metric. + * + * @return {string} The icon of this metric. + */ +beestat.component.metric.runtime_per_heating_degree_day.prototype.get_icon_ = function() { + return 'fire'; +}; + +/** + * Get the color of this metric. + * + * @return {string} The color of this metric. + */ +beestat.component.metric.runtime_per_heating_degree_day.prototype.get_color_ = function() { + return beestat.series.compressor_heat_1.color; +}; + +/** + * Get the minimum value of this metric (within two standard deviations). + * + * @return {mixed} The minimum value of this metric. + */ +beestat.component.metric.runtime_per_heating_degree_day.prototype.get_min_ = function() { + var standard_deviation = + beestat.cache.data.metrics.runtime_per_heating_degree_day.standard_deviation; + return (beestat.cache.data.metrics.runtime_per_heating_degree_day.median - (standard_deviation * 2)).toFixed(1); +}; + +/** + * Get the maximum value of this metric (within two standard deviations). + * + * @return {mixed} The maximum value of this metric. + */ +beestat.component.metric.runtime_per_heating_degree_day.prototype.get_max_ = function() { + var standard_deviation = + beestat.cache.data.metrics.runtime_per_heating_degree_day.standard_deviation; + return (beestat.cache.data.metrics.runtime_per_heating_degree_day.median + (standard_deviation * 2)).toFixed(1); +}; + +/** + * Get the value of this metric. + * + * @return {mixed} The value of this metric. + */ +beestat.component.metric.runtime_per_heating_degree_day.prototype.get_value_ = function() { + var thermostat_group = beestat.cache.thermostat_group[ + this.thermostat_group_id_ + ]; + // todo: store this explicitly on the profile so it doesn't have to be calculated in JS? + return (thermostat_group.profile.runtime.heat_1 / + thermostat_group.profile.degree_days.heat).toFixed(1); +}; + +/** + * Get a histogram between the min and max values of this metric. + * + * @return {array} The histogram. + */ +beestat.component.metric.runtime_per_heating_degree_day.prototype.get_histogram_ = function() { + var histogram = []; + for (var value in beestat.cache.data.metrics.runtime_per_heating_degree_day.histogram) { + if ( + value >= this.get_min_() && + value <= this.get_max_() + ) { + var count = beestat.cache.data.metrics.runtime_per_heating_degree_day.histogram[value]; + histogram.push({ + 'value': value, + 'count': count + }); + } + } + return histogram; +}; diff --git a/js/component/metric/setpoint_cool.js b/js/component/metric/setpoint_cool.js new file mode 100644 index 0000000..7495739 --- /dev/null +++ b/js/component/metric/setpoint_cool.js @@ -0,0 +1,119 @@ +/** + * Cool setpoint metric. + * + * @param {number} thermostat_group_id The thermostat group. + */ +beestat.component.metric.setpoint_cool = function(thermostat_group_id) { + this.thermostat_group_id_ = thermostat_group_id; + + beestat.component.metric.apply(this, arguments); +}; +beestat.extend(beestat.component.metric.setpoint_cool, beestat.component.metric); + +beestat.component.metric.setpoint_cool.prototype.rerender_on_breakpoint_ = false; + +/** + * Get the title of this metric. + * + * @return {string} The title of this metric. + */ +beestat.component.metric.setpoint_cool.prototype.get_title_ = function() { + return 'Cool Setpoint'; +}; + +/** + * Get the icon of this metric. + * + * @return {string} The icon of this metric. + */ +beestat.component.metric.setpoint_cool.prototype.get_icon_ = function() { + return 'snowflake'; +}; + +/** + * Get the color of this metric. + * + * @return {string} The color of this metric. + */ +beestat.component.metric.setpoint_cool.prototype.get_color_ = function() { + return beestat.series.compressor_cool_1.color; +}; + +/** + * Get the minimum value of this metric (within two standard deviations). + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {mixed} The minimum value of this metric. + */ +beestat.component.metric.setpoint_cool.prototype.get_min_ = function(units) { + var standard_deviation = + beestat.cache.data.metrics.setpoint_cool.standard_deviation; + return beestat.temperature({ + 'temperature': beestat.cache.data.metrics.setpoint_cool.median - (standard_deviation * 2), + 'round': 0, + 'units': units + }); +}; + +/** + * Get the maximum value of this metric (within two standard deviations). + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {mixed} The maximum value of this metric. + */ +beestat.component.metric.setpoint_cool.prototype.get_max_ = function(units) { + var standard_deviation = + beestat.cache.data.metrics.setpoint_cool.standard_deviation; + return beestat.temperature({ + 'temperature': beestat.cache.data.metrics.setpoint_cool.median + (standard_deviation * 2), + 'round': 0, + 'units': units + }); +}; + +/** + * Get the value of this metric. + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {mixed} The value of this metric. + */ +beestat.component.metric.setpoint_cool.prototype.get_value_ = function(units) { + var thermostat_group = beestat.cache.thermostat_group[ + this.thermostat_group_id_ + ]; + return beestat.temperature({ + 'temperature': thermostat_group.profile.setpoint.cool, + 'units': units + }); +}; + +/** + * Get a histogram between the min and max values of this metric. + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {array} The histogram. + */ +beestat.component.metric.setpoint_cool.prototype.get_histogram_ = function(units) { + var histogram = []; + for (var temperature in beestat.cache.data.metrics.setpoint_cool.histogram) { + if ( + temperature >= this.get_min_(units) && + temperature <= this.get_max_(units) + ) { + var count = beestat.cache.data.metrics.setpoint_cool.histogram[temperature]; + histogram.push({ + 'value': beestat.temperature(temperature), + 'count': count + }); + } + } + return histogram; +}; diff --git a/js/component/metric/setpoint_heat.js b/js/component/metric/setpoint_heat.js new file mode 100644 index 0000000..442f199 --- /dev/null +++ b/js/component/metric/setpoint_heat.js @@ -0,0 +1,119 @@ +/** + * Heat setpoint metric. + * + * @param {number} thermostat_group_id The thermostat group. + */ +beestat.component.metric.setpoint_heat = function(thermostat_group_id) { + this.thermostat_group_id_ = thermostat_group_id; + + beestat.component.metric.apply(this, arguments); +}; +beestat.extend(beestat.component.metric.setpoint_heat, beestat.component.metric); + +beestat.component.metric.setpoint_heat.prototype.rerender_on_breakpoint_ = false; + +/** + * Get the title of this metric. + * + * @return {string} The title of this metric. + */ +beestat.component.metric.setpoint_heat.prototype.get_title_ = function() { + return 'Heat Setpoint'; +}; + +/** + * Get the icon of this metric. + * + * @return {string} The icon of this metric. + */ +beestat.component.metric.setpoint_heat.prototype.get_icon_ = function() { + return 'fire'; +}; + +/** + * Get the color of this metric. + * + * @return {string} The color of this metric. + */ +beestat.component.metric.setpoint_heat.prototype.get_color_ = function() { + return beestat.series.compressor_heat_1.color; +}; + +/** + * Get the minimum value of this metric (within two standard deviations). + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {mixed} The minimum value of this metric. + */ +beestat.component.metric.setpoint_heat.prototype.get_min_ = function(units) { + var standard_deviation = + beestat.cache.data.metrics.setpoint_heat.standard_deviation; + return beestat.temperature({ + 'temperature': beestat.cache.data.metrics.setpoint_heat.median - (standard_deviation * 2), + 'round': 0, + 'units': units + }); +}; + +/** + * Get the maximum value of this metric (within two standard deviations). + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {mixed} The maximum value of this metric. + */ +beestat.component.metric.setpoint_heat.prototype.get_max_ = function(units) { + var standard_deviation = + beestat.cache.data.metrics.setpoint_heat.standard_deviation; + return beestat.temperature({ + 'temperature': beestat.cache.data.metrics.setpoint_heat.median + (standard_deviation * 2), + 'round': 0, + 'units': units + }); +}; + +/** + * Get the value of this metric. + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {mixed} The value of this metric. + */ +beestat.component.metric.setpoint_heat.prototype.get_value_ = function(units) { + var thermostat_group = beestat.cache.thermostat_group[ + this.thermostat_group_id_ + ]; + return beestat.temperature({ + 'temperature': thermostat_group.profile.setpoint.heat, + 'units': units + }); +}; + +/** + * Get a histogram between the min and max values of this metric. + * + * @param {boolean} units Whether or not to return a numerical value or a + * string with units. + * + * @return {array} The histogram. + */ +beestat.component.metric.setpoint_heat.prototype.get_histogram_ = function(units) { + var histogram = []; + for (var temperature in beestat.cache.data.metrics.setpoint_heat.histogram) { + if ( + temperature >= this.get_min_(units) && + temperature <= this.get_max_(units) + ) { + var count = beestat.cache.data.metrics.setpoint_heat.histogram[temperature]; + histogram.push({ + 'value': beestat.temperature(temperature), + 'count': count + }); + } + } + return histogram; +}; diff --git a/js/component/modal/change_system_type.js b/js/component/modal/change_system_type.js index 974e21a..3a6567d 100644 --- a/js/component/modal/change_system_type.js +++ b/js/component/modal/change_system_type.js @@ -148,7 +148,7 @@ beestat.component.modal.change_system_type.prototype.get_buttons_ = function() { // Re-run comparison scores as they are invalid for the new system // type. - beestat.home_comparisons.get_comparison_scores(); + beestat.comparisons.get_comparison_scores(); // Close the modal. self.dispose(); diff --git a/js/js.php b/js/js.php index d4dbcc5..fcf3781 100755 --- a/js/js.php +++ b/js/js.php @@ -27,7 +27,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; - echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; @@ -40,7 +40,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; - echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; // Component @@ -51,7 +51,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; - echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; @@ -65,9 +65,12 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; @@ -98,6 +101,10 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; + echo '' . PHP_EOL; + echo '' . PHP_EOL; + echo '' . PHP_EOL; } else { echo '' . PHP_EOL; diff --git a/js/layer/home_comparisons.js b/js/layer/comparisons.js similarity index 67% rename from js/layer/home_comparisons.js rename to js/layer/comparisons.js index d2a6846..f630007 100644 --- a/js/layer/home_comparisons.js +++ b/js/layer/comparisons.js @@ -1,100 +1,103 @@ -/** - * Home comparisons layer. - */ -beestat.layer.home_comparisons = function() { - beestat.layer.apply(this, arguments); -}; -beestat.extend(beestat.layer.home_comparisons, beestat.layer); - -beestat.layer.home_comparisons.prototype.decorate_ = function(parent) { - /* - * Set the overflow on the body so the scrollbar is always present so - * highcharts graphs render properly. - */ - $('body').style({ - 'overflow-y': 'scroll', - 'background': beestat.style.color.bluegray.light, - 'padding': '0 ' + beestat.style.size.gutter + 'px' - }); - - (new beestat.component.header('home_comparisons')).render(parent); - - var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; - var thermostat_group = beestat.cache.thermostat_group[thermostat.thermostat_group_id]; - - // All the cards - var cards = []; - - if (window.is_demo === true) { - cards.push([ - { - 'card': new beestat.component.card.demo(), - 'size': 12 - } - ]); - } - - cards.push([ - { - 'card': new beestat.component.card.comparison_settings(), - 'size': 6 - }, - { - 'card': new beestat.component.card.my_home(), - 'size': 6 - } - ]); - - // Scores and graph - if (thermostat_group.temperature_profile !== null) { - cards.push([ - { - 'card': new beestat.component.card.score.heat(), - 'size': 4 - }, - { - 'card': new beestat.component.card.score.cool(), - 'size': 4 - }, - { - 'card': new beestat.component.card.score.resist(), - 'size': 4 - } - ]); - - if ( - ( - thermostat_group.temperature_profile.heat !== undefined && - thermostat_group.temperature_profile.heat.linear_trendline.slope < 0 - ) || - ( - thermostat_group.temperature_profile.cool !== undefined && - thermostat_group.temperature_profile.cool.linear_trendline.slope < 0 - ) - ) { - cards.push([ - { - 'card': new beestat.component.card.comparison_issue(), - 'size': 12 - } - ]); - } - - cards.push([ - { - 'card': new beestat.component.card.temperature_profiles(thermostat_group.thermostat_group_id), - 'size': 12 - } - ]); - } - - // Footer - cards.push([ - { - 'card': new beestat.component.card.footer(), - 'size': 12 - } - ]); - - (new beestat.component.layout(cards)).render(parent); -}; +/** + * Home comparisons layer. + */ +beestat.layer.comparisons = function() { + beestat.layer.apply(this, arguments); +}; +beestat.extend(beestat.layer.comparisons, beestat.layer); + +beestat.layer.comparisons.prototype.decorate_ = function(parent) { + /* + * Set the overflow on the body so the scrollbar is always present so + * highcharts graphs render properly. + */ + $('body').style({ + 'overflow-y': 'scroll', + 'background': beestat.style.color.bluegray.light, + 'padding': '0 ' + beestat.style.size.gutter + 'px' + }); + + (new beestat.component.header('comparisons')).render(parent); + + var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + var thermostat_group = beestat.cache.thermostat_group[thermostat.thermostat_group_id]; + + // All the cards + var cards = []; + + if (window.is_demo === true) { + cards.push([ + { + 'card': new beestat.component.card.demo(), + 'size': 12 + } + ]); + } + + cards.push([ + { + 'card': new beestat.component.card.comparison_settings(), + 'size': 6 + }, + { + 'card': new beestat.component.card.my_home(), + 'size': 6 + } + ]); + + // Scores and graph + if (thermostat_group.profile !== null) { + cards.push([ + { + 'card': new beestat.component.card.score.heat(), + 'size': 4 + }, + { + 'card': new beestat.component.card.score.cool(), + 'size': 4 + }, + { + 'card': new beestat.component.card.score.resist(), + 'size': 4 + } + ]); + + cards.push([ + { + 'card': new beestat.component.card.temperature_profiles(thermostat_group.thermostat_group_id), + 'size': 12 + } + ]); + + if (beestat.user.has_early_access() === true) { + cards.push([ + { + 'card': new beestat.component.card.early_access(), + 'size': 12 + } + ]); + cards.push([ + { + 'card': new beestat.component.card.metrics(thermostat_group.thermostat_group_id), + 'size': 12 + } + ]); + cards.push([ + { + 'card': new beestat.component.card.temperature_profiles_new(thermostat_group.thermostat_group_id), + 'size': 12 + } + ]); + } + } + + // Footer + cards.push([ + { + 'card': new beestat.component.card.footer(), + 'size': 12 + } + ]); + + (new beestat.component.layout(cards)).render(parent); +};