1
0
mirror of https://github.com/beestat/app.git synced 2025-05-24 02:14:03 -04:00
beestat/js/component/card/runtime_sensor_detail.js
Jon Ziebell 8d6ed9dba5 Fixed #226 - Sometimes switching thermostats errors
The API call to thermostat.read_id in runtime detail was not excluding inactive thermostats. That messed up the cache, which caused an inactive thermostat to try and display in the thermostat switcher.
2020-01-22 08:19:24 -05:00

585 lines
17 KiB
JavaScript

/**
* Runtime detail card. Shows a graph similar to what ecobee shows with the
* runtime info for a recent period of time.
*
* @param {number} thermostat_id The thermostat_id this card is displaying
* data for
*/
beestat.component.card.runtime_sensor_detail = function(thermostat_id) {
var self = this;
this.thermostat_id_ = thermostat_id;
/*
* When a setting is changed clear all of the data. Then rerender which will
* trigger the loading state. Also do this when the cache changes.
*
* 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 change_function = beestat.debounce(function() {
self.rerender();
}, 10);
beestat.dispatcher.addEventListener(
[
'setting.runtime_sensor_detail_smoothing',
'setting.runtime_sensor_detail_range_type',
'setting.runtime_sensor_detail_range_dynamic',
'cache.runtime_sensor'
],
change_function
);
beestat.component.card.apply(this, arguments);
};
beestat.extend(beestat.component.card.runtime_sensor_detail, beestat.component.card);
/**
* Decorate
*
* @param {rocket.ELements} parent
*/
beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = function(parent) {
var self = this;
var data = this.get_data_();
this.chart_ = new beestat.component.chart.runtime_sensor_detail(data);
this.chart_.render(parent);
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
var required_begin;
var required_end;
if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
required_begin = moment()
.subtract(
beestat.setting('runtime_sensor_detail_range_dynamic'),
'day'
)
.second(0);
required_end = moment()
.subtract(1, 'hour')
.second(0);
} else {
required_begin = moment(
beestat.setting('runtime_sensor_detail_range_static_begin') + ' 00:00:00'
);
required_end = moment(
beestat.setting('runtime_sensor_detail_range_static_end') + ' 23:59:59'
);
}
// Don't go before there's data.
required_begin = moment.max(
required_begin,
moment(thermostat.first_connected)
);
// Don't go after now.
required_end = moment.min(
required_end,
moment().subtract(1, 'hour')
);
/**
* If the needed data exists in the database and the runtime_sensor
* cache is empty, then query the data. If the needed data does not exist in
* the database, check every 2 seconds until it does.
*/
if (this.data_synced_(required_begin, required_end) === true) {
if (beestat.cache.runtime_sensor === undefined) {
this.show_loading_('Loading Runtime Detail');
var value;
var operator;
if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
value = required_begin.format();
operator = '>=';
} else {
value = [
required_begin.format(),
required_end.format()
];
operator = 'between';
}
var api_call = new beestat.api();
this.get_sensors_().forEach(function(sensor) {
if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
api_call.add_call(
'runtime_sensor',
'read',
{
'attributes': {
'sensor_id': sensor.sensor_id,
'timestamp': {
'value': value,
'operator': operator
}
}
}
);
}
});
api_call.set_callback(function(response) {
var runtime_sensors = [];
response.forEach(function(r) {
runtime_sensors = runtime_sensors.concat(r);
});
beestat.cache.set('runtime_sensor', runtime_sensors);
});
api_call.send();
}
} else {
this.show_loading_('Syncing Runtime Detail');
setTimeout(function() {
new beestat.api()
.add_call(
'thermostat',
'read_id',
{
'attributes': {
'inactive': 0
}
},
'thermostat'
)
.set_callback(function(response) {
beestat.cache.set('thermostat', response);
self.rerender();
})
.send();
}, 2000);
}
};
/**
* Decorate the menu
*
* @param {rocket.Elements} parent
*/
beestat.component.card.runtime_sensor_detail.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('Past 1 Day')
.set_icon('numeric_1_box')
.set_callback(function() {
if (
beestat.setting('runtime_sensor_detail_range_dynamic') !== 1 ||
beestat.setting('runtime_sensor_detail_range_type') !== 'dynamic'
) {
beestat.cache.delete('runtime_sensor');
beestat.setting({
'runtime_sensor_detail_range_dynamic': 1,
'runtime_sensor_detail_range_type': 'dynamic'
});
}
}));
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Past 3 Days')
.set_icon('numeric_3_box')
.set_callback(function() {
if (
beestat.setting('runtime_sensor_detail_range_dynamic') !== 3 ||
beestat.setting('runtime_sensor_detail_range_type') !== 'dynamic'
) {
beestat.cache.delete('runtime_sensor');
beestat.setting({
'runtime_sensor_detail_range_dynamic': 3,
'runtime_sensor_detail_range_type': 'dynamic'
});
}
}));
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Past 7 Days')
.set_icon('numeric_7_box')
.set_callback(function() {
if (
beestat.setting('runtime_sensor_detail_range_dynamic') !== 7 ||
beestat.setting('runtime_sensor_detail_range_type') !== 'dynamic'
) {
beestat.cache.delete('runtime_sensor');
beestat.setting({
'runtime_sensor_detail_range_dynamic': 7,
'runtime_sensor_detail_range_type': 'dynamic'
});
}
}));
// menu.add_menu_item(new beestat.component.menu_item()
// .set_text('Custom')
// .set_icon('calendar_edit')
// .set_callback(function() {
// (new beestat.component.modal.runtime_sensor_detail_custom()).render();
// }));
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('Reset Zoom')
.set_icon('magnify_minus')
.set_callback(function() {
self.chart_.reset_zoom();
}));
if (beestat.setting('runtime_sensor_detail_smoothing') === true) {
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Disable Smothing')
.set_icon('chart_line')
.set_callback(function() {
beestat.setting('runtime_sensor_detail_smoothing', false);
}));
} else {
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Enable Smoothing')
.set_icon('chart_bell_curve')
.set_callback(function() {
beestat.setting('runtime_sensor_detail_smoothing', true);
}));
}
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Help')
.set_icon('help_circle')
.set_callback(function() {
window.open('https://www.notion.so/beestat/891f94a6bdb34895a453b7b91591ec29');
}));
};
/**
* Get all of the series data.
*
* @return {object} The series data.
*/
beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() {
var self = this;
var data = {
'x': [],
'series': {},
'metadata': {
'series': {},
'chart': {
'title': this.get_title_(),
'subtitle': this.get_subtitle_(),
'y_min': Infinity,
'y_max': -Infinity,
'sensors': null
}
}
};
// A couple private helper functions for manipulating the min/max y values.
var y_min_max = function(value) {
if (value !== null) {
data.metadata.chart.y_min = Math.min(data.metadata.chart.y_min, value);
data.metadata.chart.y_max = Math.max(data.metadata.chart.y_max, value);
}
};
var series_codes = [];
// Get and sort all the sensors.
var sensors = this.get_sensors_();
data.metadata.sensors = sensors;
// Set up the series_codes.
sensors.forEach(function(sensor) {
if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
series_codes.push('temperature_' + sensor.sensor_id);
series_codes.push('occupancy_' + sensor.sensor_id);
}
});
series_codes.push('dummy');
// Initialize a bunch of stuff.
var sequential = {};
series_codes.forEach(function(series_code) {
sequential[series_code] = 0;
data.series[series_code] = [];
data.metadata.series[series_code] = {
'active': false
};
if (series_code === 'dummy') {
data.metadata.series[series_code].name = null;
} else {
var sensor_id = series_code.replace(/[^0-9]/g, '');
data.metadata.series[series_code].name = beestat.cache.sensor[sensor_id].name;
}
});
var begin_m;
var end_m;
if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
begin_m = moment().subtract(
beestat.setting('runtime_sensor_detail_range_dynamic'),
'day'
);
end_m = moment().subtract(1, 'hour');
} else {
begin_m = moment(
beestat.setting('runtime_sensor_detail_range_static_begin') + ' 00:00:00'
);
end_m = moment(
beestat.setting('runtime_sensor_detail_range_static_end') + ' 23:59:59'
);
}
// TODO: This needs to be max of begin and when I actually have sensor data
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
begin_m = moment.max(
begin_m,
moment(thermostat.first_connected)
);
begin_m
.minute(Math.ceil(begin_m.minute() / 5) * 5)
.second(0)
.millisecond(0);
var runtime_sensors = this.get_runtime_sensor_by_date_();
// Initialize moving average.
var moving = [];
var moving_count;
if (beestat.setting('runtime_sensor_detail_smoothing') === true) {
moving_count = 5;
} else {
moving_count = 1;
}
var offset;
for (var i = 0; i < moving_count; i++) {
offset = (i - Math.floor(moving_count / 2)) * 300000;
moving.push(runtime_sensors[begin_m.valueOf() + offset]);
}
// TODO: Garage sensor is not returned in runtime data until halfway
// through...so the series data never got added early on so it just gets
// slapped on the beginning. It also takes over a previous "j" value and
// pushes everything around. Instead of looping over runtime_sensor I need to loop over sensor and grab the values.
// Loop.
var current_m = begin_m;
while (
// beestat.cache.runtime_sensor.length > 0 &&
current_m.isSameOrAfter(end_m) === false
) {
data.x.push(current_m.clone());
// Without this series the chart will jump to the nearest value if there is a chunk of missing data.
data.series.dummy.push(1);
data.metadata.series.dummy.active = true;
if (runtime_sensors[current_m.valueOf()] !== undefined) {
sensors.forEach(function(sensor, j) {
var runtime_sensor = runtime_sensors[current_m.valueOf()][sensor.sensor_id];
if (runtime_sensor === undefined) {
data.series['temperature_' + sensor.sensor_id].push(null);
data.series['occupancy_' + sensor.sensor_id].push(null);
return;
}
var temperature_moving = beestat.temperature(
self.get_average_(moving, sensor.sensor_id)
);
data.series['temperature_' + runtime_sensor.sensor_id].push(temperature_moving);
y_min_max(temperature_moving);
data.metadata.series['temperature_' + runtime_sensor.sensor_id].active = true;
if (runtime_sensor.occupancy === true) {
let swimlane_properties =
beestat.component.chart.runtime_sensor_detail.get_swimlane_properties(
sensors.length,
j
);
sequential['occupancy_' + runtime_sensor.sensor_id]++;
data.series['occupancy_' + runtime_sensor.sensor_id].push(swimlane_properties.y);
} else {
if (sequential['occupancy_' + runtime_sensor.sensor_id] > 0) {
let swimlane_properties =
beestat.component.chart.runtime_sensor_detail.get_swimlane_properties(
sensors.length,
j
);
data.series['occupancy_' + runtime_sensor.sensor_id].push(swimlane_properties.y);
} else {
data.series['occupancy_' + runtime_sensor.sensor_id].push(null);
}
sequential['occupancy_' + runtime_sensor.sensor_id] = 0;
}
});
} else {
sensors.forEach(function(sensor) {
if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
data.series['temperature_' + sensor.sensor_id].push(null);
data.series['occupancy_' + sensor.sensor_id].push(null);
}
});
}
current_m.add(5, 'minute');
/**
* Remove the first row in the moving average and add the next one. Yes
* this could introduce undefined values; that's ok. Those are handled in
* the get_average_ function.
*/
moving.shift();
moving.push(runtime_sensors[current_m.valueOf() + offset]);
}
return data;
};
/**
* Get all the runtime_sensor rows indexed by date.
*
* @return {array} The runtime_sensor rows.
*/
beestat.component.card.runtime_sensor_detail.prototype.get_runtime_sensor_by_date_ = function() {
var runtime_sensors = {};
if (beestat.cache.runtime_sensor !== undefined) {
beestat.cache.runtime_sensor.forEach(function(runtime_sensor) {
var timestamp = [moment(runtime_sensor.timestamp).valueOf()];
if (runtime_sensors[timestamp] === undefined) {
// runtime_sensors[timestamp] = [];
runtime_sensors[timestamp] = {};
}
// runtime_sensors[timestamp].push(runtime_sensor);
runtime_sensors[timestamp][runtime_sensor.sensor_id] = runtime_sensor;
});
}
return runtime_sensors;
};
/**
* Given an array of runtime_sensors, get the average value of one of the
* keys. Allows and ignores undefined values in order to keep a more accurate
* moving average.
*
* @param {array} runtime_sensors
* @param {string} sensor_id The index in the sub-array
*
* @return {number} The average.
*/
beestat.component.card.runtime_sensor_detail.prototype.get_average_ = function(runtime_sensors, sensor_id) {
var average = 0;
var count = 0;
for (var i = 0; i < runtime_sensors.length; i++) {
if (
runtime_sensors[i] !== undefined &&
runtime_sensors[i][sensor_id] !== undefined &&
runtime_sensors[i][sensor_id].temperature !== null
) {
average += runtime_sensors[i][sensor_id].temperature;
count++;
}
}
if (count === 0) {
return null;
}
return average / count;
};
/**
* Get the title of the card.
*
* @return {string} Title
*/
beestat.component.card.runtime_sensor_detail.prototype.get_title_ = function() {
return 'Runtime Detail';
};
/**
* Get the subtitle of the card.
*
* @return {string} Subtitle
*/
beestat.component.card.runtime_sensor_detail.prototype.get_subtitle_ = function() {
if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
var s = (beestat.setting('runtime_sensor_detail_range_dynamic') > 1) ? 's' : '';
return 'Past ' +
beestat.setting('runtime_sensor_detail_range_dynamic') +
' day' +
s;
}
var begin = moment(beestat.setting('runtime_sensor_detail_range_static_begin'))
.format('MMM D, YYYY');
var end = moment(beestat.setting('runtime_sensor_detail_range_static_end'))
.format('MMM D, YYYY');
return begin + ' to ' + end;
};
/**
* Determine whether or not the data to render the desired date range has been
* synced.
*
* @param {moment} required_sync_begin
* @param {moment} required_sync_end
*
* @return {boolean} Whether or not the data is synced.
*/
beestat.component.card.runtime_sensor_detail.prototype.data_synced_ = function(required_sync_begin, required_sync_end) {
// Demo can just grab whatever data is there.
if (window.is_demo === true) {
return true;
}
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
var current_sync_begin = moment.utc(thermostat.sync_begin);
var current_sync_end = moment.utc(thermostat.sync_end);
return (
current_sync_begin.isSameOrBefore(required_sync_begin) &&
current_sync_end.isSameOrAfter(required_sync_end)
);
};
/**
* Get a sorted list of all sensors attached to the current thermostat.
*
* @return {array} The sensors.
*/
beestat.component.card.runtime_sensor_detail.prototype.get_sensors_ = function() {
// Get and sort all the sensors.
var sensors = [];
$.values(beestat.cache.sensor).forEach(function(sensor) {
if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
sensors.push(sensor);
}
});
sensors.sort(function(a, b) {
return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'});
});
return sensors;
};