mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
859 lines
27 KiB
JavaScript
859 lines
27 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_thermostat_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_thermostat_detail_smoothing',
|
|
'setting.runtime_thermostat_detail_range_type',
|
|
'setting.runtime_thermostat_detail_range_dynamic',
|
|
'cache.runtime_thermostat'
|
|
],
|
|
change_function
|
|
);
|
|
|
|
beestat.component.card.apply(this, arguments);
|
|
};
|
|
beestat.extend(beestat.component.card.runtime_thermostat_detail, beestat.component.card);
|
|
|
|
/**
|
|
* Decorate
|
|
*
|
|
* @param {rocket.ELements} parent
|
|
*/
|
|
beestat.component.card.runtime_thermostat_detail.prototype.decorate_contents_ = function(parent) {
|
|
var self = this;
|
|
|
|
var data = this.get_data_();
|
|
this.chart_ = new beestat.component.chart.runtime_thermostat_detail(data);
|
|
this.chart_.render(parent);
|
|
|
|
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
|
|
|
|
var required_begin;
|
|
var required_end;
|
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
|
required_begin = moment()
|
|
.subtract(
|
|
beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
|
'day'
|
|
)
|
|
.second(0);
|
|
|
|
required_end = moment()
|
|
.subtract(1, 'hour')
|
|
.second(0);
|
|
} else {
|
|
required_begin = moment(
|
|
beestat.setting('runtime_thermostat_detail_range_static_begin') + ' 00:00:00'
|
|
);
|
|
required_end = moment(
|
|
beestat.setting('runtime_thermostat_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_thermostat
|
|
* 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_thermostat === undefined) {
|
|
this.show_loading_('Loading Runtime Detail');
|
|
|
|
var value;
|
|
var operator;
|
|
|
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
|
value = required_begin.format();
|
|
operator = '>=';
|
|
} else {
|
|
value = [
|
|
required_begin.format(),
|
|
required_end.format()
|
|
];
|
|
operator = 'between';
|
|
}
|
|
|
|
new beestat.api()
|
|
.add_call(
|
|
'runtime_thermostat',
|
|
'read',
|
|
{
|
|
'attributes': {
|
|
'thermostat_id': thermostat.thermostat_id,
|
|
'timestamp': {
|
|
'value': value,
|
|
'operator': operator
|
|
}
|
|
}
|
|
}
|
|
)
|
|
.set_callback(function(response) {
|
|
beestat.cache.set('runtime_thermostat', response);
|
|
})
|
|
.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_thermostat_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_thermostat_detail_range_dynamic') !== 1 ||
|
|
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
|
) {
|
|
beestat.cache.delete('runtime_thermostat');
|
|
beestat.setting({
|
|
'runtime_thermostat_detail_range_dynamic': 1,
|
|
'runtime_thermostat_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_thermostat_detail_range_dynamic') !== 3 ||
|
|
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
|
) {
|
|
beestat.cache.delete('runtime_thermostat');
|
|
beestat.setting({
|
|
'runtime_thermostat_detail_range_dynamic': 3,
|
|
'runtime_thermostat_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_thermostat_detail_range_dynamic') !== 7 ||
|
|
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
|
) {
|
|
beestat.cache.delete('runtime_thermostat');
|
|
beestat.setting({
|
|
'runtime_thermostat_detail_range_dynamic': 7,
|
|
'runtime_thermostat_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_thermostat_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_thermostat_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_thermostat_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_thermostat_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/Runtime-Detail-e499fb13fd4441f4b3f096baca1cb138');
|
|
}));
|
|
};
|
|
|
|
/**
|
|
* Get all of the series data.
|
|
*
|
|
* @return {object} The series data.
|
|
*/
|
|
beestat.component.card.runtime_thermostat_detail.prototype.get_data_ = function() {
|
|
var data = {
|
|
'x': [],
|
|
'series': {},
|
|
'metadata': {
|
|
'series': {},
|
|
'chart': {
|
|
'title': this.get_title_(),
|
|
'subtitle': this.get_subtitle_(),
|
|
'y_min': Infinity,
|
|
'y_max': -Infinity
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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);
|
|
}
|
|
};
|
|
|
|
// Duration objects. These are passed by reference into the metadata.
|
|
var durations = {};
|
|
|
|
// Y values for equipment swimlane data.
|
|
var equipment_y = {
|
|
'calendar_event_smartrecovery': 94,
|
|
'calendar_event_home': 94,
|
|
'calendar_event_away': 94,
|
|
'calendar_event_sleep': 94,
|
|
'calendar_event_smarthome': 94,
|
|
'calendar_event_smartaway': 94,
|
|
'calendar_event_hold': 94,
|
|
'calendar_event_vacation': 94,
|
|
'calendar_event_quicksave': 94,
|
|
'calendar_event_other': 94,
|
|
'calendar_event_custom': 94,
|
|
'compressor_heat_1': 67,
|
|
'compressor_heat_2': 67,
|
|
'auxiliary_heat_1': 67,
|
|
'auxiliary_heat_2': 67,
|
|
'compressor_cool_1': 67,
|
|
'compressor_cool_2': 67,
|
|
'fan': 47,
|
|
'humidifier': 31,
|
|
'dehumidifier': 31,
|
|
'ventilator': 31,
|
|
'economizer': 31
|
|
};
|
|
|
|
// Initialize a bunch of stuff.
|
|
[
|
|
'calendar_event_smartrecovery',
|
|
'calendar_event_home',
|
|
'calendar_event_away',
|
|
'calendar_event_sleep',
|
|
'calendar_event_smarthome',
|
|
'calendar_event_smartaway',
|
|
'calendar_event_hold',
|
|
'calendar_event_vacation',
|
|
'calendar_event_quicksave',
|
|
'calendar_event_other',
|
|
'calendar_event_custom',
|
|
'outdoor_temperature',
|
|
'indoor_temperature',
|
|
'indoor_humidity',
|
|
'outdoor_humidity',
|
|
'setpoint_heat',
|
|
'setpoint_cool',
|
|
'fan',
|
|
'compressor_heat_1',
|
|
'compressor_heat_2',
|
|
'auxiliary_heat_1',
|
|
'auxiliary_heat_2',
|
|
'compressor_cool_1',
|
|
'compressor_cool_2',
|
|
'humidifier',
|
|
'dehumidifier',
|
|
'ventilator',
|
|
'economizer',
|
|
'dummy'
|
|
].forEach(function(series_code) {
|
|
data.series[series_code] = [];
|
|
data.metadata.series[series_code] = {
|
|
'active': false,
|
|
'durations': {},
|
|
|
|
/**
|
|
* Note to future self: This can be used for all series. Need to
|
|
* populate the raw data points for each series here. The tooltip should
|
|
* get data from here and not the chart points array. Then the series
|
|
* data can be whatever is necessary to produce a performance-optimized
|
|
* chart as long as there is one series (dummy) that has a point at
|
|
* every x-value. That will allow a smooth tooltip, lightweight lines,
|
|
* and accurate data.
|
|
*/
|
|
'data': {}
|
|
};
|
|
durations[series_code] = {'seconds': 0};
|
|
});
|
|
|
|
data.metadata.series.calendar_event_name = {};
|
|
data.metadata.series.system_mode = {};
|
|
|
|
var begin_m;
|
|
var end_m;
|
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
|
begin_m = moment().subtract(
|
|
beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
|
'day'
|
|
);
|
|
end_m = moment().subtract(1, 'hour');
|
|
} else {
|
|
begin_m = moment(
|
|
beestat.setting('runtime_thermostat_detail_range_static_begin') + ' 00:00:00'
|
|
);
|
|
end_m = moment(
|
|
beestat.setting('runtime_thermostat_detail_range_static_end') + ' 23:59:59'
|
|
);
|
|
}
|
|
|
|
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_thermostats = this.get_runtime_thermostat_by_date_();
|
|
|
|
// Initialize moving average.
|
|
var moving = [];
|
|
var moving_count;
|
|
if (beestat.setting('runtime_thermostat_detail_smoothing') === true) {
|
|
moving_count = 10;
|
|
} 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_thermostats[begin_m.valueOf() + offset]);
|
|
}
|
|
|
|
// Loop.
|
|
var current_m = begin_m;
|
|
while (
|
|
// beestat.cache.runtime_thermostat.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;
|
|
|
|
var runtime_thermostat = runtime_thermostats[
|
|
current_m.valueOf()
|
|
];
|
|
|
|
if (runtime_thermostat !== undefined) {
|
|
/**
|
|
* Things that use the moving average.
|
|
*/
|
|
var indoor_humidity_moving = this.get_average_(moving, 'indoor_humidity');
|
|
data.series.indoor_humidity.push(indoor_humidity_moving);
|
|
data.metadata.series.indoor_humidity.data[current_m.valueOf()] =
|
|
runtime_thermostat.indoor_humidity;
|
|
data.metadata.series.indoor_humidity.active = true;
|
|
|
|
var outdoor_humidity_moving = this.get_average_(moving, 'outdoor_humidity');
|
|
data.series.outdoor_humidity.push(outdoor_humidity_moving);
|
|
data.metadata.series.outdoor_humidity.data[current_m.valueOf()] =
|
|
runtime_thermostat.outdoor_humidity;
|
|
data.metadata.series.outdoor_humidity.active = true;
|
|
|
|
var indoor_temperature_moving = beestat.temperature(
|
|
this.get_average_(moving, 'indoor_temperature')
|
|
);
|
|
data.series.indoor_temperature.push(indoor_temperature_moving);
|
|
data.metadata.series.indoor_temperature.data[current_m.valueOf()] =
|
|
runtime_thermostat.indoor_temperature;
|
|
y_min_max(indoor_temperature_moving);
|
|
data.metadata.series.indoor_temperature.active = true;
|
|
|
|
var outdoor_temperature_moving = beestat.temperature(
|
|
this.get_average_(moving, 'outdoor_temperature')
|
|
);
|
|
data.series.outdoor_temperature.push(outdoor_temperature_moving);
|
|
data.metadata.series.outdoor_temperature.data[current_m.valueOf()] =
|
|
runtime_thermostat.outdoor_temperature;
|
|
y_min_max(outdoor_temperature_moving);
|
|
data.metadata.series.outdoor_temperature.active = true;
|
|
|
|
/**
|
|
* Add setpoints, but only when relevant. For example: Only show the
|
|
* heat setpoint line when the heat is actually on.
|
|
*/
|
|
if (
|
|
runtime_thermostat.system_mode === 'auto' ||
|
|
runtime_thermostat.system_mode === 'heat' ||
|
|
runtime_thermostat.system_mode === 'auxiliary_heat'
|
|
) {
|
|
var setpoint_heat = beestat.temperature(
|
|
runtime_thermostat.setpoint_heat
|
|
);
|
|
data.series.setpoint_heat.push(setpoint_heat);
|
|
y_min_max(setpoint_heat);
|
|
|
|
data.metadata.series.setpoint_heat.active = true;
|
|
|
|
} else {
|
|
data.series.setpoint_heat.push(null);
|
|
}
|
|
|
|
if (
|
|
runtime_thermostat.system_mode === 'auto' ||
|
|
runtime_thermostat.system_mode === 'cool'
|
|
) {
|
|
var setpoint_cool = beestat.temperature(
|
|
runtime_thermostat.setpoint_cool
|
|
);
|
|
data.series.setpoint_cool.push(setpoint_cool);
|
|
y_min_max(setpoint_cool);
|
|
|
|
data.metadata.series.setpoint_cool.active = true;
|
|
|
|
} else {
|
|
data.series.setpoint_cool.push(null);
|
|
}
|
|
|
|
/*
|
|
* HVAC Mode. This isn't graphed but it's available for the tooltip.
|
|
* series.system_mode.chart_data.push([x, runtime_thermostat.system_mode]);
|
|
*/
|
|
data.metadata.series.system_mode[current_m.valueOf()] = runtime_thermostat.system_mode;
|
|
|
|
/*
|
|
* Thanks, ecobee...I more or less copied this code from the ecobee Follow
|
|
* Me graph to make sure it's accurate.
|
|
*/
|
|
var this_calendar_event;
|
|
var this_calendar_event_name;
|
|
|
|
if (runtime_thermostat.event === null) {
|
|
if (runtime_thermostat.climate === null) {
|
|
this_calendar_event = 'calendar_event_other';
|
|
this_calendar_event_name = 'Other';
|
|
} else {
|
|
switch (runtime_thermostat.climate.toLowerCase()) {
|
|
case 'home':
|
|
case 'sleep':
|
|
case 'away':
|
|
this_calendar_event = 'calendar_event_' + runtime_thermostat.climate.toLowerCase();
|
|
this_calendar_event_name = runtime_thermostat.climate;
|
|
break;
|
|
default:
|
|
this_calendar_event = 'calendar_event_custom';
|
|
this_calendar_event_name = runtime_thermostat.climate;
|
|
break;
|
|
}
|
|
}
|
|
} else if (runtime_thermostat.event.match(/SmartRecovery/i) !== null) {
|
|
this_calendar_event = 'calendar_event_smartrecovery';
|
|
this_calendar_event_name = 'Smart Recovery';
|
|
} else if (runtime_thermostat.event.match(/^home$/i) !== null) {
|
|
this_calendar_event = 'calendar_event_home';
|
|
this_calendar_event_name = 'Home';
|
|
} else if (runtime_thermostat.event.match(/^away$/i) !== null) {
|
|
this_calendar_event = 'calendar_event_away';
|
|
this_calendar_event_name = 'Away';
|
|
} else if (runtime_thermostat.event.match(/^smarthome$/i) !== null) {
|
|
this_calendar_event = 'calendar_event_smarthome';
|
|
this_calendar_event_name = 'Smart Home';
|
|
} else if (runtime_thermostat.event.match(/^smartaway$/i) !== null) {
|
|
this_calendar_event = 'calendar_event_smartaway';
|
|
this_calendar_event_name = 'Smart Away';
|
|
} else if (
|
|
runtime_thermostat.event.match(/^auto$/i) !== null ||
|
|
runtime_thermostat.event.match(/^today$/i) !== null ||
|
|
runtime_thermostat.event.match(/^hold$/i) !== null
|
|
) {
|
|
this_calendar_event = 'calendar_event_hold';
|
|
this_calendar_event_name = 'Hold';
|
|
} else if (
|
|
runtime_thermostat.event.match(/^vacation$/i) !== null ||
|
|
runtime_thermostat.event.match(/(\S\S\S\s\d+\s\d\d\d\d)|(\d{12})/i) !== null
|
|
) {
|
|
this_calendar_event = 'calendar_event_vacation';
|
|
this_calendar_event_name = 'Vacation';
|
|
} else if (runtime_thermostat.event.match(/^quicksave$/i) !== null) {
|
|
this_calendar_event = 'calendar_event_quicksave';
|
|
this_calendar_event_name = 'Quick Save';
|
|
} else {
|
|
this_calendar_event = 'calendar_event_other';
|
|
this_calendar_event_name = 'Other';
|
|
}
|
|
|
|
[
|
|
'calendar_event_smartrecovery',
|
|
'calendar_event_home',
|
|
'calendar_event_away',
|
|
'calendar_event_sleep',
|
|
'calendar_event_smarthome',
|
|
'calendar_event_smartaway',
|
|
'calendar_event_hold',
|
|
'calendar_event_vacation',
|
|
'calendar_event_quicksave',
|
|
'calendar_event_other',
|
|
'calendar_event_custom'
|
|
].forEach(function(calendar_event) {
|
|
if (calendar_event === this_calendar_event) {
|
|
data.series[calendar_event].push(equipment_y[calendar_event]);
|
|
data.metadata.series[calendar_event].active = true;
|
|
} else {
|
|
data.series[calendar_event].push(null);
|
|
}
|
|
});
|
|
|
|
data.metadata.series.calendar_event_name[current_m.valueOf()] =
|
|
this_calendar_event_name;
|
|
|
|
/**
|
|
* If all stages of the compressor are off, clear the durations. It is
|
|
* important that this only get reset if the seconds values are also
|
|
* zero to support backfilling.
|
|
*/
|
|
if (
|
|
runtime_thermostat.compressor_1 === 0 &&
|
|
runtime_thermostat.compressor_2 === 0 &&
|
|
(
|
|
durations.compressor_heat_1.seconds > 0 ||
|
|
durations.compressor_heat_2.seconds > 0 ||
|
|
durations.compressor_cool_1.seconds > 0 ||
|
|
durations.compressor_cool_2.seconds > 0
|
|
)
|
|
) {
|
|
durations.compressor_heat_1 = {'seconds': 0};
|
|
durations.compressor_heat_2 = {'seconds': 0};
|
|
durations.compressor_cool_1 = {'seconds': 0};
|
|
durations.compressor_cool_2 = {'seconds': 0};
|
|
}
|
|
|
|
if (
|
|
runtime_thermostat.auxiliary_heat_1 === 0 &&
|
|
runtime_thermostat.auxiliary_heat_2 === 0 &&
|
|
(
|
|
durations.auxiliary_heat_1.seconds > 0 ||
|
|
durations.auxiliary_heat_2.seconds > 0
|
|
)
|
|
) {
|
|
durations.auxiliary_heat_1 = {'seconds': 0};
|
|
durations.auxiliary_heat_2 = {'seconds': 0};
|
|
}
|
|
|
|
// Reset fan to 0
|
|
if (runtime_thermostat.fan === 0) {
|
|
durations.fan = {'seconds': 0};
|
|
}
|
|
|
|
// Reset accessories
|
|
if (runtime_thermostat.accessory === 0) {
|
|
durations[runtime_thermostat.accessory_type] = {'seconds': 0};
|
|
}
|
|
|
|
// Equipment
|
|
[
|
|
'fan',
|
|
'compressor_heat_1',
|
|
'compressor_heat_2',
|
|
'auxiliary_heat_1',
|
|
'auxiliary_heat_2',
|
|
'compressor_cool_1',
|
|
'compressor_cool_2',
|
|
'humidifier',
|
|
'dehumidifier',
|
|
'ventilator',
|
|
'economizer'
|
|
].forEach(function(series_code) {
|
|
var runtime_thermostat_series_code;
|
|
switch (series_code) {
|
|
case 'compressor_heat_1':
|
|
case 'compressor_heat_2':
|
|
runtime_thermostat_series_code = series_code
|
|
.replace('compressor_heat', 'compressor');
|
|
break;
|
|
case 'compressor_cool_1':
|
|
case 'compressor_cool_2':
|
|
runtime_thermostat_series_code = series_code
|
|
.replace('compressor_cool', 'compressor');
|
|
break;
|
|
case 'humidifier':
|
|
case 'dehumidifier':
|
|
case 'ventilator':
|
|
case 'economizer':
|
|
runtime_thermostat_series_code = 'accessory';
|
|
break;
|
|
default:
|
|
runtime_thermostat_series_code = series_code;
|
|
break;
|
|
}
|
|
|
|
var equipment_on = function(series_code_on, runtime_thermostat_series_code_on) {
|
|
switch (series_code_on) {
|
|
case 'compressor_heat_1':
|
|
case 'compressor_heat_2':
|
|
return runtime_thermostat[runtime_thermostat_series_code_on] > 0 &&
|
|
runtime_thermostat.compressor_mode === 'heat';
|
|
case 'compressor_cool_1':
|
|
case 'compressor_cool_2':
|
|
return runtime_thermostat[runtime_thermostat_series_code_on] > 0 &&
|
|
runtime_thermostat.compressor_mode === 'cool';
|
|
case 'humidifier':
|
|
case 'dehumidifier':
|
|
case 'ventilator':
|
|
case 'economizer':
|
|
return runtime_thermostat[runtime_thermostat_series_code_on] > 0 &&
|
|
runtime_thermostat.accessory_type === series_code;
|
|
default:
|
|
return runtime_thermostat[series_code] > 0;
|
|
}
|
|
};
|
|
|
|
if (equipment_on(series_code, runtime_thermostat_series_code) === true) {
|
|
data.metadata.series[series_code].active = true;
|
|
data.metadata.series[series_code].durations[current_m.valueOf()] = durations[series_code];
|
|
data.series[series_code].push(equipment_y[series_code]);
|
|
|
|
if (
|
|
series_code === 'auxiliary_heat_1' ||
|
|
series_code === 'compressor_heat_1' ||
|
|
series_code === 'compressor_cool_1'
|
|
) {
|
|
var series_code_2 = series_code.replace('1', '2');
|
|
data.metadata.series[series_code_2].durations[current_m.valueOf()] = durations[series_code_2];
|
|
}
|
|
durations[series_code].seconds += runtime_thermostat[runtime_thermostat_series_code];
|
|
|
|
/*
|
|
* If heat/cool/aux 2 is on, extend the bar from heat/cool/aux 1
|
|
* behind and set the duration.
|
|
*/
|
|
if (series_code.slice(-1) === '2') {
|
|
var series_code_1 = series_code.replace('2', '1');
|
|
data.series[series_code_1]
|
|
.splice(-1, 1, equipment_y[series_code_1]);
|
|
data.metadata.series[series_code_1]
|
|
.durations[current_m.valueOf()] = durations[series_code_1];
|
|
}
|
|
} else {
|
|
data.series[series_code].push(null);
|
|
}
|
|
});
|
|
} else {
|
|
data.series.calendar_event_smartrecovery.push(null);
|
|
data.series.calendar_event_home.push(null);
|
|
data.series.calendar_event_away.push(null);
|
|
data.series.calendar_event_sleep.push(null);
|
|
data.series.calendar_event_smarthome.push(null);
|
|
data.series.calendar_event_smartaway.push(null);
|
|
data.series.calendar_event_hold.push(null);
|
|
data.series.calendar_event_vacation.push(null);
|
|
data.series.calendar_event_quicksave.push(null);
|
|
data.series.calendar_event_other.push(null);
|
|
data.series.calendar_event_custom.push(null);
|
|
data.series.indoor_temperature.push(null);
|
|
data.series.outdoor_temperature.push(null);
|
|
data.series.indoor_humidity.push(null);
|
|
data.series.outdoor_humidity.push(null);
|
|
data.series.setpoint_heat.push(null);
|
|
data.series.setpoint_cool.push(null);
|
|
data.series.fan.push(null);
|
|
data.series.compressor_heat_1.push(null);
|
|
data.series.compressor_heat_2.push(null);
|
|
data.series.auxiliary_heat_1.push(null);
|
|
data.series.auxiliary_heat_2.push(null);
|
|
data.series.compressor_cool_1.push(null);
|
|
data.series.compressor_cool_2.push(null);
|
|
data.series.humidifier.push(null);
|
|
data.series.dehumidifier.push(null);
|
|
data.series.ventilator.push(null);
|
|
data.series.economizer.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_thermostats[current_m.valueOf() + offset]);
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* Get all the runtime_thermostat rows indexed by date.
|
|
*
|
|
* @return {array} The runtime_thermostat rows.
|
|
*/
|
|
beestat.component.card.runtime_thermostat_detail.prototype.get_runtime_thermostat_by_date_ = function() {
|
|
var runtime_thermostats = {};
|
|
if (beestat.cache.runtime_thermostat !== undefined) {
|
|
beestat.cache.runtime_thermostat.forEach(function(runtime_thermostat) {
|
|
runtime_thermostats[moment(runtime_thermostat.timestamp).valueOf()] = runtime_thermostat;
|
|
});
|
|
}
|
|
return runtime_thermostats;
|
|
};
|
|
|
|
/**
|
|
* Given an array of runtime thermostats, 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_thermostats
|
|
* @param {string} series_code
|
|
*
|
|
* @return {number} The average.
|
|
*/
|
|
beestat.component.card.runtime_thermostat_detail.prototype.get_average_ = function(runtime_thermostats, series_code) {
|
|
var average = 0;
|
|
var count = 0;
|
|
for (var i = 0; i < runtime_thermostats.length; i++) {
|
|
if (runtime_thermostats[i] !== undefined) {
|
|
average += runtime_thermostats[i][series_code];
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return average / count;
|
|
};
|
|
|
|
/**
|
|
* Get the title of the card.
|
|
*
|
|
* @return {string} Title
|
|
*/
|
|
beestat.component.card.runtime_thermostat_detail.prototype.get_title_ = function() {
|
|
return 'Runtime Detail';
|
|
};
|
|
|
|
/**
|
|
* Get the subtitle of the card.
|
|
*
|
|
* @return {string} Subtitle
|
|
*/
|
|
beestat.component.card.runtime_thermostat_detail.prototype.get_subtitle_ = function() {
|
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
|
var s = (beestat.setting('runtime_thermostat_detail_range_dynamic') > 1) ? 's' : '';
|
|
|
|
return 'Past ' +
|
|
beestat.setting('runtime_thermostat_detail_range_dynamic') +
|
|
' day' +
|
|
s;
|
|
}
|
|
|
|
var begin = moment(beestat.setting('runtime_thermostat_detail_range_static_begin'))
|
|
.format('MMM D, YYYY');
|
|
var end = moment(beestat.setting('runtime_thermostat_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_thermostat_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)
|
|
);
|
|
};
|