1
0
mirror of https://github.com/beestat/app.git synced 2025-05-24 02:14:03 -04:00
beestat/js/component/card/recent_activity.js
2019-10-16 20:46:41 -04:00

1439 lines
45 KiB
JavaScript
Executable File

/**
* Recent activity card. Shows a graph similar to what ecobee shows with the
* runtime info for a recent period of time.
*/
beestat.component.card.recent_activity = function() {
beestat.component.card.apply(this, arguments);
};
beestat.extend(beestat.component.card.recent_activity, beestat.component.card);
beestat.component.card.recent_activity.optional_series = [
'compressor_heat_1',
'compressor_heat_2',
'compressor_cool_1',
'compressor_cool_2',
'auxiliary_heat_1',
'auxiliary_heat_2',
'fan',
'dehumidifier',
'economizer',
'humidifier',
'ventilator'
];
beestat.component.card.recent_activity.calendar_events = [
'calendar_event_home',
'calendar_event_away',
'calendar_event_sleep',
'calendar_event_vacation',
'calendar_event_smarthome',
'calendar_event_smartaway',
'calendar_event_smartrecovery',
'calendar_event_hold',
'calendar_event_quicksave',
'calendar_event_other'
];
/**
* Decorate
*
* @param {rocket.ELements} parent
*/
beestat.component.card.recent_activity.prototype.decorate_contents_ = function(parent) {
var self = this;
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
this.chart_ = new beestat.component.chart();
var series = this.get_series_();
this.chart_.options.chart.backgroundColor = beestat.style.color.bluegray.base;
this.chart_.options.exporting.filename = thermostat.name + ' - Recent Activity';
this.chart_.options.exporting.chartOptions.title.text = this.get_title_();
this.chart_.options.exporting.chartOptions.subtitle.text = this.get_subtitle_();
var current_day;
var current_hour;
this.chart_.options.xAxis = {
'categories': series.x.chart_data,
'type': 'datetime',
'lineColor': beestat.style.color.bluegray.light,
'min': series.x.chart_data[0],
'max': series.x.chart_data[series.x.chart_data.length - 1],
'minRange': 21600000,
'tickLength': 0,
'gridLineWidth': 0,
'labels': {
'style': {'color': beestat.style.color.gray.base},
'formatter': function() {
var m = moment(this.value);
var hour = m.format('ha');
var day = m.format('ddd');
var label_parts = [];
if (day !== current_day) {
label_parts.push(day);
}
if (hour !== current_hour) {
label_parts.push(hour);
}
current_hour = hour;
current_day = day;
return label_parts.join(' ');
}
}
};
// Add some space for the top of the graph.
this.y_max_ += 30;
// Because higcharts isn't respecting the tickInterval parameter...seems to
// have to do with the secondary axis; as removing it makes it work a lot
// better.
var tick_positions = [];
var tick_interval = (thermostat.temperature_unit === '°F') ? 10 : 5;
var current_tick_position =
Math.floor(this.y_min_ / tick_interval) * tick_interval;
while (current_tick_position <= this.y_max_) {
tick_positions.push(current_tick_position);
current_tick_position += tick_interval;
}
this.chart_.options.yAxis = [
// Temperature
{
// 'alignTicks': false, // Uncommenting this will allow the humidity series to line up but it will also force the y-axis to be a bit larger. For example, a y min of 17 will get set to a min of 0 instead of 15 because the spacing is set to 20.
'gridLineColor': beestat.style.color.bluegray.light,
'gridLineDashStyle': 'longdash',
'title': {'text': null},
'labels': {
'style': {'color': beestat.style.color.gray.base},
'formatter': function() {
return this.value + thermostat.temperature_unit;
}
},
'tickPositions': tick_positions
},
// Top bars
{
'height': 100,
'min': 0,
'max': 100,
'gridLineWidth': 0,
'title': {'text': null},
'labels': {'enabled': false}
},
// Humidity
{
'alignTicks': false,
'gridLineColor': null,
'tickInterval': 10,
// 'gridLineDashStyle': 'longdash',
'opposite': true,
'title': {'text': null},
'labels': {
'style': {'color': beestat.style.color.gray.base},
'formatter': function() {
return this.value + '%';
}
},
/*
* If you set a min/max highcharts always shows the axis. Setting these
* attributes prevents the "always show" logic and the 0-100 is achieved
* with this set of parameters.
* https://github.com/highcharts/highcharts/issues/3403
*/
'min': 0,
'minRange': 100,
'ceiling': 100
}
];
this.chart_.options.tooltip = {
'shared': true,
'useHTML': true,
'borderWidth': 0,
'shadow': false,
'backgroundColor': null,
'followPointer': true,
'crosshairs': {
'width': 1,
'zIndex': 100,
'color': beestat.style.color.gray.light,
'dashStyle': 'shortDot',
'snap': false
},
'positioner': function(tooltip_width, tooltip_height, point) {
return beestat.component.chart.tooltip_positioner(
self.chart_.get_chart(),
tooltip_width,
tooltip_height,
point
);
},
'formatter': function() {
var self = this;
var sections = [];
// HVAC Mode
var system_mode;
var system_mode_color;
switch (series.system_mode.data[self.x]) {
case 'auto':
system_mode = 'Auto';
system_mode_color = beestat.style.color.gray.base;
break;
case 'heat':
system_mode = 'Heat';
system_mode_color = beestat.series.compressor_heat_1.color;
break;
case 'cool':
system_mode = 'Cool';
system_mode_color = beestat.series.compressor_cool_1.color;
break;
case 'off':
system_mode = 'Off';
system_mode_color = beestat.style.color.gray.base;
break;
case 'auxiliary_heat':
system_mode = 'Aux';
system_mode_color = beestat.series.auxiliary_heat_1.color;
break;
}
var section_1 = [];
sections.push(section_1);
if (system_mode !== undefined) {
section_1.push({
'label': 'Mode',
'value': system_mode,
'color': system_mode_color
});
}
// Calendar Event / Comfort Profile
var event;
var event_color;
for (var i = 0; i < beestat.component.card.recent_activity.calendar_events.length; i++) {
var calendar_event = beestat.component.card.recent_activity.calendar_events[i];
if (series[calendar_event].data[self.x] !== null) {
event = beestat.series[calendar_event].name;
event_color = beestat.series[calendar_event].color;
break;
}
}
if (event !== undefined) {
section_1.push({
'label': 'Comfort Profile',
'value': event,
'color': event_color
});
}
var section_2 = [];
sections.push(section_2);
[
'setpoint_heat',
'setpoint_cool',
'indoor_temperature',
'outdoor_temperature',
'indoor_humidity',
'outdoor_humidity'
].forEach(function(series_code) {
var value;
if (series_code === 'setpoint_cool') {
return; // Grab it when doing setpoint_heat
} else if (series_code === 'setpoint_heat') {
if (
series[series_code].data[self.x] === null
) {
return;
}
switch (series.system_mode.data[self.x]) {
case 'heat':
if (series.setpoint_heat.data[self.x] === null) {
return;
}
value = beestat.temperature({
'temperature': series.setpoint_heat.data[self.x],
'convert': false,
'units': true
});
break;
case 'cool':
if (series.setpoint_cool.data[self.x] === null) {
return;
}
value = beestat.temperature({
'temperature': series.setpoint_cool.data[self.x],
'convert': false,
'units': true
});
break;
case 'auto':
if (
series.setpoint_heat.data[self.x] === null ||
series.setpoint_cool.data[self.x] === null
) {
return;
}
value = beestat.temperature({
'temperature': series.setpoint_heat.data[self.x],
'convert': false,
'units': true
});
value += ' - ';
value += beestat.temperature({
'temperature': series.setpoint_cool.data[self.x],
'convert': false,
'units': true
});
break;
default:
return;
break;
}
} else if (
series_code === 'indoor_humidity' ||
series_code === 'outdoor_humidity'
) {
if (series[series_code].data[self.x] === null) {
return;
}
value = series[series_code].data[self.x] + '%';
} else {
if (series[series_code].data[self.x] === null) {
return;
}
value = beestat.temperature({
'temperature': series[series_code].data[self.x],
'convert': false,
'units': true
});
}
section_2.push({
'label': beestat.series[series_code].name,
'value': value,
'color': beestat.style.color.gray.light
});
});
var section_3 = [];
sections.push(section_3);
beestat.component.card.recent_activity.optional_series.forEach(function(series_code) {
if (
series[series_code].data[self.x] !== undefined &&
series[series_code].data[self.x] !== null
) {
section_3.push({
'label': beestat.series[series_code].name,
'value': beestat.time(series[series_code].durations[self.x].seconds),
'color': beestat.series[series_code].color
});
}
});
return beestat.component.chart.tooltip_formatter(
moment(this.x).format('ddd, MMM D @ h:mma'),
sections
);
}
};
this.chart_.options.series = [];
beestat.component.card.recent_activity.calendar_events.forEach(function(calendar_event) {
self.chart_.options.series.push({
'id': calendar_event,
'linkedTo': (calendar_event !== 'calendar_event_home') ? 'calendar_event_home' : undefined,
'data': series[calendar_event].chart_data,
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': 'Comfort Profile',
'type': 'line',
'color': beestat.series[calendar_event].color,
'lineWidth': 5,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
});
if (series.compressor_cool_1.enabled === true) {
this.chart_.options.series.push({
'id': 'compressor_cool_1',
'data': series.compressor_cool_1.chart_data,
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': 'Cool',
'type': 'line',
'color': beestat.series.compressor_cool_1.color,
'lineWidth': 10,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
}
if (series.compressor_cool_2.enabled === true) {
this.chart_.options.series.push({
'data': series.compressor_cool_2.chart_data,
'linkedTo': 'compressor_cool_1',
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': beestat.series.compressor_cool_2.name,
'type': 'line',
'color': beestat.series.compressor_cool_2.color,
'lineWidth': 10,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
}
if (series.compressor_heat_1.enabled === true) {
this.chart_.options.series.push({
'id': 'compressor_heat_1',
'data': series.compressor_heat_1.chart_data,
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': 'Heat',
'type': 'line',
'color': beestat.series.compressor_heat_1.color,
'lineWidth': 10,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
}
if (series.compressor_heat_2.enabled === true) {
this.chart_.options.series.push({
'linkedTo': 'compressor_heat_1',
'data': series.compressor_heat_2.chart_data,
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': beestat.series.compressor_heat_2.name,
'type': 'line',
'color': beestat.series.compressor_heat_2.color,
'lineWidth': 10,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
}
[
'auxiliary_heat_1',
'auxiliary_heat_2'
].forEach(function(equipment) {
if (series[equipment].enabled === true) {
self.chart_.options.series.push({
'data': series[equipment].chart_data,
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': beestat.series[equipment].name,
'type': 'line',
'color': beestat.series[equipment].color,
'lineWidth': 10,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
}
});
if (series.fan.enabled === true) {
this.chart_.options.series.push({
'data': series.fan.chart_data,
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': beestat.series.fan.name,
'type': 'line',
'color': beestat.series.fan.color,
'lineWidth': 5,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
}
[
'dehumidifier',
'economizer',
'humidifier',
'ventilator'
].forEach(function(equipment) {
if (series[equipment].enabled === true) {
self.chart_.options.series.push({
'data': series[equipment].chart_data,
'yAxis': 1,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': beestat.series[equipment].name,
'type': 'line',
'color': beestat.series[equipment].color,
'lineWidth': 5,
'linecap': 'square',
'states': {'hover': {'lineWidthPlus': 0}}
});
}
});
this.chart_.options.series.push({
'id': 'indoor_humidity',
'data': series.indoor_humidity.chart_data,
'yAxis': 2,
'name': beestat.series.indoor_humidity.name,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'spline',
'dashStyle': 'DashDot',
'visible': false,
'lineWidth': 1,
'color': beestat.series.indoor_humidity.color,
'states': {'hover': {'lineWidthPlus': 0}},
/*
* Weird HighCharts bug...
* https://stackoverflow.com/questions/48374093/highcharts-highstock-line-change-to-area-bug
* https://github.com/highcharts/highcharts/issues/766
*/
'linecap': 'square'
});
this.chart_.options.series.push({
'id': 'outdoor_humidity',
'data': series.outdoor_humidity.chart_data,
'yAxis': 2,
'name': beestat.series.outdoor_humidity.name,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'spline',
'dashStyle': 'DashDot',
'visible': false,
'lineWidth': 1,
'color': beestat.series.outdoor_humidity.color,
'states': {'hover': {'lineWidthPlus': 0}},
/*
* Weird HighCharts bug...
* https://stackoverflow.com/questions/48374093/highcharts-highstock-line-change-to-area-bug
* https://github.com/highcharts/highcharts/issues/766
*/
'linecap': 'square'
});
this.chart_.options.series.push({
'data': series.indoor_temperature.chart_data,
'yAxis': 0,
'name': beestat.series.indoor_temperature.name,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'spline',
'lineWidth': 2,
'color': beestat.series.indoor_temperature.color,
'states': {'hover': {'lineWidthPlus': 0}},
/*
* Weird HighCharts bug...
* https://stackoverflow.com/questions/48374093/highcharts-highstock-line-change-to-area-bug
* https://github.com/highcharts/highcharts/issues/766
*/
'linecap': 'square'
});
this.chart_.options.series.push({
'color': beestat.series.outdoor_temperature.color,
'data': series.outdoor_temperature.chart_data,
// 'zones': beestat.component.chart.get_outdoor_temperature_zones(),
'yAxis': 0,
'name': beestat.series.outdoor_temperature.name,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'spline',
'dashStyle': 'ShortDash',
'lineWidth': 1,
'states': {'hover': {'lineWidthPlus': 0}}
});
this.chart_.options.series.push({
'data': series.setpoint_heat.chart_data,
'id': 'setpoint_heat',
'yAxis': 0,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': beestat.series.setpoint_heat.name,
'type': 'line',
'color': beestat.series.setpoint_heat.color,
'lineWidth': 1,
'states': {'hover': {'lineWidthPlus': 0}},
'step': 'right'
});
this.chart_.options.series.push({
'data': series.setpoint_cool.chart_data,
'yAxis': 0,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'name': beestat.series.setpoint_cool.name,
'type': 'line',
'color': beestat.series.setpoint_cool.color,
'lineWidth': 1,
'states': {'hover': {'lineWidthPlus': 0}},
'step': 'right'
});
this.chart_.render(parent);
this.show_loading_('Syncing Recent Activity');
/*
* 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 (this.data_available_() === true) {
if (beestat.cache.runtime_thermostat.length === 0) {
this.get_data_();
} else {
this.hide_loading_();
}
} else {
var poll_interval = 10000;
beestat.add_poll_interval(poll_interval);
beestat.dispatcher.addEventListener('poll.recent_activity_load', function() {
if (self.data_available_() === true) {
beestat.remove_poll_interval(poll_interval);
beestat.dispatcher.removeEventListener('poll.recent_activity_load');
self.get_data_();
}
});
}
};
/**
* Decorate the menu
*
* @param {rocket.Elements} parent
*/
beestat.component.card.recent_activity.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('recent_activity_time_count') !== 1 ||
beestat.setting('recent_activity_time_period') !== 'day'
) {
beestat.setting({
'recent_activity_time_count': 1,
'recent_activity_time_period': 'day'
});
/*
* Rerender; the timeout lets the menu close immediately without being
* blocked by the time it takes to rerender the chart.
*/
setTimeout(function() {
self.rerender();
}, 0);
}
}));
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('recent_activity_time_count') !== 3 ||
beestat.setting('recent_activity_time_period') !== 'day'
) {
beestat.setting({
'recent_activity_time_count': 3,
'recent_activity_time_period': 'day'
});
setTimeout(function() {
self.rerender();
}, 0);
}
}));
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('recent_activity_time_count') !== 7 ||
beestat.setting('recent_activity_time_period') !== 'day'
) {
beestat.setting({
'recent_activity_time_count': 7,
'recent_activity_time_period': 'day'
});
setTimeout(function() {
self.rerender();
}, 0);
}
}));
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Download Chart')
.set_icon('download')
.set_callback(function() {
self.chart_.get_chart().exportChartLocal();
}));
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Reset Zoom')
.set_icon('magnify_minus')
.set_callback(function() {
self.chart_.get_chart().zoomOut();
}));
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Help')
.set_icon('help_circle')
.set_callback(function() {
(new beestat.component.modal.help_recent_activity()).render();
}));
};
/**
* Get all of the series data.
*
* @return {object} The series data.
*/
beestat.component.card.recent_activity.prototype.get_series_ = function() {
var self = this;
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
/*
* The more data that gets shown the larger the smoothing factor should be
* (less points, smoother graph).
*/
var smoothing_factor = beestat.setting('recent_activity_time_count') * 3;
this.y_min_ = Infinity;
this.y_max_ = -Infinity;
/*
* The chart_data property is what Highcharts uses. The data property is the
* same data indexed by the x value to make it easy to access.
*/
var series = {
'x': {
'enabled': true,
'chart_data': [],
'data': {}
},
'setpoint_heat': {
'enabled': true,
'chart_data': [],
'data': {}
},
'setpoint_cool': {
'enabled': true,
'chart_data': [],
'data': {}
},
'outdoor_temperature': {
'enabled': true,
'chart_data': [],
'data': {}
},
'indoor_temperature': {
'enabled': true,
'chart_data': [],
'data': {}
},
'indoor_humidity': {
'enabled': true,
'chart_data': [],
'data': {}
},
'outdoor_humidity': {
'enabled': true,
'chart_data': [],
'data': {}
},
'system_mode': {
'enabled': true,
'chart_data': [],
'data': {}
}
};
// Initialize the optional series.
beestat.component.card.recent_activity.optional_series.forEach(function(optional_series) {
series[optional_series] = {
'enabled': false,
'chart_data': [],
'data': {},
'durations': {}
};
});
// Initialize the calendar event series.
beestat.component.card.recent_activity.calendar_events.forEach(function(calendar_event) {
series[calendar_event] = {
'enabled': false,
'chart_data': [],
'data': {}
};
});
/*
* Overrides the %10 smoothing for when there is missing data. Basically just
* ensures that the graph starts back up right away instead of waiting for a
* 10th data point.
*/
var previous_indoor_temperature_value = null;
var previous_outdoor_temperature_value = null;
var previous_indoor_humidity_value = null;
var previous_outdoor_humidity_value = null;
var min_x = moment()
.subtract(
beestat.setting('recent_activity_time_count'),
beestat.setting('recent_activity_time_period')
)
.valueOf();
/*
* This creates a distinct object for each chunk of runtime so the total on
* time can be computed for any given segment.
*/
var durations = {};
beestat.cache.runtime_thermostat.forEach(function(runtime_thermostat, i) {
// if (runtime_thermostat.ecobee_thermostat_id !== thermostat.ecobee_thermostat_id) {
// return;
// }
//
if (runtime_thermostat.compressor_mode === 'heat') {
runtime_thermostat.compressor_heat_1 = runtime_thermostat.compressor_1;
runtime_thermostat.compressor_heat_2 = runtime_thermostat.compressor_2;
runtime_thermostat.compressor_cool_1 = 0;
runtime_thermostat.compressor_cool_2 = 0;
} else if (runtime_thermostat.compressor_mode === 'cool') {
runtime_thermostat.compressor_heat_1 = 0;
runtime_thermostat.compressor_heat_2 = 0;
runtime_thermostat.compressor_cool_1 = runtime_thermostat.compressor_1;
runtime_thermostat.compressor_cool_2 = runtime_thermostat.compressor_2;
} else if (runtime_thermostat.compressor_mode === 'off') {
runtime_thermostat.compressor_heat_1 = 0;
runtime_thermostat.compressor_heat_2 = 0;
runtime_thermostat.compressor_cool_1 = 0;
runtime_thermostat.compressor_cool_2 = 0;
} else {
runtime_thermostat.compressor_heat_1 = null;
runtime_thermostat.compressor_heat_2 = null;
runtime_thermostat.compressor_cool_1 = null;
runtime_thermostat.compressor_cool_2 = null;
}
runtime_thermostat.humidifier = 0;
runtime_thermostat.dehumidifier = 0;
runtime_thermostat.ventilator = 0;
runtime_thermostat.economizer = 0;
// The string includes +00:00 as the UTC offset but moment knows what time
// zone my PC is in...or at least it has a guess. This means that beestat
// graphs can now show up in local time instead of thermostat time.
var x = moment(runtime_thermostat.timestamp).valueOf();
if (x < min_x) {
return;
}
series.x.chart_data.push(x);
var original_durations = {};
if (runtime_thermostat.compressor_heat_2 > 0) {
original_durations.compressor_heat_1 = runtime_thermostat.compressor_heat_1;
runtime_thermostat.compressor_heat_1 = runtime_thermostat.compressor_heat_2;
}
// TODO DO THIS FOR AUX
// TODO DO THIS FOR COOL
beestat.component.card.recent_activity.optional_series.forEach(function(series_code) {
if (durations[series_code] === undefined) {
durations[series_code] = [{'seconds': 0}];
}
// if (series_code === 'compressor_heat_1') {
// runtime_thermostat
// }
if (
runtime_thermostat[series_code] !== null &&
runtime_thermostat[series_code] > 0
) {
var value;
switch (series_code) {
case 'fan':
value = 70;
break;
case 'dehumidifier':
case 'economizer':
case 'humidifier':
case 'ventilator':
value = 62;
break;
default:
value = 80;
break;
}
series[series_code].enabled = true;
series[series_code].chart_data.push([
x,
value
]);
series[series_code].data[x] = value;
var duration = original_durations[series_code] !== undefined
? original_durations[series_code]
: runtime_thermostat[series_code];
durations[series_code][durations[series_code].length - 1].seconds += duration;
// durations[series_code][durations[series_code].length - 1].seconds += runtime_thermostat[series_code];
series[series_code].durations[x] = durations[series_code][durations[series_code].length - 1];
} else {
series[series_code].chart_data.push([
x,
null
]);
series[series_code].data[x] = null;
if (durations[series_code][durations[series_code].length - 1].seconds > 0) {
durations[series_code].push({'seconds': 0});
}
}
});
/*
* This is the ecobee code.
*
* var normalizedString = eventString;
* var vacationPattern = /(\S\S\S\s\d+\s\d\d\d\d)|(\d{12})/i;
* var smartRecoveryPattern = /smartRecovery/i;
* var smartAwayPattern = /smartAway/i;
* var smartHomePattern = /smartHome/i;
* var quickSavePattern = /quickSave/i;
*
* if (typeof eventString === 'string') {
* eventString = eventString.toLowerCase();
* normalizedString = eventString;
*
* if (eventString === 'auto' || eventString === 'today' || eventString === 'hold' || typeof thermostatClimates.climates[eventString] !== 'undefined') {
* normalizedString = 'hold';
* } else if (vacationPattern.test(eventString) || eventString.toLowerCase().indexOf('vacation') === 0) {
* normalizedString = 'vacation';
* } else if(smartRecoveryPattern.test(eventString)) {
* normalizedString = 'smartRecovery';
* } else if(smartHomePattern.test(eventString)) {
* normalizedString = 'smartHome';
* } else if(smartAwayPattern.test(eventString)) {
* normalizedString = 'smartAway';
* } else if(quickSavePattern.test(eventString)) {
* normalizedString = 'quickSave';
* } else {
* normalizedString = 'customEvent';
* }
* }
*/
/*
* Here are some examples of what I get in the database and what they map to
*
* calendar_event_home home
* calendar_event_away away
* calendar_event_smartrecovery (SmartRecovery)
* calendar_event_smartrecovery smartAway(SmartRecovery)
* calendar_event_smartrecovery auto(SmartRecovery)
* calendar_event_smartrecovery hold(SmartRecovery)
* calendar_event_smartrecovery 149831444185(SmartRecovery)
* calendar_event_smartrecovery Vacation(SmartRecovery)
* calendar_event_smartrecovery 152304757299(SmartRecovery)
* calendar_event_smartrecovery Apr 29 2016(SmartRecovery)
* calendar_event_smarthome smartHome
* calendar_event_smartaway smartAway
* calendar_event_hold hold
* calendar_event_vacation Vacation
* calendar_event_quicksave QuickSave
* calendar_event_vacation 151282889098
* calendar_event_vacation May 14 2016
* calendar_event_hold auto
* calendar_event_other NULL
* calendar_event_other HKhold
* calendar_event_other 8915FC00B0DA
* calendar_event_other 769347151
*/
/*
* Thanks, ecobee...I more or less copied this code from the ecobee Follow
* Me graph to make sure it's as accurate as possible.
*/
var this_calendar_event;
/*
* Display a fixed schedule in demo mode.
*/
if (window.is_demo === true) {
var m = moment(runtime_thermostat.timestamp);
// Moment and ecobee use different indexes for the days of the week
var day_of_week_index = (m.day() + 6) % 7;
// Ecobee splits the schedule up into 30 minute chunks; find the right one
var m_midnight = m.clone().startOf('day');
var minute_of_day = m.diff(m_midnight, 'minutes');
var chunk_of_day_index = Math.floor(minute_of_day / 30); // max 47
var ecobee_thermostat = beestat.cache.ecobee_thermostat[
thermostat.ecobee_thermostat_id
];
this_calendar_event = 'calendar_event_' + ecobee_thermostat.json_program.schedule[day_of_week_index][chunk_of_day_index];
} else {
if (runtime_thermostat.event === null) {
if (runtime_thermostat.climate === null) {
this_calendar_event = 'calendar_event_other';
} else {
this_calendar_event = 'calendar_event_' + runtime_thermostat.climate.toLowerCase();
}
} else if (runtime_thermostat.event.match(/SmartRecovery/i) !== null) {
this_calendar_event = 'calendar_event_smartrecovery';
} else if (runtime_thermostat.event.match(/^home$/i) !== null) {
this_calendar_event = 'calendar_event_home';
} else if (runtime_thermostat.event.match(/^away$/i) !== null) {
this_calendar_event = 'calendar_event_away';
} else if (runtime_thermostat.event.match(/^smarthome$/i) !== null) {
this_calendar_event = 'calendar_event_smarthome';
} else if (runtime_thermostat.event.match(/^smartaway$/i) !== null) {
this_calendar_event = 'calendar_event_smartaway';
} else if (runtime_thermostat.event.match(/^auto$/i) !== null) {
this_calendar_event = 'calendar_event_hold';
} else if (runtime_thermostat.event.match(/^today$/i) !== null) {
this_calendar_event = 'calendar_event_hold';
} else if (runtime_thermostat.event.match(/^hold$/i) !== null) {
this_calendar_event = 'calendar_event_hold';
} else if (runtime_thermostat.event.match(/^vacation$/i) !== null) {
this_calendar_event = 'calendar_event_vacation';
} else if (runtime_thermostat.event.match(/(\S\S\S\s\d+\s\d\d\d\d)|(\d{12})/i) !== null) {
this_calendar_event = 'calendar_event_vacation';
} else if (runtime_thermostat.event.match(/^quicksave$/i) !== null) {
this_calendar_event = 'calendar_event_quicksave';
} else {
this_calendar_event = 'calendar_event_other';
}
}
// Dynamically add new calendar events for custom climates.
if (
beestat.component.card.recent_activity.calendar_events.indexOf(this_calendar_event) === -1
) {
beestat.component.card.recent_activity.calendar_events.push(this_calendar_event);
series[this_calendar_event] = {
'enabled': false,
'chart_data': [],
'data': {},
'durations': {}
};
beestat.series[this_calendar_event] = {
'name': runtime_thermostat.climate,
'color': beestat.style.color.bluegreen.base
};
}
beestat.component.card.recent_activity.calendar_events.forEach(function(calendar_event) {
if (calendar_event === this_calendar_event && this_calendar_event !== 'calendar_event_other') {
var value = 95;
series[calendar_event].enabled = true;
series[calendar_event].chart_data.push([
x,
value
]);
series[calendar_event].data[x] = value;
} else {
series[calendar_event].chart_data.push([
x,
null
]);
series[calendar_event].data[x] = 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]);
*/
series.system_mode.data[x] = runtime_thermostat.system_mode;
// Setpoints
var setpoint_value_heat = beestat.temperature({'temperature': runtime_thermostat.setpoint_heat});
var setpoint_value_cool = beestat.temperature({'temperature': runtime_thermostat.setpoint_cool});
// NOTE: At one point I was also factoring in your heat/cool differential
// plus the extra degree offset ecobee adds when you are "away". That made
// the graph very exact but it wasn't really "setpoint" so I felt that would
// be confusing.
if (
runtime_thermostat.system_mode === 'auto' ||
runtime_thermostat.system_mode === 'heat' ||
runtime_thermostat.system_mode === 'auxiliary_heat' ||
runtime_thermostat.system_mode === null // Need this for the explicit null to remove from the graph.
) {
series.setpoint_heat.data[x] = setpoint_value_heat;
series.setpoint_heat.chart_data.push([
x,
setpoint_value_heat
]);
if (setpoint_value_heat !== null) {
self.y_min_ = Math.min(self.y_min_, setpoint_value_heat);
self.y_max_ = Math.max(self.y_max_, setpoint_value_heat);
}
} else {
/**
* Explicitly add a null entry to force an empty spot on the line.
* Otherwise Highcharts will connect gaps (see #119).
*/
series.setpoint_heat.data[x] = null;
series.setpoint_heat.chart_data.push([
x,
null
]);
}
if (
runtime_thermostat.system_mode === 'auto' ||
runtime_thermostat.system_mode === 'cool' ||
runtime_thermostat.system_mode === null // Need this for the explicit null to remove from the graph.
) {
series.setpoint_cool.data[x] = setpoint_value_cool;
series.setpoint_cool.chart_data.push([
x,
setpoint_value_cool
]);
if (setpoint_value_cool !== null) {
self.y_min_ = Math.min(self.y_min_, setpoint_value_cool);
self.y_max_ = Math.max(self.y_max_, setpoint_value_cool);
}
} else {
/**
* Explicitly add a null entry to force an empty spot on the line.
* Otherwise Highcharts will connect gaps (see #119).
*/
series.setpoint_cool.data[x] = null;
series.setpoint_cool.chart_data.push([
x,
null
]);
}
// Indoor temperature
var indoor_temperature_value = beestat.temperature(runtime_thermostat.indoor_temperature);
series.indoor_temperature.data[x] = indoor_temperature_value;
/*
* Draw a data point if:
* It's one of the nth data points (smoothing) OR
* The previous value is null (forces data point right when null data stops instead of on the 10th) OR
* The current value is null (forces null data to display as a blank section) PR
* The next value is null (forces data point right when null data starts instead of on the 10th)
* The current value is the last value (forces data point right at the end)
*/
if (
i % smoothing_factor === 0 ||
(
previous_indoor_temperature_value === null &&
indoor_temperature_value !== null
) ||
indoor_temperature_value === null ||
(
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
beestat.cache.runtime_thermostat[i + 1].indoor_temperature === null
) ||
i === (beestat.cache.runtime_thermostat.length - 1)
) {
series.indoor_temperature.enabled = true;
series.indoor_temperature.chart_data.push([
x,
indoor_temperature_value
]);
if (indoor_temperature_value !== null) {
self.y_min_ = Math.min(self.y_min_, indoor_temperature_value);
self.y_max_ = Math.max(self.y_max_, indoor_temperature_value);
}
}
// Outdoor temperature
var outdoor_temperature_value = beestat.temperature(runtime_thermostat.outdoor_temperature);
series.outdoor_temperature.data[x] = outdoor_temperature_value;
/*
* Draw a data point if:
* It's one of the 10th data points (smoothing) OR
* The previous value is null (forces data point right when null data stops instead of on the 10th) OR
* The current value is null (forces null data to display as a blank section) PR
* The next value is null (forces data point right when null data starts instead of on the 10th)
* The current value is the last value (forces data point right at the end)
*/
if (
i % smoothing_factor === 0 ||
(
previous_outdoor_temperature_value === null &&
outdoor_temperature_value !== null
) ||
outdoor_temperature_value === null ||
(
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
beestat.cache.runtime_thermostat[i + 1].outdoor_temperature === null
) ||
i === (beestat.cache.runtime_thermostat.length - 1)
) {
series.outdoor_temperature.enabled = true;
series.outdoor_temperature.chart_data.push([
x,
outdoor_temperature_value
]);
if (outdoor_temperature_value !== null) {
self.y_min_ = Math.min(self.y_min_, outdoor_temperature_value);
self.y_max_ = Math.max(self.y_max_, outdoor_temperature_value);
}
}
// Indoor humidity
var indoor_humidity_value;
if (runtime_thermostat.indoor_humidity !== null) {
indoor_humidity_value = parseInt(
runtime_thermostat.indoor_humidity,
10
);
} else {
indoor_humidity_value = null;
}
series.indoor_humidity.data[x] = indoor_humidity_value;
/*
* Draw a data point if:
* It's one of the 10th data points (smoothing) OR
* The previous value is null (forces data point right when null data stops instead of on the 10th) OR
* The current value is null (forces null data to display as a blank section) PR
* The next value is null (forces data point right when null data starts instead of on the 10th)
* The current value is the last value (forces data point right at the end)
*/
if (
i % smoothing_factor === 0 ||
(
previous_indoor_humidity_value === null &&
indoor_humidity_value !== null
) ||
indoor_humidity_value === null ||
(
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
beestat.cache.runtime_thermostat[i + 1].indoor_humidity === null
) ||
i === (beestat.cache.runtime_thermostat.length - 1)
) {
series.indoor_humidity.enabled = true;
series.indoor_humidity.chart_data.push([
x,
indoor_humidity_value
]);
}
// Outdoor humidity
var outdoor_humidity_value;
if (runtime_thermostat.outdoor_humidity !== null) {
outdoor_humidity_value = parseInt(
runtime_thermostat.outdoor_humidity,
10
);
} else {
outdoor_humidity_value = null;
}
series.outdoor_humidity.data[x] = outdoor_humidity_value;
/*
* Draw a data point if:
* It's one of the 10th data points (smoothing) OR
* The previous value is null (forces data point right when null data stops instead of on the 10th) OR
* The current value is null (forces null data to display as a blank section) PR
* The next value is null (forces data point right when null data starts instead of on the 10th)
* The current value is the last value (forces data point right at the end)
*/
if (
i % smoothing_factor === 0 ||
(
previous_outdoor_humidity_value === null &&
outdoor_humidity_value !== null
) ||
outdoor_humidity_value === null ||
(
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
beestat.cache.runtime_thermostat[i + 1].outdoor_humidity === null
) ||
i === (beestat.cache.runtime_thermostat.length - 1)
) {
series.outdoor_humidity.enabled = true;
series.outdoor_humidity.chart_data.push([
x,
outdoor_humidity_value
]);
}
previous_indoor_temperature_value = indoor_temperature_value;
previous_outdoor_temperature_value = outdoor_temperature_value;
previous_indoor_humidity_value = indoor_humidity_value;
previous_outdoor_humidity_value = outdoor_humidity_value;
});
return series;
};
/**
* Get the title of the card.
*
* @return {string} Title
*/
beestat.component.card.recent_activity.prototype.get_title_ = function() {
return 'Recent Activity';
};
/**
* Get the subtitle of the card.
*
* @return {string} Subtitle
*/
beestat.component.card.recent_activity.prototype.get_subtitle_ = function() {
var s = (beestat.setting('recent_activity_time_count') > 1) ? 's' : '';
return 'Past ' +
beestat.setting('recent_activity_time_count') +
' ' +
beestat.setting('recent_activity_time_period') +
s;
};
/**
* Determine whether or not enough data is currently available to render this
* card. In this particular case require data from 7 days to an hour ago to be synced.
*
* @return {boolean} Whether or not the data is available.
*/
beestat.component.card.recent_activity.prototype.data_available_ = function() {
// Demo can juse 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);
var required_sync_begin = moment().subtract(7, 'day');
required_sync_begin = moment.max(
required_sync_begin,
moment(thermostat.first_connected)
);
var required_sync_end = moment().subtract(1, 'hour');
return (
current_sync_begin.isSameOrBefore(required_sync_begin) &&
current_sync_end.isSameOrAfter(required_sync_end)
);
};
/**
* Get the data needed to render this card.
*/
beestat.component.card.recent_activity.prototype.get_data_ = function() {
var self = this;
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
new beestat.api()
.add_call(
'runtime_thermostat',
'read',
{
'attributes': {
'thermostat_id': thermostat.thermostat_id,
'timestamp': {
'value': moment()
.subtract(7, 'd')
.format('YYYY-MM-DD'),
'operator': '>'
}
}
}
)
.set_callback(function(response) {
beestat.cache.set('runtime_thermostat', response);
self.rerender();
})
.send();
};