mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
Sensor Data 🤞
Finished outstanding features, fixed lots of bugs, enabled for the general population. #17
This commit is contained in:
parent
c45a5595e9
commit
36c24ffa92
244
js/beestat/runtime_sensor.js
Normal file
244
js/beestat/runtime_sensor.js
Normal file
@ -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;
|
||||
};
|
560
js/beestat/runtime_thermostat.js
Normal file
560
js/beestat/runtime_thermostat.js
Normal file
@ -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;
|
||||
};
|
22
js/beestat/sensor.js
Normal file
22
js/beestat/sensor.js
Normal file
@ -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;
|
||||
};
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
};
|
188
js/component/chart/runtime_sensor_detail_occupancy.js
Normal file
188
js/component/chart/runtime_sensor_detail_occupancy.js
Normal file
@ -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;
|
||||
};
|
244
js/component/chart/runtime_sensor_detail_temperature.js
Normal file
244
js/component/chart/runtime_sensor_detail_temperature.js
Normal file
@ -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;
|
||||
};
|
131
js/component/chart/runtime_thermostat_detail_equipment.js
Normal file
131
js/component/chart/runtime_thermostat_detail_equipment.js
Normal file
@ -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;
|
||||
};
|
@ -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;
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
];
|
||||
};
|
@ -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';
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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';
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -32,6 +32,9 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/beestat/get_sync_progress.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/user.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/ecobee.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/runtime_thermostat.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/runtime_sensor.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/sensor.js"></script>' . PHP_EOL;
|
||||
|
||||
// Layer
|
||||
echo '<script src="/js/layer.js"></script>' . PHP_EOL;
|
||||
@ -65,8 +68,10 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component/chart.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_thermostat_summary.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/temperature_profiles.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_thermostat_detail.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_sensor_detail.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_thermostat_detail_temperature.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_thermostat_detail_equipment.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_sensor_detail_temperature.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_sensor_detail_occupancy.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/header.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/icon.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/layout.js"></script>' . PHP_EOL;
|
||||
|
Loading…
x
Reference in New Issue
Block a user