mirror of
https://github.com/beestat/app.git
synced 2025-07-09 03:04:07 -04:00
A bunch of changes in preparation for sensor data.
The big stuff is done; sending this live to identify any issues early.
This commit is contained in:
parent
e5b1f5538f
commit
24020e300f
@ -176,6 +176,8 @@ class ecobee_sensor extends cora\crud {
|
|||||||
$attributes['name'] = $api_sensor['name'];
|
$attributes['name'] = $api_sensor['name'];
|
||||||
$attributes['type'] = $api_sensor['type'];
|
$attributes['type'] = $api_sensor['type'];
|
||||||
$attributes['in_use'] = $api_sensor['inUse'];
|
$attributes['in_use'] = $api_sensor['inUse'];
|
||||||
|
$attributes['identifier'] = $api_sensor['id'];
|
||||||
|
$attributes['capability'] = $api_sensor['capability'];
|
||||||
$attributes['inactive'] = 0;
|
$attributes['inactive'] = 0;
|
||||||
|
|
||||||
$attributes['temperature'] = null;
|
$attributes['temperature'] = null;
|
||||||
|
843
api/runtime.php
Normal file
843
api/runtime.php
Normal file
@ -0,0 +1,843 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All of the raw thermostat data sits here. Many millions of rows.
|
||||||
|
*
|
||||||
|
* @author Jon Ziebell
|
||||||
|
*/
|
||||||
|
class runtime extends cora\api {
|
||||||
|
|
||||||
|
public static $exposed = [
|
||||||
|
'private' => [
|
||||||
|
'sync',
|
||||||
|
'download'
|
||||||
|
],
|
||||||
|
'public' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
public static $cache = [
|
||||||
|
'sync' => 900 // 15 Minutes
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user_id column is not present on this table to reduce data overhead.
|
||||||
|
* No public reads are performed on this table so as long as thermostat_id
|
||||||
|
* is always used on reads in this class then this is fine.
|
||||||
|
*/
|
||||||
|
public static $user_locked = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function for syncing thermostat data. Looks at the current state of
|
||||||
|
* things and decides which direction (forwards or backwards) makes the most
|
||||||
|
* sense.
|
||||||
|
*
|
||||||
|
* @param int $thermostat_id Optional thermostat_id to sync. If not set will
|
||||||
|
* sync all thermostats attached to this user.
|
||||||
|
*/
|
||||||
|
public function sync($thermostat_id = null) {
|
||||||
|
// Skip this for the demo
|
||||||
|
if($this->setting->is_demo() === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_time_limit(0);
|
||||||
|
|
||||||
|
if($thermostat_id === null) {
|
||||||
|
$thermostat_ids = array_keys(
|
||||||
|
$this->api(
|
||||||
|
'thermostat',
|
||||||
|
'read_id',
|
||||||
|
[
|
||||||
|
'attributes' => [
|
||||||
|
'inactive' => 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->user_lock($thermostat_id);
|
||||||
|
$thermostat_ids = [$thermostat_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($thermostat_ids as $thermostat_id) {
|
||||||
|
// Get a lock to ensure that this is not invoked more than once at a time
|
||||||
|
// per thermostat.
|
||||||
|
$lock_name = 'runtime->sync(' . $thermostat_id . ')';
|
||||||
|
$this->database->get_lock($lock_name);
|
||||||
|
|
||||||
|
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||||
|
|
||||||
|
if(
|
||||||
|
$thermostat['sync_begin'] === $thermostat['first_connected'] ||
|
||||||
|
(
|
||||||
|
$thermostat['sync_begin'] !== null &&
|
||||||
|
strtotime($thermostat['sync_begin']) <= strtotime('-1 year')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$this->sync_forwards($thermostat_id);
|
||||||
|
} else {
|
||||||
|
$this->sync_backwards($thermostat_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only syncing one thermostat this will delay the sync of the other
|
||||||
|
// thermostat. Not a huge deal, just FYI.
|
||||||
|
$this->api(
|
||||||
|
'user',
|
||||||
|
'update_sync_status',
|
||||||
|
[
|
||||||
|
'key' => 'runtime'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->database->release_lock($lock_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync backwards. When running for the first time it will sync from now all
|
||||||
|
* the way back to the first connected date. If it is called again it will
|
||||||
|
* check to see if a full backwards sync has already completed. If it has,
|
||||||
|
* it will throw an exception. If not, it will resume the backwards sync.
|
||||||
|
*
|
||||||
|
* @param int $thermostat_id
|
||||||
|
*/
|
||||||
|
private function sync_backwards($thermostat_id) {
|
||||||
|
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||||
|
|
||||||
|
if($thermostat['sync_begin'] === $thermostat['first_connected']) {
|
||||||
|
throw new \Exception('Full sync already performed; must call sync_forwards() now.', 10200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($thermostat['sync_begin'] === null) {
|
||||||
|
// Sync from when the thermostat was first connected until now.
|
||||||
|
$sync_begin = strtotime($thermostat['first_connected']);
|
||||||
|
$sync_end = time();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Sync from when the thermostat was first connected until sync_end.
|
||||||
|
$sync_begin = strtotime($thermostat['first_connected']);
|
||||||
|
$sync_end = strtotime($thermostat['sync_begin']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only sync up to the past year of data. Outside of this there won't even
|
||||||
|
// be a partition for the data to go into.
|
||||||
|
$sync_begin = max(strtotime('-1 year'), $sync_begin);
|
||||||
|
|
||||||
|
$chunk_begin = $sync_end;
|
||||||
|
$chunk_end = $sync_end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop over the dates and do the actual sync. Each chunk is wrapped in a
|
||||||
|
* transaction for a little bit of protection against exceptions
|
||||||
|
* introducing bad data and causing the whole sync to fail. Commit any
|
||||||
|
* open transactions first though.
|
||||||
|
*/
|
||||||
|
$this->database->commit_transaction();
|
||||||
|
do {
|
||||||
|
$this->database->start_transaction();
|
||||||
|
|
||||||
|
$chunk_begin = strtotime('-1 week', $chunk_end);
|
||||||
|
$chunk_begin = max($chunk_begin, $sync_begin);
|
||||||
|
|
||||||
|
$this->sync_($thermostat['thermostat_id'], $chunk_begin, $chunk_end);
|
||||||
|
|
||||||
|
// Update the thermostat with the current sync range
|
||||||
|
$this->api(
|
||||||
|
'thermostat',
|
||||||
|
'update',
|
||||||
|
[
|
||||||
|
'attributes' => [
|
||||||
|
'thermostat_id' => $thermostat['thermostat_id'],
|
||||||
|
'sync_begin' => date('Y-m-d H:i:s', $chunk_begin),
|
||||||
|
'sync_end' => date(
|
||||||
|
'Y-m-d H:i:s',
|
||||||
|
max(
|
||||||
|
$sync_end,
|
||||||
|
strtotime($thermostat['sync_end'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Populate on the fly.
|
||||||
|
$this->api(
|
||||||
|
'runtime_thermostat_summary',
|
||||||
|
'populate',
|
||||||
|
$thermostat_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Because I am doing day-level syncing this will end up fetching an
|
||||||
|
// overlapping day of data every time. But if I properly switch this to
|
||||||
|
// interval-level syncing this should be correct or at the very least
|
||||||
|
// return a minimal one extra row of data.
|
||||||
|
$chunk_end = $chunk_begin;
|
||||||
|
|
||||||
|
$this->database->commit_transaction();
|
||||||
|
} while ($chunk_begin > $sync_begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync forwards from thermostat.sync_end until now. This should be used for
|
||||||
|
* all syncs except the first one.
|
||||||
|
*
|
||||||
|
* @param int $thermostat_id
|
||||||
|
*/
|
||||||
|
private function sync_forwards($thermostat_id) {
|
||||||
|
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||||
|
|
||||||
|
// Sync from the last sync time until now. Go a couple hours back in time to
|
||||||
|
// cover that 1 hour delay.
|
||||||
|
$sync_begin = strtotime($thermostat['sync_end'] . ' -2 hours');
|
||||||
|
$sync_end = time();
|
||||||
|
|
||||||
|
$chunk_begin = $sync_begin;
|
||||||
|
$chunk_end = $sync_begin;
|
||||||
|
|
||||||
|
// Loop over the dates and do the actual sync. Each chunk is wrapped in a
|
||||||
|
// transaction for a little bit of protection against exceptions introducing
|
||||||
|
// bad data and causing the whole sync to fail.
|
||||||
|
do {
|
||||||
|
$this->database->start_transaction();
|
||||||
|
|
||||||
|
$chunk_end = strtotime('+1 week', $chunk_begin);
|
||||||
|
$chunk_end = min($chunk_end, $sync_end);
|
||||||
|
|
||||||
|
$this->sync_($thermostat['thermostat_id'], $chunk_begin, $chunk_end);
|
||||||
|
|
||||||
|
// Update the thermostat with the current sync range
|
||||||
|
$this->api(
|
||||||
|
'thermostat',
|
||||||
|
'update',
|
||||||
|
[
|
||||||
|
'attributes' => [
|
||||||
|
'thermostat_id' => $thermostat['thermostat_id'],
|
||||||
|
'sync_end' => date('Y-m-d H:i:s', $chunk_end)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$chunk_begin = strtotime('+1 day', $chunk_end);
|
||||||
|
|
||||||
|
$this->database->commit_transaction();
|
||||||
|
} while ($chunk_end < $sync_end);
|
||||||
|
|
||||||
|
// Populate at the end of a full sync forwards.
|
||||||
|
$this->api(
|
||||||
|
'runtime_thermostat_summary',
|
||||||
|
'populate',
|
||||||
|
$thermostat_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the runtime report data for a specified thermostat. Originally this
|
||||||
|
* was basically a 1:1 import from ecobee. But due to the size of the data
|
||||||
|
* this now does some fancy logic to merge certain columns down and alter
|
||||||
|
* their data types just a bit.
|
||||||
|
*
|
||||||
|
* @param int $thermostat_id
|
||||||
|
* @param int $begin
|
||||||
|
* @param int $end
|
||||||
|
*/
|
||||||
|
private function sync_($thermostat_id, $begin, $end) {
|
||||||
|
$this->user_lock($thermostat_id);
|
||||||
|
|
||||||
|
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||||
|
$ecobee_thermostat = $this->api('ecobee_thermostat', 'get', $thermostat['ecobee_thermostat_id']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round begin/end down to the next 5 minutes. This keep things tidy.
|
||||||
|
* Without this, I would query for rows between 10:35:16, for example, and
|
||||||
|
* not get the row at 10:35:00. But the interval function would return it
|
||||||
|
* and then duplicate databse entry.
|
||||||
|
*/
|
||||||
|
$begin = floor($begin / 300) * 300;
|
||||||
|
$end = floor($end / 300) * 300;
|
||||||
|
|
||||||
|
$begin_interval = $this->get_interval($begin);
|
||||||
|
$end_interval = $this->get_interval($end);
|
||||||
|
|
||||||
|
$begin_date = date('Y-m-d', $begin);
|
||||||
|
$end_date = date('Y-m-d', $end);
|
||||||
|
|
||||||
|
$response = $this->api(
|
||||||
|
'ecobee',
|
||||||
|
'ecobee_api',
|
||||||
|
[
|
||||||
|
'method' => 'GET',
|
||||||
|
'endpoint' => 'runtimeReport',
|
||||||
|
'arguments' => [
|
||||||
|
'body' => json_encode([
|
||||||
|
'selection' => [
|
||||||
|
'selectionType' => 'thermostats',
|
||||||
|
'selectionMatch' => $ecobee_thermostat['identifier']
|
||||||
|
],
|
||||||
|
'startDate' => $begin_date,
|
||||||
|
'endDate' => $end_date,
|
||||||
|
'startInterval' => $begin_interval,
|
||||||
|
'endInterval' => $end_interval,
|
||||||
|
'columns' => implode(
|
||||||
|
',',
|
||||||
|
[
|
||||||
|
'compCool1', // compressor_1
|
||||||
|
'compCool2', // compressor_2
|
||||||
|
'compHeat1', // compressor_1
|
||||||
|
'compHeat2', // compressor_2
|
||||||
|
'auxHeat1', // auxiliary_heat_1
|
||||||
|
'auxHeat2', // auxiliary_heat_2
|
||||||
|
'fan', // fan
|
||||||
|
'humidifier', // accessory
|
||||||
|
'dehumidifier', // accessory
|
||||||
|
'ventilator', // accessory
|
||||||
|
'economizer', // accessory
|
||||||
|
'hvacMode', // system_mode
|
||||||
|
'zoneAveTemp', // indoor_temperature
|
||||||
|
'zoneHumidity', // indoor_humidity
|
||||||
|
'outdoorTemp', // outdoor_temperature
|
||||||
|
'outdoorHumidity', // outdoor_humidity
|
||||||
|
'zoneCalendarEvent', // event_runtime_thermostat_text_id
|
||||||
|
'zoneClimate', // climate_runtime_thermostat_text_id
|
||||||
|
'zoneCoolTemp', // setpoint_cool
|
||||||
|
'zoneHeatTemp' // setpoint_heat
|
||||||
|
]
|
||||||
|
),
|
||||||
|
'includeSensors' => true
|
||||||
|
])
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
$this->sync_runtime_thermostat($thermostat, $response);
|
||||||
|
// $this->sync_runtime_sensor($thermostat, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sync_runtime_thermostat($thermostat, $response) {
|
||||||
|
/**
|
||||||
|
* Read any existing rows from the database so we know if this is an
|
||||||
|
* insert or an update. Note that even though I have $begin and $end
|
||||||
|
* already defined, I always look in the database according to what ecobee
|
||||||
|
* actually returned just in case the returned data goes outside of what I
|
||||||
|
* requested for some reason.
|
||||||
|
*/
|
||||||
|
$ecobee_columns = array_merge(['date', 'time'], explode(',', $response['columns']));
|
||||||
|
$columns_begin = $this->get_columns(
|
||||||
|
$response['reportList'][0]['rowList'][0],
|
||||||
|
$ecobee_columns
|
||||||
|
);
|
||||||
|
$columns_end = $this->get_columns(
|
||||||
|
$response['reportList'][0]['rowList'][count($response['reportList'][0]['rowList']) - 1],
|
||||||
|
$ecobee_columns
|
||||||
|
);
|
||||||
|
|
||||||
|
$existing_rows = $this->database->read(
|
||||||
|
'runtime_thermostat',
|
||||||
|
[
|
||||||
|
'thermostat_id' => $thermostat['thermostat_id'],
|
||||||
|
'timestamp' => [
|
||||||
|
'value' => [
|
||||||
|
$this->get_utc_datetime(
|
||||||
|
date(
|
||||||
|
'Y-m-d H:i:s',
|
||||||
|
strtotime($columns_begin['date'] . ' ' . $columns_begin['time'] . ' -1 hour')
|
||||||
|
),
|
||||||
|
$thermostat['time_zone']
|
||||||
|
),
|
||||||
|
$this->get_utc_datetime(
|
||||||
|
date(
|
||||||
|
'Y-m-d H:i:s',
|
||||||
|
strtotime($columns_end['date'] . ' ' . $columns_end['time'] . ' +1 hour')
|
||||||
|
),
|
||||||
|
$thermostat['time_zone']
|
||||||
|
)
|
||||||
|
],
|
||||||
|
'operator' => 'between'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$existing_timestamps = [];
|
||||||
|
foreach($existing_rows as $existing_row) {
|
||||||
|
$existing_timestamps[$existing_row['timestamp']] = $existing_row['runtime_thermostat_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over the ecobee data
|
||||||
|
foreach ($response['reportList'][0]['rowList'] as $row) {
|
||||||
|
$columns = $this->get_columns(
|
||||||
|
$row,
|
||||||
|
$ecobee_columns
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If any of these values are null just throw the whole row away. This
|
||||||
|
* should very rarely happen as ecobee reports 0 values. The driver of
|
||||||
|
* this is the summary table...trying to aggregate sums for these values
|
||||||
|
* is easy, but in order to accurately represent those sums I need to
|
||||||
|
* know how many rows were included. I would have to store one count per
|
||||||
|
* sum in the summary table to manage that...or just not store the data
|
||||||
|
* to begin with.
|
||||||
|
*
|
||||||
|
* Also threw in null checks on a bunch of other fields just to simplify
|
||||||
|
* the code later on. This happens so rarely that throwing away a whole
|
||||||
|
* row for a null value shouldn't have any noticeable negative effect.
|
||||||
|
*
|
||||||
|
* Note: Don't ignore zoneHeatTemp or zoneCoolTemp. Sometimes those are
|
||||||
|
* legit null as the thermostat may be for heat/cool only (see #160).
|
||||||
|
*/
|
||||||
|
if(
|
||||||
|
$columns['HVACmode'] === null ||
|
||||||
|
$columns['zoneAveTemp'] === null ||
|
||||||
|
$columns['zoneHumidity'] === null ||
|
||||||
|
$columns['outdoorTemp'] === null ||
|
||||||
|
$columns['outdoorHumidity'] === null ||
|
||||||
|
$columns['compHeat1'] === null ||
|
||||||
|
$columns['compHeat2'] === null ||
|
||||||
|
$columns['compCool1'] === null ||
|
||||||
|
$columns['compCool2'] === null ||
|
||||||
|
$columns['auxHeat1'] === null ||
|
||||||
|
$columns['auxHeat2'] === null ||
|
||||||
|
$columns['fan'] === null ||
|
||||||
|
$columns['humidifier'] === null ||
|
||||||
|
$columns['dehumidifier'] === null ||
|
||||||
|
$columns['ventilator'] === null ||
|
||||||
|
$columns['economizer'] === null
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date and time are first two columns of the returned data. It is
|
||||||
|
// returned in thermostat time, so convert it to UTC first.
|
||||||
|
$timestamp = $this->get_utc_datetime(
|
||||||
|
$columns['date'] . ' ' . $columns['time'],
|
||||||
|
$thermostat['time_zone']
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$data['thermostat_id'] = $thermostat['thermostat_id'];
|
||||||
|
$data['timestamp'] = $timestamp;
|
||||||
|
|
||||||
|
if ($columns['compCool1'] > 0 || $columns['compCool2'] > 0) {
|
||||||
|
$data['compressor_mode'] = 'cool';
|
||||||
|
$data['compressor_1'] = $columns['compCool1'] - $columns['compCool2'];
|
||||||
|
$data['compressor_2'] = $columns['compCool2'];
|
||||||
|
} else if ($columns['compHeat1'] > 0 || $columns['compHeat2'] > 0) {
|
||||||
|
$data['compressor_mode'] = 'heat';
|
||||||
|
$data['compressor_1'] = $columns['compHeat1'] - $columns['compHeat2'];
|
||||||
|
$data['compressor_2'] = $columns['compHeat2'];
|
||||||
|
} else {
|
||||||
|
$data['compressor_mode'] = 'off';
|
||||||
|
$data['compressor_1'] = 0;
|
||||||
|
$data['compressor_2'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['auxiliary_heat_1'] = $columns['auxHeat1'] - $columns['auxHeat2'];
|
||||||
|
$data['auxiliary_heat_2'] = $columns['auxHeat2'];
|
||||||
|
|
||||||
|
if($columns['humidifier'] > 0) {
|
||||||
|
$data['accessory_type'] = 'humidifier';
|
||||||
|
$data['accessory'] = $columns['humidifier'];
|
||||||
|
} else if($columns['dehumidifier'] > 0) {
|
||||||
|
$data['accessory_type'] = 'dehumidifier';
|
||||||
|
$data['accessory'] = $columns['dehumidifier'];
|
||||||
|
} else if($columns['ventilator'] > 0) {
|
||||||
|
$data['accessory_type'] = 'ventilator';
|
||||||
|
$data['accessory'] = $columns['ventilator'];
|
||||||
|
} else if($columns['economizer'] > 0) {
|
||||||
|
$data['accessory_type'] = 'economizer';
|
||||||
|
$data['accessory'] = $columns['economizer'];
|
||||||
|
} else {
|
||||||
|
$data['accessory_type'] = 'off';
|
||||||
|
$data['accessory'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ecobee does not report fan usage when it does not control the fan, so
|
||||||
|
// this will mark the fan as running when certain equipment is on.
|
||||||
|
$data['fan'] = max(
|
||||||
|
$columns['fan'],
|
||||||
|
$data['compressor_1'],
|
||||||
|
$data['compressor_2'],
|
||||||
|
$data['auxiliary_heat_1'],
|
||||||
|
$data['auxiliary_heat_2'],
|
||||||
|
$data['accessory']
|
||||||
|
);
|
||||||
|
|
||||||
|
$system_modes = [
|
||||||
|
'auto' => 'auto',
|
||||||
|
'cool' => 'cool',
|
||||||
|
'heat' => 'heat',
|
||||||
|
'auxHeatOnly' => 'auxiliary_heat',
|
||||||
|
'off' => 'off'
|
||||||
|
];
|
||||||
|
$data['system_mode'] = $system_modes[$columns['HVACmode']];
|
||||||
|
|
||||||
|
$data['indoor_temperature'] = $columns['zoneAveTemp'] * 10;
|
||||||
|
$data['indoor_humidity'] = round($columns['zoneHumidity']);
|
||||||
|
|
||||||
|
$data['outdoor_temperature'] = $columns['outdoorTemp'] * 10;
|
||||||
|
$data['outdoor_humidity'] = round($columns['outdoorHumidity']);
|
||||||
|
|
||||||
|
$data['event_runtime_thermostat_text_id'] = $this->api(
|
||||||
|
'runtime_thermostat_text',
|
||||||
|
'get_create',
|
||||||
|
$columns['zoneCalendarEvent']
|
||||||
|
)['runtime_thermostat_text_id'];
|
||||||
|
$data['climate_runtime_thermostat_text_id'] = $this->api(
|
||||||
|
'runtime_thermostat_text',
|
||||||
|
'get_create',
|
||||||
|
$columns['zoneClimate']
|
||||||
|
)['runtime_thermostat_text_id'];
|
||||||
|
|
||||||
|
$data['setpoint_cool'] = $columns['zoneCoolTemp'] * 10;
|
||||||
|
$data['setpoint_heat'] = $columns['zoneHeatTemp'] * 10;
|
||||||
|
|
||||||
|
// Create or update the database
|
||||||
|
if(isset($existing_timestamps[$timestamp]) === true) {
|
||||||
|
$data['runtime_thermostat_id'] = $existing_timestamps[$timestamp];
|
||||||
|
$this->database->update('runtime_thermostat', $data, 'id');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$existing_timestamps[$timestamp] = $this->database->create('runtime_thermostat', $data, 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sync_runtime_sensor($thermostat, $response) {
|
||||||
|
/**
|
||||||
|
* Read any existing rows from the database so we know if this is an
|
||||||
|
* insert or an update. Note that even though I have $begin and $end
|
||||||
|
* already defined, I always look in the database according to what ecobee
|
||||||
|
* actually returned just in case the returned data goes outside of what I
|
||||||
|
* requested for some reason.
|
||||||
|
*/
|
||||||
|
$ecobee_columns = $response['sensorList'][0]['columns'];
|
||||||
|
$columns_begin = $this->get_columns(
|
||||||
|
$response['sensorList'][0]['data'][0],
|
||||||
|
$response['sensorList'][0]['columns']
|
||||||
|
);
|
||||||
|
$columns_end = $this->get_columns(
|
||||||
|
$response['sensorList'][0]['data'][count($response['sensorList'][0]['data']) - 1],
|
||||||
|
$ecobee_columns
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get a list of sensors
|
||||||
|
$sensors = $this->api(
|
||||||
|
'sensor',
|
||||||
|
'read',
|
||||||
|
[
|
||||||
|
'attributes' => [
|
||||||
|
'thermostat_id' => $thermostat['thermostat_id']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get a list of sensors indexed by code
|
||||||
|
$sensors_by_identifier = [];
|
||||||
|
foreach($sensors as $sensor) {
|
||||||
|
$sensors_by_identifier[$sensor['identifier']] = $sensor;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing_rows = $this->database->read(
|
||||||
|
'runtime_sensor',
|
||||||
|
[
|
||||||
|
'sensor_id' => array_column($sensors, 'sensor_id'),
|
||||||
|
'timestamp' => [
|
||||||
|
'value' => [
|
||||||
|
$this->get_utc_datetime(
|
||||||
|
date(
|
||||||
|
'Y-m-d H:i:s',
|
||||||
|
strtotime($columns_begin['date'] . ' ' . $columns_begin['time'] . ' -1 hour')
|
||||||
|
),
|
||||||
|
$thermostat['time_zone']
|
||||||
|
),
|
||||||
|
$this->get_utc_datetime(
|
||||||
|
date(
|
||||||
|
'Y-m-d H:i:s',
|
||||||
|
strtotime($columns_end['date'] . ' ' . $columns_end['time'] . ' +1 hour')
|
||||||
|
),
|
||||||
|
$thermostat['time_zone']
|
||||||
|
)
|
||||||
|
],
|
||||||
|
'operator' => 'between'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$existing_timestamps = [];
|
||||||
|
foreach($existing_rows as $existing_row) {
|
||||||
|
if (isset($existing_timestamps[$existing_row['sensor_id']]) === false) {
|
||||||
|
$existing_timestamps[$existing_row['sensor_id']] = [];
|
||||||
|
}
|
||||||
|
$existing_timestamps[$existing_row['sensor_id']][$existing_row['timestamp']] = $existing_row['runtime_sensor_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over the ecobee data. Ecobee if you're reading this please don't
|
||||||
|
// format your identifiers like this in the future.
|
||||||
|
foreach ($response['sensorList'][0]['data'] as $row) {
|
||||||
|
$columns = $this->get_columns(
|
||||||
|
$row,
|
||||||
|
$ecobee_columns
|
||||||
|
);
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
foreach ($columns as $key => $value) {
|
||||||
|
if ($key === 'date' || $key === 'time') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sensor_identifier = substr($key, 0, strrpos($key, ':'));
|
||||||
|
$capability_identifier = substr($key, strrpos($key, ':') + 1);
|
||||||
|
|
||||||
|
$sensor = $sensors_by_identifier[$sensor_identifier];
|
||||||
|
$sensor_id = $sensors_by_identifier[$sensor_identifier]['sensor_id'];
|
||||||
|
|
||||||
|
if (isset($datas[$sensor['sensor_id']]) === false) {
|
||||||
|
$datas[$sensor['sensor_id']] = [
|
||||||
|
'sensor_id' => $sensor['sensor_id'],
|
||||||
|
'timestamp' => $this->get_utc_datetime(
|
||||||
|
$columns['date'] . ' ' . $columns['time'],
|
||||||
|
$thermostat['time_zone']
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($sensor['capability'] as $capability) {
|
||||||
|
if(
|
||||||
|
$capability['id'] == $capability_identifier &&
|
||||||
|
in_array($capability['type'], ['temperature', 'occupancy']) === true
|
||||||
|
) {
|
||||||
|
$datas[$sensor['sensor_id']][$capability['type']] = ($capability['type'] === 'temperature') ? ($value * 10) : $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the database
|
||||||
|
foreach ($datas as $data) {
|
||||||
|
if(isset($existing_timestamps[$data['sensor_id']][$data['timestamp']]) === true) {
|
||||||
|
$data['runtime_sensor_id'] = $existing_timestamps[$data['sensor_id']][$data['timestamp']];
|
||||||
|
$this->database->update('runtime_sensor', $data, 'id');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$existing_timestamps[$data['sensor_id']][$data['timestamp']] = $this->database->create('runtime_sensor', $data, 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ecobee "interval" value from a timestamp.
|
||||||
|
*
|
||||||
|
* @param string $timestamp The timestamp.
|
||||||
|
*
|
||||||
|
* @return int The interval.
|
||||||
|
*/
|
||||||
|
private function get_interval($timestamp) {
|
||||||
|
$hours = date('G', $timestamp);
|
||||||
|
$minutes = date('i', $timestamp);
|
||||||
|
|
||||||
|
return ($hours * 12) + floor($minutes / 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string CSV row to cleaned up array of columns.
|
||||||
|
*
|
||||||
|
* @param string $row The CSV row string.
|
||||||
|
* @param array $columns The column headers.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function get_columns($row, $columns) {
|
||||||
|
$return = [];
|
||||||
|
|
||||||
|
// Explode and remove trailing comma if present.
|
||||||
|
if (substr($row, -1, 1) === ',') {
|
||||||
|
$row = substr($row, 0, -1);
|
||||||
|
}
|
||||||
|
$data = explode(',', $row);
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($data); $i++) {
|
||||||
|
$data[$i] = trim($data[$i]);
|
||||||
|
$return[$columns[$i]] = $data[$i] === '' ? null : $data[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a local datetime string to a UTC datetime string.
|
||||||
|
*
|
||||||
|
* @param string $local_datetime Local datetime string.
|
||||||
|
* @param string $local_time_zone The local time zone to convert from.
|
||||||
|
*
|
||||||
|
* @return string The UTC datetime string.
|
||||||
|
*/
|
||||||
|
private function get_utc_datetime($local_datetime, $local_time_zone) {
|
||||||
|
$local_time_zone = new DateTimeZone($local_time_zone);
|
||||||
|
$utc_time_zone = new DateTimeZone('UTC');
|
||||||
|
$date_time = new DateTime($local_datetime, $local_time_zone);
|
||||||
|
$date_time->setTimezone($utc_time_zone);
|
||||||
|
|
||||||
|
return $date_time->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a UTC datetime string to a UTC datetime string.
|
||||||
|
*
|
||||||
|
* @param string $utc_datetime Local datetime string.
|
||||||
|
* @param string $local_time_zone The local time zone to convert from.
|
||||||
|
*
|
||||||
|
* @return string The UTC datetime string.
|
||||||
|
*/
|
||||||
|
private function get_local_datetime($utc_datetime, $local_time_zone) {
|
||||||
|
$local_time_zone = new DateTimeZone($local_time_zone);
|
||||||
|
$utc_time_zone = new DateTimeZone('UTC');
|
||||||
|
$date_time = new DateTime($utc_datetime, $utc_time_zone);
|
||||||
|
$date_time->setTimezone($local_time_zone);
|
||||||
|
|
||||||
|
return $date_time->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download all data that exists for a specific thermostat.
|
||||||
|
*
|
||||||
|
* @param int $thermostat_id
|
||||||
|
* @param string $download_begin Optional; the date to begin the download.
|
||||||
|
* @param string $download_end Optional; the date to end the download.
|
||||||
|
*/
|
||||||
|
public function download($thermostat_id, $download_begin = null, $download_end = null) {
|
||||||
|
set_time_limit(120);
|
||||||
|
|
||||||
|
$this->user_lock($thermostat_id);
|
||||||
|
|
||||||
|
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||||
|
$ecobee_thermostat = $this->api(
|
||||||
|
'ecobee_thermostat',
|
||||||
|
'get',
|
||||||
|
$thermostat['ecobee_thermostat_id']
|
||||||
|
);
|
||||||
|
|
||||||
|
if($download_begin === null) {
|
||||||
|
$download_begin = strtotime($thermostat['first_connected']);
|
||||||
|
} else {
|
||||||
|
$download_begin = strtotime($download_begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($download_end === null) {
|
||||||
|
$download_end = time();
|
||||||
|
} else {
|
||||||
|
$download_end = strtotime($download_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunk_begin = $download_begin;
|
||||||
|
$chunk_end = $download_begin;
|
||||||
|
|
||||||
|
$bytes = 0;
|
||||||
|
|
||||||
|
$output = fopen('php://output', 'w');
|
||||||
|
$needs_header = true;
|
||||||
|
do {
|
||||||
|
$chunk_end = strtotime('+1 week', $chunk_begin);
|
||||||
|
$chunk_end = min($chunk_end, $download_end);
|
||||||
|
|
||||||
|
$runtime_thermostats = $this->database->read(
|
||||||
|
'runtime_thermostat',
|
||||||
|
[
|
||||||
|
'thermostat_id' => $thermostat_id,
|
||||||
|
'timestamp' => [
|
||||||
|
'value' => [date('Y-m-d H:i:s', $chunk_begin), date('Y-m-d H:i:s', $chunk_end)] ,
|
||||||
|
'operator' => 'between'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
'timestamp' // order by
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the appropriate runtime_thermostat_texts.
|
||||||
|
$runtime_thermostat_text_ids = array_unique(array_merge(
|
||||||
|
array_column($runtime_thermostats, 'event_runtime_thermostat_text_id'),
|
||||||
|
array_column($runtime_thermostats, 'climate_runtime_thermostat_text_id')
|
||||||
|
));
|
||||||
|
$runtime_thermostat_texts = $this->api(
|
||||||
|
'runtime_thermostat_text',
|
||||||
|
'read_id',
|
||||||
|
[
|
||||||
|
'attributes' => [
|
||||||
|
'runtime_thermostat_text_id' => $runtime_thermostat_text_ids
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if ($needs_header === true && count($runtime_thermostats) > 0) {
|
||||||
|
$headers = array_keys($runtime_thermostats[0]);
|
||||||
|
|
||||||
|
// Remove the IDs and rename two columns.
|
||||||
|
unset($headers[array_search('runtime_thermostat_id', $headers)]);
|
||||||
|
unset($headers[array_search('thermostat_id', $headers)]);
|
||||||
|
$headers[array_search('event_runtime_thermostat_text_id', $headers)] = 'event';
|
||||||
|
$headers[array_search('climate_runtime_thermostat_text_id', $headers)] = 'climate';
|
||||||
|
|
||||||
|
$bytes += fputcsv($output, $headers);
|
||||||
|
$needs_header = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($runtime_thermostats as $runtime_thermostat) {
|
||||||
|
unset($runtime_thermostat['runtime_thermostat_id']);
|
||||||
|
unset($runtime_thermostat['thermostat_id']);
|
||||||
|
|
||||||
|
$runtime_thermostat['timestamp'] = $this->get_local_datetime(
|
||||||
|
$runtime_thermostat['timestamp'],
|
||||||
|
$thermostat['time_zone']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return temperatures in a human-readable format.
|
||||||
|
foreach(['indoor_temperature', 'outdoor_temperature', 'setpoint_heat', 'setpoint_cool'] as $key) {
|
||||||
|
if($runtime_thermostat[$key] !== null) {
|
||||||
|
$runtime_thermostat[$key] /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace event and climate with their string values.
|
||||||
|
if ($runtime_thermostat['event_runtime_thermostat_text_id'] !== null) {
|
||||||
|
$runtime_thermostat['event_runtime_thermostat_text_id'] = $runtime_thermostat_texts[$runtime_thermostat['event_runtime_thermostat_text_id']]['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($runtime_thermostat['climate_runtime_thermostat_text_id'] !== null) {
|
||||||
|
$runtime_thermostat['climate_runtime_thermostat_text_id'] = $runtime_thermostat_texts[$runtime_thermostat['climate_runtime_thermostat_text_id']]['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes += fputcsv($output, $runtime_thermostat);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunk_begin = strtotime('+1 day', $chunk_end);
|
||||||
|
} while ($chunk_end < $download_end);
|
||||||
|
fclose($output);
|
||||||
|
|
||||||
|
$this->cora->set_headers([
|
||||||
|
'Content-Type' => 'text/csv',
|
||||||
|
'Content-Length' => $bytes,
|
||||||
|
'Content-Disposition' => 'attachment; filename="Beestat Export - ' . $ecobee_thermostat['identifier'] . '.csv"',
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
'Expires' => '0',
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since this table does not have a user_id column, security must be handled
|
||||||
|
* manually. Call this with a thermostat_id to verify that the current user
|
||||||
|
* has access to the requested thermostat.
|
||||||
|
*
|
||||||
|
* @param int $thermostat_id
|
||||||
|
*
|
||||||
|
* @throws \Exception If the current user doesn't have access to the
|
||||||
|
* requested thermostat.
|
||||||
|
*/
|
||||||
|
private function user_lock($thermostat_id) {
|
||||||
|
$thermostats = $this->api('thermostat', 'read_id');
|
||||||
|
if (isset($thermostats[$thermostat_id]) === false) {
|
||||||
|
throw new \Exception('Invalid thermostat_id.', 10203);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
138
api/runtime_sensor.php
Normal file
138
api/runtime_sensor.php
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All of the raw sensor data sits here. Many millions of rows.
|
||||||
|
*
|
||||||
|
* @author Jon Ziebell
|
||||||
|
*/
|
||||||
|
class runtime_sensor extends cora\crud {
|
||||||
|
|
||||||
|
public static $exposed = [
|
||||||
|
'private' => [
|
||||||
|
'read'
|
||||||
|
],
|
||||||
|
'public' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
public static $cache = [
|
||||||
|
'read' => 900 // 15 Minutes
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user_id column is not present on this table to reduce data overhead.
|
||||||
|
* No public reads are performed on this table so as long as thermostat_id
|
||||||
|
* is always used on reads in this class then this is fine.
|
||||||
|
*/
|
||||||
|
public static $user_locked = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data from the runtime_sensor table. Basically just a crud read
|
||||||
|
* but has custom security for thermostat_id since user_id is not on the
|
||||||
|
* table.
|
||||||
|
*
|
||||||
|
* @param array $attributes Timestamps can be specified in any format. If no
|
||||||
|
* time zone information is sent, UTC is assumed.
|
||||||
|
* @param array $columns
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function read($attributes = [], $columns = []) {
|
||||||
|
$this->user_lock($attributes['sensor_id']);
|
||||||
|
|
||||||
|
// Check for exceptions.
|
||||||
|
if (isset($attributes['sensor_id']) === false) {
|
||||||
|
throw new \Exception('Missing required attribute: sensor_id.', 10401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($attributes['timestamp']) === false) {
|
||||||
|
throw new \Exception('Missing required attribute: timestamp.', 10402);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
is_array($attributes['timestamp']) === true &&
|
||||||
|
in_array($attributes['timestamp']['operator'], ['>', '>=', '<', '<=']) === true &&
|
||||||
|
is_array($attributes['timestamp']['value']) === true
|
||||||
|
) {
|
||||||
|
if(count($attributes['timestamp']['value']) === 1) {
|
||||||
|
$attributes['timestamp']['value'] = $attributes['timestamp']['value'][0];
|
||||||
|
} else {
|
||||||
|
throw new \Exception('Must only specify one timestamp value unless using the "between" operator.', 10404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sensor = $this->api('sensor', 'get', $attributes['sensor_id']);
|
||||||
|
$thermostat = $this->api('thermostat', 'get', $sensor['thermostat_id']);
|
||||||
|
$max_range = 2678000; // 31 days
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
is_array($attributes['timestamp']) === true &&
|
||||||
|
$attributes['timestamp']['operator'] === 'between' &&
|
||||||
|
abs(strtotime($attributes['timestamp']['value'][0]) - strtotime($attributes['timestamp']['value'][1])) > $max_range
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
is_array($attributes['timestamp']) === true &&
|
||||||
|
in_array($attributes['timestamp']['operator'], ['>', '>=']) === true &&
|
||||||
|
time() - strtotime($attributes['timestamp']['value']) > $max_range
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
is_array($attributes['timestamp']) === true &&
|
||||||
|
in_array($attributes['timestamp']['operator'], ['<', '<=']) === true &&
|
||||||
|
strtotime($attributes['timestamp']['value']) - min(strtotime($thermostat['first_connected']), strtotime($thermostat['sync_begin'])) > $max_range
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new \Exception('Max range is 31 days. ' . (time() - strtotime($attributes['timestamp']['value'])), 10405);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept timestamps in roughly any format; always convert back to something nice and in UTC
|
||||||
|
if (is_array($attributes['timestamp']['value']) === true) {
|
||||||
|
$attributes['timestamp']['value'][0] = date('c', strtotime($attributes['timestamp']['value'][0]));
|
||||||
|
$attributes['timestamp']['value'][1] = date('c', strtotime($attributes['timestamp']['value'][1]));
|
||||||
|
} else {
|
||||||
|
$attributes['timestamp']['value'] = date('c', strtotime($attributes['timestamp']['value']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the data.
|
||||||
|
$runtime_sensors = $this->database->read(
|
||||||
|
'runtime_sensor',
|
||||||
|
[
|
||||||
|
'timestamp' => $attributes['timestamp'],
|
||||||
|
'sensor_id' => $attributes['sensor_id']
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
'timestamp' // order by
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up the data just a bit.
|
||||||
|
foreach ($runtime_sensors as &$runtime_sensor) {
|
||||||
|
$runtime_sensor['timestamp'] = date(
|
||||||
|
'c',
|
||||||
|
strtotime($runtime_sensor['timestamp'])
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($runtime_sensor['temperature'] !== null) {
|
||||||
|
$runtime_sensor['temperature'] /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $runtime_sensors;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since this table does not have a user_id column, security must be handled
|
||||||
|
* manually. Call this with a sensor_id to verify that the current user has
|
||||||
|
* access to the requested sensor_id.
|
||||||
|
*
|
||||||
|
* @param int $sensor_id
|
||||||
|
*
|
||||||
|
* @throws \Exception If the current user doesn't have access to the
|
||||||
|
* requested sensor.
|
||||||
|
*/
|
||||||
|
private function user_lock($sensor_id) {
|
||||||
|
$sensors = $this->api('sensor', 'read_id');
|
||||||
|
if (isset($sensors[$sensor_id]) === false) {
|
||||||
|
throw new \Exception('Invalid sensor_id.', 10403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,16 +9,13 @@ class runtime_thermostat extends cora\crud {
|
|||||||
|
|
||||||
public static $exposed = [
|
public static $exposed = [
|
||||||
'private' => [
|
'private' => [
|
||||||
'read',
|
'read'
|
||||||
'sync',
|
|
||||||
'download'
|
|
||||||
],
|
],
|
||||||
'public' => []
|
'public' => []
|
||||||
];
|
];
|
||||||
|
|
||||||
public static $cache = [
|
public static $cache = [
|
||||||
'sync' => 900, // 15 Minutes
|
'read' => 900 // 15 Minutes
|
||||||
'read' => 900, // 15 Minutes
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,543 +25,6 @@ class runtime_thermostat extends cora\crud {
|
|||||||
*/
|
*/
|
||||||
public static $user_locked = false;
|
public static $user_locked = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* List of columns that are synced from ecobee.
|
|
||||||
*/
|
|
||||||
private static $ecobee_columns = [
|
|
||||||
'compCool1', // compressor_1
|
|
||||||
'compCool2', // compressor_2
|
|
||||||
'compHeat1', // compressor_1
|
|
||||||
'compHeat2', // compressor_2
|
|
||||||
'auxHeat1', // auxiliary_heat_1
|
|
||||||
'auxHeat2', // auxiliary_heat_2
|
|
||||||
'fan', // fan
|
|
||||||
'humidifier', // accessory
|
|
||||||
'dehumidifier', // accessory
|
|
||||||
'ventilator', // accessory
|
|
||||||
'economizer', // accessory
|
|
||||||
'hvacMode', // system_mode
|
|
||||||
'zoneAveTemp', // indoor_temperature
|
|
||||||
'zoneHumidity', // indoor_humidity
|
|
||||||
'outdoorTemp', // outdoor_temperature
|
|
||||||
'outdoorHumidity', // outdoor_humidity
|
|
||||||
'zoneCalendarEvent', // event_runtime_thermostat_text_id
|
|
||||||
'zoneClimate', // climate_runtime_thermostat_text_id
|
|
||||||
'zoneCoolTemp', // setpoint_cool
|
|
||||||
'zoneHeatTemp' // setpoint_heat
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main function for syncing thermostat data. Looks at the current state of
|
|
||||||
* things and decides which direction (forwards or backwards) makes the most
|
|
||||||
* sense.
|
|
||||||
*
|
|
||||||
* @param int $thermostat_id Optional thermostat_id to sync. If not set will
|
|
||||||
* sync all thermostats attached to this user.
|
|
||||||
*/
|
|
||||||
public function sync($thermostat_id = null) {
|
|
||||||
// Skip this for the demo
|
|
||||||
if($this->setting->is_demo() === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_time_limit(0);
|
|
||||||
|
|
||||||
if($thermostat_id === null) {
|
|
||||||
$thermostat_ids = array_keys(
|
|
||||||
$this->api(
|
|
||||||
'thermostat',
|
|
||||||
'read_id',
|
|
||||||
[
|
|
||||||
'attributes' => [
|
|
||||||
'inactive' => 0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$this->user_lock($thermostat_id);
|
|
||||||
$thermostat_ids = [$thermostat_id];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($thermostat_ids as $thermostat_id) {
|
|
||||||
// Get a lock to ensure that this is not invoked more than once at a time
|
|
||||||
// per thermostat.
|
|
||||||
$lock_name = 'runtime->sync(' . $thermostat_id . ')';
|
|
||||||
$this->database->get_lock($lock_name);
|
|
||||||
|
|
||||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
|
||||||
|
|
||||||
if(
|
|
||||||
$thermostat['sync_begin'] === $thermostat['first_connected'] ||
|
|
||||||
(
|
|
||||||
$thermostat['sync_begin'] !== null &&
|
|
||||||
strtotime($thermostat['sync_begin']) <= strtotime('-1 year')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
$this->sync_forwards($thermostat_id);
|
|
||||||
} else {
|
|
||||||
$this->sync_backwards($thermostat_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only syncing one thermostat this will delay the sync of the other
|
|
||||||
// thermostat. Not a huge deal, just FYI.
|
|
||||||
$this->api(
|
|
||||||
'user',
|
|
||||||
'update_sync_status',
|
|
||||||
[
|
|
||||||
'key' => 'runtime'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->database->release_lock($lock_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync backwards. When running for the first time it will sync from now all
|
|
||||||
* the way back to the first connected date. If it is called again it will
|
|
||||||
* check to see if a full backwards sync has already completed. If it has,
|
|
||||||
* it will throw an exception. If not, it will resume the backwards sync.
|
|
||||||
*
|
|
||||||
* @param int $thermostat_id
|
|
||||||
*/
|
|
||||||
private function sync_backwards($thermostat_id) {
|
|
||||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
|
||||||
|
|
||||||
if($thermostat['sync_begin'] === $thermostat['first_connected']) {
|
|
||||||
throw new \Exception('Full sync already performed; must call sync_forwards() now.', 10200);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($thermostat['sync_begin'] === null) {
|
|
||||||
// Sync from when the thermostat was first connected until now.
|
|
||||||
$sync_begin = strtotime($thermostat['first_connected']);
|
|
||||||
$sync_end = time();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Sync from when the thermostat was first connected until sync_end.
|
|
||||||
$sync_begin = strtotime($thermostat['first_connected']);
|
|
||||||
$sync_end = strtotime($thermostat['sync_begin']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only sync up to the past year of data. Outside of this there won't even
|
|
||||||
// be a partition for the data to go into.
|
|
||||||
$sync_begin = max(strtotime('-1 year'), $sync_begin);
|
|
||||||
|
|
||||||
$chunk_begin = $sync_end;
|
|
||||||
$chunk_end = $sync_end;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loop over the dates and do the actual sync. Each chunk is wrapped in a
|
|
||||||
* transaction for a little bit of protection against exceptions
|
|
||||||
* introducing bad data and causing the whole sync to fail. Commit any
|
|
||||||
* open transactions first though.
|
|
||||||
*/
|
|
||||||
$this->database->commit_transaction();
|
|
||||||
do {
|
|
||||||
$this->database->start_transaction();
|
|
||||||
|
|
||||||
$chunk_begin = strtotime('-1 week', $chunk_end);
|
|
||||||
$chunk_begin = max($chunk_begin, $sync_begin);
|
|
||||||
|
|
||||||
$this->sync_($thermostat['thermostat_id'], $chunk_begin, $chunk_end);
|
|
||||||
|
|
||||||
// Update the thermostat with the current sync range
|
|
||||||
$this->api(
|
|
||||||
'thermostat',
|
|
||||||
'update',
|
|
||||||
[
|
|
||||||
'attributes' => [
|
|
||||||
'thermostat_id' => $thermostat['thermostat_id'],
|
|
||||||
'sync_begin' => date('Y-m-d H:i:s', $chunk_begin),
|
|
||||||
'sync_end' => date(
|
|
||||||
'Y-m-d H:i:s',
|
|
||||||
max(
|
|
||||||
$sync_end,
|
|
||||||
strtotime($thermostat['sync_end'])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Populate on the fly.
|
|
||||||
$this->api(
|
|
||||||
'runtime_thermostat_summary',
|
|
||||||
'populate',
|
|
||||||
$thermostat_id
|
|
||||||
);
|
|
||||||
|
|
||||||
// Because I am doing day-level syncing this will end up fetching an
|
|
||||||
// overlapping day of data every time. But if I properly switch this to
|
|
||||||
// interval-level syncing this should be correct or at the very least
|
|
||||||
// return a minimal one extra row of data.
|
|
||||||
$chunk_end = $chunk_begin;
|
|
||||||
|
|
||||||
$this->database->commit_transaction();
|
|
||||||
} while ($chunk_begin > $sync_begin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync forwards from thermostat.sync_end until now. This should be used for
|
|
||||||
* all syncs except the first one.
|
|
||||||
*
|
|
||||||
* @param int $thermostat_id
|
|
||||||
*/
|
|
||||||
private function sync_forwards($thermostat_id) {
|
|
||||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
|
||||||
|
|
||||||
// Sync from the last sync time until now. Go a couple hours back in time to
|
|
||||||
// cover that 1 hour delay.
|
|
||||||
$sync_begin = strtotime($thermostat['sync_end'] . ' -2 hours');
|
|
||||||
$sync_end = time();
|
|
||||||
|
|
||||||
$chunk_begin = $sync_begin;
|
|
||||||
$chunk_end = $sync_begin;
|
|
||||||
|
|
||||||
// Loop over the dates and do the actual sync. Each chunk is wrapped in a
|
|
||||||
// transaction for a little bit of protection against exceptions introducing
|
|
||||||
// bad data and causing the whole sync to fail.
|
|
||||||
do {
|
|
||||||
$this->database->start_transaction();
|
|
||||||
|
|
||||||
$chunk_end = strtotime('+1 week', $chunk_begin);
|
|
||||||
$chunk_end = min($chunk_end, $sync_end);
|
|
||||||
|
|
||||||
$this->sync_($thermostat['thermostat_id'], $chunk_begin, $chunk_end);
|
|
||||||
|
|
||||||
// Update the thermostat with the current sync range
|
|
||||||
$this->api(
|
|
||||||
'thermostat',
|
|
||||||
'update',
|
|
||||||
[
|
|
||||||
'attributes' => [
|
|
||||||
'thermostat_id' => $thermostat['thermostat_id'],
|
|
||||||
'sync_end' => date('Y-m-d H:i:s', $chunk_end)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$chunk_begin = strtotime('+1 day', $chunk_end);
|
|
||||||
|
|
||||||
$this->database->commit_transaction();
|
|
||||||
} while ($chunk_end < $sync_end);
|
|
||||||
|
|
||||||
// Populate at the end of a full sync forwards.
|
|
||||||
$this->api(
|
|
||||||
'runtime_thermostat_summary',
|
|
||||||
'populate',
|
|
||||||
$thermostat_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the runtime report data for a specified thermostat. Originally this
|
|
||||||
* was basically a 1:1 import from ecobee. But due to the size of the data
|
|
||||||
* this now does some fancy logic to merge certain columns down and alter
|
|
||||||
* their data types just a bit.
|
|
||||||
*
|
|
||||||
* @param int $thermostat_id
|
|
||||||
* @param int $begin
|
|
||||||
* @param int $end
|
|
||||||
*/
|
|
||||||
private function sync_($thermostat_id, $begin, $end) {
|
|
||||||
$this->user_lock($thermostat_id);
|
|
||||||
|
|
||||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
|
||||||
$ecobee_thermostat = $this->api('ecobee_thermostat', 'get', $thermostat['ecobee_thermostat_id']);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Round begin/end down to the next 5 minutes. This keep things tidy.
|
|
||||||
* Without this, I would query for rows between 10:35:16, for example, and
|
|
||||||
* not get the row at 10:35:00. But the interval function would return it
|
|
||||||
* and then duplicate databse entry.
|
|
||||||
*/
|
|
||||||
$begin = floor($begin / 300) * 300;
|
|
||||||
$end = floor($end / 300) * 300;
|
|
||||||
|
|
||||||
$begin_interval = $this->get_interval($begin);
|
|
||||||
$end_interval = $this->get_interval($end);
|
|
||||||
|
|
||||||
$begin_date = date('Y-m-d', $begin);
|
|
||||||
$end_date = date('Y-m-d', $end);
|
|
||||||
|
|
||||||
$response = $this->api(
|
|
||||||
'ecobee',
|
|
||||||
'ecobee_api',
|
|
||||||
[
|
|
||||||
'method' => 'GET',
|
|
||||||
'endpoint' => 'runtimeReport',
|
|
||||||
'arguments' => [
|
|
||||||
'body' => json_encode([
|
|
||||||
'selection' => [
|
|
||||||
'selectionType' => 'thermostats',
|
|
||||||
'selectionMatch' => $ecobee_thermostat['identifier']
|
|
||||||
],
|
|
||||||
'startDate' => $begin_date,
|
|
||||||
'endDate' => $end_date,
|
|
||||||
'startInterval' => $begin_interval,
|
|
||||||
'endInterval' => $end_interval,
|
|
||||||
'columns' => implode(',', self::$ecobee_columns),
|
|
||||||
'includeSensors' => false
|
|
||||||
])
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read any existing rows from the database so we know if this is an
|
|
||||||
* insert or an update. Note that even though I have $begin and $end
|
|
||||||
* already defined, I always look in the database according to what ecobee
|
|
||||||
* actually returned just in case the returned data goes outside of what I
|
|
||||||
* requested for some reason.
|
|
||||||
*/
|
|
||||||
$columns_begin = $this->get_columns(
|
|
||||||
$response['reportList'][0]['rowList'][0]
|
|
||||||
);
|
|
||||||
$columns_end = $this->get_columns(
|
|
||||||
$response['reportList'][0]['rowList'][count($response['reportList'][0]['rowList']) - 1]
|
|
||||||
);
|
|
||||||
|
|
||||||
$existing_rows = $this->database->read(
|
|
||||||
'runtime_thermostat',
|
|
||||||
[
|
|
||||||
'thermostat_id' => $thermostat_id,
|
|
||||||
'timestamp' => [
|
|
||||||
'value' => [
|
|
||||||
$this->get_utc_datetime(
|
|
||||||
date(
|
|
||||||
'Y-m-d H:i:s',
|
|
||||||
strtotime($columns_begin['date'] . ' ' . $columns_begin['time'] . ' -1 hour')
|
|
||||||
),
|
|
||||||
$thermostat['time_zone']
|
|
||||||
),
|
|
||||||
$this->get_utc_datetime(
|
|
||||||
date(
|
|
||||||
'Y-m-d H:i:s',
|
|
||||||
strtotime($columns_end['date'] . ' ' . $columns_end['time'] . ' +1 hour')
|
|
||||||
),
|
|
||||||
$thermostat['time_zone']
|
|
||||||
)
|
|
||||||
],
|
|
||||||
'operator' => 'between'
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$existing_timestamps = [];
|
|
||||||
foreach($existing_rows as $existing_row) {
|
|
||||||
$existing_timestamps[$existing_row['timestamp']] = $existing_row['runtime_thermostat_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop over the ecobee data
|
|
||||||
foreach ($response['reportList'][0]['rowList'] as $row) {
|
|
||||||
$columns = $this->get_columns($row);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If any of these values are null just throw the whole row away. This
|
|
||||||
* should very rarely happen as ecobee reports 0 values. The driver of
|
|
||||||
* this is the summary table...trying to aggregate sums for these values
|
|
||||||
* is easy, but in order to accurately represent those sums I need to
|
|
||||||
* know how many rows were included. I would have to store one count per
|
|
||||||
* sum in the summary table to manage that...or just not store the data
|
|
||||||
* to begin with.
|
|
||||||
*
|
|
||||||
* Also threw in null checks on a bunch of other fields just to simplify
|
|
||||||
* the code later on. This happens so rarely that throwing away a whole
|
|
||||||
* row for a null value shouldn't have any noticeable negative effect.
|
|
||||||
*
|
|
||||||
* Note: Don't ignore zoneHeatTemp or zoneCoolTemp. Sometimes those are
|
|
||||||
* legit null as the thermostat may be for heat/cool only (see #160).
|
|
||||||
*/
|
|
||||||
if(
|
|
||||||
$columns['hvacMode'] === null ||
|
|
||||||
$columns['zoneAveTemp'] === null ||
|
|
||||||
$columns['zoneHumidity'] === null ||
|
|
||||||
$columns['outdoorTemp'] === null ||
|
|
||||||
$columns['outdoorHumidity'] === null ||
|
|
||||||
$columns['compHeat1'] === null ||
|
|
||||||
$columns['compHeat2'] === null ||
|
|
||||||
$columns['compCool1'] === null ||
|
|
||||||
$columns['compCool2'] === null ||
|
|
||||||
$columns['auxHeat1'] === null ||
|
|
||||||
$columns['auxHeat2'] === null ||
|
|
||||||
$columns['fan'] === null ||
|
|
||||||
$columns['humidifier'] === null ||
|
|
||||||
$columns['dehumidifier'] === null ||
|
|
||||||
$columns['ventilator'] === null ||
|
|
||||||
$columns['economizer'] === null
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date and time are first two columns of the returned data. It is
|
|
||||||
// returned in thermostat time, so convert it to UTC first.
|
|
||||||
$timestamp = $this->get_utc_datetime(
|
|
||||||
$columns['date'] . ' ' . $columns['time'],
|
|
||||||
$thermostat['time_zone']
|
|
||||||
);
|
|
||||||
|
|
||||||
$data = [];
|
|
||||||
|
|
||||||
$data['thermostat_id'] = $thermostat_id;
|
|
||||||
$data['timestamp'] = $timestamp;
|
|
||||||
|
|
||||||
if ($columns['compCool1'] > 0 || $columns['compCool2'] > 0) {
|
|
||||||
$data['compressor_mode'] = 'cool';
|
|
||||||
$data['compressor_1'] = $columns['compCool1'] - $columns['compCool2'];
|
|
||||||
$data['compressor_2'] = $columns['compCool2'];
|
|
||||||
} else if ($columns['compHeat1'] > 0 || $columns['compHeat2'] > 0) {
|
|
||||||
$data['compressor_mode'] = 'heat';
|
|
||||||
$data['compressor_1'] = $columns['compHeat1'] - $columns['compHeat2'];
|
|
||||||
$data['compressor_2'] = $columns['compHeat2'];
|
|
||||||
} else {
|
|
||||||
$data['compressor_mode'] = 'off';
|
|
||||||
$data['compressor_1'] = 0;
|
|
||||||
$data['compressor_2'] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data['auxiliary_heat_1'] = $columns['auxHeat1'] - $columns['auxHeat2'];
|
|
||||||
$data['auxiliary_heat_2'] = $columns['auxHeat2'];
|
|
||||||
|
|
||||||
if($columns['humidifier'] > 0) {
|
|
||||||
$data['accessory_type'] = 'humidifier';
|
|
||||||
$data['accessory'] = $columns['humidifier'];
|
|
||||||
} else if($columns['dehumidifier'] > 0) {
|
|
||||||
$data['accessory_type'] = 'dehumidifier';
|
|
||||||
$data['accessory'] = $columns['dehumidifier'];
|
|
||||||
} else if($columns['ventilator'] > 0) {
|
|
||||||
$data['accessory_type'] = 'ventilator';
|
|
||||||
$data['accessory'] = $columns['ventilator'];
|
|
||||||
} else if($columns['economizer'] > 0) {
|
|
||||||
$data['accessory_type'] = 'economizer';
|
|
||||||
$data['accessory'] = $columns['economizer'];
|
|
||||||
} else {
|
|
||||||
$data['accessory_type'] = 'off';
|
|
||||||
$data['accessory'] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ecobee does not report fan usage when it does not control the fan, so
|
|
||||||
// this will mark the fan as running when certain equipment is on.
|
|
||||||
$data['fan'] = max(
|
|
||||||
$columns['fan'],
|
|
||||||
$data['compressor_1'],
|
|
||||||
$data['compressor_2'],
|
|
||||||
$data['auxiliary_heat_1'],
|
|
||||||
$data['auxiliary_heat_2'],
|
|
||||||
$data['accessory']
|
|
||||||
);
|
|
||||||
|
|
||||||
$system_modes = [
|
|
||||||
'auto' => 'auto',
|
|
||||||
'cool' => 'cool',
|
|
||||||
'heat' => 'heat',
|
|
||||||
'auxHeatOnly' => 'auxiliary_heat',
|
|
||||||
'off' => 'off'
|
|
||||||
];
|
|
||||||
$data['system_mode'] = $system_modes[$columns['hvacMode']];
|
|
||||||
|
|
||||||
$data['indoor_temperature'] = $columns['zoneAveTemp'] * 10;
|
|
||||||
$data['indoor_humidity'] = round($columns['zoneHumidity']);
|
|
||||||
|
|
||||||
$data['outdoor_temperature'] = $columns['outdoorTemp'] * 10;
|
|
||||||
$data['outdoor_humidity'] = round($columns['outdoorHumidity']);
|
|
||||||
|
|
||||||
$data['event_runtime_thermostat_text_id'] = $this->api(
|
|
||||||
'runtime_thermostat_text',
|
|
||||||
'get_create',
|
|
||||||
$columns['zoneCalendarEvent']
|
|
||||||
)['runtime_thermostat_text_id'];
|
|
||||||
$data['climate_runtime_thermostat_text_id'] = $this->api(
|
|
||||||
'runtime_thermostat_text',
|
|
||||||
'get_create',
|
|
||||||
$columns['zoneClimate']
|
|
||||||
)['runtime_thermostat_text_id'];
|
|
||||||
|
|
||||||
$data['setpoint_cool'] = $columns['zoneCoolTemp'] * 10;
|
|
||||||
$data['setpoint_heat'] = $columns['zoneHeatTemp'] * 10;
|
|
||||||
|
|
||||||
// Create or update the database
|
|
||||||
if(isset($existing_timestamps[$timestamp]) === true) {
|
|
||||||
$data['runtime_thermostat_id'] = $existing_timestamps[$timestamp];
|
|
||||||
$this->database->update('runtime_thermostat', $data, 'id');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$existing_timestamps[$timestamp] = $this->database->create('runtime_thermostat', $data, 'id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ecobee "interval" value from a timestamp.
|
|
||||||
*
|
|
||||||
* @param string $timestamp The timestamp.
|
|
||||||
*
|
|
||||||
* @return int The interval.
|
|
||||||
*/
|
|
||||||
private function get_interval($timestamp) {
|
|
||||||
$hours = date('G', $timestamp);
|
|
||||||
$minutes = date('i', $timestamp);
|
|
||||||
|
|
||||||
return ($hours * 12) + floor($minutes / 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a string CSV row to cleaned up array of columns.
|
|
||||||
*
|
|
||||||
* @param string $row
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private function get_columns($row) {
|
|
||||||
$return = [];
|
|
||||||
|
|
||||||
$columns = explode(',', substr($row, 0, -1));
|
|
||||||
$return['date'] = array_splice($columns, 0, 1)[0];
|
|
||||||
$return['time'] = array_splice($columns, 0, 1)[0];
|
|
||||||
|
|
||||||
for($i = 0; $i < count($columns); $i++) {
|
|
||||||
$columns[$i] = trim($columns[$i]);
|
|
||||||
$return[self::$ecobee_columns[$i]] = $columns[$i] === '' ? null : $columns[$i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a local datetime string to a UTC datetime string.
|
|
||||||
*
|
|
||||||
* @param string $local_datetime Local datetime string.
|
|
||||||
* @param string $local_time_zone The local time zone to convert from.
|
|
||||||
*
|
|
||||||
* @return string The UTC datetime string.
|
|
||||||
*/
|
|
||||||
private function get_utc_datetime($local_datetime, $local_time_zone) {
|
|
||||||
$local_time_zone = new DateTimeZone($local_time_zone);
|
|
||||||
$utc_time_zone = new DateTimeZone('UTC');
|
|
||||||
$date_time = new DateTime($local_datetime, $local_time_zone);
|
|
||||||
$date_time->setTimezone($utc_time_zone);
|
|
||||||
|
|
||||||
return $date_time->format('Y-m-d H:i:s');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a UTC datetime string to a UTC datetime string.
|
|
||||||
*
|
|
||||||
* @param string $utc_datetime Local datetime string.
|
|
||||||
* @param string $local_time_zone The local time zone to convert from.
|
|
||||||
*
|
|
||||||
* @return string The UTC datetime string.
|
|
||||||
*/
|
|
||||||
private function get_local_datetime($utc_datetime, $local_time_zone) {
|
|
||||||
$local_time_zone = new DateTimeZone($local_time_zone);
|
|
||||||
$utc_time_zone = new DateTimeZone('UTC');
|
|
||||||
$date_time = new DateTime($utc_datetime, $utc_time_zone);
|
|
||||||
$date_time->setTimezone($local_time_zone);
|
|
||||||
|
|
||||||
return $date_time->format('Y-m-d H:i:s');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read data from the runtime_thermostat table. Basically just a crud read
|
* Read data from the runtime_thermostat table. Basically just a crud read
|
||||||
* but has custom security for thermostat_id since user_id is not on the
|
* but has custom security for thermostat_id since user_id is not on the
|
||||||
@ -696,131 +156,6 @@ class runtime_thermostat extends cora\crud {
|
|||||||
return $runtime_thermostats;
|
return $runtime_thermostats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Download all data that exists for a specific thermostat.
|
|
||||||
*
|
|
||||||
* @param int $thermostat_id
|
|
||||||
* @param string $download_begin Optional; the date to begin the download.
|
|
||||||
* @param string $download_end Optional; the date to end the download.
|
|
||||||
*/
|
|
||||||
public function download($thermostat_id, $download_begin = null, $download_end = null) {
|
|
||||||
set_time_limit(120);
|
|
||||||
|
|
||||||
$this->user_lock($thermostat_id);
|
|
||||||
|
|
||||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
|
||||||
$ecobee_thermostat = $this->api(
|
|
||||||
'ecobee_thermostat',
|
|
||||||
'get',
|
|
||||||
$thermostat['ecobee_thermostat_id']
|
|
||||||
);
|
|
||||||
|
|
||||||
if($download_begin === null) {
|
|
||||||
$download_begin = strtotime($thermostat['first_connected']);
|
|
||||||
} else {
|
|
||||||
$download_begin = strtotime($download_begin);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($download_end === null) {
|
|
||||||
$download_end = time();
|
|
||||||
} else {
|
|
||||||
$download_end = strtotime($download_end);
|
|
||||||
}
|
|
||||||
|
|
||||||
$chunk_begin = $download_begin;
|
|
||||||
$chunk_end = $download_begin;
|
|
||||||
|
|
||||||
$bytes = 0;
|
|
||||||
|
|
||||||
$output = fopen('php://output', 'w');
|
|
||||||
$needs_header = true;
|
|
||||||
do {
|
|
||||||
$chunk_end = strtotime('+1 week', $chunk_begin);
|
|
||||||
$chunk_end = min($chunk_end, $download_end);
|
|
||||||
|
|
||||||
$runtime_thermostats = $this->database->read(
|
|
||||||
'runtime_thermostat',
|
|
||||||
[
|
|
||||||
'thermostat_id' => $thermostat_id,
|
|
||||||
'timestamp' => [
|
|
||||||
'value' => [date('Y-m-d H:i:s', $chunk_begin), date('Y-m-d H:i:s', $chunk_end)] ,
|
|
||||||
'operator' => 'between'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
'timestamp' // order by
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the appropriate runtime_thermostat_texts.
|
|
||||||
$runtime_thermostat_text_ids = array_unique(array_merge(
|
|
||||||
array_column($runtime_thermostats, 'event_runtime_thermostat_text_id'),
|
|
||||||
array_column($runtime_thermostats, 'climate_runtime_thermostat_text_id')
|
|
||||||
));
|
|
||||||
$runtime_thermostat_texts = $this->api(
|
|
||||||
'runtime_thermostat_text',
|
|
||||||
'read_id',
|
|
||||||
[
|
|
||||||
'attributes' => [
|
|
||||||
'runtime_thermostat_text_id' => $runtime_thermostat_text_ids
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
if ($needs_header === true && count($runtime_thermostats) > 0) {
|
|
||||||
$headers = array_keys($runtime_thermostats[0]);
|
|
||||||
|
|
||||||
// Remove the IDs and rename two columns.
|
|
||||||
unset($headers[array_search('runtime_thermostat_id', $headers)]);
|
|
||||||
unset($headers[array_search('thermostat_id', $headers)]);
|
|
||||||
$headers[array_search('event_runtime_thermostat_text_id', $headers)] = 'event';
|
|
||||||
$headers[array_search('climate_runtime_thermostat_text_id', $headers)] = 'climate';
|
|
||||||
|
|
||||||
$bytes += fputcsv($output, $headers);
|
|
||||||
$needs_header = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($runtime_thermostats as $runtime_thermostat) {
|
|
||||||
unset($runtime_thermostat['runtime_thermostat_id']);
|
|
||||||
unset($runtime_thermostat['thermostat_id']);
|
|
||||||
|
|
||||||
$runtime_thermostat['timestamp'] = $this->get_local_datetime(
|
|
||||||
$runtime_thermostat['timestamp'],
|
|
||||||
$thermostat['time_zone']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return temperatures in a human-readable format.
|
|
||||||
foreach(['indoor_temperature', 'outdoor_temperature', 'setpoint_heat', 'setpoint_cool'] as $key) {
|
|
||||||
if($runtime_thermostat[$key] !== null) {
|
|
||||||
$runtime_thermostat[$key] /= 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace event and climate with their string values.
|
|
||||||
if ($runtime_thermostat['event_runtime_thermostat_text_id'] !== null) {
|
|
||||||
$runtime_thermostat['event_runtime_thermostat_text_id'] = $runtime_thermostat_texts[$runtime_thermostat['event_runtime_thermostat_text_id']]['value'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($runtime_thermostat['climate_runtime_thermostat_text_id'] !== null) {
|
|
||||||
$runtime_thermostat['climate_runtime_thermostat_text_id'] = $runtime_thermostat_texts[$runtime_thermostat['climate_runtime_thermostat_text_id']]['value'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$bytes += fputcsv($output, $runtime_thermostat);
|
|
||||||
}
|
|
||||||
|
|
||||||
$chunk_begin = strtotime('+1 day', $chunk_end);
|
|
||||||
} while ($chunk_end < $download_end);
|
|
||||||
fclose($output);
|
|
||||||
|
|
||||||
$this->cora->set_headers([
|
|
||||||
'Content-Type' => 'text/csv',
|
|
||||||
'Content-Length' => $bytes,
|
|
||||||
'Content-Disposition' => 'attachment; filename="Beestat Export - ' . $ecobee_thermostat['identifier'] . '.csv"',
|
|
||||||
'Pragma' => 'no-cache',
|
|
||||||
'Expires' => '0',
|
|
||||||
], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since this table does not have a user_id column, security must be handled
|
* Since this table does not have a user_id column, security must be handled
|
||||||
* manually. Call this with a thermostat_id to verify that the current user
|
* manually. Call this with a thermostat_id to verify that the current user
|
||||||
|
@ -350,12 +350,12 @@ a.inverted:active {
|
|||||||
.icon.eye_off:before { content: "\F209"; }
|
.icon.eye_off:before { content: "\F209"; }
|
||||||
.icon.fan:before { content: "\F210"; }
|
.icon.fan:before { content: "\F210"; }
|
||||||
.icon.fire:before { content: "\F238"; }
|
.icon.fire:before { content: "\F238"; }
|
||||||
.icon.gauge:before { content: "\F29A"; }
|
|
||||||
.icon.google_play:before { content: "\F2BC"; }
|
.icon.google_play:before { content: "\F2BC"; }
|
||||||
.icon.heart:before { content: "\F2D1"; }
|
.icon.heart:before { content: "\F2D1"; }
|
||||||
.icon.help_circle:before { content: "\F2D7"; }
|
.icon.help_circle:before { content: "\F2D7"; }
|
||||||
.icon.home:before { content: "\F2DC"; }
|
.icon.home:before { content: "\F2DC"; }
|
||||||
.icon.home_floor_a:before { content: "\FD5F"; }
|
.icon.home_floor_a:before { content: "\FD5F"; }
|
||||||
|
.icon.home_group:before { content: "\FDB0"; }
|
||||||
.icon.information:before { content: "\F2FC"; }
|
.icon.information:before { content: "\F2FC"; }
|
||||||
.icon.key:before { content: "\F306"; }
|
.icon.key:before { content: "\F306"; }
|
||||||
.icon.layers:before { content: "\F328"; }
|
.icon.layers:before { content: "\F328"; }
|
||||||
@ -371,6 +371,8 @@ a.inverted:active {
|
|||||||
.icon.pound:before { content: "\F423"; }
|
.icon.pound:before { content: "\F423"; }
|
||||||
.icon.snowflake:before { content: "\F716"; }
|
.icon.snowflake:before { content: "\F716"; }
|
||||||
.icon.swap_horizontal:before { content: "\F4E1"; }
|
.icon.swap_horizontal:before { content: "\F4E1"; }
|
||||||
|
.icon.signal_variant:before { content: "\F60A"; }
|
||||||
|
.icon.tablet_dashboard:before { content: "\FEEB"; }
|
||||||
.icon.thermostat:before { content: "\F393"; }
|
.icon.thermostat:before { content: "\F393"; }
|
||||||
.icon.thumb_up:before { content: "\F513"; }
|
.icon.thumb_up:before { content: "\F513"; }
|
||||||
.icon.tune:before { content: "\F62E"; }
|
.icon.tune:before { content: "\F62E"; }
|
||||||
|
@ -143,8 +143,8 @@ if ('serviceWorker' in navigator) {
|
|||||||
beestat.width = window.innerWidth;
|
beestat.width = window.innerWidth;
|
||||||
window.addEventListener('resize', rocket.throttle(100, function() {
|
window.addEventListener('resize', rocket.throttle(100, function() {
|
||||||
var breakpoints = [
|
var breakpoints = [
|
||||||
500,
|
600,
|
||||||
600
|
650
|
||||||
];
|
];
|
||||||
|
|
||||||
breakpoints.forEach(function(breakpoint) {
|
breakpoints.forEach(function(breakpoint) {
|
||||||
|
@ -12,13 +12,21 @@ beestat.setting = function(key, opt_value, opt_callback) {
|
|||||||
var user = beestat.get_user();
|
var user = beestat.get_user();
|
||||||
|
|
||||||
var defaults = {
|
var defaults = {
|
||||||
'runtime_detail_smoothing': true,
|
'runtime_thermostat_detail_smoothing': true,
|
||||||
'runtime_detail_range_type': 'dynamic',
|
'runtime_thermostat_detail_range_type': 'dynamic',
|
||||||
'runtime_detail_range_static_begin': moment()
|
'runtime_thermostat_detail_range_static_begin': moment()
|
||||||
.subtract(3, 'day')
|
.subtract(3, 'day')
|
||||||
.format('MM/DD/YYYY'),
|
.format('MM/DD/YYYY'),
|
||||||
'runtime_detail_range_static_end': moment().format('MM/DD/YYYY'),
|
'runtime_thermostat_detail_range_static_end': moment().format('MM/DD/YYYY'),
|
||||||
'runtime_detail_range_dynamic': 3,
|
'runtime_thermostat_detail_range_dynamic': 3,
|
||||||
|
|
||||||
|
'runtime_sensor_detail_smoothing': true,
|
||||||
|
'runtime_sensor_detail_range_type': 'dynamic',
|
||||||
|
'runtime_sensor_detail_range_static_begin': moment()
|
||||||
|
.subtract(3, 'day')
|
||||||
|
.format('MM/DD/YYYY'),
|
||||||
|
'runtime_sensor_detail_range_static_end': moment().format('MM/DD/YYYY'),
|
||||||
|
'runtime_sensor_detail_range_dynamic': 3,
|
||||||
|
|
||||||
'runtime_thermostat_summary_time_count': 0,
|
'runtime_thermostat_summary_time_count': 0,
|
||||||
'runtime_thermostat_summary_time_period': 'all',
|
'runtime_thermostat_summary_time_period': 'all',
|
||||||
|
528
js/component/card/runtime_sensor_detail.js
Normal file
528
js/component/card/runtime_sensor_detail.js
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
/**
|
||||||
|
* Runtime detail card. Shows a graph similar to what ecobee shows with the
|
||||||
|
* runtime info for a recent period of time.
|
||||||
|
*
|
||||||
|
* @param {number} thermostat_id The thermostat_id this card is displaying
|
||||||
|
* data for
|
||||||
|
*/
|
||||||
|
beestat.component.card.runtime_sensor_detail = function(thermostat_id) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.thermostat_id_ = thermostat_id;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When a setting is changed clear all of the data. Then rerender which will
|
||||||
|
* trigger the loading state. Also do this when the cache changes.
|
||||||
|
*
|
||||||
|
* Debounce so that multiple setting changes don't re-trigger the same
|
||||||
|
* event. This fires on the trailing edge so that all changes are accounted
|
||||||
|
* for when rerendering.
|
||||||
|
*/
|
||||||
|
var change_function = beestat.debounce(function() {
|
||||||
|
self.rerender();
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
beestat.dispatcher.addEventListener(
|
||||||
|
[
|
||||||
|
'setting.runtime_sensor_detail_smoothing',
|
||||||
|
'setting.runtime_sensor_detail_range_type',
|
||||||
|
'setting.runtime_sensor_detail_range_dynamic',
|
||||||
|
'cache.runtime_sensor'
|
||||||
|
],
|
||||||
|
change_function
|
||||||
|
);
|
||||||
|
|
||||||
|
beestat.component.card.apply(this, arguments);
|
||||||
|
};
|
||||||
|
beestat.extend(beestat.component.card.runtime_sensor_detail, beestat.component.card);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorate
|
||||||
|
*
|
||||||
|
* @param {rocket.ELements} parent
|
||||||
|
*/
|
||||||
|
beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = function(parent) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var data = this.get_data_();
|
||||||
|
|
||||||
|
this.chart_ = new beestat.component.chart.runtime_sensor_detail(data);
|
||||||
|
this.chart_.render(parent);
|
||||||
|
|
||||||
|
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
|
||||||
|
|
||||||
|
var required_begin;
|
||||||
|
var required_end;
|
||||||
|
if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
|
||||||
|
required_begin = moment()
|
||||||
|
.subtract(
|
||||||
|
beestat.setting('runtime_sensor_detail_range_dynamic'),
|
||||||
|
'day'
|
||||||
|
)
|
||||||
|
.second(0);
|
||||||
|
|
||||||
|
required_end = moment()
|
||||||
|
.subtract(1, 'hour')
|
||||||
|
.second(0);
|
||||||
|
} else {
|
||||||
|
required_begin = moment(
|
||||||
|
beestat.setting('runtime_sensor_detail_range_static_begin') + ' 00:00:00'
|
||||||
|
);
|
||||||
|
required_end = moment(
|
||||||
|
beestat.setting('runtime_sensor_detail_range_static_end') + ' 23:59:59'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't go before there's data.
|
||||||
|
required_begin = moment.max(
|
||||||
|
required_begin,
|
||||||
|
moment(thermostat.first_connected)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't go after now.
|
||||||
|
required_end = moment.min(
|
||||||
|
required_end,
|
||||||
|
moment().subtract(1, 'hour')
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the needed data exists in the database and the runtime_sensor
|
||||||
|
* cache is empty, then query the data. If the needed data does not exist in
|
||||||
|
* the database, check every 2 seconds until it does.
|
||||||
|
*/
|
||||||
|
if (this.data_synced_(required_begin, required_end) === true) {
|
||||||
|
if (beestat.cache.runtime_sensor === undefined) {
|
||||||
|
this.show_loading_('Loading Runtime Detail');
|
||||||
|
|
||||||
|
var value;
|
||||||
|
var operator;
|
||||||
|
|
||||||
|
if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
|
||||||
|
value = required_begin.format();
|
||||||
|
operator = '>=';
|
||||||
|
} else {
|
||||||
|
value = [
|
||||||
|
required_begin.format(),
|
||||||
|
required_end.format()
|
||||||
|
];
|
||||||
|
operator = 'between';
|
||||||
|
}
|
||||||
|
|
||||||
|
var api_call = new beestat.api();
|
||||||
|
Object.values(beestat.cache.sensor).forEach(function(sensor) {
|
||||||
|
if (sensor.thermostat_id === beestat.setting('thermostat_id')) {
|
||||||
|
api_call.add_call(
|
||||||
|
'runtime_sensor',
|
||||||
|
'read',
|
||||||
|
{
|
||||||
|
'attributes': {
|
||||||
|
'sensor_id': sensor.sensor_id,
|
||||||
|
'timestamp': {
|
||||||
|
'value': value,
|
||||||
|
'operator': operator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api_call.set_callback(function(response) {
|
||||||
|
var runtime_sensors = [];
|
||||||
|
response.forEach(function(r) {
|
||||||
|
runtime_sensors = runtime_sensors.concat(r);
|
||||||
|
});
|
||||||
|
beestat.cache.set('runtime_sensor', runtime_sensors);
|
||||||
|
});
|
||||||
|
|
||||||
|
api_call.send();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.show_loading_('Syncing Runtime Detail');
|
||||||
|
setTimeout(function() {
|
||||||
|
new beestat.api()
|
||||||
|
.add_call(
|
||||||
|
'thermostat',
|
||||||
|
'read_id',
|
||||||
|
{},
|
||||||
|
'thermostat'
|
||||||
|
)
|
||||||
|
.set_callback(function(response) {
|
||||||
|
beestat.cache.set('thermostat', response);
|
||||||
|
self.rerender();
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorate the menu
|
||||||
|
*
|
||||||
|
* @param {rocket.Elements} parent
|
||||||
|
*/
|
||||||
|
beestat.component.card.runtime_sensor_detail.prototype.decorate_top_right_ = function(parent) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var menu = (new beestat.component.menu()).render(parent);
|
||||||
|
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Past 1 Day')
|
||||||
|
.set_icon('numeric_1_box')
|
||||||
|
.set_callback(function() {
|
||||||
|
if (
|
||||||
|
beestat.setting('runtime_sensor_detail_range_dynamic') !== 1 ||
|
||||||
|
beestat.setting('runtime_sensor_detail_range_type') !== 'dynamic'
|
||||||
|
) {
|
||||||
|
beestat.cache.delete('runtime_sensor');
|
||||||
|
beestat.setting({
|
||||||
|
'runtime_sensor_detail_range_dynamic': 1,
|
||||||
|
'runtime_sensor_detail_range_type': 'dynamic'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Past 3 Days')
|
||||||
|
.set_icon('numeric_3_box')
|
||||||
|
.set_callback(function() {
|
||||||
|
if (
|
||||||
|
beestat.setting('runtime_sensor_detail_range_dynamic') !== 3 ||
|
||||||
|
beestat.setting('runtime_sensor_detail_range_type') !== 'dynamic'
|
||||||
|
) {
|
||||||
|
beestat.cache.delete('runtime_sensor');
|
||||||
|
beestat.setting({
|
||||||
|
'runtime_sensor_detail_range_dynamic': 3,
|
||||||
|
'runtime_sensor_detail_range_type': 'dynamic'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Past 7 Days')
|
||||||
|
.set_icon('numeric_7_box')
|
||||||
|
.set_callback(function() {
|
||||||
|
if (
|
||||||
|
beestat.setting('runtime_sensor_detail_range_dynamic') !== 7 ||
|
||||||
|
beestat.setting('runtime_sensor_detail_range_type') !== 'dynamic'
|
||||||
|
) {
|
||||||
|
beestat.cache.delete('runtime_sensor');
|
||||||
|
beestat.setting({
|
||||||
|
'runtime_sensor_detail_range_dynamic': 7,
|
||||||
|
'runtime_sensor_detail_range_type': 'dynamic'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Custom')
|
||||||
|
.set_icon('calendar_edit')
|
||||||
|
.set_callback(function() {
|
||||||
|
(new beestat.component.modal.runtime_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();
|
||||||
|
}));
|
||||||
|
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Reset Zoom')
|
||||||
|
.set_icon('magnify_minus')
|
||||||
|
.set_callback(function() {
|
||||||
|
self.chart_.reset_zoom();
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (beestat.setting('runtime_sensor_detail_smoothing') === true) {
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Disable Smothing')
|
||||||
|
.set_icon('chart_line')
|
||||||
|
.set_callback(function() {
|
||||||
|
beestat.setting('runtime_sensor_detail_smoothing', false);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Enable Smoothing')
|
||||||
|
.set_icon('chart_bell_curve')
|
||||||
|
.set_callback(function() {
|
||||||
|
beestat.setting('runtime_sensor_detail_smoothing', true);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
|
.set_text('Help')
|
||||||
|
.set_icon('help_circle')
|
||||||
|
.set_callback(function() {
|
||||||
|
window.open('https://www.notion.so/beestat/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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 = [];
|
||||||
|
Object.values(beestat.cache.sensor).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 = 15;
|
||||||
|
} else {
|
||||||
|
moving_count = 1;
|
||||||
|
}
|
||||||
|
var offset;
|
||||||
|
for (var i = 0; i < moving_count; i++) {
|
||||||
|
offset = (i - Math.floor(moving_count / 2)) * 300000;
|
||||||
|
moving.push(runtime_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(1);
|
||||||
|
data.metadata.series.dummy.active = true;
|
||||||
|
|
||||||
|
if (runtime_sensors[current_m.valueOf()] !== undefined) {
|
||||||
|
runtime_sensors[current_m.valueOf()].forEach(function(runtime_sensor, j) {
|
||||||
|
var temperature_moving = beestat.temperature(
|
||||||
|
self.get_average_(moving, j)
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
runtime_sensors[current_m.valueOf()].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(
|
||||||
|
runtime_sensors[current_m.valueOf()].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 {
|
||||||
|
Object.values(beestat.cache.sensor).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].push(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} j The index in the sub-array
|
||||||
|
*
|
||||||
|
* @return {number} The average.
|
||||||
|
*/
|
||||||
|
beestat.component.card.runtime_sensor_detail.prototype.get_average_ = function(runtime_sensors, j) {
|
||||||
|
var average = 0;
|
||||||
|
var count = 0;
|
||||||
|
for (var i = 0; i < runtime_sensors.length; i++) {
|
||||||
|
if (runtime_sensors[i] !== undefined) {
|
||||||
|
average += runtime_sensors[i][j].temperature;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the subtitle of the card.
|
||||||
|
*
|
||||||
|
* @return {string} Subtitle
|
||||||
|
*/
|
||||||
|
beestat.component.card.runtime_sensor_detail.prototype.get_subtitle_ = function() {
|
||||||
|
if (beestat.setting('runtime_sensor_detail_range_type') === 'dynamic') {
|
||||||
|
var s = (beestat.setting('runtime_sensor_detail_range_dynamic') > 1) ? 's' : '';
|
||||||
|
|
||||||
|
return 'Past ' +
|
||||||
|
beestat.setting('runtime_sensor_detail_range_dynamic') +
|
||||||
|
' day' +
|
||||||
|
s;
|
||||||
|
}
|
||||||
|
|
||||||
|
var begin = moment(beestat.setting('runtime_sensor_detail_range_static_begin'))
|
||||||
|
.format('MMM D, YYYY');
|
||||||
|
var end = moment(beestat.setting('runtime_sensor_detail_range_static_end'))
|
||||||
|
.format('MMM D, YYYY');
|
||||||
|
|
||||||
|
return begin + ' to ' + end;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether or not the data to render the desired date range has been
|
||||||
|
* synced.
|
||||||
|
*
|
||||||
|
* @param {moment} required_sync_begin
|
||||||
|
* @param {moment} required_sync_end
|
||||||
|
*
|
||||||
|
* @return {boolean} Whether or not the data is synced.
|
||||||
|
*/
|
||||||
|
beestat.component.card.runtime_sensor_detail.prototype.data_synced_ = function(required_sync_begin, required_sync_end) {
|
||||||
|
// Demo can just grab whatever data is there.
|
||||||
|
if (window.is_demo === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
|
||||||
|
|
||||||
|
var current_sync_begin = moment.utc(thermostat.sync_begin);
|
||||||
|
var current_sync_end = moment.utc(thermostat.sync_end);
|
||||||
|
|
||||||
|
return (
|
||||||
|
current_sync_begin.isSameOrBefore(required_sync_begin) &&
|
||||||
|
current_sync_end.isSameOrAfter(required_sync_end)
|
||||||
|
);
|
||||||
|
};
|
@ -5,7 +5,7 @@
|
|||||||
* @param {number} thermostat_id The thermostat_id this card is displaying
|
* @param {number} thermostat_id The thermostat_id this card is displaying
|
||||||
* data for
|
* data for
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail = function(thermostat_id) {
|
beestat.component.card.runtime_thermostat_detail = function(thermostat_id) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.thermostat_id_ = thermostat_id;
|
this.thermostat_id_ = thermostat_id;
|
||||||
@ -24,9 +24,9 @@ beestat.component.card.runtime_detail = function(thermostat_id) {
|
|||||||
|
|
||||||
beestat.dispatcher.addEventListener(
|
beestat.dispatcher.addEventListener(
|
||||||
[
|
[
|
||||||
'setting.runtime_detail_smoothing',
|
'setting.runtime_thermostat_detail_smoothing',
|
||||||
'setting.runtime_detail_range_type',
|
'setting.runtime_thermostat_detail_range_type',
|
||||||
'setting.runtime_detail_range_dynamic',
|
'setting.runtime_thermostat_detail_range_dynamic',
|
||||||
'cache.runtime_thermostat'
|
'cache.runtime_thermostat'
|
||||||
],
|
],
|
||||||
change_function
|
change_function
|
||||||
@ -34,28 +34,28 @@ beestat.component.card.runtime_detail = function(thermostat_id) {
|
|||||||
|
|
||||||
beestat.component.card.apply(this, arguments);
|
beestat.component.card.apply(this, arguments);
|
||||||
};
|
};
|
||||||
beestat.extend(beestat.component.card.runtime_detail, beestat.component.card);
|
beestat.extend(beestat.component.card.runtime_thermostat_detail, beestat.component.card);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decorate
|
* Decorate
|
||||||
*
|
*
|
||||||
* @param {rocket.ELements} parent
|
* @param {rocket.ELements} parent
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.decorate_contents_ = function(parent) {
|
beestat.component.card.runtime_thermostat_detail.prototype.decorate_contents_ = function(parent) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var data = this.get_data_();
|
var data = this.get_data_();
|
||||||
this.chart_ = new beestat.component.chart.runtime_detail(data);
|
this.chart_ = new beestat.component.chart.runtime_thermostat_detail(data);
|
||||||
this.chart_.render(parent);
|
this.chart_.render(parent);
|
||||||
|
|
||||||
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
|
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
|
||||||
|
|
||||||
var required_begin;
|
var required_begin;
|
||||||
var required_end;
|
var required_end;
|
||||||
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
||||||
required_begin = moment()
|
required_begin = moment()
|
||||||
.subtract(
|
.subtract(
|
||||||
beestat.setting('runtime_detail_range_dynamic'),
|
beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
||||||
'day'
|
'day'
|
||||||
)
|
)
|
||||||
.second(0);
|
.second(0);
|
||||||
@ -65,10 +65,10 @@ beestat.component.card.runtime_detail.prototype.decorate_contents_ = function(pa
|
|||||||
.second(0);
|
.second(0);
|
||||||
} else {
|
} else {
|
||||||
required_begin = moment(
|
required_begin = moment(
|
||||||
beestat.setting('runtime_detail_range_static_begin') + ' 00:00:00'
|
beestat.setting('runtime_thermostat_detail_range_static_begin') + ' 00:00:00'
|
||||||
);
|
);
|
||||||
required_end = moment(
|
required_end = moment(
|
||||||
beestat.setting('runtime_detail_range_static_end') + ' 23:59:59'
|
beestat.setting('runtime_thermostat_detail_range_static_end') + ' 23:59:59'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ beestat.component.card.runtime_detail.prototype.decorate_contents_ = function(pa
|
|||||||
var value;
|
var value;
|
||||||
var operator;
|
var operator;
|
||||||
|
|
||||||
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
||||||
value = required_begin.format();
|
value = required_begin.format();
|
||||||
operator = '>=';
|
operator = '>=';
|
||||||
} else {
|
} else {
|
||||||
@ -150,7 +150,7 @@ beestat.component.card.runtime_detail.prototype.decorate_contents_ = function(pa
|
|||||||
*
|
*
|
||||||
* @param {rocket.Elements} parent
|
* @param {rocket.Elements} parent
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(parent) {
|
beestat.component.card.runtime_thermostat_detail.prototype.decorate_top_right_ = function(parent) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var menu = (new beestat.component.menu()).render(parent);
|
var menu = (new beestat.component.menu()).render(parent);
|
||||||
@ -160,13 +160,13 @@ beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(p
|
|||||||
.set_icon('numeric_1_box')
|
.set_icon('numeric_1_box')
|
||||||
.set_callback(function() {
|
.set_callback(function() {
|
||||||
if (
|
if (
|
||||||
beestat.setting('runtime_detail_range_dynamic') !== 1 ||
|
beestat.setting('runtime_thermostat_detail_range_dynamic') !== 1 ||
|
||||||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
|
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
||||||
) {
|
) {
|
||||||
beestat.cache.delete('runtime_thermostat');
|
beestat.cache.delete('runtime_thermostat');
|
||||||
beestat.setting({
|
beestat.setting({
|
||||||
'runtime_detail_range_dynamic': 1,
|
'runtime_thermostat_detail_range_dynamic': 1,
|
||||||
'runtime_detail_range_type': 'dynamic'
|
'runtime_thermostat_detail_range_type': 'dynamic'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -176,13 +176,13 @@ beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(p
|
|||||||
.set_icon('numeric_3_box')
|
.set_icon('numeric_3_box')
|
||||||
.set_callback(function() {
|
.set_callback(function() {
|
||||||
if (
|
if (
|
||||||
beestat.setting('runtime_detail_range_dynamic') !== 3 ||
|
beestat.setting('runtime_thermostat_detail_range_dynamic') !== 3 ||
|
||||||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
|
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
||||||
) {
|
) {
|
||||||
beestat.cache.delete('runtime_thermostat');
|
beestat.cache.delete('runtime_thermostat');
|
||||||
beestat.setting({
|
beestat.setting({
|
||||||
'runtime_detail_range_dynamic': 3,
|
'runtime_thermostat_detail_range_dynamic': 3,
|
||||||
'runtime_detail_range_type': 'dynamic'
|
'runtime_thermostat_detail_range_type': 'dynamic'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -192,13 +192,13 @@ beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(p
|
|||||||
.set_icon('numeric_7_box')
|
.set_icon('numeric_7_box')
|
||||||
.set_callback(function() {
|
.set_callback(function() {
|
||||||
if (
|
if (
|
||||||
beestat.setting('runtime_detail_range_dynamic') !== 7 ||
|
beestat.setting('runtime_thermostat_detail_range_dynamic') !== 7 ||
|
||||||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
|
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
||||||
) {
|
) {
|
||||||
beestat.cache.delete('runtime_thermostat');
|
beestat.cache.delete('runtime_thermostat');
|
||||||
beestat.setting({
|
beestat.setting({
|
||||||
'runtime_detail_range_dynamic': 7,
|
'runtime_thermostat_detail_range_dynamic': 7,
|
||||||
'runtime_detail_range_type': 'dynamic'
|
'runtime_thermostat_detail_range_type': 'dynamic'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -207,7 +207,7 @@ beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(p
|
|||||||
.set_text('Custom')
|
.set_text('Custom')
|
||||||
.set_icon('calendar_edit')
|
.set_icon('calendar_edit')
|
||||||
.set_callback(function() {
|
.set_callback(function() {
|
||||||
(new beestat.component.modal.runtime_detail_custom()).render();
|
(new beestat.component.modal.runtime_thermostat_detail_custom()).render();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
menu.add_menu_item(new beestat.component.menu_item()
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
@ -224,19 +224,19 @@ beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(p
|
|||||||
self.chart_.reset_zoom();
|
self.chart_.reset_zoom();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (beestat.setting('runtime_detail_smoothing') === true) {
|
if (beestat.setting('runtime_thermostat_detail_smoothing') === true) {
|
||||||
menu.add_menu_item(new beestat.component.menu_item()
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
.set_text('Disable Smothing')
|
.set_text('Disable Smothing')
|
||||||
.set_icon('chart_line')
|
.set_icon('chart_line')
|
||||||
.set_callback(function() {
|
.set_callback(function() {
|
||||||
beestat.setting('runtime_detail_smoothing', false);
|
beestat.setting('runtime_thermostat_detail_smoothing', false);
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
menu.add_menu_item(new beestat.component.menu_item()
|
menu.add_menu_item(new beestat.component.menu_item()
|
||||||
.set_text('Enable Smoothing')
|
.set_text('Enable Smoothing')
|
||||||
.set_icon('chart_bell_curve')
|
.set_icon('chart_bell_curve')
|
||||||
.set_callback(function() {
|
.set_callback(function() {
|
||||||
beestat.setting('runtime_detail_smoothing', true);
|
beestat.setting('runtime_thermostat_detail_smoothing', true);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(p
|
|||||||
*
|
*
|
||||||
* @return {object} The series data.
|
* @return {object} The series data.
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
beestat.component.card.runtime_thermostat_detail.prototype.get_data_ = function() {
|
||||||
var data = {
|
var data = {
|
||||||
'x': [],
|
'x': [],
|
||||||
'series': {},
|
'series': {},
|
||||||
@ -352,7 +352,7 @@ beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
|||||||
* Figure out what date range to use.
|
* Figure out what date range to use.
|
||||||
* var begin_m = moment()
|
* var begin_m = moment()
|
||||||
* .subtract(
|
* .subtract(
|
||||||
* beestat.setting('runtime_detail_range_dynamic'),
|
* beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
||||||
* 'day'
|
* 'day'
|
||||||
* );
|
* );
|
||||||
* begin_m
|
* begin_m
|
||||||
@ -364,18 +364,18 @@ beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
|||||||
|
|
||||||
var begin_m;
|
var begin_m;
|
||||||
var end_m;
|
var end_m;
|
||||||
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
||||||
begin_m = moment().subtract(
|
begin_m = moment().subtract(
|
||||||
beestat.setting('runtime_detail_range_dynamic'),
|
beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
||||||
'day'
|
'day'
|
||||||
);
|
);
|
||||||
end_m = moment().subtract(1, 'hour');
|
end_m = moment().subtract(1, 'hour');
|
||||||
} else {
|
} else {
|
||||||
begin_m = moment(
|
begin_m = moment(
|
||||||
beestat.setting('runtime_detail_range_static_begin') + ' 00:00:00'
|
beestat.setting('runtime_thermostat_detail_range_static_begin') + ' 00:00:00'
|
||||||
);
|
);
|
||||||
end_m = moment(
|
end_m = moment(
|
||||||
beestat.setting('runtime_detail_range_static_end') + ' 23:59:59'
|
beestat.setting('runtime_thermostat_detail_range_static_end') + ' 23:59:59'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,7 +395,7 @@ beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
|||||||
// Initialize moving average.
|
// Initialize moving average.
|
||||||
var moving = [];
|
var moving = [];
|
||||||
var moving_count;
|
var moving_count;
|
||||||
if (beestat.setting('runtime_detail_smoothing') === true) {
|
if (beestat.setting('runtime_thermostat_detail_smoothing') === true) {
|
||||||
moving_count = 15;
|
moving_count = 15;
|
||||||
} else {
|
} else {
|
||||||
moving_count = 1;
|
moving_count = 1;
|
||||||
@ -757,7 +757,7 @@ beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
|||||||
*
|
*
|
||||||
* @return {array} The runtime_thermostat rows.
|
* @return {array} The runtime_thermostat rows.
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.get_runtime_thermostat_by_date_ = function() {
|
beestat.component.card.runtime_thermostat_detail.prototype.get_runtime_thermostat_by_date_ = function() {
|
||||||
var runtime_thermostats = {};
|
var runtime_thermostats = {};
|
||||||
if (beestat.cache.runtime_thermostat !== undefined) {
|
if (beestat.cache.runtime_thermostat !== undefined) {
|
||||||
beestat.cache.runtime_thermostat.forEach(function(runtime_thermostat) {
|
beestat.cache.runtime_thermostat.forEach(function(runtime_thermostat) {
|
||||||
@ -777,7 +777,7 @@ beestat.component.card.runtime_detail.prototype.get_runtime_thermostat_by_date_
|
|||||||
*
|
*
|
||||||
* @return {number} The average.
|
* @return {number} The average.
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.get_average_ = function(runtime_thermostats, series_code) {
|
beestat.component.card.runtime_thermostat_detail.prototype.get_average_ = function(runtime_thermostats, series_code) {
|
||||||
var average = 0;
|
var average = 0;
|
||||||
var count = 0;
|
var count = 0;
|
||||||
for (var i = 0; i < runtime_thermostats.length; i++) {
|
for (var i = 0; i < runtime_thermostats.length; i++) {
|
||||||
@ -794,7 +794,7 @@ beestat.component.card.runtime_detail.prototype.get_average_ = function(runtime_
|
|||||||
*
|
*
|
||||||
* @return {string} Title
|
* @return {string} Title
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.get_title_ = function() {
|
beestat.component.card.runtime_thermostat_detail.prototype.get_title_ = function() {
|
||||||
return 'Runtime Detail';
|
return 'Runtime Detail';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -803,19 +803,19 @@ beestat.component.card.runtime_detail.prototype.get_title_ = function() {
|
|||||||
*
|
*
|
||||||
* @return {string} Subtitle
|
* @return {string} Subtitle
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.get_subtitle_ = function() {
|
beestat.component.card.runtime_thermostat_detail.prototype.get_subtitle_ = function() {
|
||||||
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
|
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
||||||
var s = (beestat.setting('runtime_detail_range_dynamic') > 1) ? 's' : '';
|
var s = (beestat.setting('runtime_thermostat_detail_range_dynamic') > 1) ? 's' : '';
|
||||||
|
|
||||||
return 'Past ' +
|
return 'Past ' +
|
||||||
beestat.setting('runtime_detail_range_dynamic') +
|
beestat.setting('runtime_thermostat_detail_range_dynamic') +
|
||||||
' day' +
|
' day' +
|
||||||
s;
|
s;
|
||||||
}
|
}
|
||||||
|
|
||||||
var begin = moment(beestat.setting('runtime_detail_range_static_begin'))
|
var begin = moment(beestat.setting('runtime_thermostat_detail_range_static_begin'))
|
||||||
.format('MMM D, YYYY');
|
.format('MMM D, YYYY');
|
||||||
var end = moment(beestat.setting('runtime_detail_range_static_end'))
|
var end = moment(beestat.setting('runtime_thermostat_detail_range_static_end'))
|
||||||
.format('MMM D, YYYY');
|
.format('MMM D, YYYY');
|
||||||
|
|
||||||
return begin + ' to ' + end;
|
return begin + ' to ' + end;
|
||||||
@ -830,7 +830,7 @@ beestat.component.card.runtime_detail.prototype.get_subtitle_ = function() {
|
|||||||
*
|
*
|
||||||
* @return {boolean} Whether or not the data is synced.
|
* @return {boolean} Whether or not the data is synced.
|
||||||
*/
|
*/
|
||||||
beestat.component.card.runtime_detail.prototype.data_synced_ = function(required_sync_begin, required_sync_end) {
|
beestat.component.card.runtime_thermostat_detail.prototype.data_synced_ = function(required_sync_begin, required_sync_end) {
|
||||||
// Demo can just grab whatever data is there.
|
// Demo can just grab whatever data is there.
|
||||||
if (window.is_demo === true) {
|
if (window.is_demo === true) {
|
||||||
return true;
|
return true;
|
281
js/component/chart/runtime_sensor_detail.js
Normal file
281
js/component/chart/runtime_sensor_detail.js
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
];
|
||||||
|
|
||||||
|
Object.values(beestat.cache.sensor).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(sensor_count, 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 sensor_count = (Object.keys(this.data_.series).length - 1) / 2;
|
||||||
|
|
||||||
|
var y_min = Math.floor((this.data_.metadata.chart.y_min - 5) / 10) * 10;
|
||||||
|
var y_max = Math.ceil((this.data_.metadata.chart.y_max + 10) / 10) * 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, setting the swimlane axis to a fixed height and
|
||||||
|
* having it sit on top of a full height axis works well enough. Adding a
|
||||||
|
* bit of padding to the primary axis prevents those values from flowing on
|
||||||
|
* top. It's not perfect because you get the main axis all the way up the
|
||||||
|
* side but it's not terrible.
|
||||||
|
*/
|
||||||
|
y_max += ((sensor_count > 8) ? 20 : 10);
|
||||||
|
|
||||||
|
var tick_positions = [];
|
||||||
|
var tick_interval = (beestat.setting('temperature_unit') === '°F') ? 10 : 5;
|
||||||
|
var current_tick_position =
|
||||||
|
Math.floor(y_min / tick_interval) * tick_interval;
|
||||||
|
while (current_tick_position <= y_max) {
|
||||||
|
tick_positions.push(current_tick_position);
|
||||||
|
current_tick_position += tick_interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Temperature
|
||||||
|
{
|
||||||
|
'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': 15,
|
||||||
|
'min': 0,
|
||||||
|
'max': 100,
|
||||||
|
'reversed': true,
|
||||||
|
'gridLineWidth': 0,
|
||||||
|
'title': {'text': null},
|
||||||
|
'labels': {'enabled': false}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = [];
|
||||||
|
|
||||||
|
var values = {};
|
||||||
|
this.points.forEach(function(point) {
|
||||||
|
values[point.series.name] = point.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.points.forEach(function(point) {
|
||||||
|
var label;
|
||||||
|
var value;
|
||||||
|
var color;
|
||||||
|
|
||||||
|
if (point.series.name.includes('temperature') === true) {
|
||||||
|
label = self.data_.metadata.series[point.series.name].name;
|
||||||
|
color = point.series.color;
|
||||||
|
value = beestat.temperature({
|
||||||
|
'temperature': values[point.series.name],
|
||||||
|
'convert': false,
|
||||||
|
'units': true
|
||||||
|
});
|
||||||
|
|
||||||
|
var occupancy_key = point.series.name.replace('temperature', 'occupancy');
|
||||||
|
if (values[occupancy_key] !== undefined && values[occupancy_key] !== null) {
|
||||||
|
value += ' ●';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
var height = 50;
|
||||||
|
var max_line_width = 16;
|
||||||
|
|
||||||
|
// Spacing.
|
||||||
|
var spacing = 4;
|
||||||
|
|
||||||
|
// Base line width is a percentage height of the container.
|
||||||
|
var line_width = Math.round(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;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'line_width': line_width,
|
||||||
|
'y': y
|
||||||
|
};
|
||||||
|
};
|
@ -1,21 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Runtime thermostat summary chart.
|
* Runtime thermostat detail chart.
|
||||||
*
|
*
|
||||||
* @param {object} data The chart data.
|
* @param {object} data The chart data.
|
||||||
*/
|
*/
|
||||||
beestat.component.chart.runtime_detail = function(data) {
|
beestat.component.chart.runtime_thermostat_detail = function(data) {
|
||||||
this.data_ = data;
|
this.data_ = data;
|
||||||
|
|
||||||
beestat.component.chart.apply(this, arguments);
|
beestat.component.chart.apply(this, arguments);
|
||||||
};
|
};
|
||||||
beestat.extend(beestat.component.chart.runtime_detail, beestat.component.chart);
|
beestat.extend(beestat.component.chart.runtime_thermostat_detail, beestat.component.chart);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override for get_options_xAxis_labels_formatter_.
|
* Override for get_options_xAxis_labels_formatter_.
|
||||||
*
|
*
|
||||||
* @return {Function} xAxis labels formatter.
|
* @return {Function} xAxis labels formatter.
|
||||||
*/
|
*/
|
||||||
beestat.component.chart.runtime_detail.prototype.get_options_xAxis_labels_formatter_ = function() {
|
beestat.component.chart.runtime_thermostat_detail.prototype.get_options_xAxis_labels_formatter_ = function() {
|
||||||
var current_day;
|
var current_day;
|
||||||
var current_hour;
|
var current_hour;
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ beestat.component.chart.runtime_detail.prototype.get_options_xAxis_labels_format
|
|||||||
*
|
*
|
||||||
* @return {Array} All of the series to display on the chart.
|
* @return {Array} All of the series to display on the chart.
|
||||||
*/
|
*/
|
||||||
beestat.component.chart.runtime_detail.prototype.get_options_series_ = function() {
|
beestat.component.chart.runtime_thermostat_detail.prototype.get_options_series_ = function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var series = [];
|
var series = [];
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ beestat.component.chart.runtime_detail.prototype.get_options_series_ = function(
|
|||||||
*
|
*
|
||||||
* @return {Array} The y-axis options.
|
* @return {Array} The y-axis options.
|
||||||
*/
|
*/
|
||||||
beestat.component.chart.runtime_detail.prototype.get_options_yAxis_ = function() {
|
beestat.component.chart.runtime_thermostat_detail.prototype.get_options_yAxis_ = function() {
|
||||||
/**
|
/**
|
||||||
* Highcharts doesn't seem to respect axis behavior well so just overriding
|
* Highcharts doesn't seem to respect axis behavior well so just overriding
|
||||||
* it completely here.
|
* it completely here.
|
||||||
@ -245,7 +245,7 @@ beestat.component.chart.runtime_detail.prototype.get_options_yAxis_ = function()
|
|||||||
*
|
*
|
||||||
* @return {Function} The tooltip formatter.
|
* @return {Function} The tooltip formatter.
|
||||||
*/
|
*/
|
||||||
beestat.component.chart.runtime_detail.prototype.get_options_tooltip_formatter_ = function() {
|
beestat.component.chart.runtime_thermostat_detail.prototype.get_options_tooltip_formatter_ = function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
return function() {
|
return function() {
|
@ -22,18 +22,39 @@ beestat.component.header.prototype.decorate_ = function(parent) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var pages;
|
var pages;
|
||||||
pages = [
|
|
||||||
{
|
if (false && beestat.has_early_access() === true) {
|
||||||
'layer': 'dashboard',
|
pages = [
|
||||||
'text': 'Dashboard',
|
{
|
||||||
'icon': 'gauge'
|
'layer': 'dashboard',
|
||||||
},
|
'text': 'Dashboard',
|
||||||
{
|
'icon': 'tablet_dashboard'
|
||||||
'layer': 'home_comparisons',
|
},
|
||||||
'text': 'Home Comparisons',
|
{
|
||||||
'icon': 'home'
|
'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'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
var gutter = beestat.style.size.gutter;
|
var gutter = beestat.style.size.gutter;
|
||||||
|
|
||||||
@ -77,7 +98,7 @@ beestat.component.header.prototype.decorate_ = function(parent) {
|
|||||||
.set_icon(page.icon)
|
.set_icon(page.icon)
|
||||||
.set_text_color(beestat.style.color.bluegray.dark);
|
.set_text_color(beestat.style.color.bluegray.dark);
|
||||||
|
|
||||||
if (beestat.width > 500) {
|
if (beestat.width > 650) {
|
||||||
button.set_text(page.text);
|
button.set_text(page.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
js/component/modal/runtime_sensor_detail_custom.js
Normal file
0
js/component/modal/runtime_sensor_detail_custom.js
Normal file
@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Custom date range for the Runtime Detail chart.
|
* Custom date range for the Runtime Detail chart.
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom = function() {
|
beestat.component.modal.runtime_thermostat_detail_custom = function() {
|
||||||
beestat.component.modal.apply(this, arguments);
|
beestat.component.modal.apply(this, arguments);
|
||||||
this.state_.runtime_detail_range_type = beestat.setting('runtime_detail_range_type');
|
this.state_.runtime_thermostat_detail_range_type = beestat.setting('runtime_thermostat_detail_range_type');
|
||||||
this.state_.runtime_detail_range_dynamic = beestat.setting('runtime_detail_range_dynamic');
|
this.state_.runtime_thermostat_detail_range_dynamic = beestat.setting('runtime_thermostat_detail_range_dynamic');
|
||||||
this.state_.runtime_detail_range_static_begin = beestat.setting('runtime_detail_range_static_begin');
|
this.state_.runtime_thermostat_detail_range_static_begin = beestat.setting('runtime_thermostat_detail_range_static_begin');
|
||||||
this.state_.runtime_detail_range_static_end = beestat.setting('runtime_detail_range_static_end');
|
this.state_.runtime_thermostat_detail_range_static_end = beestat.setting('runtime_thermostat_detail_range_static_end');
|
||||||
this.state_.error = {
|
this.state_.error = {
|
||||||
'max_range': false,
|
'max_range': false,
|
||||||
'invalid_range_begin': false,
|
'invalid_range_begin': false,
|
||||||
@ -14,19 +14,19 @@ beestat.component.modal.runtime_detail_custom = function() {
|
|||||||
'out_of_sync_range': false
|
'out_of_sync_range': false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
beestat.extend(beestat.component.modal.runtime_detail_custom, beestat.component.modal);
|
beestat.extend(beestat.component.modal.runtime_thermostat_detail_custom, beestat.component.modal);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decorate.
|
* Decorate.
|
||||||
*
|
*
|
||||||
* @param {rocket.Elements} parent
|
* @param {rocket.Elements} parent
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom.prototype.decorate_contents_ = function(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 Runtime Detail chart.'));
|
||||||
|
|
||||||
this.decorate_range_type_(parent);
|
this.decorate_range_type_(parent);
|
||||||
|
|
||||||
if (this.state_.runtime_detail_range_type === 'dynamic') {
|
if (this.state_.runtime_thermostat_detail_range_type === 'dynamic') {
|
||||||
this.decorate_range_dynamic_(parent);
|
this.decorate_range_dynamic_(parent);
|
||||||
} else {
|
} else {
|
||||||
this.decorate_range_static_(parent);
|
this.decorate_range_static_(parent);
|
||||||
@ -40,7 +40,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_contents_ = fun
|
|||||||
*
|
*
|
||||||
* @param {rocket.Elements} parent
|
* @param {rocket.Elements} parent
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom.prototype.decorate_range_type_ = function(parent) {
|
beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_range_type_ = function(parent) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var button_group = new beestat.component.button_group();
|
var button_group = new beestat.component.button_group();
|
||||||
@ -49,13 +49,13 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_type_ = f
|
|||||||
.set_background_hover_color(beestat.style.color.lightblue.base)
|
.set_background_hover_color(beestat.style.color.lightblue.base)
|
||||||
.set_text_color('#fff')
|
.set_text_color('#fff')
|
||||||
.set_background_color(
|
.set_background_color(
|
||||||
this.state_.runtime_detail_range_type === 'dynamic'
|
this.state_.runtime_thermostat_detail_range_type === 'dynamic'
|
||||||
? beestat.style.color.lightblue.base
|
? beestat.style.color.lightblue.base
|
||||||
: beestat.style.color.bluegray.base
|
: beestat.style.color.bluegray.base
|
||||||
)
|
)
|
||||||
.set_text('Dynamic')
|
.set_text('Dynamic')
|
||||||
.addEventListener('click', function() {
|
.addEventListener('click', function() {
|
||||||
self.state_.runtime_detail_range_type = 'dynamic';
|
self.state_.runtime_thermostat_detail_range_type = 'dynamic';
|
||||||
self.rerender();
|
self.rerender();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -63,13 +63,13 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_type_ = f
|
|||||||
.set_background_hover_color(beestat.style.color.lightblue.base)
|
.set_background_hover_color(beestat.style.color.lightblue.base)
|
||||||
.set_text_color('#fff')
|
.set_text_color('#fff')
|
||||||
.set_background_color(
|
.set_background_color(
|
||||||
this.state_.runtime_detail_range_type === 'static'
|
this.state_.runtime_thermostat_detail_range_type === 'static'
|
||||||
? beestat.style.color.lightblue.base
|
? beestat.style.color.lightblue.base
|
||||||
: beestat.style.color.bluegray.base
|
: beestat.style.color.bluegray.base
|
||||||
)
|
)
|
||||||
.set_text('Static')
|
.set_text('Static')
|
||||||
.addEventListener('click', function() {
|
.addEventListener('click', function() {
|
||||||
self.state_.runtime_detail_range_type = 'static';
|
self.state_.runtime_thermostat_detail_range_type = 'static';
|
||||||
self.rerender();
|
self.rerender();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -86,11 +86,11 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_type_ = f
|
|||||||
*
|
*
|
||||||
* @param {rocket.Elements} parent
|
* @param {rocket.Elements} parent
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ = function(parent) {
|
beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_range_static_ = function(parent) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var runtime_detail_static_range_begin;
|
var runtime_thermostat_detail_static_range_begin;
|
||||||
var runtime_detail_static_range_end;
|
var runtime_thermostat_detail_static_range_end;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether or not a value is outside of where data is synced.
|
* Check whether or not a value is outside of where data is synced.
|
||||||
@ -104,13 +104,13 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
var max = moment(thermostat.sync_end);
|
var max = moment(thermostat.sync_end);
|
||||||
|
|
||||||
var begin = moment.min(
|
var begin = moment.min(
|
||||||
moment(runtime_detail_static_range_begin.get_value()),
|
moment(runtime_thermostat_detail_static_range_begin.get_value()),
|
||||||
moment(runtime_detail_static_range_end.get_value())
|
moment(runtime_thermostat_detail_static_range_end.get_value())
|
||||||
);
|
);
|
||||||
|
|
||||||
var end = moment.max(
|
var end = moment.max(
|
||||||
moment(runtime_detail_static_range_begin.get_value() + ' 00:00:00'),
|
moment(runtime_thermostat_detail_static_range_begin.get_value() + ' 00:00:00'),
|
||||||
moment(runtime_detail_static_range_end.get_value() + ' 23:59:59')
|
moment(runtime_thermostat_detail_static_range_end.get_value() + ' 23:59:59')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -123,7 +123,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
runtime_detail_static_range_begin = new beestat.component.input.text()
|
runtime_thermostat_detail_static_range_begin = new beestat.component.input.text()
|
||||||
.set_style({
|
.set_style({
|
||||||
'width': 110,
|
'width': 110,
|
||||||
'text-align': 'center',
|
'text-align': 'center',
|
||||||
@ -133,16 +133,16 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
'maxlength': 10
|
'maxlength': 10
|
||||||
})
|
})
|
||||||
.set_icon('calendar')
|
.set_icon('calendar')
|
||||||
.set_value(this.state_.runtime_detail_range_static_begin);
|
.set_value(this.state_.runtime_thermostat_detail_range_static_begin);
|
||||||
|
|
||||||
runtime_detail_static_range_begin.addEventListener('blur', function() {
|
runtime_thermostat_detail_static_range_begin.addEventListener('blur', function() {
|
||||||
var m = moment(this.get_value());
|
var m = moment(this.get_value());
|
||||||
if (m.isValid() === true) {
|
if (m.isValid() === true) {
|
||||||
self.state_.error.invalid_range_begin = false;
|
self.state_.error.invalid_range_begin = false;
|
||||||
|
|
||||||
var value = m.format('M/D/YYYY');
|
var value = m.format('M/D/YYYY');
|
||||||
|
|
||||||
var diff = Math.abs(m.diff(moment(runtime_detail_static_range_end.get_value()), 'day')) + 1;
|
var diff = Math.abs(m.diff(moment(runtime_thermostat_detail_static_range_end.get_value()), 'day')) + 1;
|
||||||
if (diff > 30) {
|
if (diff > 30) {
|
||||||
self.state_.error.max_range = true;
|
self.state_.error.max_range = true;
|
||||||
} else {
|
} else {
|
||||||
@ -151,16 +151,16 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
|
|
||||||
check_out_of_sync_range();
|
check_out_of_sync_range();
|
||||||
|
|
||||||
self.state_.runtime_detail_range_static_begin = value;
|
self.state_.runtime_thermostat_detail_range_static_begin = value;
|
||||||
self.rerender();
|
self.rerender();
|
||||||
} else {
|
} else {
|
||||||
self.state_.runtime_detail_range_static_begin = this.get_value();
|
self.state_.runtime_thermostat_detail_range_static_begin = this.get_value();
|
||||||
self.state_.error.invalid_range_begin = true;
|
self.state_.error.invalid_range_begin = true;
|
||||||
self.rerender();
|
self.rerender();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime_detail_static_range_end = new beestat.component.input.text()
|
runtime_thermostat_detail_static_range_end = new beestat.component.input.text()
|
||||||
.set_style({
|
.set_style({
|
||||||
'width': 110,
|
'width': 110,
|
||||||
'text-align': 'center',
|
'text-align': 'center',
|
||||||
@ -170,16 +170,16 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
'maxlength': 10
|
'maxlength': 10
|
||||||
})
|
})
|
||||||
.set_icon('calendar')
|
.set_icon('calendar')
|
||||||
.set_value(this.state_.runtime_detail_range_static_end);
|
.set_value(this.state_.runtime_thermostat_detail_range_static_end);
|
||||||
|
|
||||||
runtime_detail_static_range_end.addEventListener('blur', function() {
|
runtime_thermostat_detail_static_range_end.addEventListener('blur', function() {
|
||||||
var m = moment(this.get_value());
|
var m = moment(this.get_value());
|
||||||
if (m.isValid() === true) {
|
if (m.isValid() === true) {
|
||||||
self.state_.error.invalid_range_end = false;
|
self.state_.error.invalid_range_end = false;
|
||||||
|
|
||||||
var value = m.format('M/D/YYYY');
|
var value = m.format('M/D/YYYY');
|
||||||
|
|
||||||
var diff = Math.abs(m.diff(moment(runtime_detail_static_range_begin.get_value()), 'day')) + 1;
|
var diff = Math.abs(m.diff(moment(runtime_thermostat_detail_static_range_begin.get_value()), 'day')) + 1;
|
||||||
if (diff > 30) {
|
if (diff > 30) {
|
||||||
self.state_.error.max_range = true;
|
self.state_.error.max_range = true;
|
||||||
} else {
|
} else {
|
||||||
@ -188,10 +188,10 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
|
|
||||||
check_out_of_sync_range();
|
check_out_of_sync_range();
|
||||||
|
|
||||||
self.state_.runtime_detail_range_static_end = value;
|
self.state_.runtime_thermostat_detail_range_static_end = value;
|
||||||
self.rerender();
|
self.rerender();
|
||||||
} else {
|
} else {
|
||||||
self.state_.runtime_detail_range_static_end = this.get_value();
|
self.state_.runtime_thermostat_detail_range_static_end = this.get_value();
|
||||||
self.state_.error.invalid_range_end = true;
|
self.state_.error.invalid_range_end = true;
|
||||||
self.rerender();
|
self.rerender();
|
||||||
}
|
}
|
||||||
@ -205,7 +205,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
row.appendChild(column);
|
row.appendChild(column);
|
||||||
|
|
||||||
span = $.createElement('span').style('display', 'inline-block');
|
span = $.createElement('span').style('display', 'inline-block');
|
||||||
runtime_detail_static_range_begin.render(span);
|
runtime_thermostat_detail_static_range_begin.render(span);
|
||||||
column.appendChild(span);
|
column.appendChild(span);
|
||||||
|
|
||||||
span = $.createElement('span')
|
span = $.createElement('span')
|
||||||
@ -218,7 +218,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
column.appendChild(span);
|
column.appendChild(span);
|
||||||
|
|
||||||
span = $.createElement('span').style('display', 'inline-block');
|
span = $.createElement('span').style('display', 'inline-block');
|
||||||
runtime_detail_static_range_end.render(span);
|
runtime_thermostat_detail_static_range_end.render(span);
|
||||||
column.appendChild(span);
|
column.appendChild(span);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -227,10 +227,10 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
|||||||
*
|
*
|
||||||
* @param {rocket.Elements} parent
|
* @param {rocket.Elements} parent
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_ = function(parent) {
|
beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_range_dynamic_ = function(parent) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var runtime_detail_range_dynamic = new beestat.component.input.text()
|
var runtime_thermostat_detail_range_dynamic = new beestat.component.input.text()
|
||||||
.set_style({
|
.set_style({
|
||||||
'width': 75,
|
'width': 75,
|
||||||
'text-align': 'center',
|
'text-align': 'center',
|
||||||
@ -240,9 +240,9 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
|||||||
'maxlength': 2
|
'maxlength': 2
|
||||||
})
|
})
|
||||||
.set_icon('pound')
|
.set_icon('pound')
|
||||||
.set_value(beestat.setting('runtime_detail_range_dynamic'));
|
.set_value(beestat.setting('runtime_thermostat_detail_range_dynamic'));
|
||||||
|
|
||||||
runtime_detail_range_dynamic.addEventListener('blur', function() {
|
runtime_thermostat_detail_range_dynamic.addEventListener('blur', function() {
|
||||||
var value = parseInt(this.get_value(), 10);
|
var value = parseInt(this.get_value(), 10);
|
||||||
if (isNaN(value) === true || value === 0) {
|
if (isNaN(value) === true || value === 0) {
|
||||||
value = 1;
|
value = 1;
|
||||||
@ -250,7 +250,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
|||||||
value = 30;
|
value = 30;
|
||||||
}
|
}
|
||||||
this.set_value(value);
|
this.set_value(value);
|
||||||
self.state_.runtime_detail_range_dynamic = value;
|
self.state_.runtime_thermostat_detail_range_dynamic = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
var span;
|
var span;
|
||||||
@ -261,7 +261,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
|||||||
row.appendChild(column);
|
row.appendChild(column);
|
||||||
|
|
||||||
span = $.createElement('span').style('display', 'inline-block');
|
span = $.createElement('span').style('display', 'inline-block');
|
||||||
runtime_detail_range_dynamic.render(span);
|
runtime_thermostat_detail_range_dynamic.render(span);
|
||||||
column.appendChild(span);
|
column.appendChild(span);
|
||||||
|
|
||||||
span = $.createElement('span')
|
span = $.createElement('span')
|
||||||
@ -278,7 +278,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
|||||||
*
|
*
|
||||||
* @param {rocket.Elements} parent
|
* @param {rocket.Elements} parent
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom.prototype.decorate_error_ = function(parent) {
|
beestat.component.modal.runtime_thermostat_detail_custom.prototype.decorate_error_ = function(parent) {
|
||||||
var div = $.createElement('div').style('color', beestat.style.color.red.base);
|
var div = $.createElement('div').style('color', beestat.style.color.red.base);
|
||||||
if (this.state_.error.max_range === true) {
|
if (this.state_.error.max_range === true) {
|
||||||
div.appendChild($.createElement('div').innerText('Max range is 30 days.'));
|
div.appendChild($.createElement('div').innerText('Max range is 30 days.'));
|
||||||
@ -300,7 +300,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_error_ = functi
|
|||||||
*
|
*
|
||||||
* @return {string} Title
|
* @return {string} Title
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom.prototype.get_title_ = function() {
|
beestat.component.modal.runtime_thermostat_detail_custom.prototype.get_title_ = function() {
|
||||||
return 'Runtime Detail - Custom Range';
|
return 'Runtime Detail - Custom Range';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -309,7 +309,7 @@ beestat.component.modal.runtime_detail_custom.prototype.get_title_ = function()
|
|||||||
*
|
*
|
||||||
* @return {[beestat.component.button]} The buttons.
|
* @return {[beestat.component.button]} The buttons.
|
||||||
*/
|
*/
|
||||||
beestat.component.modal.runtime_detail_custom.prototype.get_buttons_ = function() {
|
beestat.component.modal.runtime_thermostat_detail_custom.prototype.get_buttons_ = function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var cancel = new beestat.component.button()
|
var cancel = new beestat.component.button()
|
||||||
@ -344,19 +344,19 @@ beestat.component.modal.runtime_detail_custom.prototype.get_buttons_ = function(
|
|||||||
.set_background_hover_color()
|
.set_background_hover_color()
|
||||||
.removeEventListener('click');
|
.removeEventListener('click');
|
||||||
|
|
||||||
if (moment(self.state_.runtime_detail_range_static_begin).isAfter(moment(self.state_.runtime_detail_range_static_end)) === true) {
|
if (moment(self.state_.runtime_thermostat_detail_range_static_begin).isAfter(moment(self.state_.runtime_thermostat_detail_range_static_end)) === true) {
|
||||||
var temp = self.state_.runtime_detail_range_static_begin;
|
var temp = self.state_.runtime_thermostat_detail_range_static_begin;
|
||||||
self.state_.runtime_detail_range_static_begin = self.state_.runtime_detail_range_static_end;
|
self.state_.runtime_thermostat_detail_range_static_begin = self.state_.runtime_thermostat_detail_range_static_end;
|
||||||
self.state_.runtime_detail_range_static_end = temp;
|
self.state_.runtime_thermostat_detail_range_static_end = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
beestat.cache.delete('runtime_thermostat');
|
beestat.cache.delete('runtime_thermostat');
|
||||||
beestat.setting(
|
beestat.setting(
|
||||||
{
|
{
|
||||||
'runtime_detail_range_type': self.state_.runtime_detail_range_type,
|
'runtime_thermostat_detail_range_type': self.state_.runtime_thermostat_detail_range_type,
|
||||||
'runtime_detail_range_dynamic': self.state_.runtime_detail_range_dynamic,
|
'runtime_thermostat_detail_range_dynamic': self.state_.runtime_thermostat_detail_range_dynamic,
|
||||||
'runtime_detail_range_static_begin': self.state_.runtime_detail_range_static_begin,
|
'runtime_thermostat_detail_range_static_begin': self.state_.runtime_thermostat_detail_range_static_begin,
|
||||||
'runtime_detail_range_static_end': self.state_.runtime_detail_range_static_end
|
'runtime_thermostat_detail_range_static_end': self.state_.runtime_thermostat_detail_range_static_end
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
function() {
|
function() {
|
10
js/js.php
10
js/js.php
@ -37,6 +37,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
|||||||
echo '<script src="/js/layer/load.js"></script>' . PHP_EOL;
|
echo '<script src="/js/layer/load.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/layer/dashboard.js"></script>' . PHP_EOL;
|
echo '<script src="/js/layer/dashboard.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/layer/home_comparisons.js"></script>' . PHP_EOL;
|
echo '<script src="/js/layer/home_comparisons.js"></script>' . PHP_EOL;
|
||||||
|
echo '<script src="/js/layer/sensors.js"></script>' . PHP_EOL;
|
||||||
|
|
||||||
// Component
|
// Component
|
||||||
echo '<script src="/js/component.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component.js"></script>' . PHP_EOL;
|
||||||
@ -50,7 +51,8 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
|||||||
echo '<script src="/js/component/card/footer.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/card/footer.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/card/my_home.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/card/my_home.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/card/patreon.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/card/patreon.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/card/runtime_detail.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/card/runtime_thermostat_detail.js"></script>' . PHP_EOL;
|
||||||
|
echo '<script src="/js/component/card/runtime_sensor_detail.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/card/score.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/card/score.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/card/score/cool.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/card/score/cool.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/card/score/heat.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/card/score/heat.js"></script>' . PHP_EOL;
|
||||||
@ -61,7 +63,8 @@ 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.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/chart/runtime_thermostat_summary.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/temperature_profiles.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/chart/runtime_detail.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/header.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/icon.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/layout.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/layout.js"></script>' . PHP_EOL;
|
||||||
@ -78,7 +81,8 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
|||||||
echo '<script src="/js/component/modal/error.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/modal/error.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/modal/filter_info.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/modal/filter_info.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/modal/enjoy_beestat.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/modal/enjoy_beestat.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/modal/runtime_detail_custom.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/modal/runtime_thermostat_detail_custom.js"></script>' . PHP_EOL;
|
||||||
|
echo '<script src="/js/component/modal/runtime_sensor_detail_custom.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/modal/thermostat_info.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/modal/thermostat_info.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/modal/weather.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/modal/weather.js"></script>' . PHP_EOL;
|
||||||
echo '<script src="/js/component/input.js"></script>' . PHP_EOL;
|
echo '<script src="/js/component/input.js"></script>' . PHP_EOL;
|
||||||
|
@ -79,7 +79,7 @@ beestat.layer.dashboard.prototype.decorate_ = function(parent) {
|
|||||||
|
|
||||||
cards.push([
|
cards.push([
|
||||||
{
|
{
|
||||||
'card': new beestat.component.card.runtime_detail(
|
'card': new beestat.component.card.runtime_thermostat_detail(
|
||||||
beestat.setting('thermostat_id')
|
beestat.setting('thermostat_id')
|
||||||
),
|
),
|
||||||
'size': 12
|
'size': 12
|
||||||
|
@ -204,7 +204,7 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
|
|||||||
*/
|
*/
|
||||||
new beestat.api()
|
new beestat.api()
|
||||||
.add_call(
|
.add_call(
|
||||||
'runtime_thermostat',
|
'runtime',
|
||||||
'sync',
|
'sync',
|
||||||
{
|
{
|
||||||
'thermostat_id': thermostat.thermostat_id
|
'thermostat_id': thermostat.thermostat_id
|
||||||
|
59
js/layer/sensors.js
Normal file
59
js/layer/sensors.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Sensors layer.
|
||||||
|
*/
|
||||||
|
beestat.layer.sensors = function() {
|
||||||
|
beestat.layer.apply(this, arguments);
|
||||||
|
};
|
||||||
|
beestat.extend(beestat.layer.sensors, beestat.layer);
|
||||||
|
|
||||||
|
beestat.layer.sensors.prototype.decorate_ = function(parent) {
|
||||||
|
/*
|
||||||
|
* Set the overflow on the body so the scrollbar is always present so
|
||||||
|
* highcharts graphs render properly.
|
||||||
|
*/
|
||||||
|
$('body').style({
|
||||||
|
'overflow-y': 'scroll',
|
||||||
|
'background': beestat.style.color.bluegray.light,
|
||||||
|
'padding': '0 ' + beestat.style.size.gutter + 'px'
|
||||||
|
});
|
||||||
|
|
||||||
|
(new beestat.component.header('sensors')).render(parent);
|
||||||
|
|
||||||
|
// All the cards
|
||||||
|
var cards = [];
|
||||||
|
|
||||||
|
if (window.is_demo === true) {
|
||||||
|
cards.push([
|
||||||
|
{
|
||||||
|
'card': new beestat.component.card.demo(),
|
||||||
|
'size': 12
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cards.push([
|
||||||
|
{
|
||||||
|
'card': new beestat.component.card.sensors(),
|
||||||
|
'size': 12
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
cards.push([
|
||||||
|
{
|
||||||
|
'card': new beestat.component.card.runtime_sensor_detail(
|
||||||
|
beestat.setting('thermostat_id')
|
||||||
|
),
|
||||||
|
'size': 12
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
cards.push([
|
||||||
|
{
|
||||||
|
'card': new beestat.component.card.footer(),
|
||||||
|
'size': 12
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
(new beestat.component.layout(cards)).render(parent);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user