mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
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.
585 lines
17 KiB
JavaScript
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;
|
|
};
|