diff --git a/js/beestat/runtime_sensor.js b/js/beestat/runtime_sensor.js
new file mode 100644
index 0000000..992275d
--- /dev/null
+++ b/js/beestat/runtime_sensor.js
@@ -0,0 +1,244 @@
+beestat.runtime_sensor = {};
+
+/**
+ * Get a bunch of data for the current runtime_sensor rows. Includes basically
+ * everything you need to make a cool chart.
+ *
+ * @param {number} thermostat_id The thermostat_id to get data for.
+ * @param {object} range Range settings.
+ *
+ * @return {object} The data.
+ */
+beestat.runtime_sensor.get_data = function(thermostat_id, range) {
+ var data = {
+ 'x': [],
+ 'series': {},
+ 'metadata': {
+ 'series': {},
+ 'chart': {
+ 'y_min': Infinity,
+ 'y_max': -Infinity,
+ 'sensors': null
+ }
+ }
+ };
+
+ // A couple private helper functions for manipulating the min/max y values.
+ var y_min_max = function(value) {
+ if (value !== null) {
+ data.metadata.chart.y_min = Math.min(data.metadata.chart.y_min, value);
+ data.metadata.chart.y_max = Math.max(data.metadata.chart.y_max, value);
+ }
+ };
+
+ // Duration objects. These are passed by reference into the metadata.
+ var durations = {};
+
+ var series_codes = [];
+
+ // Get and sort all the sensors.
+ var sensors = beestat.sensor.get_sorted();
+ data.metadata.sensors = sensors;
+
+ // Set up the series_codes.
+ sensors.forEach(function(sensor) {
+ if (sensor.thermostat_id === thermostat_id) {
+ series_codes.push('temperature_' + sensor.sensor_id);
+ series_codes.push('occupancy_' + sensor.sensor_id);
+ }
+ });
+
+ series_codes.push('dummy');
+
+ // Initialize a bunch of stuff.
+ var sequential = {};
+ series_codes.forEach(function(series_code) {
+ sequential[series_code] = 0;
+
+ data.series[series_code] = [];
+ data.metadata.series[series_code] = {
+ 'active': false,
+ 'durations': {},
+ 'data': {}
+ };
+ if (series_code === 'dummy') {
+ data.metadata.series[series_code].name = null;
+ } else {
+ var sensor_id = series_code.replace(/[^0-9]/g, '');
+ data.metadata.series[series_code].name = beestat.cache.sensor[sensor_id].name;
+ }
+
+ durations[series_code] = {'seconds': 0};
+ });
+
+ var begin_m;
+ var end_m;
+ if (range.type === 'dynamic') {
+ begin_m = moment().subtract(
+ range.dynamic,
+ 'day'
+ );
+ end_m = moment().subtract(1, 'hour');
+ } else {
+ begin_m = moment(
+ range.static_begin + ' 00:00:00'
+ );
+ end_m = moment(
+ range.static_end + ' 23:59:59'
+ );
+ }
+
+ // TODO: This needs to be max of begin and when I actually have sensor data
+ var thermostat = beestat.cache.thermostat[thermostat_id];
+ begin_m = moment.max(
+ begin_m,
+ moment(thermostat.first_connected)
+ );
+
+ begin_m
+ .minute(Math.ceil(begin_m.minute() / 5) * 5)
+ .second(0)
+ .millisecond(0);
+
+ var runtime_sensors = beestat.runtime_sensor.get_runtime_sensors_by_date_();
+
+ // Initialize moving average.
+ var moving = [];
+ var moving_count;
+ if (beestat.setting('runtime_sensor_detail_smoothing') === true) {
+ moving_count = 5;
+ } else {
+ moving_count = 1;
+ }
+ var offset;
+ for (var i = 0; i < moving_count; i++) {
+ offset = (i - Math.floor(moving_count / 2)) * 300000;
+ moving.push(runtime_sensors[begin_m.valueOf() + offset]);
+ }
+
+ // Loop.
+ var current_m = begin_m;
+ while (
+ // beestat.cache.runtime_sensor.length > 0 &&
+ current_m.isSameOrAfter(end_m) === false
+ ) {
+ data.x.push(current_m.clone());
+
+ // Without this series the chart will jump to the nearest value if there is a chunk of missing data.
+ data.series.dummy.push(70);
+ data.metadata.series.dummy.active = true;
+
+ if (runtime_sensors[current_m.valueOf()] !== undefined) {
+ sensors.forEach(function(sensor, j) {
+ var runtime_sensor = runtime_sensors[current_m.valueOf()][sensor.sensor_id];
+ if (runtime_sensor === undefined) {
+ data.series['temperature_' + sensor.sensor_id].push(null);
+ data.series['occupancy_' + sensor.sensor_id].push(null);
+ return;
+ }
+
+ var temperature_moving = beestat.temperature(
+ beestat.runtime_sensor.get_average_(moving, sensor.sensor_id)
+ );
+ data.series['temperature_' + runtime_sensor.sensor_id].push(temperature_moving);
+ y_min_max(temperature_moving);
+ data.metadata.series['temperature_' + runtime_sensor.sensor_id].active = true;
+
+ if (runtime_sensor.occupancy === true) {
+ let swimlane_properties =
+ beestat.component.chart.runtime_sensor_detail_occupancy.get_swimlane_properties(
+ sensors.length,
+ j
+ );
+
+ sequential['occupancy_' + runtime_sensor.sensor_id]++;
+ data.series['occupancy_' + runtime_sensor.sensor_id].push(swimlane_properties.y);
+ data.metadata.series['occupancy_' + runtime_sensor.sensor_id].data[current_m.valueOf()] = swimlane_properties.y;
+ } else {
+ if (sequential['occupancy_' + runtime_sensor.sensor_id] > 0) {
+ let swimlane_properties =
+ beestat.component.chart.runtime_sensor_detail_occupancy.get_swimlane_properties(
+ sensors.length,
+ j
+ );
+
+ data.series['occupancy_' + runtime_sensor.sensor_id].push(swimlane_properties.y);
+ data.metadata.series['occupancy_' + runtime_sensor.sensor_id].data[current_m.valueOf()] = swimlane_properties.y;
+ } else {
+ data.series['occupancy_' + runtime_sensor.sensor_id].push(null);
+ }
+ sequential['occupancy_' + runtime_sensor.sensor_id] = 0;
+ }
+ });
+ } else {
+ sensors.forEach(function(sensor) {
+ if (sensor.thermostat_id === thermostat_id) {
+ data.series['temperature_' + sensor.sensor_id].push(null);
+ data.series['occupancy_' + sensor.sensor_id].push(null);
+ }
+ });
+ }
+
+ current_m.add(5, 'minute');
+
+ /**
+ * Remove the first row in the moving average and add the next one. Yes
+ * this could introduce undefined values; that's ok. Those are handled in
+ * the get_average_ function.
+ */
+ moving.shift();
+ moving.push(runtime_sensors[current_m.valueOf() + offset]);
+ }
+
+ return data;
+};
+
+/**
+ * Get all the runtime_sensor rows indexed by date.
+ *
+ * @return {array} The runtime_sensor rows.
+ */
+beestat.runtime_sensor.get_runtime_sensors_by_date_ = function() {
+ var runtime_sensors = {};
+ if (beestat.cache.runtime_sensor !== undefined) {
+ beestat.cache.runtime_sensor.forEach(function(runtime_sensor) {
+ var timestamp = [moment(runtime_sensor.timestamp).valueOf()];
+ if (runtime_sensors[timestamp] === undefined) {
+ runtime_sensors[timestamp] = {};
+ }
+ runtime_sensors[timestamp][runtime_sensor.sensor_id] = runtime_sensor;
+ });
+ }
+ return runtime_sensors;
+};
+
+/**
+ * Given an array of runtime_sensors, get the average value of one of the
+ * keys. Allows and ignores undefined values in order to keep a more accurate
+ * moving average.
+ *
+ * @param {array} runtime_sensors
+ * @param {string} sensor_id The index in the sub-array
+ *
+ * @return {number} The average.
+ */
+beestat.runtime_sensor.get_average_ = function(runtime_sensors, sensor_id) {
+ var average = 0;
+ var count = 0;
+ for (var i = 0; i < runtime_sensors.length; i++) {
+ if (
+ runtime_sensors[i] !== undefined &&
+ runtime_sensors[i][sensor_id] !== undefined &&
+ runtime_sensors[i][sensor_id].temperature !== null
+ ) {
+ average += runtime_sensors[i][sensor_id].temperature;
+ count++;
+ }
+ }
+
+ if (count === 0) {
+ return null;
+ }
+
+ return average / count;
+};
diff --git a/js/beestat/runtime_thermostat.js b/js/beestat/runtime_thermostat.js
new file mode 100644
index 0000000..50005f1
--- /dev/null
+++ b/js/beestat/runtime_thermostat.js
@@ -0,0 +1,560 @@
+beestat.runtime_thermostat = {};
+
+/**
+ * Get a bunch of data for the current runtime_thermostat rows. Includes
+ * basically everything you need to make a cool chart.
+ *
+ * @param {number} thermostat_id The thermostat_id to get data for.
+ * @param {object} range Range settings.
+ *
+ * @return {object} The data.
+ */
+beestat.runtime_thermostat.get_data = function(thermostat_id, range) {
+ var data = {
+ 'x': [],
+ 'series': {},
+ 'metadata': {
+ 'series': {},
+ 'chart': {
+ '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': 3,
+ 'calendar_event_home': 3,
+ 'calendar_event_away': 3,
+ 'calendar_event_sleep': 3,
+ 'calendar_event_smarthome': 3,
+ 'calendar_event_smartaway': 3,
+ 'calendar_event_hold': 3,
+ 'calendar_event_vacation': 3,
+ 'calendar_event_quicksave': 3,
+ 'calendar_event_other': 3,
+ 'calendar_event_custom': 3,
+ 'compressor_heat_1': 16,
+ 'compressor_heat_2': 16,
+ 'auxiliary_heat_1': 16,
+ 'auxiliary_heat_2': 16,
+ 'compressor_cool_1': 16,
+ 'compressor_cool_2': 16,
+ 'fan': 29,
+ 'humidifier': 39,
+ 'dehumidifier': 39,
+ 'ventilator': 39,
+ 'economizer': 39
+ };
+
+ // 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',
+ 'compressor_heat_1',
+ 'compressor_heat_2',
+ 'auxiliary_heat_1',
+ 'auxiliary_heat_2',
+ 'compressor_cool_1',
+ 'compressor_cool_2',
+ 'fan',
+ 'humidifier',
+ 'dehumidifier',
+ 'ventilator',
+ 'economizer',
+ 'dummy'
+ ].forEach(function(series_code) {
+ data.series[series_code] = [];
+ data.metadata.series[series_code] = {
+ 'active': false,
+ 'durations': {},
+
+ /**
+ * Note to future self: This can be used for all series. Need to
+ * populate the raw data points for each series here. The tooltip should
+ * get data from here and not the chart points array. Then the series
+ * data can be whatever is necessary to produce a performance-optimized
+ * chart as long as there is one series (dummy) that has a point at
+ * every x-value. That will allow a smooth tooltip, lightweight lines,
+ * and accurate data.
+ */
+ 'data': {}
+ };
+ durations[series_code] = {'seconds': 0};
+ });
+
+ data.metadata.series.calendar_event_name = {};
+ data.metadata.series.system_mode = {};
+
+ var begin_m;
+ var end_m;
+ if (range.type === 'dynamic') {
+ begin_m = moment().subtract(
+ range.dynamic,
+ 'day'
+ );
+ end_m = moment().subtract(1, 'hour');
+ } else {
+ begin_m = moment(
+ range.static_begin + ' 00:00:00'
+ );
+ end_m = moment(
+ range.static_end + ' 23:59:59'
+ );
+ }
+
+ var thermostat = beestat.cache.thermostat[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 = beestat.runtime_thermostat.get_runtime_thermostats_by_date_();
+
+ // Initialize moving average.
+ var moving = [];
+ var moving_count;
+ if (beestat.setting('runtime_thermostat_detail_smoothing') === true) {
+ moving_count = 10;
+ } else {
+ moving_count = 1;
+ }
+ var offset;
+ for (var i = 0; i < moving_count; i++) {
+ offset = (i - Math.floor(moving_count / 2)) * 300000;
+ moving.push(runtime_thermostats[begin_m.valueOf() + offset]);
+ }
+
+ // Loop.
+ var current_m = begin_m;
+ while (
+ 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(70);
+ 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 = beestat.runtime_thermostat.get_average_(moving, 'indoor_humidity');
+ data.series.indoor_humidity.push(indoor_humidity_moving);
+ data.metadata.series.indoor_humidity.data[current_m.valueOf()] =
+ runtime_thermostat.indoor_humidity;
+ data.metadata.series.indoor_humidity.active = true;
+
+ var outdoor_humidity_moving = beestat.runtime_thermostat.get_average_(moving, 'outdoor_humidity');
+ data.series.outdoor_humidity.push(outdoor_humidity_moving);
+ data.metadata.series.outdoor_humidity.data[current_m.valueOf()] =
+ runtime_thermostat.outdoor_humidity;
+ data.metadata.series.outdoor_humidity.active = true;
+
+ var indoor_temperature_moving = beestat.temperature(
+ beestat.runtime_thermostat.get_average_(moving, 'indoor_temperature')
+ );
+ data.series.indoor_temperature.push(indoor_temperature_moving);
+ data.metadata.series.indoor_temperature.data[current_m.valueOf()] =
+ beestat.temperature(runtime_thermostat.indoor_temperature);
+ y_min_max(indoor_temperature_moving);
+ data.metadata.series.indoor_temperature.active = true;
+
+ var outdoor_temperature_moving = beestat.temperature(
+ beestat.runtime_thermostat.get_average_(moving, 'outdoor_temperature')
+ );
+ data.series.outdoor_temperature.push(outdoor_temperature_moving);
+ data.metadata.series.outdoor_temperature.data[current_m.valueOf()] =
+ beestat.temperature(runtime_thermostat.outdoor_temperature);
+ y_min_max(outdoor_temperature_moving);
+ data.metadata.series.outdoor_temperature.active = true;
+
+ /**
+ * Add setpoints, but only when relevant. For example: Only show the
+ * heat setpoint line when the heat is actually on.
+ */
+ if (
+ runtime_thermostat.system_mode === 'auto' ||
+ runtime_thermostat.system_mode === 'heat' ||
+ runtime_thermostat.system_mode === 'auxiliary_heat'
+ ) {
+ var setpoint_heat = beestat.temperature(
+ runtime_thermostat.setpoint_heat
+ );
+ data.series.setpoint_heat.push(setpoint_heat);
+ y_min_max(setpoint_heat);
+
+ data.metadata.series.setpoint_heat.active = true;
+
+ } else {
+ data.series.setpoint_heat.push(null);
+ }
+
+ if (
+ runtime_thermostat.system_mode === 'auto' ||
+ runtime_thermostat.system_mode === 'cool'
+ ) {
+ var setpoint_cool = beestat.temperature(
+ runtime_thermostat.setpoint_cool
+ );
+ data.series.setpoint_cool.push(setpoint_cool);
+ y_min_max(setpoint_cool);
+
+ data.metadata.series.setpoint_cool.active = true;
+
+ } else {
+ data.series.setpoint_cool.push(null);
+ }
+
+ /*
+ * HVAC Mode. This isn't graphed but it's available for the tooltip.
+ * series.system_mode.chart_data.push([x, runtime_thermostat.system_mode]);
+ */
+ data.metadata.series.system_mode[current_m.valueOf()] = runtime_thermostat.system_mode;
+
+ /*
+ * Thanks, ecobee...I more or less copied this code from the ecobee Follow
+ * Me graph to make sure it's accurate.
+ */
+ var this_calendar_event;
+ var this_calendar_event_name;
+
+ if (runtime_thermostat.event === null) {
+ if (runtime_thermostat.climate === null) {
+ this_calendar_event = 'calendar_event_other';
+ this_calendar_event_name = 'Other';
+ } else {
+ switch (runtime_thermostat.climate.toLowerCase()) {
+ case 'home':
+ case 'sleep':
+ case 'away':
+ this_calendar_event = 'calendar_event_' + runtime_thermostat.climate.toLowerCase();
+ this_calendar_event_name = runtime_thermostat.climate;
+ break;
+ default:
+ this_calendar_event = 'calendar_event_custom';
+ this_calendar_event_name = runtime_thermostat.climate;
+ break;
+ }
+ }
+ } else if (runtime_thermostat.event.match(/SmartRecovery/i) !== null) {
+ this_calendar_event = 'calendar_event_smartrecovery';
+ this_calendar_event_name = 'Smart Recovery';
+ } else if (runtime_thermostat.event.match(/^home$/i) !== null) {
+ this_calendar_event = 'calendar_event_home';
+ this_calendar_event_name = 'Home';
+ } else if (runtime_thermostat.event.match(/^away$/i) !== null) {
+ this_calendar_event = 'calendar_event_away';
+ this_calendar_event_name = 'Away';
+ } else if (runtime_thermostat.event.match(/^smarthome$/i) !== null) {
+ this_calendar_event = 'calendar_event_smarthome';
+ this_calendar_event_name = 'Smart Home';
+ } else if (runtime_thermostat.event.match(/^smartaway$/i) !== null) {
+ this_calendar_event = 'calendar_event_smartaway';
+ this_calendar_event_name = 'Smart Away';
+ } else if (
+ runtime_thermostat.event.match(/^auto$/i) !== null ||
+ runtime_thermostat.event.match(/^today$/i) !== null ||
+ runtime_thermostat.event.match(/^hold$/i) !== null
+ ) {
+ this_calendar_event = 'calendar_event_hold';
+ this_calendar_event_name = 'Hold';
+ } else if (
+ runtime_thermostat.event.match(/^vacation$/i) !== null ||
+ runtime_thermostat.event.match(/(\S\S\S\s\d+\s\d\d\d\d)|(\d{12})/i) !== null
+ ) {
+ this_calendar_event = 'calendar_event_vacation';
+ this_calendar_event_name = 'Vacation';
+ } else if (runtime_thermostat.event.match(/^quicksave$/i) !== null) {
+ this_calendar_event = 'calendar_event_quicksave';
+ this_calendar_event_name = 'Quick Save';
+ } else {
+ this_calendar_event = 'calendar_event_other';
+ this_calendar_event_name = 'Other';
+ }
+
+ [
+ 'calendar_event_smartrecovery',
+ 'calendar_event_home',
+ 'calendar_event_away',
+ 'calendar_event_sleep',
+ 'calendar_event_smarthome',
+ 'calendar_event_smartaway',
+ 'calendar_event_hold',
+ 'calendar_event_vacation',
+ 'calendar_event_quicksave',
+ 'calendar_event_other',
+ 'calendar_event_custom'
+ ].forEach(function(calendar_event) {
+ if (calendar_event === this_calendar_event) {
+ data.series[calendar_event].push(equipment_y[calendar_event]);
+ data.metadata.series[calendar_event].data[current_m.valueOf()] =
+ 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.humidifier = {'seconds': 0};
+ durations.dehumidifier = {'seconds': 0};
+ durations.ventilator = {'seconds': 0};
+ durations.economizer = {'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]);
+ data.metadata.series[series_code].data[current_m.valueOf()] =
+ 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];
+
+ data.metadata.series[series_code_1].data[current_m.valueOf()] =
+ equipment_y[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;
+};
+
+/**
+ * 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.runtime_thermostat.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 all the runtime_thermostat rows indexed by date.
+ *
+ * @return {array} The runtime_thermostat rows.
+ */
+beestat.runtime_thermostat.get_runtime_thermostats_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;
+};
diff --git a/js/beestat/sensor.js b/js/beestat/sensor.js
new file mode 100644
index 0000000..8a7c529
--- /dev/null
+++ b/js/beestat/sensor.js
@@ -0,0 +1,22 @@
+beestat.sensor = {};
+
+/**
+ * Get a sorted list of all sensors attached to the current thermostat.
+ *
+ * @return {array} The sensors.
+ */
+beestat.sensor.get_sorted = function() {
+ // Get and sort all the sensors.
+ var sensors = [];
+ $.values(beestat.cache.sensor).forEach(function(sensor) {
+ if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
+ sensors.push(sensor);
+ }
+ });
+
+ sensors.sort(function(a, b) {
+ return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'});
+ });
+
+ return sensors;
+};
diff --git a/js/component/card.js b/js/component/card.js
index dd9bbb1..cb321e4 100644
--- a/js/component/card.js
+++ b/js/component/card.js
@@ -87,7 +87,7 @@ beestat.component.card.prototype.decorate_title_ = function(parent) {
: (beestat.style.size.gutter);
if (title !== null) {
parent.appendChild($.createElement('div')
- .innerHTML(title)
+ .innerText(title)
.style({
'font-weight': beestat.style.font_weight.bold,
'font-size': beestat.style.font_size.large,
diff --git a/js/component/card/runtime_sensor_detail.js b/js/component/card/runtime_sensor_detail.js
index 83b0df5..431599e 100644
--- a/js/component/card/runtime_sensor_detail.js
+++ b/js/component/card/runtime_sensor_detail.js
@@ -27,6 +27,7 @@ beestat.component.card.runtime_sensor_detail = function(thermostat_id) {
'setting.runtime_sensor_detail_smoothing',
'setting.runtime_sensor_detail_range_type',
'setting.runtime_sensor_detail_range_dynamic',
+ 'cache.runtime_thermostat',
'cache.runtime_sensor'
],
change_function
@@ -44,10 +45,52 @@ beestat.extend(beestat.component.card.runtime_sensor_detail, beestat.component.c
beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = function(parent) {
var self = this;
- var data = this.get_data_();
+ var range = {
+ 'type': beestat.setting('runtime_sensor_detail_range_type'),
+ 'dynamic': beestat.setting('runtime_sensor_detail_range_dynamic'),
+ 'static_begin': beestat.setting('runtime_sensor_detail_range_static_begin'),
+ 'static_end': beestat.setting('runtime_sensor_detail_range_static_end')
+ };
- this.chart_ = new beestat.component.chart.runtime_sensor_detail(data);
- this.chart_.render(parent);
+ var sensor_data = beestat.runtime_sensor.get_data(this.thermostat_id_, range);
+ var thermostat_data = beestat.runtime_thermostat.get_data(this.thermostat_id_, range);
+
+ var data = sensor_data;
+
+ Object.assign(data.series, thermostat_data.series);
+ Object.assign(data.metadata.series, thermostat_data.metadata.series);
+
+ this.charts_ = {
+ 'equipment': new beestat.component.chart.runtime_thermostat_detail_equipment(data),
+ 'occupancy': new beestat.component.chart.runtime_sensor_detail_occupancy(data),
+ 'temperature': new beestat.component.chart.runtime_sensor_detail_temperature(data)
+ };
+
+ this.charts_.equipment.render(parent);
+ this.charts_.occupancy.render(parent);
+ this.charts_.temperature.render(parent);
+
+ // Sync extremes and crosshair.
+ Object.values(this.charts_).forEach(function(source_chart) {
+ Object.values(self.charts_).forEach(function(target_chart) {
+ if (source_chart !== target_chart) {
+ target_chart.sync_extremes(source_chart);
+ target_chart.sync_crosshair(source_chart);
+ }
+ });
+ });
+
+ // Keep the series list in sync across charts.
+ this.charts_.temperature.addEventListener('legend_item_click', function() {
+ this.get_chart().series.forEach(function(temperature_series) {
+ var occupancy_key = temperature_series.name.replace('temperature', 'occupancy');
+ self.charts_.occupancy.get_chart().series.forEach(function(occupancy_series) {
+ if (occupancy_series.name === occupancy_key) {
+ occupancy_series.setVisible(temperature_series.visible);
+ }
+ });
+ });
+ });
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
@@ -91,8 +134,11 @@ beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = func
* the database, check every 2 seconds until it does.
*/
if (this.data_synced_(required_begin, required_end) === true) {
- if (beestat.cache.runtime_sensor === undefined) {
- this.show_loading_('Loading Runtime Detail');
+ if (
+ beestat.cache.runtime_sensor === undefined ||
+ beestat.cache.data.runtime_thermostat_last !== 'runtime_sensor_detail'
+ ) {
+ this.show_loading_('Loading Sensor Detail');
var value;
var operator;
@@ -108,9 +154,28 @@ beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = func
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();*/
+
var api_call = new beestat.api();
- this.get_sensors_().forEach(function(sensor) {
- if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
+ beestat.sensor.get_sorted().forEach(function(sensor) {
+ if (sensor.thermostat_id === self.thermostat_id_) {
api_call.add_call(
'runtime_sensor',
'read',
@@ -122,23 +187,45 @@ beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = func
'operator': operator
}
}
- }
+ },
+ 'runtime_sensor_' + sensor.sensor_id
);
}
});
+ api_call.add_call(
+ 'runtime_thermostat',
+ 'read',
+ {
+ 'attributes': {
+ 'thermostat_id': thermostat.thermostat_id,
+ 'timestamp': {
+ 'value': value,
+ 'operator': operator
+ }
+ }
+ },
+ 'runtime_thermostat'
+ );
+
api_call.set_callback(function(response) {
var runtime_sensors = [];
- response.forEach(function(r) {
- runtime_sensors = runtime_sensors.concat(r);
- });
+ for (var alias in response) {
+ var r = response[alias];
+ if (alias === 'runtime_thermostat') {
+ beestat.cache.set('data.runtime_thermostat_last', 'runtime_sensor_detail');
+ beestat.cache.set('runtime_thermostat', r);
+ } else {
+ runtime_sensors = runtime_sensors.concat(r);
+ }
+ }
beestat.cache.set('runtime_sensor', runtime_sensors);
});
api_call.send();
}
} else {
- this.show_loading_('Syncing Runtime Detail');
+ this.show_loading_('Syncing Sensor Detail');
setTimeout(function() {
new beestat.api()
.add_call(
@@ -218,25 +305,25 @@ beestat.component.card.runtime_sensor_detail.prototype.decorate_top_right_ = fun
}
}));
- // menu.add_menu_item(new beestat.component.menu_item()
- // .set_text('Custom')
- // .set_icon('calendar_edit')
- // .set_callback(function() {
- // (new beestat.component.modal.runtime_sensor_detail_custom()).render();
- // }));
+ menu.add_menu_item(new beestat.component.menu_item()
+ .set_text('Custom')
+ .set_icon('calendar_edit')
+ .set_callback(function() {
+ (new beestat.component.modal.runtime_sensor_detail_custom()).render();
+ }));
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Download Chart')
.set_icon('download')
.set_callback(function() {
- self.chart_.export();
+ self.charts_.temperature.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();
+ self.charts_.temperature.reset_zoom();
}));
if (beestat.setting('runtime_sensor_detail_smoothing') === true) {
@@ -259,258 +346,17 @@ beestat.component.card.runtime_sensor_detail.prototype.decorate_top_right_ = fun
.set_text('Help')
.set_icon('help_circle')
.set_callback(function() {
- window.open('https://www.notion.so/beestat/891f94a6bdb34895a453b7b91591ec29');
+ window.open('https://doc.beestat.io/891f94a6bdb34895a453b7b91591ec29');
}));
};
-/**
- * Get all of the series data.
- *
- * @return {object} The series data.
- */
-beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() {
- var self = this;
-
- var data = {
- 'x': [],
- 'series': {},
- 'metadata': {
- 'series': {},
- 'chart': {
- 'title': this.get_title_(),
- 'subtitle': this.get_subtitle_(),
- 'y_min': Infinity,
- 'y_max': -Infinity,
- 'sensors': null
- }
- }
- };
-
- // A couple private helper functions for manipulating the min/max y values.
- var y_min_max = function(value) {
- if (value !== null) {
- data.metadata.chart.y_min = Math.min(data.metadata.chart.y_min, value);
- data.metadata.chart.y_max = Math.max(data.metadata.chart.y_max, value);
- }
- };
-
-
- var series_codes = [];
-
- // Get and sort all the sensors.
- var sensors = this.get_sensors_();
- data.metadata.sensors = sensors;
-
- // Set up the series_codes.
- sensors.forEach(function(sensor) {
- if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
- series_codes.push('temperature_' + sensor.sensor_id);
- series_codes.push('occupancy_' + sensor.sensor_id);
- }
- });
- series_codes.push('dummy');
-
- // Initialize a bunch of stuff.
- var sequential = {};
- series_codes.forEach(function(series_code) {
- sequential[series_code] = 0;
-
- data.series[series_code] = [];
- data.metadata.series[series_code] = {
- 'active': false
- };
- if (series_code === 'dummy') {
- data.metadata.series[series_code].name = null;
- } else {
- var sensor_id = series_code.replace(/[^0-9]/g, '');
- data.metadata.series[series_code].name = beestat.cache.sensor[sensor_id].name;
- }
- });
-
- var begin_m;
- var end_m;
- if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
- begin_m = moment().subtract(
- beestat.setting('runtime_sensor_detail_range_dynamic'),
- 'day'
- );
- end_m = moment().subtract(1, 'hour');
- } else {
- begin_m = moment(
- beestat.setting('runtime_sensor_detail_range_static_begin') + ' 00:00:00'
- );
- end_m = moment(
- beestat.setting('runtime_sensor_detail_range_static_end') + ' 23:59:59'
- );
- }
-
- // TODO: This needs to be max of begin and when I actually have sensor data
- var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
- begin_m = moment.max(
- begin_m,
- moment(thermostat.first_connected)
- );
-
- begin_m
- .minute(Math.ceil(begin_m.minute() / 5) * 5)
- .second(0)
- .millisecond(0);
-
- var runtime_sensors = this.get_runtime_sensor_by_date_();
-
- // Initialize moving average.
- var moving = [];
- var moving_count;
- if (beestat.setting('runtime_sensor_detail_smoothing') === true) {
- moving_count = 5;
- } else {
- moving_count = 1;
- }
- var offset;
- for (var i = 0; i < moving_count; i++) {
- offset = (i - Math.floor(moving_count / 2)) * 300000;
- moving.push(runtime_sensors[begin_m.valueOf() + offset]);
- }
-
- // TODO: Garage sensor is not returned in runtime data until halfway
- // through...so the series data never got added early on so it just gets
- // slapped on the beginning. It also takes over a previous "j" value and
- // pushes everything around. Instead of looping over runtime_sensor I need to loop over sensor and grab the values.
-
- // Loop.
- var current_m = begin_m;
- while (
- // beestat.cache.runtime_sensor.length > 0 &&
- current_m.isSameOrAfter(end_m) === false
- ) {
- data.x.push(current_m.clone());
-
- // Without this series the chart will jump to the nearest value if there is a chunk of missing data.
- data.series.dummy.push(1);
- data.metadata.series.dummy.active = true;
-
- if (runtime_sensors[current_m.valueOf()] !== undefined) {
- sensors.forEach(function(sensor, j) {
- var runtime_sensor = runtime_sensors[current_m.valueOf()][sensor.sensor_id];
- if (runtime_sensor === undefined) {
- data.series['temperature_' + sensor.sensor_id].push(null);
- data.series['occupancy_' + sensor.sensor_id].push(null);
- return;
- }
-
- var temperature_moving = beestat.temperature(
- self.get_average_(moving, sensor.sensor_id)
- );
- data.series['temperature_' + runtime_sensor.sensor_id].push(temperature_moving);
- y_min_max(temperature_moving);
- data.metadata.series['temperature_' + runtime_sensor.sensor_id].active = true;
-
- if (runtime_sensor.occupancy === true) {
- let swimlane_properties =
- beestat.component.chart.runtime_sensor_detail.get_swimlane_properties(
- sensors.length,
- j
- );
-
- sequential['occupancy_' + runtime_sensor.sensor_id]++;
- data.series['occupancy_' + runtime_sensor.sensor_id].push(swimlane_properties.y);
- } else {
- if (sequential['occupancy_' + runtime_sensor.sensor_id] > 0) {
- let swimlane_properties =
- beestat.component.chart.runtime_sensor_detail.get_swimlane_properties(
- sensors.length,
- j
- );
-
- data.series['occupancy_' + runtime_sensor.sensor_id].push(swimlane_properties.y);
- } else {
- data.series['occupancy_' + runtime_sensor.sensor_id].push(null);
- }
- sequential['occupancy_' + runtime_sensor.sensor_id] = 0;
- }
- });
- } else {
- sensors.forEach(function(sensor) {
- if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
- data.series['temperature_' + sensor.sensor_id].push(null);
- data.series['occupancy_' + sensor.sensor_id].push(null);
- }
- });
- }
-
- current_m.add(5, 'minute');
-
- /**
- * Remove the first row in the moving average and add the next one. Yes
- * this could introduce undefined values; that's ok. Those are handled in
- * the get_average_ function.
- */
- moving.shift();
- moving.push(runtime_sensors[current_m.valueOf() + offset]);
- }
-
- return data;
-};
-
-/**
- * Get all the runtime_sensor rows indexed by date.
- *
- * @return {array} The runtime_sensor rows.
- */
-beestat.component.card.runtime_sensor_detail.prototype.get_runtime_sensor_by_date_ = function() {
- var runtime_sensors = {};
- if (beestat.cache.runtime_sensor !== undefined) {
- beestat.cache.runtime_sensor.forEach(function(runtime_sensor) {
- var timestamp = [moment(runtime_sensor.timestamp).valueOf()];
- if (runtime_sensors[timestamp] === undefined) {
- // runtime_sensors[timestamp] = [];
- runtime_sensors[timestamp] = {};
- }
- // runtime_sensors[timestamp].push(runtime_sensor);
- runtime_sensors[timestamp][runtime_sensor.sensor_id] = runtime_sensor;
- });
- }
- return runtime_sensors;
-};
-
-/**
- * Given an array of runtime_sensors, get the average value of one of the
- * keys. Allows and ignores undefined values in order to keep a more accurate
- * moving average.
- *
- * @param {array} runtime_sensors
- * @param {string} sensor_id The index in the sub-array
- *
- * @return {number} The average.
- */
-beestat.component.card.runtime_sensor_detail.prototype.get_average_ = function(runtime_sensors, sensor_id) {
- var average = 0;
- var count = 0;
- for (var i = 0; i < runtime_sensors.length; i++) {
- if (
- runtime_sensors[i] !== undefined &&
- runtime_sensors[i][sensor_id] !== undefined &&
- runtime_sensors[i][sensor_id].temperature !== null
- ) {
- average += runtime_sensors[i][sensor_id].temperature;
- count++;
- }
- }
-
- if (count === 0) {
- return null;
- }
-
- return average / count;
-};
-
/**
* Get the title of the card.
*
* @return {string} Title
*/
beestat.component.card.runtime_sensor_detail.prototype.get_title_ = function() {
- return 'Runtime Detail';
+ return 'Sensor Detail';
};
/**
@@ -561,24 +407,3 @@ beestat.component.card.runtime_sensor_detail.prototype.data_synced_ = function(r
current_sync_end.isSameOrAfter(required_sync_end)
);
};
-
-/**
- * Get a sorted list of all sensors attached to the current thermostat.
- *
- * @return {array} The sensors.
- */
-beestat.component.card.runtime_sensor_detail.prototype.get_sensors_ = function() {
- // Get and sort all the sensors.
- var sensors = [];
- $.values(beestat.cache.sensor).forEach(function(sensor) {
- if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
- sensors.push(sensor);
- }
- });
-
- sensors.sort(function(a, b) {
- return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'});
- });
-
- return sensors;
-};
diff --git a/js/component/card/runtime_thermostat_detail.js b/js/component/card/runtime_thermostat_detail.js
index 00036e0..eb1f0e0 100644
--- a/js/component/card/runtime_thermostat_detail.js
+++ b/js/component/card/runtime_thermostat_detail.js
@@ -44,9 +44,35 @@ beestat.extend(beestat.component.card.runtime_thermostat_detail, beestat.compone
beestat.component.card.runtime_thermostat_detail.prototype.decorate_contents_ = function(parent) {
var self = this;
- var data = this.get_data_();
- this.chart_ = new beestat.component.chart.runtime_thermostat_detail(data);
- this.chart_.render(parent);
+ var range = {
+ 'type': beestat.setting('runtime_thermostat_detail_range_type'),
+ 'dynamic': beestat.setting('runtime_thermostat_detail_range_dynamic'),
+ 'static_begin': beestat.setting('runtime_thermostat_detail_range_static_begin'),
+ 'static_end': beestat.setting('runtime_thermostat_detail_range_static_end')
+ };
+
+ var data = beestat.runtime_thermostat.get_data(this.thermostat_id_, range);
+
+ data.metadata.chart.title = this.get_title_();
+ data.metadata.chart.subtitle = this.get_subtitle_();
+
+ this.charts_ = {
+ 'equipment': new beestat.component.chart.runtime_thermostat_detail_equipment(data),
+ 'temperature': new beestat.component.chart.runtime_thermostat_detail_temperature(data)
+ };
+
+ this.charts_.equipment.render(parent);
+ this.charts_.temperature.render(parent);
+
+ // Sync extremes and crosshair.
+ Object.values(this.charts_).forEach(function(source_chart) {
+ Object.values(self.charts_).forEach(function(target_chart) {
+ if (source_chart !== target_chart) {
+ target_chart.sync_extremes(source_chart);
+ target_chart.sync_crosshair(source_chart);
+ }
+ });
+ });
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
@@ -90,8 +116,11 @@ beestat.component.card.runtime_thermostat_detail.prototype.decorate_contents_ =
* 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');
+ if (
+ beestat.cache.runtime_thermostat === undefined ||
+ beestat.cache.data.runtime_thermostat_last !== 'runtime_thermostat_detail'
+ ) {
+ this.show_loading_('Loading Thermostat Detail');
var value;
var operator;
@@ -122,12 +151,13 @@ beestat.component.card.runtime_thermostat_detail.prototype.decorate_contents_ =
}
)
.set_callback(function(response) {
+ beestat.cache.set('data.runtime_thermostat_last', 'runtime_thermostat_detail');
beestat.cache.set('runtime_thermostat', response);
})
.send();
}
} else {
- this.show_loading_('Syncing Runtime Detail');
+ this.show_loading_('Syncing Thermostat Detail');
setTimeout(function() {
new beestat.api()
.add_call(
@@ -218,14 +248,14 @@ beestat.component.card.runtime_thermostat_detail.prototype.decorate_top_right_ =
.set_text('Download Chart')
.set_icon('download')
.set_callback(function() {
- self.chart_.export();
+ self.charts_.temperature.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();
+ self.charts_.temperature.reset_zoom();
}));
if (beestat.setting('runtime_thermostat_detail_smoothing') === true) {
@@ -248,567 +278,17 @@ beestat.component.card.runtime_thermostat_detail.prototype.decorate_top_right_ =
.set_text('Help')
.set_icon('help_circle')
.set_callback(function() {
- window.open('https://www.notion.so/Runtime-Detail-e499fb13fd4441f4b3f096baca1cb138');
+ window.open('https://doc.beestat.io/e499fb13fd4441f4b3f096baca1cb138');
}));
};
-/**
- * Get all of the series data.
- *
- * @return {object} The series data.
- */
-beestat.component.card.runtime_thermostat_detail.prototype.get_data_ = function() {
- var data = {
- 'x': [],
- 'series': {},
- 'metadata': {
- 'series': {},
- 'chart': {
- 'title': this.get_title_(),
- 'subtitle': this.get_subtitle_(),
- 'y_min': Infinity,
- 'y_max': -Infinity
- }
- }
- };
-
- // A couple private helper functions for manipulating the min/max y values.
- var y_min_max = function(value) {
- if (value !== null) {
- data.metadata.chart.y_min = Math.min(data.metadata.chart.y_min, value);
- data.metadata.chart.y_max = Math.max(data.metadata.chart.y_max, value);
- }
- };
-
- // Duration objects. These are passed by reference into the metadata.
- var durations = {};
-
- // Y values for equipment swimlane data.
- var equipment_y = {
- 'calendar_event_smartrecovery': 94,
- 'calendar_event_home': 94,
- 'calendar_event_away': 94,
- 'calendar_event_sleep': 94,
- 'calendar_event_smarthome': 94,
- 'calendar_event_smartaway': 94,
- 'calendar_event_hold': 94,
- 'calendar_event_vacation': 94,
- 'calendar_event_quicksave': 94,
- 'calendar_event_other': 94,
- 'calendar_event_custom': 94,
- 'compressor_heat_1': 67,
- 'compressor_heat_2': 67,
- 'auxiliary_heat_1': 67,
- 'auxiliary_heat_2': 67,
- 'compressor_cool_1': 67,
- 'compressor_cool_2': 67,
- 'fan': 47,
- 'humidifier': 31,
- 'dehumidifier': 31,
- 'ventilator': 31,
- 'economizer': 31
- };
-
- // Initialize a bunch of stuff.
- [
- 'calendar_event_smartrecovery',
- 'calendar_event_home',
- 'calendar_event_away',
- 'calendar_event_sleep',
- 'calendar_event_smarthome',
- 'calendar_event_smartaway',
- 'calendar_event_hold',
- 'calendar_event_vacation',
- 'calendar_event_quicksave',
- 'calendar_event_other',
- 'calendar_event_custom',
- 'outdoor_temperature',
- 'indoor_temperature',
- 'indoor_humidity',
- 'outdoor_humidity',
- 'setpoint_heat',
- 'setpoint_cool',
- 'fan',
- 'compressor_heat_1',
- 'compressor_heat_2',
- 'auxiliary_heat_1',
- 'auxiliary_heat_2',
- 'compressor_cool_1',
- 'compressor_cool_2',
- 'humidifier',
- 'dehumidifier',
- 'ventilator',
- 'economizer',
- 'dummy'
- ].forEach(function(series_code) {
- data.series[series_code] = [];
- data.metadata.series[series_code] = {
- 'active': false,
- 'durations': {},
-
- /**
- * Note to future self: This can be used for all series. Need to
- * populate the raw data points for each series here. The tooltip should
- * get data from here and not the chart points array. Then the series
- * data can be whatever is necessary to produce a performance-optimized
- * chart as long as there is one series (dummy) that has a point at
- * every x-value. That will allow a smooth tooltip, lightweight lines,
- * and accurate data.
- */
- 'data': {}
- };
- durations[series_code] = {'seconds': 0};
- });
-
- data.metadata.series.calendar_event_name = {};
- data.metadata.series.system_mode = {};
-
- var begin_m;
- var end_m;
- if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
- begin_m = moment().subtract(
- beestat.setting('runtime_thermostat_detail_range_dynamic'),
- 'day'
- );
- end_m = moment().subtract(1, 'hour');
- } else {
- begin_m = moment(
- beestat.setting('runtime_thermostat_detail_range_static_begin') + ' 00:00:00'
- );
- end_m = moment(
- beestat.setting('runtime_thermostat_detail_range_static_end') + ' 23:59:59'
- );
- }
-
- var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
- begin_m = moment.max(
- begin_m,
- moment(thermostat.first_connected)
- );
-
- begin_m
- .minute(Math.ceil(begin_m.minute() / 5) * 5)
- .second(0)
- .millisecond(0);
-
- var runtime_thermostats = this.get_runtime_thermostat_by_date_();
-
- // Initialize moving average.
- var moving = [];
- var moving_count;
- if (beestat.setting('runtime_thermostat_detail_smoothing') === true) {
- moving_count = 10;
- } else {
- moving_count = 1;
- }
- var offset;
- for (var i = 0; i < moving_count; i++) {
- offset = (i - Math.floor(moving_count / 2)) * 300000;
- moving.push(runtime_thermostats[begin_m.valueOf() + offset]);
- }
-
- // Loop.
- var current_m = begin_m;
- while (
- // beestat.cache.runtime_thermostat.length > 0 &&
- current_m.isSameOrAfter(end_m) === false
- ) {
- data.x.push(current_m.clone());
-
- // Without this series the chart will jump to the nearest value if there is a chunk of missing data.
- data.series.dummy.push(1);
- data.metadata.series.dummy.active = true;
-
- var runtime_thermostat = runtime_thermostats[
- current_m.valueOf()
- ];
-
- if (runtime_thermostat !== undefined) {
- /**
- * Things that use the moving average.
- */
- var indoor_humidity_moving = this.get_average_(moving, 'indoor_humidity');
- data.series.indoor_humidity.push(indoor_humidity_moving);
- data.metadata.series.indoor_humidity.data[current_m.valueOf()] =
- runtime_thermostat.indoor_humidity;
- data.metadata.series.indoor_humidity.active = true;
-
- var outdoor_humidity_moving = this.get_average_(moving, 'outdoor_humidity');
- data.series.outdoor_humidity.push(outdoor_humidity_moving);
- data.metadata.series.outdoor_humidity.data[current_m.valueOf()] =
- runtime_thermostat.outdoor_humidity;
- data.metadata.series.outdoor_humidity.active = true;
-
- var indoor_temperature_moving = beestat.temperature(
- this.get_average_(moving, 'indoor_temperature')
- );
- data.series.indoor_temperature.push(indoor_temperature_moving);
- data.metadata.series.indoor_temperature.data[current_m.valueOf()] =
- beestat.temperature(runtime_thermostat.indoor_temperature);
- y_min_max(indoor_temperature_moving);
- data.metadata.series.indoor_temperature.active = true;
-
- var outdoor_temperature_moving = beestat.temperature(
- this.get_average_(moving, 'outdoor_temperature')
- );
- data.series.outdoor_temperature.push(outdoor_temperature_moving);
- data.metadata.series.outdoor_temperature.data[current_m.valueOf()] =
- beestat.temperature(runtime_thermostat.outdoor_temperature);
- y_min_max(outdoor_temperature_moving);
- data.metadata.series.outdoor_temperature.active = true;
-
- /**
- * Add setpoints, but only when relevant. For example: Only show the
- * heat setpoint line when the heat is actually on.
- */
- if (
- runtime_thermostat.system_mode === 'auto' ||
- runtime_thermostat.system_mode === 'heat' ||
- runtime_thermostat.system_mode === 'auxiliary_heat'
- ) {
- var setpoint_heat = beestat.temperature(
- runtime_thermostat.setpoint_heat
- );
- data.series.setpoint_heat.push(setpoint_heat);
- y_min_max(setpoint_heat);
-
- data.metadata.series.setpoint_heat.active = true;
-
- } else {
- data.series.setpoint_heat.push(null);
- }
-
- if (
- runtime_thermostat.system_mode === 'auto' ||
- runtime_thermostat.system_mode === 'cool'
- ) {
- var setpoint_cool = beestat.temperature(
- runtime_thermostat.setpoint_cool
- );
- data.series.setpoint_cool.push(setpoint_cool);
- y_min_max(setpoint_cool);
-
- data.metadata.series.setpoint_cool.active = true;
-
- } else {
- data.series.setpoint_cool.push(null);
- }
-
- /*
- * HVAC Mode. This isn't graphed but it's available for the tooltip.
- * series.system_mode.chart_data.push([x, runtime_thermostat.system_mode]);
- */
- data.metadata.series.system_mode[current_m.valueOf()] = runtime_thermostat.system_mode;
-
- /*
- * Thanks, ecobee...I more or less copied this code from the ecobee Follow
- * Me graph to make sure it's accurate.
- */
- var this_calendar_event;
- var this_calendar_event_name;
-
- if (runtime_thermostat.event === null) {
- if (runtime_thermostat.climate === null) {
- this_calendar_event = 'calendar_event_other';
- this_calendar_event_name = 'Other';
- } else {
- switch (runtime_thermostat.climate.toLowerCase()) {
- case 'home':
- case 'sleep':
- case 'away':
- this_calendar_event = 'calendar_event_' + runtime_thermostat.climate.toLowerCase();
- this_calendar_event_name = runtime_thermostat.climate;
- break;
- default:
- this_calendar_event = 'calendar_event_custom';
- this_calendar_event_name = runtime_thermostat.climate;
- break;
- }
- }
- } else if (runtime_thermostat.event.match(/SmartRecovery/i) !== null) {
- this_calendar_event = 'calendar_event_smartrecovery';
- this_calendar_event_name = 'Smart Recovery';
- } else if (runtime_thermostat.event.match(/^home$/i) !== null) {
- this_calendar_event = 'calendar_event_home';
- this_calendar_event_name = 'Home';
- } else if (runtime_thermostat.event.match(/^away$/i) !== null) {
- this_calendar_event = 'calendar_event_away';
- this_calendar_event_name = 'Away';
- } else if (runtime_thermostat.event.match(/^smarthome$/i) !== null) {
- this_calendar_event = 'calendar_event_smarthome';
- this_calendar_event_name = 'Smart Home';
- } else if (runtime_thermostat.event.match(/^smartaway$/i) !== null) {
- this_calendar_event = 'calendar_event_smartaway';
- this_calendar_event_name = 'Smart Away';
- } else if (
- runtime_thermostat.event.match(/^auto$/i) !== null ||
- runtime_thermostat.event.match(/^today$/i) !== null ||
- runtime_thermostat.event.match(/^hold$/i) !== null
- ) {
- this_calendar_event = 'calendar_event_hold';
- this_calendar_event_name = 'Hold';
- } else if (
- runtime_thermostat.event.match(/^vacation$/i) !== null ||
- runtime_thermostat.event.match(/(\S\S\S\s\d+\s\d\d\d\d)|(\d{12})/i) !== null
- ) {
- this_calendar_event = 'calendar_event_vacation';
- this_calendar_event_name = 'Vacation';
- } else if (runtime_thermostat.event.match(/^quicksave$/i) !== null) {
- this_calendar_event = 'calendar_event_quicksave';
- this_calendar_event_name = 'Quick Save';
- } else {
- this_calendar_event = 'calendar_event_other';
- this_calendar_event_name = 'Other';
- }
-
- [
- 'calendar_event_smartrecovery',
- 'calendar_event_home',
- 'calendar_event_away',
- 'calendar_event_sleep',
- 'calendar_event_smarthome',
- 'calendar_event_smartaway',
- 'calendar_event_hold',
- 'calendar_event_vacation',
- 'calendar_event_quicksave',
- 'calendar_event_other',
- 'calendar_event_custom'
- ].forEach(function(calendar_event) {
- if (calendar_event === this_calendar_event) {
- data.series[calendar_event].push(equipment_y[calendar_event]);
- data.metadata.series[calendar_event].active = true;
- } else {
- data.series[calendar_event].push(null);
- }
- });
-
- data.metadata.series.calendar_event_name[current_m.valueOf()] =
- this_calendar_event_name;
-
- /**
- * If all stages of the compressor are off, clear the durations. It is
- * important that this only get reset if the seconds values are also
- * zero to support backfilling.
- */
- if (
- runtime_thermostat.compressor_1 === 0 &&
- runtime_thermostat.compressor_2 === 0 &&
- (
- durations.compressor_heat_1.seconds > 0 ||
- durations.compressor_heat_2.seconds > 0 ||
- durations.compressor_cool_1.seconds > 0 ||
- durations.compressor_cool_2.seconds > 0
- )
- ) {
- durations.compressor_heat_1 = {'seconds': 0};
- durations.compressor_heat_2 = {'seconds': 0};
- durations.compressor_cool_1 = {'seconds': 0};
- durations.compressor_cool_2 = {'seconds': 0};
- }
-
- if (
- runtime_thermostat.auxiliary_heat_1 === 0 &&
- runtime_thermostat.auxiliary_heat_2 === 0 &&
- (
- durations.auxiliary_heat_1.seconds > 0 ||
- durations.auxiliary_heat_2.seconds > 0
- )
- ) {
- durations.auxiliary_heat_1 = {'seconds': 0};
- durations.auxiliary_heat_2 = {'seconds': 0};
- }
-
- // Reset fan to 0
- if (runtime_thermostat.fan === 0) {
- durations.fan = {'seconds': 0};
- }
-
- // Reset accessories
- if (runtime_thermostat.accessory === 0) {
- durations.humidifier = {'seconds': 0};
- durations.dehumidifier = {'seconds': 0};
- durations.ventilator = {'seconds': 0};
- durations.economizer = {'seconds': 0};
- }
-
- // Equipment
- [
- 'fan',
- 'compressor_heat_1',
- 'compressor_heat_2',
- 'auxiliary_heat_1',
- 'auxiliary_heat_2',
- 'compressor_cool_1',
- 'compressor_cool_2',
- 'humidifier',
- 'dehumidifier',
- 'ventilator',
- 'economizer'
- ].forEach(function(series_code) {
- var runtime_thermostat_series_code;
- switch (series_code) {
- case 'compressor_heat_1':
- case 'compressor_heat_2':
- runtime_thermostat_series_code = series_code
- .replace('compressor_heat', 'compressor');
- break;
- case 'compressor_cool_1':
- case 'compressor_cool_2':
- runtime_thermostat_series_code = series_code
- .replace('compressor_cool', 'compressor');
- break;
- case 'humidifier':
- case 'dehumidifier':
- case 'ventilator':
- case 'economizer':
- runtime_thermostat_series_code = 'accessory';
- break;
- default:
- runtime_thermostat_series_code = series_code;
- break;
- }
-
- var equipment_on = function(series_code_on, runtime_thermostat_series_code_on) {
- switch (series_code_on) {
- case 'compressor_heat_1':
- case 'compressor_heat_2':
- return runtime_thermostat[runtime_thermostat_series_code_on] > 0 &&
- runtime_thermostat.compressor_mode === 'heat';
- case 'compressor_cool_1':
- case 'compressor_cool_2':
- return runtime_thermostat[runtime_thermostat_series_code_on] > 0 &&
- runtime_thermostat.compressor_mode === 'cool';
- case 'humidifier':
- case 'dehumidifier':
- case 'ventilator':
- case 'economizer':
- return runtime_thermostat[runtime_thermostat_series_code_on] > 0 &&
- runtime_thermostat.accessory_type === series_code;
- default:
- return runtime_thermostat[series_code] > 0;
- }
- };
-
- if (equipment_on(series_code, runtime_thermostat_series_code) === true) {
- data.metadata.series[series_code].active = true;
- data.metadata.series[series_code].durations[current_m.valueOf()] = durations[series_code];
- data.series[series_code].push(equipment_y[series_code]);
-
- if (
- series_code === 'auxiliary_heat_1' ||
- series_code === 'compressor_heat_1' ||
- series_code === 'compressor_cool_1'
- ) {
- var series_code_2 = series_code.replace('1', '2');
- data.metadata.series[series_code_2].durations[current_m.valueOf()] = durations[series_code_2];
- }
- durations[series_code].seconds += runtime_thermostat[runtime_thermostat_series_code];
-
- /*
- * If heat/cool/aux 2 is on, extend the bar from heat/cool/aux 1
- * behind and set the duration.
- */
- if (series_code.slice(-1) === '2') {
- var series_code_1 = series_code.replace('2', '1');
- data.series[series_code_1]
- .splice(-1, 1, equipment_y[series_code_1]);
- data.metadata.series[series_code_1]
- .durations[current_m.valueOf()] = durations[series_code_1];
- }
- } else {
- data.series[series_code].push(null);
- }
- });
- } else {
- data.series.calendar_event_smartrecovery.push(null);
- data.series.calendar_event_home.push(null);
- data.series.calendar_event_away.push(null);
- data.series.calendar_event_sleep.push(null);
- data.series.calendar_event_smarthome.push(null);
- data.series.calendar_event_smartaway.push(null);
- data.series.calendar_event_hold.push(null);
- data.series.calendar_event_vacation.push(null);
- data.series.calendar_event_quicksave.push(null);
- data.series.calendar_event_other.push(null);
- data.series.calendar_event_custom.push(null);
- data.series.indoor_temperature.push(null);
- data.series.outdoor_temperature.push(null);
- data.series.indoor_humidity.push(null);
- data.series.outdoor_humidity.push(null);
- data.series.setpoint_heat.push(null);
- data.series.setpoint_cool.push(null);
- data.series.fan.push(null);
- data.series.compressor_heat_1.push(null);
- data.series.compressor_heat_2.push(null);
- data.series.auxiliary_heat_1.push(null);
- data.series.auxiliary_heat_2.push(null);
- data.series.compressor_cool_1.push(null);
- data.series.compressor_cool_2.push(null);
- data.series.humidifier.push(null);
- data.series.dehumidifier.push(null);
- data.series.ventilator.push(null);
- data.series.economizer.push(null);
- }
-
- current_m.add(5, 'minute');
-
- /**
- * Remove the first row in the moving average and add the next one. Yes
- * this could introduce undefined values; that's ok. Those are handled in
- * the get_average_ function.
- */
- moving.shift();
- moving.push(runtime_thermostats[current_m.valueOf() + offset]);
- }
-
- return data;
-};
-
-/**
- * Get all the runtime_thermostat rows indexed by date.
- *
- * @return {array} The runtime_thermostat rows.
- */
-beestat.component.card.runtime_thermostat_detail.prototype.get_runtime_thermostat_by_date_ = function() {
- var runtime_thermostats = {};
- if (beestat.cache.runtime_thermostat !== undefined) {
- beestat.cache.runtime_thermostat.forEach(function(runtime_thermostat) {
- runtime_thermostats[moment(runtime_thermostat.timestamp).valueOf()] = runtime_thermostat;
- });
- }
- return runtime_thermostats;
-};
-
-/**
- * Given an array of runtime thermostats, get the average value of one of the
- * keys. Allows and ignores undefined values in order to keep a more accurate
- * moving average.
- *
- * @param {array} runtime_thermostats
- * @param {string} series_code
- *
- * @return {number} The average.
- */
-beestat.component.card.runtime_thermostat_detail.prototype.get_average_ = function(runtime_thermostats, series_code) {
- var average = 0;
- var count = 0;
- for (var i = 0; i < runtime_thermostats.length; i++) {
- if (runtime_thermostats[i] !== undefined) {
- average += runtime_thermostats[i][series_code];
- count++;
- }
- }
-
- return average / count;
-};
-
/**
* Get the title of the card.
*
* @return {string} Title
*/
beestat.component.card.runtime_thermostat_detail.prototype.get_title_ = function() {
- return 'Runtime Detail';
+ return 'Thermostat Detail';
};
/**
@@ -849,7 +329,7 @@ beestat.component.card.runtime_thermostat_detail.prototype.data_synced_ = functi
return true;
}
- var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
+ var thermostat = beestat.cache.thermostat[this.thermostat_id_];
var current_sync_begin = moment.utc(thermostat.sync_begin);
var current_sync_end = moment.utc(thermostat.sync_end);
diff --git a/js/component/card/runtime_thermostat_summary.js b/js/component/card/runtime_thermostat_summary.js
index ddabbb3..586ad1f 100755
--- a/js/component/card/runtime_thermostat_summary.js
+++ b/js/component/card/runtime_thermostat_summary.js
@@ -469,7 +469,7 @@ beestat.component.card.runtime_thermostat_summary.prototype.gap_fill_ = function
* @return {string} The title.
*/
beestat.component.card.runtime_thermostat_summary.prototype.get_title_ = function() {
- return 'Runtime Summary';
+ return 'Thermostat Summary';
};
/**
diff --git a/js/component/card/sensors.js b/js/component/card/sensors.js
index a217d07..663216d 100644
--- a/js/component/card/sensors.js
+++ b/js/component/card/sensors.js
@@ -25,7 +25,7 @@ beestat.component.card.sensors.prototype.decorate_contents_ = function(parent) {
var sensors = [];
var internal_sensor;
- $.values(beestat.cache.sensor).forEach(function(sensor) {
+ beestat.sensor.get_sorted().forEach(function(sensor) {
if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
if (sensor.type === 'thermostat') {
internal_sensor = sensor;
@@ -35,10 +35,6 @@ beestat.component.card.sensors.prototype.decorate_contents_ = function(parent) {
}
});
- sensors.sort(function(a, b) {
- return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'});
- });
-
/*
* Decorate the thermostat's internal sensor, if it has one. The Cor
* thermostats, for example, do not.
diff --git a/js/component/chart.js b/js/component/chart.js
index dff6168..6c22225 100644
--- a/js/component/chart.js
+++ b/js/component/chart.js
@@ -61,6 +61,7 @@ beestat.component.chart.prototype.export = function() {
*/
beestat.component.chart.prototype.get_options_legend_ = function() {
return {
+ 'enabled': this.get_options_legend_enabled_(),
'itemStyle': {
'color': '#ecf0f1',
'font-weight': '500'
@@ -71,19 +72,7 @@ beestat.component.chart.prototype.get_options_legend_ = function() {
'itemHiddenStyle': {
'color': '#7f8c8d'
},
- 'labelFormatter': this.get_options_legend_labelFormatter_(),
-
-
- // 'layout': 'vertical',
- // 'align': 'right',
- // 'verticalAlign': 'top'
-
- // 'maxHeight': 1000, // To prevent the navigation thing
- // 'floating': true,
- // 'verticalAlign': 'top',
- // 'y': 50,
- // 'borderWidth': 1
- //
+ 'labelFormatter': this.get_options_legend_labelFormatter_()
};
};
@@ -98,6 +87,15 @@ beestat.component.chart.prototype.get_options_legend_labelFormatter_ = function(
};
};
+/**
+ * Get the legend enabled options.
+ *
+ * @return {Function} The legend enabled options.
+ */
+beestat.component.chart.prototype.get_options_legend_enabled_ = function() {
+ return true;
+};
+
/**
* Get the plotOptions.
*
@@ -172,12 +170,9 @@ beestat.component.chart.prototype.get_options_chart_ = function() {
'style': {
'fontFamily': 'Montserrat'
},
- 'spacing': [
- beestat.style.size.gutter,
- 0,
- 0,
- 0
- ],
+ 'spacing': this.get_options_chart_spacing_(),
+ // For consistent left spacing on charts with no y-axis values
+ 'marginLeft': this.get_options_chart_marginLeft_(),
'zoomType': this.get_options_chart_zoomType_(),
'panning': true,
'panKey': 'ctrl',
@@ -192,6 +187,29 @@ beestat.component.chart.prototype.get_options_chart_ = function() {
};
};
+/**
+ * Get the left margin for the chart.
+ *
+ * @return {number} The left margin for the chart.
+ */
+beestat.component.chart.prototype.get_options_chart_marginLeft_ = function() {
+ return undefined;
+};
+
+/**
+ * Get the spacing for the chart.
+ *
+ * @return {number} The spacing for the chart.
+ */
+beestat.component.chart.prototype.get_options_chart_spacing_ = function() {
+ return [
+ (beestat.style.size.gutter / 2),
+ 0,
+ 0,
+ 0
+ ];
+};
+
/**
* Get the events list for the chart.
*
@@ -201,6 +219,20 @@ beestat.component.chart.prototype.get_options_chart_events_ = function() {
return null;
};
+/**
+ * Get the spacing for the chart.
+ *
+ * @return {number} The spacing for the chart.
+ */
+// beestat.component.chart.prototype.get_options_chart_spacing_ = function() {
+// return [
+// beestat.style.size.gutter,
+// 0,
+// 0,
+// 0
+// ];
+// };
+
/**
* Get the height of the chart.
*
@@ -337,6 +369,8 @@ beestat.component.chart.prototype.get_options_credits_ = function() {
* @return {object} The xAxis options.
*/
beestat.component.chart.prototype.get_options_xAxis_ = function() {
+ var self = this;
+
return {
'categories': this.data_.x,
'lineColor': beestat.style.color.bluegray.light,
@@ -346,6 +380,14 @@ beestat.component.chart.prototype.get_options_xAxis_ = function() {
'color': beestat.style.color.gray.base
},
'formatter': this.get_options_xAxis_labels_formatter_()
+ },
+ 'events': {
+ 'afterSetExtremes': function() {
+ // Make sure the extremes are set prior to firing the event.
+ // setTimeout(function() {
+ self.dispatchEvent('after_set_extremes');
+ // }, 0)
+ }
}
};
};
@@ -419,28 +461,53 @@ beestat.component.chart.prototype.get_options_tooltip_formatter_ = function() {
beestat.component.chart.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
+ 'x': self.get_options_tooltip_positioner_x_(tooltip_width, tooltip_height, point),
+ 'y': self.get_options_tooltip_positioner_y_(tooltip_width, tooltip_height, point)
};
};
};
+/**
+ * Get the tooltip positioner x value.
+ *
+ * @param {number} tooltip_width Tooltip width.
+ * @param {number} tooltip_height Tooltip height.
+ * @param {point} point Highcharts current point.
+ *
+ * @return {number} The tooltip x value.
+ */
+beestat.component.chart.prototype.get_options_tooltip_positioner_x_ = function(tooltip_width, tooltip_height, point) {
+ var plot_width = this.chart_.plotWidth;
+
+ var fits_on_left = (point.plotX - tooltip_width) > 0;
+ var fits_on_right = (point.plotX + tooltip_width) < plot_width;
+
+ var x;
+ if (fits_on_left === true) {
+ x = point.plotX - tooltip_width + this.chart_.plotLeft;
+ } else if (fits_on_right === true) {
+ x = point.plotX + this.chart_.plotLeft;
+ } else {
+ x = this.chart_.plotLeft;
+ }
+
+ return x;
+};
+
+/**
+ * Get the tooltip positioner y value.
+ *
+ * @param {number} tooltip_width Tooltip width.
+ * @param {number} tooltip_height Tooltip height.
+ * @param {point} point Highcharts current point.
+ *
+ * @return {number} The tooltip y value.
+ */
+beestat.component.chart.prototype.get_options_tooltip_positioner_y_ = function(tooltip_width, tooltip_height, point) {
+ return 60;
+};
+
/**
* Get the HTML needed to render a tooltip.
*
@@ -506,3 +573,60 @@ beestat.component.chart.prototype.tooltip_formatter_helper_ = function(title, se
return tooltip[0].outerHTML;
};
+
+/**
+ * Get the Highcharts chart object.
+ *
+ * @return {Highcharts} The Highcharts chart object.
+ */
+beestat.component.chart.prototype.get_chart = function() {
+ return this.chart_;
+};
+
+/**
+ * Sync extremes of this chart with extremes of another chart.
+ *
+ * @param {beestat.component.chart} source_chart The source chart.
+ */
+beestat.component.chart.prototype.sync_extremes = function(source_chart) {
+ var self = this;
+
+ source_chart.addEventListener('after_set_extremes', function() {
+ var extremes = source_chart.get_chart().axes[0].getExtremes();
+ self.get_chart().axes[0].setExtremes(
+ extremes.min,
+ extremes.max,
+ undefined,
+ false
+ );
+ });
+};
+
+/**
+ * Sync crosshair of this chart with crosshair of another chart.
+ *
+ * @param {beestat.component.chart} source_chart The source chart.
+ */
+beestat.component.chart.prototype.sync_crosshair = function(source_chart) {
+ var self = this;
+
+ [
+ 'mousemove',
+ 'touchmove',
+ 'touchstart'
+ ].forEach(function(event_type) {
+ source_chart.get_chart().container.addEventListener(
+ event_type,
+ function(e) {
+ var point = self.get_chart().series[0].searchPoint(
+ self.get_chart().pointer.normalize(e),
+ true
+ );
+ if (point !== undefined) {
+ point.onMouseOver();
+ point.series.chart.xAxis[0].drawCrosshair(event, this);
+ }
+ }
+ );
+ });
+};
diff --git a/js/component/chart/runtime_sensor_detail.js b/js/component/chart/runtime_sensor_detail.js
deleted file mode 100644
index 0a124ef..0000000
--- a/js/component/chart/runtime_sensor_detail.js
+++ /dev/null
@@ -1,361 +0,0 @@
-/**
- * Runtime sensor detail chart.
- *
- * @param {object} data The chart data.
- */
-beestat.component.chart.runtime_sensor_detail = function(data) {
- this.data_ = data;
-
- beestat.component.chart.apply(this, arguments);
-};
-beestat.extend(beestat.component.chart.runtime_sensor_detail, beestat.component.chart);
-
-/**
- * Override for get_options_xAxis_labels_formatter_.
- *
- * @return {Function} xAxis labels formatter.
- */
-beestat.component.chart.runtime_sensor_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(' ');
- };
-};
-
-beestat.component.chart.runtime_sensor_detail.prototype.get_options_legend_labelFormatter_ = function() {
- var self = this;
- return function() {
- return self.data_.metadata.series[this.name].name;
- };
-};
-
-/**
- * Override for get_options_series_.
- *
- * @return {Array} All of the series to display on the chart.
- */
-beestat.component.chart.runtime_sensor_detail.prototype.get_options_series_ = function() {
- var self = this;
- var series = [];
-
- var colors = [
- beestat.style.color.blue.base,
- beestat.style.color.red.base,
- beestat.style.color.yellow.base,
- beestat.style.color.green.base,
- beestat.style.color.orange.base,
- beestat.style.color.bluegreen.base,
- beestat.style.color.purple.base,
- beestat.style.color.lightblue.base,
- beestat.style.color.blue.light,
- beestat.style.color.red.light,
- beestat.style.color.yellow.light,
- beestat.style.color.green.light,
- beestat.style.color.orange.light,
- beestat.style.color.bluegreen.light,
- beestat.style.color.purple.light,
- beestat.style.color.lightblue.light,
- beestat.style.color.blue.dark,
- beestat.style.color.red.dark,
- beestat.style.color.yellow.dark,
- beestat.style.color.green.dark,
- beestat.style.color.orange.dark,
- beestat.style.color.bluegreen.dark,
- beestat.style.color.purple.dark,
- beestat.style.color.lightblue.dark
- ];
-
- this.data_.metadata.sensors.forEach(function(sensor, i) {
- if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
- series.push({
- 'name': 'temperature_' + sensor.sensor_id,
- 'data': self.data_.series['temperature_' + sensor.sensor_id],
- 'color': colors[i],
- 'yAxis': 0,
- 'type': 'spline',
- 'lineWidth': 1
- });
-
- // var sensor_count = (Object.keys(self.data_.series).length - 1) / 2;
-
- series.push({
- 'linkedTo': ':previous',
- 'name': 'occupancy_' + sensor.sensor_id,
- 'data': self.data_.series['occupancy_' + sensor.sensor_id],
- 'color': colors[i],
- 'yAxis': 1,
- 'type': 'line',
- 'lineWidth': beestat.component.chart.runtime_sensor_detail.get_swimlane_properties(self.data_.metadata.sensors.length, 1).line_width,
- 'linecap': 'square',
- 'className': 'crisp_edges'
- });
- }
- });
-
- series.push({
- 'name': '',
- 'data': self.data_.series.dummy,
- 'yAxis': 1,
- 'type': 'line',
- 'lineWidth': 0,
- 'showInLegend': false
- });
-
- return series;
-};
-
-/**
- * Override for get_options_yAxis_.
- *
- * @return {Array} The y-axis options.
- */
-beestat.component.chart.runtime_sensor_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) * 5;
- var y_max = Math.ceil((this.data_.metadata.chart.y_max) / 5) * 5;
-
- y_max += ((beestat.setting('temperature_unit') === '°F') ? 10 : 4);
-
- var tick_positions = [];
- var tick_interval = (beestat.setting('temperature_unit') === '°F') ? 5 : 2;
- 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
- {
- '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
- },
-
- // Swimlanes
- {
- 'height': 100,
- // 'top': 0,
- 'min': 0,
- 'max': 100,
- 'reversed': true,
- 'gridLineWidth': 0,
- 'title': {'text': null},
- 'labels': {'enabled': false},
- 'plotBands': {
- 'zIndex': 2,
- // 'color': 'red',
- 'color': beestat.style.color.bluegray.dark,
- 'from': 0,
- 'to': 51
- }
- }
- ];
-};
-
-/**
- * Override for get_options_tooltip_formatter_.
- *
- * @return {Function} The tooltip formatter.
- */
-beestat.component.chart.runtime_sensor_detail.prototype.get_options_tooltip_formatter_ = function() {
- var self = this;
-
- return function() {
- var sections = [];
- var group = [];
-
- // Get all the point values and index them by series_code for reference.
- var values = {};
- this.points.forEach(function(point) {
- values[point.series.name] = point.y;
- });
-
- /**
- * Get a couple of other properties and index them by series_code for
- * reference. This dives up to the chart itself because the tooltip shows
- * all series unless explicitly disabled and those aren't always in the
- * points array.
- */
- var colors = {};
- var visible = {};
- self.chart_.series.forEach(function(series) {
- colors[series.name] = series.color;
- visible[series.name] = series.visible;
- });
-
- for (var series_code in self.data_.series) {
- var label;
- var value;
- var color;
-
- if (series_code.includes('temperature') && visible[series_code] === true) {
- label = self.data_.metadata.series[series_code].name;
- color = colors[series_code];
- if (values[series_code] === undefined) {
- value = '-';
- } else {
- value = beestat.temperature({
- 'temperature': values[series_code],
- 'convert': false,
- 'units': true
- });
- }
-
- var occupancy_key = series_code.replace('temperature', 'occupancy');
- if (values[occupancy_key] !== undefined && values[occupancy_key] !== null) {
- value += ' ●';
- }
-
- group.push({
- 'label': label,
- 'value': value,
- 'color': color
- });
- }
- }
-
- if (group.length === 0) {
- group.push({
- 'label': 'No data',
- 'value': '',
- 'color': beestat.style.color.gray.base
- });
- }
-
- sections.push(group);
-
- var title = this.x.format('ddd, MMM D @ h:mma');
-
- return self.tooltip_formatter_helper_(
- title,
- sections
- );
- };
-};
-
-/**
- * Get properties of swimlane series.
- *
- * @param {number} count The number of swimlanes present.
- * @param {number} i Which swimlane this is.
- *
- * @return {Object} The swimlane line width and y position.
- */
-beestat.component.chart.runtime_sensor_detail.get_swimlane_properties = function(count, i) {
- // Available height for all swimlanes
- var height = 50;
-
- // Some sensible max height if you have very few sensors.
- var max_line_width = 16;
-
- // Spacing. This is arbitrary...spacing decreases to 0 after you hit 15 sensors.
- var spacing = Math.floor(15 / count);
-
- // Base line width is a percentage height of the container.
- var line_width = Math.floor(height / count);
-
- // Cap to a max line width.
- line_width = Math.min(line_width, max_line_width);
-
- // Set y, then shift it up slightly because the width expands out from the center.
- var y = (line_width * i);
- y += Math.round((line_width / 2));
-
- // Make the lines slightly less tall to create space between them.
- line_width -= spacing;
-
- // Center within the swimlane area.
- var occupied_space = (line_width * count) + (spacing * count);
- var empty_space = height - occupied_space;
- // y += (empty_space / 2);
-
- return {
- 'line_width': line_width,
- 'y': y
- };
-};
-
-/**
- * This is unfortunate. Axis heights can be done in either pixels or
- * percentages. If you use percentages, it's percentage of the plot height
- * which includes the y-axis labels and the legend. These heights are
- * variable, so setting a 20% height on the swimlane axis means the axis
- * height can actually change depending on external factors. When trying to
- * accurately position lanes, this variation can mess up pixel-perfect
- * spacing.
- *
- * If you use pixels you can get more exact, but since there's no way to
- * determine the available height for the chart (plot area minus y-axis labels
- * minus legend), you're left in the dark on how high to make your "rest of
- * the space" axis. There's also no way to set the height of one axis and have
- * the other axis take the remaining space.
- *
- * So, as a workaround...I simply overlay the swimlanes on the top of a
- * full-height temperature chart. Then I draw a rectangle on top of y-axis
- * labels I want to hide so it appears to be on it's own.
- *
- * Helpful: https://www.highcharts.com/demo/renderer
- *
- * @return {object} The events list for the chart.
- */
-beestat.component.chart.runtime_sensor_detail.prototype.get_options_chart_events_ = function() {
- return {
- 'load': function() {
- this.renderer.rect(0, 0, 30, 80)
- .attr({
- 'fill': beestat.style.color.bluegray.base,
- 'zIndex': 10
- })
- .add();
- }
- };
-};
-
-/**
- * See comment on get_options_chart_events_. This is done separately to
- * override the normal load event rectangle draw because on export I also add
- * padding and a title which screws up the positioning a bit.
- *
- * @return {object} The events list for the chart on export.
- */
-beestat.component.chart.runtime_sensor_detail.prototype.get_options_exporting_chart_events_ = function() {
- return {
- 'load': function() {
- this.renderer.rect(beestat.style.size.gutter, 60, 30, 60)
- .attr({
- 'fill': beestat.style.color.bluegray.base,
- 'zIndex': 10
- })
- .add();
- }
- };
-};
diff --git a/js/component/chart/runtime_sensor_detail_occupancy.js b/js/component/chart/runtime_sensor_detail_occupancy.js
new file mode 100644
index 0000000..69d786d
--- /dev/null
+++ b/js/component/chart/runtime_sensor_detail_occupancy.js
@@ -0,0 +1,188 @@
+/**
+ * Runtime sensor detail chart.
+ *
+ * @param {object} data The chart data.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy = function(data) {
+ this.data_ = data;
+
+ beestat.component.chart.apply(this, arguments);
+};
+beestat.extend(beestat.component.chart.runtime_sensor_detail_occupancy, beestat.component.chart);
+
+/**
+ * Override for get_options_xAxis_labels_formatter_.
+ *
+ * @return {Function} xAxis labels formatter.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy.prototype.get_options_xAxis_labels_formatter_ = function() {
+ return function() {
+ return null;
+ };
+};
+
+/**
+ * Override for get_options_series_.
+ *
+ * @return {Array} All of the series to display on the chart.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy.prototype.get_options_series_ = function() {
+ var self = this;
+ var series = [];
+
+ var colors = [
+ beestat.style.color.blue.base,
+ beestat.style.color.red.base,
+ beestat.style.color.yellow.base,
+ beestat.style.color.green.base,
+ beestat.style.color.orange.base,
+ beestat.style.color.bluegreen.base,
+ beestat.style.color.purple.base,
+ beestat.style.color.lightblue.base,
+ beestat.style.color.blue.light,
+ beestat.style.color.red.light,
+ beestat.style.color.yellow.light,
+ beestat.style.color.green.light,
+ beestat.style.color.orange.light,
+ beestat.style.color.bluegreen.light,
+ beestat.style.color.purple.light,
+ beestat.style.color.lightblue.light,
+ beestat.style.color.blue.dark,
+ beestat.style.color.red.dark,
+ beestat.style.color.yellow.dark,
+ beestat.style.color.green.dark,
+ beestat.style.color.orange.dark,
+ beestat.style.color.bluegreen.dark,
+ beestat.style.color.purple.dark,
+ beestat.style.color.lightblue.dark
+ ];
+
+ // Sensors
+ this.data_.metadata.sensors.forEach(function(sensor, i) {
+ series.push({
+ 'name': 'occupancy_' + sensor.sensor_id,
+ 'data': self.data_.series['occupancy_' + sensor.sensor_id],
+ 'color': colors[i],
+ 'yAxis': 0,
+ 'type': 'line',
+ 'lineWidth': beestat.component.chart.runtime_sensor_detail_occupancy.get_swimlane_properties(self.data_.metadata.sensors.length, 1).line_width,
+ 'linecap': 'square',
+ 'className': 'crisp_edges'
+ });
+ });
+
+ series.push({
+ 'name': '',
+ 'data': self.data_.series.dummy,
+ 'yAxis': 0,
+ 'type': 'line',
+ 'lineWidth': 0
+ });
+
+ return series;
+};
+
+/**
+ * Override for get_options_yAxis_.
+ *
+ * @return {Array} The y-axis options.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy.prototype.get_options_yAxis_ = function() {
+ return [
+ {
+ 'min': 0,
+ 'max': 50,
+
+ // Keeps the chart from ending on a multiple of whatever the tick interval gets set to.
+ 'endOnTick': false,
+
+ // 'min': 0,
+ // 'minRange': 100,
+ // 'ceiling': 100
+
+ 'reversed': true,
+ 'gridLineWidth': 0,
+ 'title': {'text': null},
+ 'labels': {'enabled': false},
+ 'plotBands': [
+ {
+ 'zIndex': 2,
+ 'color': beestat.style.color.bluegray.dark,
+ 'from': 0,
+ 'to': 50
+ }
+ ]
+ }
+ ];
+};
+
+/**
+ * Get properties of swimlane series.
+ *
+ * @param {number} count The number of swimlanes present.
+ * @param {number} i Which swimlane this is.
+ *
+ * @return {Object} The swimlane line width and y position.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy.get_swimlane_properties = function(count, i) {
+ // Available height for all swimlanes
+ var height = 50;
+
+ // Some sensible max height if you have very few sensors.
+ var max_line_width = 16;
+
+ // Spacing. This is arbitrary...spacing decreases to 0 after you hit 15 sensors.
+ var spacing = Math.floor(15 / count);
+ spacing = Math.min(spacing, 4);
+
+ // Base line width is a percentage height of the container.
+ var line_width = Math.floor(height / count);
+
+ // Cap to a max line width.
+ line_width = Math.min(line_width, max_line_width);
+
+ // Set y, then shift it up slightly because the width expands out from the center.
+ var y = (line_width * i);
+ y += Math.round((line_width / 2));
+
+ // Make the lines slightly less tall to create space between them.
+ line_width -= spacing;
+
+ // Center within the swimlane area.
+ // var occupied_space = (line_width * count) + (spacing * count);
+ // var empty_space = height - occupied_space;
+ // y += (empty_space / 2);
+
+ return {
+ 'line_width': line_width,
+ 'y': y
+ };
+};
+
+/**
+ * Get the height of the chart. For really precise charts, make sure to
+ * include relevant spacing.
+ *
+ * @return {number} The height of the chart.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy.prototype.get_options_chart_height_ = function() {
+ return 50 + this.get_options_chart_spacing_()[0] + this.get_options_chart_spacing_()[2];
+};
+
+/**
+ * Get the legend enabled options.
+ *
+ * @return {Function} The legend enabled options.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy.prototype.get_options_legend_enabled_ = function() {
+ return false;
+};
+
+/**
+ * Get the left margin for the chart.
+ *
+ * @return {number} The left margin for the chart.
+ */
+beestat.component.chart.runtime_sensor_detail_occupancy.prototype.get_options_chart_marginLeft_ = function() {
+ return 40;
+};
diff --git a/js/component/chart/runtime_sensor_detail_temperature.js b/js/component/chart/runtime_sensor_detail_temperature.js
new file mode 100644
index 0000000..9e46a64
--- /dev/null
+++ b/js/component/chart/runtime_sensor_detail_temperature.js
@@ -0,0 +1,244 @@
+/**
+ * Runtime sensor detail chart.
+ *
+ * @param {object} data The chart data.
+ */
+beestat.component.chart.runtime_sensor_detail_temperature = function(data) {
+ this.data_ = data;
+
+ beestat.component.chart.apply(this, arguments);
+};
+beestat.extend(beestat.component.chart.runtime_sensor_detail_temperature, beestat.component.chart);
+
+/**
+ * Override for get_options_xAxis_labels_formatter_.
+ *
+ * @return {Function} xAxis labels formatter.
+ */
+beestat.component.chart.runtime_sensor_detail_temperature.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(' ');
+ };
+};
+
+beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_legend_labelFormatter_ = function() {
+ var self = this;
+ return function() {
+ return self.data_.metadata.series[this.name].name;
+ };
+};
+
+/**
+ * Override for get_options_series_.
+ *
+ * @return {Array} All of the series to display on the chart.
+ */
+beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_series_ = function() {
+ var self = this;
+ var series = [];
+
+ var colors = [
+ beestat.style.color.blue.base,
+ beestat.style.color.red.base,
+ beestat.style.color.yellow.base,
+ beestat.style.color.green.base,
+ beestat.style.color.orange.base,
+ beestat.style.color.bluegreen.base,
+ beestat.style.color.purple.base,
+ beestat.style.color.lightblue.base,
+ beestat.style.color.blue.light,
+ beestat.style.color.red.light,
+ beestat.style.color.yellow.light,
+ beestat.style.color.green.light,
+ beestat.style.color.orange.light,
+ beestat.style.color.bluegreen.light,
+ beestat.style.color.purple.light,
+ beestat.style.color.lightblue.light,
+ beestat.style.color.blue.dark,
+ beestat.style.color.red.dark,
+ beestat.style.color.yellow.dark,
+ beestat.style.color.green.dark,
+ beestat.style.color.orange.dark,
+ beestat.style.color.bluegreen.dark,
+ beestat.style.color.purple.dark,
+ beestat.style.color.lightblue.dark
+ ];
+
+ // Sensors
+ this.data_.metadata.sensors.forEach(function(sensor, i) {
+ series.push({
+ 'name': 'temperature_' + sensor.sensor_id,
+ 'data': self.data_.series['temperature_' + sensor.sensor_id],
+ 'color': colors[i],
+ 'yAxis': 0,
+ 'type': 'spline',
+ 'lineWidth': 1,
+ 'events': {
+ 'legendItemClick': function() {
+ // Delay the event dispatch so the series is actually toggled to the correct visibility.
+ setTimeout(function() {
+ self.dispatchEvent('legend_item_click');
+ }, 0);
+ }
+ }
+ });
+ });
+
+ series.push({
+ 'name': '',
+ 'data': self.data_.series.dummy,
+ 'yAxis': 0,
+ 'type': 'line',
+ 'lineWidth': 0,
+ 'showInLegend': false
+ });
+
+ return series;
+};
+
+/**
+ * Override for get_options_yAxis_.
+ *
+ * @return {Array} The y-axis options.
+ */
+beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_yAxis_ = function() {
+ return [
+ {
+ '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');
+ }
+ }
+ }
+ ];
+};
+
+/**
+ * Override for get_options_tooltip_formatter_.
+ *
+ * @return {Function} The tooltip formatter.
+ */
+beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_tooltip_formatter_ = function() {
+ var self = this;
+
+ return function() {
+ var sections = [];
+ var group = [];
+
+ // Get all the point values and index them by series_code for reference.
+ var values = {};
+ this.points.forEach(function(point) {
+ values[point.series.name] = point.y;
+
+ var occupancy_key = point.series.name.replace('temperature', 'occupancy');
+ if (self.data_.metadata.series[occupancy_key] !== undefined) {
+ values[occupancy_key] =
+ self.data_.metadata.series[occupancy_key].data[point.x.valueOf()];
+ }
+ });
+
+ /**
+ * Get a couple of other properties and index them by series_code for
+ * reference. This dives up to the chart itself because the tooltip shows
+ * all series unless explicitly disabled and those aren't always in the
+ * points array.
+ */
+ var colors = {};
+ var visible = {};
+ self.chart_.series.forEach(function(series) {
+ colors[series.name] = series.color;
+ visible[series.name] = series.visible;
+ });
+
+ for (var series_code in self.data_.series) {
+ var label;
+ var value;
+ var color;
+
+ if (visible[series_code] === true) {
+ label = self.data_.metadata.series[series_code].name;
+ color = colors[series_code];
+ if (values[series_code] === undefined) {
+ value = '-';
+ } else {
+ value = beestat.temperature({
+ 'temperature': values[series_code],
+ 'convert': false,
+ 'units': true
+ });
+ }
+
+ var occupancy_key = series_code.replace('temperature', 'occupancy');
+ if (values[occupancy_key] !== undefined && values[occupancy_key] !== null) {
+ value += ' ●';
+ }
+
+ group.push({
+ 'label': label,
+ 'value': value,
+ 'color': color
+ });
+ }
+ }
+
+ if (group.length === 0) {
+ group.push({
+ 'label': 'No data',
+ 'value': '',
+ 'color': beestat.style.color.gray.base
+ });
+ }
+
+ sections.push(group);
+
+ var title = this.x.format('ddd, MMM D @ h:mma');
+
+ return self.tooltip_formatter_helper_(
+ title,
+ sections
+ );
+ };
+};
+
+/**
+ * Get the tooltip positioner y value.
+ *
+ * @param {number} tooltip_width Tooltip width.
+ * @param {number} tooltip_height Tooltip height.
+ * @param {point} point Highcharts current point.
+ *
+ * @return {number} The tooltip y value.
+ */
+beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_tooltip_positioner_y_ = function() {
+ return 0;
+};
+
+/**
+ * Get the height of the chart.
+ *
+ * @return {number} The height of the chart.
+ */
+beestat.component.chart.runtime_sensor_detail_temperature.prototype.get_options_chart_height_ = function() {
+ return 300;
+};
diff --git a/js/component/chart/runtime_thermostat_detail_equipment.js b/js/component/chart/runtime_thermostat_detail_equipment.js
new file mode 100644
index 0000000..13d0cff
--- /dev/null
+++ b/js/component/chart/runtime_thermostat_detail_equipment.js
@@ -0,0 +1,131 @@
+/**
+ * Runtime sensor detail chart.
+ *
+ * @param {object} data The chart data.
+ */
+beestat.component.chart.runtime_thermostat_detail_equipment = function(data) {
+ this.data_ = data;
+
+ beestat.component.chart.apply(this, arguments);
+};
+beestat.extend(beestat.component.chart.runtime_thermostat_detail_equipment, beestat.component.chart);
+
+/**
+ * Override for get_options_xAxis_labels_formatter_.
+ *
+ * @return {Function} xAxis labels formatter.
+ */
+beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_xAxis_labels_formatter_ = function() {
+ return function() {
+ return null;
+ };
+};
+
+/**
+ * Override for get_options_series_.
+ *
+ * @return {Array} All of the series to display on the chart.
+ */
+beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_series_ = function() {
+ var self = this;
+ var series = [];
+
+ [
+ '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': 0,
+ 'type': 'line',
+ 'lineWidth': line_width,
+ 'linecap': 'square',
+ 'className': 'crisp_edges'
+ });
+ }
+ });
+
+ return series;
+};
+
+/**
+ * Override for get_options_yAxis_.
+ *
+ * @return {Array} The y-axis options.
+ */
+beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_yAxis_ = function() {
+ return [
+ {
+ 'min': 0,
+ 'max': 44,
+
+ // Keeps the chart from ending on a multiple of whatever the tick interval gets set to.
+ 'endOnTick': false,
+
+ 'reversed': true,
+ 'gridLineWidth': 0,
+ 'title': {'text': null},
+ 'labels': {'enabled': false}
+ }
+ ];
+};
+
+/**
+ * Get the height of the chart.
+ *
+ * @return {number} The height of the chart.
+ */
+beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_chart_height_ = function() {
+ return 44;
+};
+
+/**
+ * Get the legend enabled options.
+ *
+ * @return {Function} The legend enabled options.
+ */
+beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_legend_enabled_ = function() {
+ return false;
+};
+
+/**
+ * Get the left margin for the chart.
+ *
+ * @return {number} The left margin for the chart.
+ */
+beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_chart_marginLeft_ = function() {
+ return 40;
+};
diff --git a/js/component/chart/runtime_thermostat_detail.js b/js/component/chart/runtime_thermostat_detail_temperature.js
similarity index 82%
rename from js/component/chart/runtime_thermostat_detail.js
rename to js/component/chart/runtime_thermostat_detail_temperature.js
index 6901b51..2236ea7 100644
--- a/js/component/chart/runtime_thermostat_detail.js
+++ b/js/component/chart/runtime_thermostat_detail_temperature.js
@@ -3,19 +3,19 @@
*
* @param {object} data The chart data.
*/
-beestat.component.chart.runtime_thermostat_detail = function(data) {
+beestat.component.chart.runtime_thermostat_detail_temperature = function(data) {
this.data_ = data;
beestat.component.chart.apply(this, arguments);
};
-beestat.extend(beestat.component.chart.runtime_thermostat_detail, beestat.component.chart);
+beestat.extend(beestat.component.chart.runtime_thermostat_detail_temperature, beestat.component.chart);
/**
* Override for get_options_xAxis_labels_formatter_.
*
* @return {Function} xAxis labels formatter.
*/
-beestat.component.chart.runtime_thermostat_detail.prototype.get_options_xAxis_labels_formatter_ = function() {
+beestat.component.chart.runtime_thermostat_detail_temperature.prototype.get_options_xAxis_labels_formatter_ = function() {
var current_day;
var current_hour;
@@ -43,7 +43,7 @@ beestat.component.chart.runtime_thermostat_detail.prototype.get_options_xAxis_la
*
* @return {Array} All of the series to display on the chart.
*/
-beestat.component.chart.runtime_thermostat_detail.prototype.get_options_series_ = function() {
+beestat.component.chart.runtime_thermostat_detail_temperature.prototype.get_options_series_ = function() {
var self = this;
var series = [];
@@ -103,60 +103,10 @@ beestat.component.chart.runtime_thermostat_detail.prototype.get_options_series_
}
});
- // 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,
+ 'yAxis': 0,
'type': 'line',
'lineWidth': 0,
'showInLegend': false
@@ -170,7 +120,7 @@ beestat.component.chart.runtime_thermostat_detail.prototype.get_options_series_
*
* @return {Array} The y-axis options.
*/
-beestat.component.chart.runtime_thermostat_detail.prototype.get_options_yAxis_ = function() {
+beestat.component.chart.runtime_thermostat_detail_temperature.prototype.get_options_yAxis_ = function() {
/**
* Highcharts doesn't seem to respect axis behavior well so just overriding
* it completely here.
@@ -190,8 +140,6 @@ beestat.component.chart.runtime_thermostat_detail.prototype.get_options_yAxis_ =
return [
// Temperature
{
- 'height': '80%',
- 'top': '20%',
'gridLineColor': beestat.style.color.bluegray.light,
'gridLineDashStyle': 'longdash',
'title': {'text': null},
@@ -206,8 +154,6 @@ beestat.component.chart.runtime_thermostat_detail.prototype.get_options_yAxis_ =
// Humidity
{
- 'height': '80%',
- 'top': '20%',
'alignTicks': false,
'gridLineColor': null,
'opposite': true,
@@ -223,32 +169,21 @@ beestat.component.chart.runtime_thermostat_detail.prototype.get_options_yAxis_ =
'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_thermostat_detail.prototype.get_options_tooltip_formatter_ = function() {
+beestat.component.chart.runtime_thermostat_detail_temperature.prototype.get_options_tooltip_formatter_ = function() {
var self = this;
return function() {
+ var self2 = this;
+
var sections = [];
var groups = {
'mode': [],
@@ -256,6 +191,43 @@ beestat.component.chart.runtime_thermostat_detail.prototype.get_options_tooltip_
'equipment': []
};
+ // Add a bunch of fake points so they appear in the tooltip.
+ [
+ 'compressor_heat_1',
+ 'compressor_heat_2',
+ 'auxiliary_heat_1',
+ 'auxiliary_heat_2',
+ 'compressor_cool_1',
+ 'compressor_cool_2',
+ 'fan',
+ 'humidifier',
+ 'dehumidifier',
+ 'ventilator',
+ 'economizer',
+ '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(series_code) {
+ if (self.data_.metadata.series[series_code].data[self2.x.valueOf()] !== undefined) {
+ self2.points.push({
+ 'series': {
+ 'name': series_code,
+ 'color': beestat.series[series_code].color
+ },
+ 'x': self2.x,
+ 'y': self.data_.metadata.series[series_code].data[self2.x.valueOf()]
+ });
+ }
+ });
+
var values = {};
this.points.forEach(function(point) {
values[point.series.name] = point.y;
diff --git a/js/component/header.js b/js/component/header.js
index 76e1b74..46b1c20 100644
--- a/js/component/header.js
+++ b/js/component/header.js
@@ -26,38 +26,23 @@ beestat.component.header.prototype.decorate_ = function(parent) {
var pages;
- if (beestat.user.has_early_access() === true) {
- pages = [
- {
- 'layer': 'dashboard',
- 'text': 'Dashboard',
- 'icon': 'tablet_dashboard'
- },
- {
- 'layer': 'sensors',
- 'text': 'Sensors',
- 'icon': 'signal_variant'
- },
- {
- 'layer': 'home_comparisons',
- 'text': 'Comparisons',
- 'icon': 'home_group'
- }
- ];
- } else {
- pages = [
- {
- 'layer': 'dashboard',
- 'text': 'Dashboard',
- 'icon': 'tablet_dashboard'
- },
- {
- 'layer': 'home_comparisons',
- 'text': 'Home Comparisons',
- 'icon': 'home_group'
- }
- ];
- }
+ pages = [
+ {
+ 'layer': 'dashboard',
+ 'text': 'Dashboard',
+ 'icon': 'tablet_dashboard'
+ },
+ {
+ 'layer': 'sensors',
+ 'text': 'Sensors',
+ 'icon': 'signal_variant'
+ },
+ {
+ 'layer': 'home_comparisons',
+ 'text': 'Comparisons',
+ 'icon': 'home_group'
+ }
+ ];
var gutter = beestat.style.size.gutter;
diff --git a/js/component/modal/runtime_sensor_detail_custom.js b/js/component/modal/runtime_sensor_detail_custom.js
index e69de29..1cdaeb2 100644
--- a/js/component/modal/runtime_sensor_detail_custom.js
+++ b/js/component/modal/runtime_sensor_detail_custom.js
@@ -0,0 +1,374 @@
+/**
+ * Custom date range for the Runtime Detail chart.
+ */
+beestat.component.modal.runtime_sensor_detail_custom = function() {
+ beestat.component.modal.apply(this, arguments);
+ this.state_.runtime_sensor_detail_range_type = beestat.setting('runtime_sensor_detail_range_type');
+ this.state_.runtime_sensor_detail_range_dynamic = beestat.setting('runtime_sensor_detail_range_dynamic');
+ this.state_.runtime_sensor_detail_range_static_begin = beestat.setting('runtime_sensor_detail_range_static_begin');
+ this.state_.runtime_sensor_detail_range_static_end = beestat.setting('runtime_sensor_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_sensor_detail_custom, beestat.component.modal);
+
+/**
+ * Decorate.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_contents_ = function(parent) {
+ parent.appendChild($.createElement('p').innerHTML('Choose a custom range to display on the Sensor Detail chart. Max range is 7 days at a time and 30 days in the past. This limit will be raised in the future.'));
+
+ this.decorate_range_type_(parent);
+
+ if (this.state_.runtime_sensor_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_sensor_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_sensor_detail_range_type === 'dynamic'
+ ? beestat.style.color.lightblue.base
+ : beestat.style.color.bluegray.base
+ )
+ .set_text('Dynamic')
+ .addEventListener('click', function() {
+ self.state_.runtime_sensor_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_sensor_detail_range_type === 'static'
+ ? beestat.style.color.lightblue.base
+ : beestat.style.color.bluegray.base
+ )
+ .set_text('Static')
+ .addEventListener('click', function() {
+ self.state_.runtime_sensor_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_sensor_detail_custom.prototype.decorate_range_static_ = function(parent) {
+ var self = this;
+
+ var runtime_sensor_detail_static_range_begin;
+ var runtime_sensor_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, 'month')
+ );
+ var max = moment(thermostat.sync_end);
+
+ var begin = moment.min(
+ moment(runtime_sensor_detail_static_range_begin.get_value()),
+ moment(runtime_sensor_detail_static_range_end.get_value())
+ );
+
+ var end = moment.max(
+ moment(runtime_sensor_detail_static_range_begin.get_value() + ' 00:00:00'),
+ moment(runtime_sensor_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_sensor_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_sensor_detail_range_static_begin);
+
+ runtime_sensor_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_sensor_detail_static_range_end.get_value()), 'day')) + 1;
+ if (diff > 7) {
+ self.state_.error.max_range = true;
+ } else {
+ self.state_.error.max_range = false;
+ }
+
+ check_out_of_sync_range();
+
+ self.state_.runtime_sensor_detail_range_static_begin = value;
+ self.rerender();
+ } else {
+ self.state_.runtime_sensor_detail_range_static_begin = this.get_value();
+ self.state_.error.invalid_range_begin = true;
+ self.rerender();
+ }
+ });
+
+ runtime_sensor_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_sensor_detail_range_static_end);
+
+ runtime_sensor_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_sensor_detail_static_range_begin.get_value()), 'day')) + 1;
+ if (diff > 7) {
+ self.state_.error.max_range = true;
+ } else {
+ self.state_.error.max_range = false;
+ }
+
+ check_out_of_sync_range();
+
+ self.state_.runtime_sensor_detail_range_static_end = value;
+ self.rerender();
+ } else {
+ self.state_.runtime_sensor_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_sensor_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_sensor_detail_static_range_end.render(span);
+ column.appendChild(span);
+};
+
+/**
+ * Decorate the dynamic range input.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_range_dynamic_ = function(parent) {
+ var self = this;
+
+ var runtime_sensor_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': 1
+ })
+ .set_icon('pound')
+ .set_value(beestat.setting('runtime_sensor_detail_range_dynamic'));
+
+ runtime_sensor_detail_range_dynamic.addEventListener('blur', function() {
+ var value = parseInt(this.get_value(), 10);
+ if (isNaN(value) === true || value === 0) {
+ value = 1;
+ } else if (value > 7) {
+ value = 7;
+ }
+ this.set_value(value);
+ self.state_.runtime_sensor_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_sensor_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_sensor_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 7 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_sensor_detail_custom.prototype.get_title_ = function() {
+ return 'Sensor Detail - Custom Range';
+};
+
+/**
+ * Get the buttons that go on the bottom of this modal.
+ *
+ * @return {[beestat.component.button]} The buttons.
+ */
+beestat.component.modal.runtime_sensor_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_sensor_detail_range_static_begin).isAfter(moment(self.state_.runtime_sensor_detail_range_static_end)) === true) {
+ var temp = self.state_.runtime_sensor_detail_range_static_begin;
+ self.state_.runtime_sensor_detail_range_static_begin = self.state_.runtime_sensor_detail_range_static_end;
+ self.state_.runtime_sensor_detail_range_static_end = temp;
+ }
+
+ beestat.cache.delete('runtime_thermostat');
+ beestat.cache.delete('runtime_sensor');
+ beestat.setting(
+ {
+ 'runtime_sensor_detail_range_type': self.state_.runtime_sensor_detail_range_type,
+ 'runtime_sensor_detail_range_dynamic': self.state_.runtime_sensor_detail_range_dynamic,
+ 'runtime_sensor_detail_range_static_begin': self.state_.runtime_sensor_detail_range_static_begin,
+ 'runtime_sensor_detail_range_static_end': self.state_.runtime_sensor_detail_range_static_end
+ },
+ undefined,
+ function() {
+ self.dispose();
+ }
+ );
+ });
+ }
+
+ return [
+ cancel,
+ save
+ ];
+};
diff --git a/js/component/modal/runtime_thermostat_detail_custom.js b/js/component/modal/runtime_thermostat_detail_custom.js
index 0203c4b..d3e1d34 100644
--- a/js/component/modal/runtime_thermostat_detail_custom.js
+++ b/js/component/modal/runtime_thermostat_detail_custom.js
@@ -22,7 +22,7 @@ beestat.extend(beestat.component.modal.runtime_thermostat_detail_custom, beestat
* @param {rocket.Elements} parent
*/
beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_contents_ = function(parent) {
- parent.appendChild($.createElement('p').innerHTML('Choose a custom range to display on the Runtime Detail chart.'));
+ parent.appendChild($.createElement('p').innerHTML('Choose a custom range to display on the Thermostat Detail chart.'));
this.decorate_range_type_(parent);
@@ -301,7 +301,7 @@ beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_erro
* @return {string} Title
*/
beestat.component.modal.runtime_thermostat_detail_custom.prototype.get_title_ = function() {
- return 'Runtime Detail - Custom Range';
+ return 'Thermostat Detail - Custom Range';
};
/**
diff --git a/js/component/modal/runtime_thermostat_summary_custom.js b/js/component/modal/runtime_thermostat_summary_custom.js
index 2a30ad6..05e0d33 100644
--- a/js/component/modal/runtime_thermostat_summary_custom.js
+++ b/js/component/modal/runtime_thermostat_summary_custom.js
@@ -143,7 +143,7 @@ beestat.component.modal.runtime_thermostat_summary_custom.prototype.decorate_con
* @return {string} Title
*/
beestat.component.modal.runtime_thermostat_summary_custom.prototype.get_title_ = function() {
- return 'Runtime Summary - Custom Range';
+ return 'Thermostat Summary - Custom Range';
};
/**
diff --git a/js/js.php b/js/js.php
index 11a964a..d4dbcc5 100755
--- a/js/js.php
+++ b/js/js.php
@@ -32,6 +32,9 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
// Layer
echo '' . PHP_EOL;
@@ -65,8 +68,10 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
- echo '' . PHP_EOL;
- echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;