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

Fixed #164 - Convert recent activity to chart2

Also: Fixed #177, Fixed #91, Fixed #86, Fixed #40. Big changes.
This commit is contained in:
Jon Ziebell 2019-12-16 21:34:01 -05:00
parent 03a40da867
commit 7adfcb4b34
20 changed files with 2084 additions and 2203 deletions

View File

@ -622,6 +622,14 @@ class runtime_thermostat extends cora\crud {
throw new \Exception('Max range is 30 days.', 10205);
}
// Accept timestamps in roughly any format; always convert back to something nice and in UTC
if (is_array($attributes['timestamp']['value']) === true) {
$attributes['timestamp']['value'][0] = date('c', strtotime($attributes['timestamp']['value'][0]));
$attributes['timestamp']['value'][1] = date('c', strtotime($attributes['timestamp']['value'][1]));
} else {
$attributes['timestamp']['value'] = date('c', strtotime($attributes['timestamp']['value']));
}
// Read the data.
$runtime_thermostats = $this->database->read(
'runtime_thermostat',

10
app.php
View File

@ -1,10 +1,10 @@
<?php
// If you're not logged in, just take you directly to the ecobee login page.
if(isset($_COOKIE['session_key']) === false) {
header('Location: http://' . $_SERVER['HTTP_HOST'] . '/api/?resource=ecobee&method=authorize&arguments={}&api_key=ER9Dz8t05qUdui0cvfWi5GiVVyHP6OB8KPuSisP2');
die();
}
// If you're not logged in, just take you directly to the ecobee login page.
if(isset($_COOKIE['session_key']) === false) {
header('Location: http://' . $_SERVER['HTTP_HOST'] . '/api/?resource=ecobee&method=authorize&arguments={}&api_key=ER9Dz8t05qUdui0cvfWi5GiVVyHP6OB8KPuSisP2');
die();
}
?>

View File

@ -6,7 +6,11 @@ html {
box-sizing: inherit;
}
/* Highcharts */
.highcharts-container, .highcharts-container svg { width: 100% !important; }
.crisp_edges {
shape-rendering: crispEdges;
}
body {
background: #111;

View File

@ -15,11 +15,16 @@ beestat.dispatcher = new beestat.dispatcher_();
*
* @param {string|array} type The event type or an array of event types.
* @param {Function} listener Event Listener.
*
* @return {beestat.dispatcher_} this.
*/
beestat.dispatcher_.prototype.addEventListener = function(type, listener) {
if (typeof type === 'object') {
for (var i = 0; i < type.length; i++) {
rocket.EventTarget.prototype.addEventListener.apply(this, [type[i], listener]);
rocket.EventTarget.prototype.addEventListener.apply(this, [
type[i],
listener
]);
}
} else {
rocket.EventTarget.prototype.addEventListener.apply(this, arguments);

View File

@ -12,8 +12,13 @@ beestat.setting = function(key, opt_value, opt_callback) {
var user = beestat.get_user();
var defaults = {
'recent_activity_time_period': 'day',
'recent_activity_time_count': 3,
'runtime_detail_smoothing': true,
'runtime_detail_range_type': 'dynamic',
'runtime_detail_range_static_begin': moment()
.subtract(3, 'day')
.format('MM/DD/YYYY'),
'runtime_detail_range_static_end': moment().format('MM/DD/YYYY'),
'runtime_detail_range_dynamic': 3,
'runtime_thermostat_summary_time_count': 0,
'runtime_thermostat_summary_time_period': 'all',
@ -82,8 +87,15 @@ beestat.setting = function(key, opt_value, opt_callback) {
}
}
// If no settings changed no API call needs to be fired.
/**
* If no settings changed no API call needs to be fired. In that case also
* fire the callback since the API isn't doing it.
*/
if (has_calls === true) {
api.send();
} else {
if (opt_callback !== undefined) {
opt_callback();
}
}
};

View File

@ -191,16 +191,16 @@ beestat.series.indoor_humidity = {
beestat.series.outdoor_humidity = {
'name': 'Outdoor Humidity',
'color': beestat.style.color.green.light
'color': beestat.style.color.bluegreen.base
};
beestat.series.setpoint_heat = {
'name': 'Setpoint',
'name': 'Heat Setpoint',
'color': beestat.style.color.orange.light
};
beestat.series.setpoint_cool = {
'name': 'Setpoint',
'name': 'Cool Setpoint',
'color': beestat.style.color.lightblue.light
};
@ -225,11 +225,6 @@ beestat.series.extreme_outdoor_temperature = {
'color': beestat.style.color.gray.dark
};
beestat.series.setpoint_cool = {
'name': 'Setpoint',
'color': beestat.style.color.lightblue.light
};
beestat.series.calendar_event_home = {
'name': 'Home',
'color': beestat.style.color.green.dark
@ -280,6 +275,11 @@ beestat.series.calendar_event_other = {
'color': beestat.style.color.gray.base
};
beestat.series.calendar_event_custom = {
'name': 'Custom',
'color': beestat.style.color.bluegreen.base
};
// Temperature Profiles
beestat.series.indoor_heat_delta = {
'name': 'Indoor Heat Δ',

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,835 @@
/**
* 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_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_detail_smoothing',
'setting.runtime_detail_range_type',
'setting.runtime_detail_range_dynamic',
'cache.runtime_thermostat'
],
change_function
);
beestat.component.card.apply(this, arguments);
};
beestat.extend(beestat.component.card.runtime_detail, beestat.component.card);
/**
* Decorate
*
* @param {rocket.ELements} parent
*/
beestat.component.card.runtime_detail.prototype.decorate_contents_ = function(parent) {
var self = this;
var data = this.get_data_();
this.chart_ = new beestat.component.chart.runtime_detail(data);
this.chart_.render(parent);
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
var required_begin;
var required_end;
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
required_begin = moment()
.subtract(
beestat.setting('runtime_detail_range_dynamic'),
'day'
)
.second(0);
required_end = moment()
.subtract(1, 'hour')
.second(0);
} else {
required_begin = moment(
beestat.setting('runtime_detail_range_static_begin') + ' 00:00:00'
);
required_end = moment(
beestat.setting('runtime_detail_range_static_end') + ' 23:59:59'
);
}
required_begin = moment.max(
required_begin,
moment(thermostat.first_connected)
);
/**
* 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_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',
{},
'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_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_detail_range_dynamic') !== 1 ||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
) {
beestat.cache.delete('runtime_thermostat');
beestat.setting({
'runtime_detail_range_dynamic': 1,
'runtime_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_detail_range_dynamic') !== 3 ||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
) {
beestat.cache.delete('runtime_thermostat');
beestat.setting({
'runtime_detail_range_dynamic': 3,
'runtime_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_detail_range_dynamic') !== 7 ||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
) {
beestat.cache.delete('runtime_thermostat');
beestat.setting({
'runtime_detail_range_dynamic': 7,
'runtime_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_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_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_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_detail_smoothing', true);
}));
}
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Help')
.set_icon('help_circle')
.set_callback(function() {
(new beestat.component.modal.help_runtime_detail()).render();
}));
};
/**
* Get all of the series data.
*
* @return {object} The series data.
*/
beestat.component.card.runtime_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': {}
};
durations[series_code] = {'seconds': 0};
});
data.metadata.series.calendar_event_name = {};
data.metadata.series.system_mode = {};
/*
* Figure out what date range to use.
* var begin_m = moment()
* .subtract(
* beestat.setting('runtime_detail_range_dynamic'),
* 'day'
* );
* begin_m
* .minute(Math.ceil(begin_m.minute() / 5) * 5)
* .second(0)
* .millisecond(0);
* var end_m = moment();
*/
var begin_m;
var end_m;
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
begin_m = moment().subtract(
beestat.setting('runtime_detail_range_dynamic'),
'day'
);
end_m = moment().subtract(1, 'hour');
} else {
begin_m = moment(
beestat.setting('runtime_detail_range_static_begin') + ' 00:00:00'
);
end_m = moment(
beestat.setting('runtime_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_detail_smoothing') === true) {
moving_count = 15;
} 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.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.active = true;
var indoor_temperature_moving = this.get_average_(moving, 'indoor_temperature');
data.series.indoor_temperature.push(indoor_temperature_moving);
y_min_max(indoor_temperature_moving);
data.metadata.series.indoor_temperature.active = true;
var outdoor_temperature_moving = this.get_average_(moving, 'outdoor_temperature');
data.series.outdoor_temperature.push(outdoor_temperature_moving);
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'
) {
data.series.setpoint_heat.push(
beestat.temperature(runtime_thermostat.setpoint_heat)
);
data.metadata.series.setpoint_heat.active = true;
y_min_max(runtime_thermostat.setpoint_heat);
} else {
data.series.setpoint_heat.push(null);
}
if (
runtime_thermostat.system_mode === 'auto' ||
runtime_thermostat.system_mode === 'cool'
) {
data.series.setpoint_cool.push(
beestat.temperature(runtime_thermostat.setpoint_cool)
);
data.metadata.series.setpoint_cool.active = true;
y_min_max(runtime_thermostat.setpoint_cool);
} 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_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_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_detail.prototype.get_title_ = function() {
return 'Runtime Detail';
};
/**
* Get the subtitle of the card.
*
* @return {string} Subtitle
*/
beestat.component.card.runtime_detail.prototype.get_subtitle_ = function() {
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
var s = (beestat.setting('runtime_detail_range_dynamic') > 1) ? 's' : '';
return 'Past ' +
beestat.setting('runtime_detail_range_dynamic') +
' day' +
s;
}
var begin = moment(beestat.setting('runtime_detail_range_static_begin'))
.format('MMM D, YYYY');
var end = moment(beestat.setting('runtime_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_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)
);
};

View File

@ -50,7 +50,7 @@ beestat.extend(beestat.component.card.runtime_thermostat_summary, beestat.compon
*/
beestat.component.card.runtime_thermostat_summary.prototype.decorate_contents_ = function(parent) {
var data = this.get_data_();
this.chart_ = new beestat.component.chart2.runtime_thermostat_summary(data);
this.chart_ = new beestat.component.chart.runtime_thermostat_summary(data);
this.chart_.render(parent);
var sync_progress = beestat.get_sync_progress(this.thermostat_id_);

View File

@ -18,7 +18,7 @@ beestat.extend(beestat.component.card.temperature_profiles, beestat.component.ca
*/
beestat.component.card.temperature_profiles.prototype.decorate_contents_ = function(parent) {
var data = this.get_data_();
this.chart_ = new beestat.component.chart2.temperature_profiles(data);
this.chart_ = new beestat.component.chart.temperature_profiles(data);
this.chart_.render(parent);
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,420 @@
/**
* Runtime thermostat summary chart.
*
* @param {object} data The chart data.
*/
beestat.component.chart.runtime_detail = function(data) {
this.data_ = data;
beestat.component.chart.apply(this, arguments);
};
beestat.extend(beestat.component.chart.runtime_detail, beestat.component.chart);
/**
* Override for get_options_xAxis_labels_formatter_.
*
* @return {Function} xAxis labels formatter.
*/
beestat.component.chart.runtime_detail.prototype.get_options_xAxis_labels_formatter_ = function() {
var current_day;
var current_hour;
return function() {
var hour = this.value.format('ha');
var day = this.value.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(' ');
};
};
/**
* Override for get_options_series_.
*
* @return {Array} All of the series to display on the chart.
*/
beestat.component.chart.runtime_detail.prototype.get_options_series_ = function() {
var self = this;
var series = [];
// Indoor/Outdoor Temperature
[
'indoor_temperature',
'outdoor_temperature'
].forEach(function(series_code) {
if (self.data_.metadata.series[series_code].active === true) {
series.push({
'name': series_code,
'data': self.data_.series[series_code],
'color': beestat.series[series_code].color,
'yAxis': 0,
'type': 'spline',
'dashStyle': (series_code === 'indoor_temperature') ? 'Solid' : 'ShortDash',
'lineWidth': (series_code === 'indoor_temperature') ? 2 : 1
});
}
});
// Setpoint Heat/Cool
[
'setpoint_heat',
'setpoint_cool'
].forEach(function(series_code) {
if (self.data_.metadata.series[series_code].active === true) {
series.push({
'name': series_code,
'data': self.data_.series[series_code],
'color': beestat.series[series_code].color,
'yAxis': 0,
'type': 'line',
'lineWidth': 1,
'step': 'right',
'className': 'crisp_edges'
});
}
});
// Indoor/Outdoor Humidity
[
'indoor_humidity',
'outdoor_humidity'
].forEach(function(series_code) {
if (self.data_.metadata.series[series_code].active === true) {
series.push({
'name': series_code,
'data': self.data_.series[series_code],
'color': beestat.series[series_code].color,
'yAxis': 1,
'type': 'spline',
'dashStyle': (series_code === 'indoor_humidity') ? 'Solid' : 'ShortDash',
'lineWidth': (series_code === 'indoor_humidity') ? 2 : 1,
'visible': false
});
}
});
// Swimlanes
[
'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',
'compressor_heat_1',
'compressor_heat_2',
'auxiliary_heat_1',
'auxiliary_heat_2',
'compressor_cool_1',
'compressor_cool_2',
'fan',
'humidifier',
'dehumidifier',
'ventilator',
'economizer'
].forEach(function(series_code) {
if (self.data_.metadata.series[series_code].active === true) {
var line_width;
if (
series_code.includes('heat') === true ||
series_code.includes('cool') === true
) {
line_width = 12;
} else {
line_width = 6;
}
series.push({
'name': series_code,
'data': self.data_.series[series_code],
'color': beestat.series[series_code].color,
'yAxis': 2,
'type': 'line',
'lineWidth': line_width,
'linecap': 'square',
'className': 'crisp_edges',
'showInLegend': false
});
}
});
series.push({
'name': '',
'data': self.data_.series.dummy,
'yAxis': 2,
'type': 'line',
'lineWidth': 0,
'showInLegend': false
});
return series;
};
/**
* Override for get_options_yAxis_.
*
* @return {Array} The y-axis options.
*/
beestat.component.chart.runtime_detail.prototype.get_options_yAxis_ = function() {
/**
* Highcharts doesn't seem to respect axis behavior well so just overriding
* it completely here.
*/
var y_min = Math.floor((this.data_.metadata.chart.y_min - 5) / 10) * 10;
var y_max = Math.ceil((this.data_.metadata.chart.y_max + 10) / 10) * 10;
var tick_positions = [];
var tick_interval = (beestat.setting('temperature_unit') === '°F') ? 10 : 5;
var current_tick_position =
Math.floor(y_min / tick_interval) * tick_interval;
while (current_tick_position <= y_max) {
tick_positions.push(current_tick_position);
current_tick_position += tick_interval;
}
return [
// Temperature
{
'height': '80%',
'top': '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 + beestat.setting('temperature_unit');
}
},
'tickPositions': tick_positions
},
// Humidity
{
'height': '80%',
'top': '20%',
'alignTicks': false,
'gridLineColor': null,
'opposite': true,
'title': {'text': null},
'labels': {
'style': {'color': beestat.style.color.gray.base},
'formatter': function() {
return this.value + '%';
}
},
// https://github.com/highcharts/highcharts/issues/3403
'min': 0,
'minRange': 100,
'ceiling': 100
},
// Swimlanes
{
'height': '20%',
'top': '0%',
'min': 0,
'max': 100,
'gridLineWidth': 0,
'title': {'text': null},
'labels': {'enabled': false}
}
];
};
// https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/stock/demo/candlestick-and-volume/
/**
* Override for get_options_tooltip_formatter_.
*
* @return {Function} The tooltip formatter.
*/
beestat.component.chart.runtime_detail.prototype.get_options_tooltip_formatter_ = function() {
var self = this;
return function() {
var sections = [];
var groups = {
'mode': [],
'data': [],
'equipment': []
};
var values = {};
this.points.forEach(function(point) {
values[point.series.name] = point.y;
});
// HVAC Mode
var system_mode;
var system_mode_color;
switch (self.data_.metadata.series.system_mode[this.x.valueOf()]) {
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;
}
if (system_mode !== undefined) {
groups.mode.push({
'label': 'System Mode',
'value': system_mode,
'color': system_mode_color
});
}
this.points.forEach(function(point) {
var label;
var value;
var color;
var group;
if (
point.series.name.includes('temperature') === true ||
point.series.name.includes('setpoint') === true
) {
group = 'data';
label = beestat.series[point.series.name].name;
color = point.series.color;
value = beestat.temperature({
'temperature': values[point.series.name],
'convert': false,
'units': true,
'round': 0
});
} else if (point.series.name.includes('humidity') === true) {
group = 'data';
label = beestat.series[point.series.name].name;
color = point.series.color;
value = Math.round(values[point.series.name]) + '%';
} else if (
point.series.name === 'fan' ||
point.series.name === 'compressor_heat_1' ||
point.series.name === 'auxiliary_heat_1' ||
point.series.name === 'compressor_cool_1' ||
point.series.name === 'dehumidifier' ||
point.series.name === 'economizer' ||
point.series.name === 'humidifier' ||
point.series.name === 'ventilator'
) {
group = 'equipment';
label = beestat.series[point.series.name].name;
color = point.series.color;
value = beestat.time(
self.data_.metadata.series[point.series.name].durations[point.x.valueOf()]
);
} else if (
point.series.name.includes('calendar_event')
) {
group = 'mode';
label = 'Comfort Profile';
color = point.series.color;
value = self.data_.metadata.series.calendar_event_name[point.x.valueOf()];
} else {
return;
}
groups[group].push({
'label': label,
'value': value,
'color': color
});
// Show stage 2 duration on stage 1, if applicable.
if (
point.series.name === 'compressor_heat_1' &&
self.data_.metadata.series.compressor_heat_2.durations[point.x.valueOf()].seconds > 0
) {
groups.equipment.push({
'label': beestat.series.compressor_heat_2.name,
'value': beestat.time(
self.data_.metadata.series.compressor_heat_2.durations[point.x.valueOf()]
),
'color': beestat.series.compressor_heat_2.color
});
}
if (
point.series.name === 'auxiliary_heat_1' &&
self.data_.metadata.series.auxiliary_heat_2.durations[point.x.valueOf()].seconds > 0
) {
groups.equipment.push({
'label': beestat.series.auxiliary_heat_2.name,
'value': beestat.time(
self.data_.metadata.series.auxiliary_heat_2.durations[point.x.valueOf()]
),
'color': beestat.series.auxiliary_heat_2.color
});
}
if (
point.series.name === 'compressor_cool_1' &&
self.data_.metadata.series.compressor_cool_2.durations[point.x.valueOf()].seconds > 0
) {
groups.equipment.push({
'label': beestat.series.compressor_cool_2.name,
'value': beestat.time(
self.data_.metadata.series.compressor_cool_2.durations[point.x.valueOf()]
),
'color': beestat.series.compressor_cool_2.color
});
}
});
if (
groups.mode.length === 0 &&
groups.equipment.length === 0 &&
groups.data.length === 0
) {
groups.mode.push({
'label': 'No data',
'value': '',
'color': beestat.style.color.gray.base
});
}
sections.push(groups.mode);
sections.push(groups.equipment);
sections.push(groups.data);
var title = this.x.format('ddd, MMM D @ h:mma');
return self.tooltip_formatter_helper_(
title,
sections
);
};
};

View File

@ -3,19 +3,19 @@
*
* @param {object} data The chart data.
*/
beestat.component.chart2.runtime_thermostat_summary = function(data) {
beestat.component.chart.runtime_thermostat_summary = function(data) {
this.data_ = data;
beestat.component.chart2.apply(this, arguments);
beestat.component.chart.apply(this, arguments);
};
beestat.extend(beestat.component.chart2.runtime_thermostat_summary, beestat.component.chart2);
beestat.extend(beestat.component.chart.runtime_thermostat_summary, beestat.component.chart);
/**
* Override for get_options_xAxis_labels_formatter_.
*
* @return {Function} xAxis labels formatter.
*/
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_xAxis_labels_formatter_ = function() {
beestat.component.chart.runtime_thermostat_summary.prototype.get_options_xAxis_labels_formatter_ = function() {
var current_month;
var current_year;
@ -71,7 +71,7 @@ beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_xAxis_
*
* @return {Array} All of the series to display on the chart.
*/
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_series_ = function() {
beestat.component.chart.runtime_thermostat_summary.prototype.get_options_series_ = function() {
var self = this;
var series = [];
@ -128,9 +128,7 @@ beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_series
*
* @return {Array} The y-axis options.
*/
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_yAxis_ = function() {
var self = this;
beestat.component.chart.runtime_thermostat_summary.prototype.get_options_yAxis_ = function() {
var y_max_hours;
var tick_interval;
switch (beestat.setting('runtime_thermostat_summary_group_by')) {
@ -199,7 +197,7 @@ beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_yAxis_
*
* @return {Function} The tooltip formatter.
*/
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_tooltip_formatter_ = function() {
beestat.component.chart.runtime_thermostat_summary.prototype.get_options_tooltip_formatter_ = function() {
var self = this;
return function() {

View File

@ -3,19 +3,19 @@
*
* @param {object} data The chart data.
*/
beestat.component.chart2.temperature_profiles = function(data) {
beestat.component.chart.temperature_profiles = function(data) {
this.data_ = data;
beestat.component.chart2.apply(this, arguments);
beestat.component.chart.apply(this, arguments);
};
beestat.extend(beestat.component.chart2.temperature_profiles, beestat.component.chart2);
beestat.extend(beestat.component.chart.temperature_profiles, beestat.component.chart);
/**
* Override for get_options_xAxis_labels_formatter_.
*
* @return {Function} xAxis labels formatter.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_xAxis_labels_formatter_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_xAxis_labels_formatter_ = function() {
return function() {
return this.value + beestat.setting('temperature_unit');
};
@ -26,7 +26,7 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_xAxis_labels
*
* @return {Array} All of the series to display on the chart.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_series_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_series_ = function() {
var series = [];
// Trendline data
@ -124,7 +124,7 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_series_ = fu
*
* @return {Array} The y-axis options.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_yAxis_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_yAxis_ = function() {
var absolute_y_max = Math.max(
Math.abs(this.data_.metadata.chart.y_min),
Math.abs(this.data_.metadata.chart.y_max)
@ -165,7 +165,7 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_yAxis_ = fun
*
* @return {Function} The tooltip formatter.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_tooltip_formatter_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_tooltip_formatter_ = function() {
var self = this;
return function() {
@ -210,7 +210,7 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_tooltip_form
*
* @return {string} The zoom type.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_chart_zoomType_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_chart_zoomType_ = function() {
return null;
};
@ -219,7 +219,7 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_chart_zoomTy
*
* @return {object} The legend options.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_legend_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_legend_ = function() {
return {
'enabled': false
};
@ -230,7 +230,7 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_legend_ = fu
*
* @return {object} The xAxis options.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_xAxis_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_xAxis_ = function() {
return {
'lineWidth': 0,
'tickLength': 0,
@ -273,7 +273,7 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_xAxis_ = fun
*
* @return {number} The height of the chart.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_chart_height_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_chart_height_ = function() {
return 300;
};
@ -282,6 +282,6 @@ beestat.component.chart2.temperature_profiles.prototype.get_options_chart_height
*
* @return {boolean} Whether or not to connect nulls.
*/
beestat.component.chart2.temperature_profiles.prototype.get_options_plotOptions_series_connectNulls_ = function() {
beestat.component.chart.temperature_profiles.prototype.get_options_plotOptions_series_connectNulls_ = function() {
return true;
};

View File

@ -1,476 +0,0 @@
/**
* A chart. Mostly just a wrapper for the Highcharts stuff so the defaults
* don't have to be set every single time.
*/
beestat.component.chart2 = function() {
var self = this;
this.addEventListener('render', function() {
self.chart_.reflow();
});
beestat.component.apply(this, arguments);
};
beestat.extend(beestat.component.chart2, beestat.component);
beestat.component.chart2.prototype.rerender_on_breakpoint_ = false;
/**
* Decorate. Calls all the option getters and renders the chart.
*
* @param {rocket.Elements} parent
*/
beestat.component.chart2.prototype.decorate_ = function(parent) {
var options = {};
options.credits = this.get_options_credits_();
options.exporting = this.get_options_exporting_();
options.chart = this.get_options_chart_();
options.title = this.get_options_title_();
options.subtitle = this.get_options_subtitle_();
options.legend = this.get_options_legend_();
options.plotOptions = this.get_options_plotOptions_();
options.xAxis = this.get_options_xAxis_();
options.yAxis = this.get_options_yAxis_();
options.series = this.get_options_series_();
options.tooltip = this.get_options_tooltip_();
options.chart.renderTo = parent[0];
this.chart_ = Highcharts.chart(options);
};
/**
* Reset the chart zoom level all the way out.
*/
beestat.component.chart2.prototype.reset_zoom = function() {
this.chart_.zoomOut();
};
/**
* Export the chart to a PNG.
*/
beestat.component.chart2.prototype.export = function() {
this.chart_.exportChartLocal();
};
/**
* Get the legend options.
*
* @return {object} The legend options.
*/
beestat.component.chart2.prototype.get_options_legend_ = function() {
return {
'itemStyle': {
'color': '#ecf0f1',
'font-weight': '500'
},
'itemHoverStyle': {
'color': '#bdc3c7'
},
'itemHiddenStyle': {
'color': '#7f8c8d'
},
'labelFormatter': this.get_options_legend_labelFormatter_()
};
};
/**
* Get the legend labelFormatter options.
*
* @return {Function} The legend labelFormatter options.
*/
beestat.component.chart2.prototype.get_options_legend_labelFormatter_ = function() {
return function() {
return beestat.series[this.name].name;
};
};
/**
* Get the plotOptions.
*
* @return {object} The plotOptions.
*/
beestat.component.chart2.prototype.get_options_plotOptions_ = function() {
return {
'series': {
'animation': false,
'marker': {
'enabled': false
},
'states': {
'hover': {
'enabled': false
},
'inactive': {
'opacity': 1
}
},
'connectNulls': this.get_options_plotOptions_series_connectNulls_()
},
'column': {
'pointPadding': 0,
'borderWidth': 0,
'stacking': 'normal',
'dataLabels': {
'enabled': false
}
}
};
};
/**
* Get whether or not to connect nulls.
*
* @return {boolean} Whether or not to connect nulls.
*/
beestat.component.chart2.prototype.get_options_plotOptions_series_connectNulls_ = function() {
return false;
};
/**
* Get the title options.
*
* @return {object} The title options.
*/
beestat.component.chart2.prototype.get_options_title_ = function() {
return {
'text': null
};
};
/**
* Get the subtitle options
*
* @return {object} The subtitle options.
*/
beestat.component.chart2.prototype.get_options_subtitle_ = function() {
return {
'text': null
};
};
/**
* Get the chart options.
*
* @return {object} The chart options.
*/
beestat.component.chart2.prototype.get_options_chart_ = function() {
return {
'style': {
'fontFamily': 'Montserrat'
},
'spacing': [
beestat.style.size.gutter,
0,
0,
0
],
'zoomType': this.get_options_chart_zoomType_(),
'panning': true,
'panKey': 'ctrl',
'backgroundColor': beestat.style.color.bluegray.base,
'resetZoomButton': {
'theme': {
'display': 'none'
}
},
'height': this.get_options_chart_height_()
};
};
/**
* Get the height of the chart.
*
* @return {number} The height of the chart.
*/
beestat.component.chart2.prototype.get_options_chart_height_ = function() {
return null;
};
/**
* Get the zoomType option. Return null for no zoom.
*
* @return {string} The zoom type.
*/
beestat.component.chart2.prototype.get_options_chart_zoomType_ = function() {
return 'x';
};
/**
* Get the export options.
*
* @return {object} The export options.
*/
beestat.component.chart2.prototype.get_options_exporting_ = function() {
return {
'enabled': false,
'sourceWidth': 980,
'scale': 1,
'filename': this.get_options_exporting_filename_(),
'chartOptions': {
'credits': {
'text': 'beestat.io'
},
'title': {
'align': 'left',
'text': this.get_options_exporting_chartOptions_title_text_(),
'margin': beestat.style.size.gutter,
'style': {
'color': '#fff',
'font-weight': beestat.style.font_weight.bold,
'font-size': beestat.style.font_size.large
}
},
'subtitle': {
'align': 'left',
'text': this.get_options_exporting_chartOptions_subtitle_text_(),
'style': {
'color': '#fff',
'font-weight': beestat.style.font_weight.light,
'font-size': beestat.style.font_size.normal
}
},
'chart': {
'style': {
'fontFamily': 'Montserrat, Helvetica, Sans-Serif'
},
'spacing': [
beestat.style.size.gutter,
beestat.style.size.gutter,
beestat.style.size.gutter,
beestat.style.size.gutter
]
}
}
};
};
/**
* Get the exported chart title.
*
* @return {string} The exported chart title.
*/
beestat.component.chart2.prototype.get_options_exporting_chartOptions_title_text_ = function() {
return this.data_.metadata.chart.title;
};
/**
* Get the exported chart subtitle.
*
* @return {string} The exported chart subtitle.
*/
beestat.component.chart2.prototype.get_options_exporting_chartOptions_subtitle_text_ = function() {
return this.data_.metadata.chart.subtitle;
};
/**
* Get the exported chart filename.
*
* @return {string} The exported chart filename.
*/
beestat.component.chart2.prototype.get_options_exporting_filename_ = function() {
var title = this.get_options_exporting_chartOptions_title_text_();
var subtitle = this.get_options_exporting_chartOptions_subtitle_text_();
var filename = [];
if (title !== null) {
filename.push(title);
}
if (subtitle !== null) {
filename.push('-');
filename.push(subtitle);
}
if (filename.length === 0) {
filename.push('beestat');
}
return filename.join(' ');
};
/**
* Get the credits options.
*
* @return {boolean} The credits options.
*/
beestat.component.chart2.prototype.get_options_credits_ = function() {
return false;
};
/**
* Get the xAxis options.
*
* @return {object} The xAxis options.
*/
beestat.component.chart2.prototype.get_options_xAxis_ = function() {
return {
'categories': this.data_.x,
'lineColor': beestat.style.color.bluegray.light,
'tickLength': 0,
'labels': {
'style': {
'color': beestat.style.color.gray.base
},
'formatter': this.get_options_xAxis_labels_formatter_()
}
};
};
/**
* Get the xAxis label formatter options. Needs to be overridden.
*
* @return {object} The xAxis label formatter options.
*/
beestat.component.chart2.prototype.get_options_xAxis_labels_formatter_ = function() {
return null;
};
/**
* Get the yAxis label formatter options. Needs to be overridden.
*
* @return {object} The yAxis label formatter options.
*/
beestat.component.chart2.prototype.get_options_yAxis_ = function() {
return null;
};
/**
* Get the series options. Needs to be overridden.
*
* @return {object} The series options.
*/
beestat.component.chart2.prototype.get_options_series_ = function() {
return null;
};
/**
* Get the tooltip options.
*
* @return {object} The tooltip options.
*/
beestat.component.chart2.prototype.get_options_tooltip_ = function() {
return {
'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': this.get_options_tooltip_positioner_(),
'formatter': this.get_options_tooltip_formatter_()
};
};
/**
* Get the tooltip formatter. Needs to be overridden.
*
* @return {Function} The tooltip formatter.
*/
beestat.component.chart2.prototype.get_options_tooltip_formatter_ = function() {
return null;
};
/**
* Get the tooltip positioner. Makes sure the tooltip is positioned nicely.
*
* @return {Function} The tooltip positioner.
*/
beestat.component.chart2.prototype.get_options_tooltip_positioner_ = function() {
var self = this;
return function(tooltip_width, tooltip_height, point) {
var plot_width = self.chart_.plotWidth;
var fits_on_left = (point.plotX - tooltip_width) > 0;
var fits_on_right = (point.plotX + tooltip_width) < plot_width;
var x;
var y = 60;
if (fits_on_left === true) {
x = point.plotX - tooltip_width + self.chart_.plotLeft;
} else if (fits_on_right === true) {
x = point.plotX + self.chart_.plotLeft;
} else {
x = self.chart_.plotLeft;
}
return {
'x': x,
'y': y
};
};
};
/**
* Get the HTML needed to render a tooltip.
*
* @param {string} title The tooltip title.
* @param {array} sections Data inside the tooltip.
*
* @return {string} The tooltip HTML.
*/
beestat.component.chart2.prototype.tooltip_formatter_helper_ = function(title, sections) {
var tooltip = $.createElement('div')
.style({
'background-color': beestat.style.color.bluegray.dark,
'padding': beestat.style.size.gutter / 2
});
var title_div = $.createElement('div')
.style({
'font-weight': beestat.style.font_weight.bold,
'font-size': beestat.style.font_size.large,
'margin-bottom': beestat.style.size.gutter / 4,
'color': beestat.style.color.gray.light
})
.innerText(title);
tooltip.appendChild(title_div);
var table = $.createElement('table')
.setAttribute({
'cellpadding': '0',
'cellspacing': '0'
});
tooltip.appendChild(table);
sections.forEach(function(section, i) {
if (section.length > 0) {
section.forEach(function(item) {
var tr = $.createElement('tr').style('color', item.color);
table.appendChild(tr);
var td_label = $.createElement('td')
.style({
'font-weight': beestat.style.font_weight.bold
})
.innerText(item.label);
tr.appendChild(td_label);
var td_value = $.createElement('td').innerText(item.value)
.style({
'padding-left': beestat.style.size.gutter / 4
});
tr.appendChild(td_value);
});
if (i < sections.length) {
var spacer_tr = $.createElement('tr');
table.appendChild(spacer_tr);
var spacer_td = $.createElement('td')
.style('padding-bottom', beestat.style.size.gutter / 4);
spacer_tr.appendChild(spacer_td);
}
}
});
return tooltip[0].outerHTML;
};

View File

@ -1,29 +1,29 @@
/**
* Help for the recent activity card.
*/
beestat.component.modal.help_recent_activity = function() {
beestat.component.modal.apply(this, arguments);
};
beestat.extend(beestat.component.modal.help_recent_activity, beestat.component.modal);
beestat.component.modal.help_recent_activity.prototype.decorate_contents_ = function(parent) {
parent.appendChild($.createElement('p').innerHTML('View up to the past 7 days of thermostat activity in 5-minute resolution. This can help you visualize daily runtime trends and identify acute system issues. Compare to the Home IQ System & Follow Me charts.'));
var table = $.createElement('table');
table.style('color', beestat.style.color.blue.base);
parent.appendChild(table);
var tr;
var td;
tr = $.createElement('tr');
table.appendChild(tr);
td = $.createElement('td');
td.setAttribute('valign', 'top');
tr.appendChild(td);
};
beestat.component.modal.help_recent_activity.prototype.get_title_ = function() {
return 'Recent Activity - Help';
};
/**
* Help for the runtime detail card.
*/
beestat.component.modal.help_runtime_detail = function() {
beestat.component.modal.apply(this, arguments);
};
beestat.extend(beestat.component.modal.help_runtime_detail, beestat.component.modal);
beestat.component.modal.help_runtime_detail.prototype.decorate_contents_ = function(parent) {
parent.appendChild($.createElement('p').innerHTML('View up to the past 7 days of thermostat activity in 5-minute resolution. This can help you visualize daily runtime trends and identify acute system issues. Compare to the Home IQ System & Follow Me charts.'));
var table = $.createElement('table');
table.style('color', beestat.style.color.blue.base);
parent.appendChild(table);
var tr;
var td;
tr = $.createElement('tr');
table.appendChild(tr);
td = $.createElement('td');
td.setAttribute('valign', 'top');
tr.appendChild(td);
};
beestat.component.modal.help_runtime_detail.prototype.get_title_ = function() {
return 'Runtime Detail - Help';
};

View File

@ -0,0 +1,373 @@
/**
* Custom date range for the Runtime Detail chart.
*/
beestat.component.modal.runtime_detail_custom = function() {
beestat.component.modal.apply(this, arguments);
this.state_.runtime_detail_range_type = beestat.setting('runtime_detail_range_type');
this.state_.runtime_detail_range_dynamic = beestat.setting('runtime_detail_range_dynamic');
this.state_.runtime_detail_range_static_begin = beestat.setting('runtime_detail_range_static_begin');
this.state_.runtime_detail_range_static_end = beestat.setting('runtime_detail_range_static_end');
this.state_.error = {
'max_range': false,
'invalid_range_begin': false,
'invalid_range_end': false,
'out_of_sync_range': false
};
};
beestat.extend(beestat.component.modal.runtime_detail_custom, beestat.component.modal);
/**
* Decorate.
*
* @param {rocket.Elements} parent
*/
beestat.component.modal.runtime_detail_custom.prototype.decorate_contents_ = function(parent) {
parent.appendChild($.createElement('p').innerHTML('Choose a custom range to display on the Runtime Detail chart.'));
this.decorate_range_type_(parent);
if (this.state_.runtime_detail_range_type === 'dynamic') {
this.decorate_range_dynamic_(parent);
} else {
this.decorate_range_static_(parent);
}
this.decorate_error_(parent);
};
/**
* Decorate the range type selector.
*
* @param {rocket.Elements} parent
*/
beestat.component.modal.runtime_detail_custom.prototype.decorate_range_type_ = function(parent) {
var self = this;
var button_group = new beestat.component.button_group();
button_group.add_button(new beestat.component.button()
.set_background_hover_color(beestat.style.color.lightblue.base)
.set_text_color('#fff')
.set_background_color(
this.state_.runtime_detail_range_type === 'dynamic'
? beestat.style.color.lightblue.base
: beestat.style.color.bluegray.base
)
.set_text('Dynamic')
.addEventListener('click', function() {
self.state_.runtime_detail_range_type = 'dynamic';
self.rerender();
}));
button_group.add_button(new beestat.component.button()
.set_background_hover_color(beestat.style.color.lightblue.base)
.set_text_color('#fff')
.set_background_color(
this.state_.runtime_detail_range_type === 'static'
? beestat.style.color.lightblue.base
: beestat.style.color.bluegray.base
)
.set_text('Static')
.addEventListener('click', function() {
self.state_.runtime_detail_range_type = 'static';
self.rerender();
}));
(new beestat.component.title('Range Type')).render(parent);
var row = $.createElement('div').addClass('row');
parent.appendChild(row);
var column = $.createElement('div').addClass(['column column_12']);
row.appendChild(column);
button_group.render(column);
};
/**
* Decorate the static range inputs.
*
* @param {rocket.Elements} parent
*/
beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ = function(parent) {
var self = this;
var runtime_detail_static_range_begin;
var runtime_detail_static_range_end;
/**
* Check whether or not a value is outside of where data is synced.
*/
var check_out_of_sync_range = function() {
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
var min = moment.max(
moment(thermostat.sync_begin),
moment().subtract(1, 'year')
);
var max = moment(thermostat.sync_end);
var begin = moment.min(
moment(runtime_detail_static_range_begin.get_value()),
moment(runtime_detail_static_range_end.get_value())
);
var end = moment.max(
moment(runtime_detail_static_range_begin.get_value() + ' 00:00:00'),
moment(runtime_detail_static_range_end.get_value() + ' 23:59:59')
);
if (
begin.isBefore(min) === true ||
end.isAfter(max) === true
) {
self.state_.error.out_of_sync_range = true;
} else {
self.state_.error.out_of_sync_range = false;
}
};
runtime_detail_static_range_begin = new beestat.component.input.text()
.set_style({
'width': 110,
'text-align': 'center',
'border-bottom': '2px solid ' + beestat.style.color.lightblue.base
})
.set_attribute({
'maxlength': 10
})
.set_icon('calendar')
.set_value(this.state_.runtime_detail_range_static_begin);
runtime_detail_static_range_begin.addEventListener('blur', function() {
var m = moment(this.get_value());
if (m.isValid() === true) {
self.state_.error.invalid_range_begin = false;
var value = m.format('M/D/YYYY');
var diff = Math.abs(m.diff(moment(runtime_detail_static_range_end.get_value()), 'day')) + 1;
if (diff > 30) {
self.state_.error.max_range = true;
} else {
self.state_.error.max_range = false;
}
check_out_of_sync_range();
self.state_.runtime_detail_range_static_begin = value;
self.rerender();
} else {
self.state_.runtime_detail_range_static_begin = this.get_value();
self.state_.error.invalid_range_begin = true;
self.rerender();
}
});
runtime_detail_static_range_end = new beestat.component.input.text()
.set_style({
'width': 110,
'text-align': 'center',
'border-bottom': '2px solid ' + beestat.style.color.lightblue.base
})
.set_attribute({
'maxlength': 10
})
.set_icon('calendar')
.set_value(this.state_.runtime_detail_range_static_end);
runtime_detail_static_range_end.addEventListener('blur', function() {
var m = moment(this.get_value());
if (m.isValid() === true) {
self.state_.error.invalid_range_end = false;
var value = m.format('M/D/YYYY');
var diff = Math.abs(m.diff(moment(runtime_detail_static_range_begin.get_value()), 'day')) + 1;
if (diff > 30) {
self.state_.error.max_range = true;
} else {
self.state_.error.max_range = false;
}
check_out_of_sync_range();
self.state_.runtime_detail_range_static_end = value;
self.rerender();
} else {
self.state_.runtime_detail_range_static_end = this.get_value();
self.state_.error.invalid_range_end = true;
self.rerender();
}
});
var span;
var row = $.createElement('div').addClass('row');
parent.appendChild(row);
var column = $.createElement('div').addClass(['column column_12']);
row.appendChild(column);
span = $.createElement('span').style('display', 'inline-block');
runtime_detail_static_range_begin.render(span);
column.appendChild(span);
span = $.createElement('span')
.style({
'display': 'inline-block',
'margin-left': beestat.style.size.gutter,
'margin-right': beestat.style.size.gutter
})
.innerText('to');
column.appendChild(span);
span = $.createElement('span').style('display', 'inline-block');
runtime_detail_static_range_end.render(span);
column.appendChild(span);
};
/**
* Decorate the dynamic range input.
*
* @param {rocket.Elements} parent
*/
beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_ = function(parent) {
var self = this;
var runtime_detail_range_dynamic = new beestat.component.input.text()
.set_style({
'width': 75,
'text-align': 'center',
'border-bottom': '2px solid ' + beestat.style.color.lightblue.base
})
.set_attribute({
'maxlength': 2
})
.set_icon('pound')
.set_value(beestat.setting('runtime_detail_range_dynamic'));
runtime_detail_range_dynamic.addEventListener('blur', function() {
var value = parseInt(this.get_value(), 10);
if (isNaN(value) === true || value === 0) {
value = 1;
} else if (value > 30) {
value = 30;
}
this.set_value(value);
self.state_.runtime_detail_range_dynamic = value;
});
var span;
var row = $.createElement('div').addClass('row');
parent.appendChild(row);
var column = $.createElement('div').addClass(['column column_12']);
row.appendChild(column);
span = $.createElement('span').style('display', 'inline-block');
runtime_detail_range_dynamic.render(span);
column.appendChild(span);
span = $.createElement('span')
.style({
'display': 'inline-block',
'margin-left': beestat.style.size.gutter
})
.innerText('days');
column.appendChild(span);
};
/**
* Decorate the error area.
*
* @param {rocket.Elements} parent
*/
beestat.component.modal.runtime_detail_custom.prototype.decorate_error_ = function(parent) {
var div = $.createElement('div').style('color', beestat.style.color.red.base);
if (this.state_.error.max_range === true) {
div.appendChild($.createElement('div').innerText('Max range is 30 days.'));
}
if (this.state_.error.invalid_range_begin === true) {
div.appendChild($.createElement('div').innerText('Invalid begin date.'));
}
if (this.state_.error.invalid_range_end === true) {
div.appendChild($.createElement('div').innerText('Invalid end date.'));
}
if (this.state_.error.out_of_sync_range === true) {
div.appendChild($.createElement('div').innerText('Detail not available for this range.'));
}
parent.appendChild(div);
};
/**
* Get title.
*
* @return {string} Title
*/
beestat.component.modal.runtime_detail_custom.prototype.get_title_ = function() {
return 'Runtime Detail - Custom Range';
};
/**
* Get the buttons that go on the bottom of this modal.
*
* @return {[beestat.component.button]} The buttons.
*/
beestat.component.modal.runtime_detail_custom.prototype.get_buttons_ = function() {
var self = this;
var cancel = new beestat.component.button()
.set_background_color('#fff')
.set_text_color(beestat.style.color.gray.base)
.set_text_hover_color(beestat.style.color.red.base)
.set_text('Cancel')
.addEventListener('click', function() {
self.dispose();
});
var save;
if (
this.state_.error.max_range === true ||
this.state_.error.invalid_range_begin === true ||
this.state_.error.invalid_range_end === true ||
this.state_.error.out_of_sync_range === true
) {
save = new beestat.component.button()
.set_background_color(beestat.style.color.gray.base)
.set_text_color('#fff')
.set_text('Save');
} else {
save = new beestat.component.button()
.set_background_color(beestat.style.color.green.base)
.set_background_hover_color(beestat.style.color.green.light)
.set_text_color('#fff')
.set_text('Save')
.addEventListener('click', function() {
this
.set_background_color(beestat.style.color.gray.base)
.set_background_hover_color()
.removeEventListener('click');
if (moment(self.state_.runtime_detail_range_static_begin).isAfter(moment(self.state_.runtime_detail_range_static_end)) === true) {
var temp = self.state_.runtime_detail_range_static_begin;
self.state_.runtime_detail_range_static_begin = self.state_.runtime_detail_range_static_end;
self.state_.runtime_detail_range_static_end = temp;
}
beestat.cache.delete('runtime_thermostat');
beestat.setting(
{
'runtime_detail_range_type': self.state_.runtime_detail_range_type,
'runtime_detail_range_dynamic': self.state_.runtime_detail_range_dynamic,
'runtime_detail_range_static_begin': self.state_.runtime_detail_range_static_begin,
'runtime_detail_range_static_end': self.state_.runtime_detail_range_static_end
},
undefined,
function() {
self.dispose();
}
);
});
}
return [
cancel,
save
];
};

View File

@ -50,7 +50,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/card/footer.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/my_home.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/patreon.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/recent_activity.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/runtime_detail.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/score.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/score/cool.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/score/heat.js"></script>' . PHP_EOL;
@ -59,9 +59,9 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/card/system.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/temperature_profiles.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart2.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/runtime_thermostat_summary.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/temperature_profiles.js"></script>' . PHP_EOL;
echo '<script src="/js/component/chart/runtime_detail.js"></script>' . PHP_EOL;
echo '<script src="/js/component/header.js"></script>' . PHP_EOL;
echo '<script src="/js/component/icon.js"></script>' . PHP_EOL;
echo '<script src="/js/component/layout.js"></script>' . PHP_EOL;
@ -81,12 +81,13 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/modal/help_alerts.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_home_efficiency.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_my_home.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_recent_activity.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_runtime_detail.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_sensors.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_system.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_comparison_settings.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_temperature_profiles.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/patreon_hide.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/runtime_detail_custom.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/thermostat_info.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/help_score.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/weather.js"></script>' . PHP_EOL;

View File

@ -79,7 +79,9 @@ beestat.layer.dashboard.prototype.decorate_ = function(parent) {
cards.push([
{
'card': new beestat.component.card.recent_activity(),
'card': new beestat.component.card.runtime_detail(
beestat.setting('thermostat_id')
),
'size': 12
}
]);

View File

@ -128,6 +128,7 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
);
api.set_callback(function(response) {
beestat.cache.set('user', response.user);
// Rollbar isn't defined on dev.
@ -152,7 +153,6 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
beestat.cache.set('ecobee_sensor', response.ecobee_sensor);
beestat.cache.set('address', response.address);
beestat.cache.set('announcement', response.announcement);
beestat.cache.set('runtime_thermostat', []);
beestat.cache.set('runtime_thermostat_summary', response.runtime_thermostat_summary);
// Set the active thermostat_id if this is your first time visiting.
@ -241,6 +241,7 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
) {
(new beestat.component.modal.announcements()).render();
}
});
api.send();