1
0
mirror of https://github.com/beestat/app.git synced 2025-05-24 02:14:03 -04:00

Enabled metrics and new profiles for early access

This commit is contained in:
Jon Ziebell 2020-02-28 05:29:24 -05:00
parent 154af5d89f
commit 8b57cfc227
19 changed files with 1369 additions and 152 deletions

View File

@ -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;
}

View File

@ -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];

View File

@ -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;

View File

@ -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'
);
};

View File

@ -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!';
};

View File

@ -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();
});

View File

@ -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!';
// };

View File

@ -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) {
};*/

View File

@ -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);

View File

@ -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');
}));
};

View File

@ -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;
};

View File

@ -38,7 +38,7 @@ beestat.component.header.prototype.decorate_ = function(parent) {
'icon': 'signal_variant'
},
{
'layer': 'home_comparisons',
'layer': 'comparisons',
'text': 'Comparisons',
'icon': 'home_group'
}

137
js/component/metric.js Normal file
View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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();

View File

@ -27,7 +27,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/beestat/time.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/setting.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/poll.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/home_comparisons.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/comparisons.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/highcharts.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/get_sync_progress.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/user.js"></script>' . PHP_EOL;
@ -40,7 +40,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/layer.js"></script>' . PHP_EOL;
echo '<script src="/js/layer/load.js"></script>' . PHP_EOL;
echo '<script src="/js/layer/dashboard.js"></script>' . PHP_EOL;
echo '<script src="/js/layer/home_comparisons.js"></script>' . PHP_EOL;
echo '<script src="/js/layer/comparisons.js"></script>' . PHP_EOL;
echo '<script src="/js/layer/sensors.js"></script>' . PHP_EOL;
// Component
@ -51,7 +51,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/card/runtime_thermostat_summary.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/alerts.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/comparison_settings.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/comparison_issue.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/early_access.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/demo.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/footer.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/my_home.js"></script>' . PHP_EOL;
@ -65,9 +65,12 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/card/sensors.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/system.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/temperature_profiles.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/temperature_profiles_new.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/metrics.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/runtime_thermostat_summary.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/temperature_profiles.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/temperature_profiles_new.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/runtime_thermostat_detail_temperature.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/runtime_thermostat_detail_equipment.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/runtime_sensor_detail_temperature.js"></script>' . PHP_EOL;
@ -98,6 +101,10 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/button.js"></script>' . PHP_EOL;
echo '<script src="/js/component/button_group.js"></script>' . PHP_EOL;
echo '<script src="/js/component/title.js"></script>' . PHP_EOL;
echo '<script src="/js/component/metric.js"></script>' . PHP_EOL;
echo '<script src="/js/component/metric/setpoint_heat.js"></script>' . PHP_EOL;
echo '<script src="/js/component/metric/setpoint_cool.js"></script>' . PHP_EOL;
echo '<script src="/js/component/metric/runtime_per_heating_degree_day.js"></script>' . PHP_EOL;
}
else {
echo '<script src="/js/beestat.js?' . $setting->get('commit') . '"></script>' . PHP_EOL;

View File

@ -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);
};