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['type'] = $api_sensor['type'];
|
||||
$attributes['in_use'] = $api_sensor['inUse'];
|
||||
$attributes['identifier'] = $api_sensor['id'];
|
||||
$attributes['capability'] = $api_sensor['capability'];
|
||||
$attributes['inactive'] = 0;
|
||||
|
||||
$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 = [
|
||||
'private' => [
|
||||
'read',
|
||||
'sync',
|
||||
'download'
|
||||
'read'
|
||||
],
|
||||
'public' => []
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -350,12 +350,12 @@ a.inverted:active {
|
||||
.icon.eye_off:before { content: "\F209"; }
|
||||
.icon.fan:before { content: "\F210"; }
|
||||
.icon.fire:before { content: "\F238"; }
|
||||
.icon.gauge:before { content: "\F29A"; }
|
||||
.icon.google_play:before { content: "\F2BC"; }
|
||||
.icon.heart:before { content: "\F2D1"; }
|
||||
.icon.help_circle:before { content: "\F2D7"; }
|
||||
.icon.home:before { content: "\F2DC"; }
|
||||
.icon.home_floor_a:before { content: "\FD5F"; }
|
||||
.icon.home_group:before { content: "\FDB0"; }
|
||||
.icon.information:before { content: "\F2FC"; }
|
||||
.icon.key:before { content: "\F306"; }
|
||||
.icon.layers:before { content: "\F328"; }
|
||||
@ -371,6 +371,8 @@ a.inverted:active {
|
||||
.icon.pound:before { content: "\F423"; }
|
||||
.icon.snowflake:before { content: "\F716"; }
|
||||
.icon.swap_horizontal:before { content: "\F4E1"; }
|
||||
.icon.signal_variant:before { content: "\F60A"; }
|
||||
.icon.tablet_dashboard:before { content: "\FEEB"; }
|
||||
.icon.thermostat:before { content: "\F393"; }
|
||||
.icon.thumb_up:before { content: "\F513"; }
|
||||
.icon.tune:before { content: "\F62E"; }
|
||||
|
@ -143,8 +143,8 @@ if ('serviceWorker' in navigator) {
|
||||
beestat.width = window.innerWidth;
|
||||
window.addEventListener('resize', rocket.throttle(100, function() {
|
||||
var breakpoints = [
|
||||
500,
|
||||
600
|
||||
600,
|
||||
650
|
||||
];
|
||||
|
||||
breakpoints.forEach(function(breakpoint) {
|
||||
|
@ -12,13 +12,21 @@ beestat.setting = function(key, opt_value, opt_callback) {
|
||||
var user = beestat.get_user();
|
||||
|
||||
var defaults = {
|
||||
'runtime_detail_smoothing': true,
|
||||
'runtime_detail_range_type': 'dynamic',
|
||||
'runtime_detail_range_static_begin': moment()
|
||||
'runtime_thermostat_detail_smoothing': true,
|
||||
'runtime_thermostat_detail_range_type': 'dynamic',
|
||||
'runtime_thermostat_detail_range_static_begin': moment()
|
||||
.subtract(3, 'day')
|
||||
.format('MM/DD/YYYY'),
|
||||
'runtime_detail_range_static_end': moment().format('MM/DD/YYYY'),
|
||||
'runtime_detail_range_dynamic': 3,
|
||||
'runtime_thermostat_detail_range_static_end': moment().format('MM/DD/YYYY'),
|
||||
'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_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
|
||||
* data for
|
||||
*/
|
||||
beestat.component.card.runtime_detail = function(thermostat_id) {
|
||||
beestat.component.card.runtime_thermostat_detail = function(thermostat_id) {
|
||||
var self = this;
|
||||
|
||||
this.thermostat_id_ = thermostat_id;
|
||||
@ -24,9 +24,9 @@ beestat.component.card.runtime_detail = function(thermostat_id) {
|
||||
|
||||
beestat.dispatcher.addEventListener(
|
||||
[
|
||||
'setting.runtime_detail_smoothing',
|
||||
'setting.runtime_detail_range_type',
|
||||
'setting.runtime_detail_range_dynamic',
|
||||
'setting.runtime_thermostat_detail_smoothing',
|
||||
'setting.runtime_thermostat_detail_range_type',
|
||||
'setting.runtime_thermostat_detail_range_dynamic',
|
||||
'cache.runtime_thermostat'
|
||||
],
|
||||
change_function
|
||||
@ -34,28 +34,28 @@ beestat.component.card.runtime_detail = function(thermostat_id) {
|
||||
|
||||
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
|
||||
*
|
||||
* @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 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);
|
||||
|
||||
var thermostat = beestat.cache.thermostat[this.thermostat_id_];
|
||||
|
||||
var required_begin;
|
||||
var required_end;
|
||||
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
|
||||
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
||||
required_begin = moment()
|
||||
.subtract(
|
||||
beestat.setting('runtime_detail_range_dynamic'),
|
||||
beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
||||
'day'
|
||||
)
|
||||
.second(0);
|
||||
@ -65,10 +65,10 @@ beestat.component.card.runtime_detail.prototype.decorate_contents_ = function(pa
|
||||
.second(0);
|
||||
} else {
|
||||
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(
|
||||
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 operator;
|
||||
|
||||
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
|
||||
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
||||
value = required_begin.format();
|
||||
operator = '>=';
|
||||
} else {
|
||||
@ -150,7 +150,7 @@ beestat.component.card.runtime_detail.prototype.decorate_contents_ = function(pa
|
||||
*
|
||||
* @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 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_callback(function() {
|
||||
if (
|
||||
beestat.setting('runtime_detail_range_dynamic') !== 1 ||
|
||||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
|
||||
beestat.setting('runtime_thermostat_detail_range_dynamic') !== 1 ||
|
||||
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
||||
) {
|
||||
beestat.cache.delete('runtime_thermostat');
|
||||
beestat.setting({
|
||||
'runtime_detail_range_dynamic': 1,
|
||||
'runtime_detail_range_type': 'dynamic'
|
||||
'runtime_thermostat_detail_range_dynamic': 1,
|
||||
'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_callback(function() {
|
||||
if (
|
||||
beestat.setting('runtime_detail_range_dynamic') !== 3 ||
|
||||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
|
||||
beestat.setting('runtime_thermostat_detail_range_dynamic') !== 3 ||
|
||||
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
||||
) {
|
||||
beestat.cache.delete('runtime_thermostat');
|
||||
beestat.setting({
|
||||
'runtime_detail_range_dynamic': 3,
|
||||
'runtime_detail_range_type': 'dynamic'
|
||||
'runtime_thermostat_detail_range_dynamic': 3,
|
||||
'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_callback(function() {
|
||||
if (
|
||||
beestat.setting('runtime_detail_range_dynamic') !== 7 ||
|
||||
beestat.setting('runtime_detail_range_type') !== 'dynamic'
|
||||
beestat.setting('runtime_thermostat_detail_range_dynamic') !== 7 ||
|
||||
beestat.setting('runtime_thermostat_detail_range_type') !== 'dynamic'
|
||||
) {
|
||||
beestat.cache.delete('runtime_thermostat');
|
||||
beestat.setting({
|
||||
'runtime_detail_range_dynamic': 7,
|
||||
'runtime_detail_range_type': 'dynamic'
|
||||
'runtime_thermostat_detail_range_dynamic': 7,
|
||||
'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_icon('calendar_edit')
|
||||
.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()
|
||||
@ -224,19 +224,19 @@ beestat.component.card.runtime_detail.prototype.decorate_top_right_ = function(p
|
||||
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()
|
||||
.set_text('Disable Smothing')
|
||||
.set_icon('chart_line')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_detail_smoothing', false);
|
||||
beestat.setting('runtime_thermostat_detail_smoothing', false);
|
||||
}));
|
||||
} else {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Enable Smoothing')
|
||||
.set_icon('chart_bell_curve')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_detail_smoothing', true);
|
||||
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.
|
||||
*/
|
||||
beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
||||
beestat.component.card.runtime_thermostat_detail.prototype.get_data_ = function() {
|
||||
var data = {
|
||||
'x': [],
|
||||
'series': {},
|
||||
@ -352,7 +352,7 @@ beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
||||
* Figure out what date range to use.
|
||||
* var begin_m = moment()
|
||||
* .subtract(
|
||||
* beestat.setting('runtime_detail_range_dynamic'),
|
||||
* beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
||||
* 'day'
|
||||
* );
|
||||
* begin_m
|
||||
@ -364,18 +364,18 @@ beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
||||
|
||||
var begin_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(
|
||||
beestat.setting('runtime_detail_range_dynamic'),
|
||||
beestat.setting('runtime_thermostat_detail_range_dynamic'),
|
||||
'day'
|
||||
);
|
||||
end_m = moment().subtract(1, 'hour');
|
||||
} else {
|
||||
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(
|
||||
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.
|
||||
var moving = [];
|
||||
var moving_count;
|
||||
if (beestat.setting('runtime_detail_smoothing') === true) {
|
||||
if (beestat.setting('runtime_thermostat_detail_smoothing') === true) {
|
||||
moving_count = 15;
|
||||
} else {
|
||||
moving_count = 1;
|
||||
@ -757,7 +757,7 @@ beestat.component.card.runtime_detail.prototype.get_data_ = function() {
|
||||
*
|
||||
* @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 = {};
|
||||
if (beestat.cache.runtime_thermostat !== undefined) {
|
||||
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.
|
||||
*/
|
||||
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 count = 0;
|
||||
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
|
||||
*/
|
||||
beestat.component.card.runtime_detail.prototype.get_title_ = function() {
|
||||
beestat.component.card.runtime_thermostat_detail.prototype.get_title_ = function() {
|
||||
return 'Runtime Detail';
|
||||
};
|
||||
|
||||
@ -803,19 +803,19 @@ beestat.component.card.runtime_detail.prototype.get_title_ = function() {
|
||||
*
|
||||
* @return {string} Subtitle
|
||||
*/
|
||||
beestat.component.card.runtime_detail.prototype.get_subtitle_ = function() {
|
||||
if (beestat.setting('runtime_detail_range_type') === 'dynamic') {
|
||||
var s = (beestat.setting('runtime_detail_range_dynamic') > 1) ? 's' : '';
|
||||
beestat.component.card.runtime_thermostat_detail.prototype.get_subtitle_ = function() {
|
||||
if (beestat.setting('runtime_thermostat_detail_range_type') === 'dynamic') {
|
||||
var s = (beestat.setting('runtime_thermostat_detail_range_dynamic') > 1) ? 's' : '';
|
||||
|
||||
return 'Past ' +
|
||||
beestat.setting('runtime_detail_range_dynamic') +
|
||||
beestat.setting('runtime_thermostat_detail_range_dynamic') +
|
||||
' day' +
|
||||
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');
|
||||
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');
|
||||
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
if (window.is_demo === 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.
|
||||
*/
|
||||
beestat.component.chart.runtime_detail = function(data) {
|
||||
beestat.component.chart.runtime_thermostat_detail = function(data) {
|
||||
this.data_ = data;
|
||||
|
||||
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_.
|
||||
*
|
||||
* @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_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.
|
||||
*/
|
||||
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 series = [];
|
||||
|
||||
@ -170,7 +170,7 @@ beestat.component.chart.runtime_detail.prototype.get_options_series_ = function(
|
||||
*
|
||||
* @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
|
||||
* it completely here.
|
||||
@ -245,7 +245,7 @@ beestat.component.chart.runtime_detail.prototype.get_options_yAxis_ = function()
|
||||
*
|
||||
* @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;
|
||||
|
||||
return function() {
|
@ -22,18 +22,39 @@ beestat.component.header.prototype.decorate_ = function(parent) {
|
||||
var self = this;
|
||||
|
||||
var pages;
|
||||
pages = [
|
||||
{
|
||||
'layer': 'dashboard',
|
||||
'text': 'Dashboard',
|
||||
'icon': 'gauge'
|
||||
},
|
||||
{
|
||||
'layer': 'home_comparisons',
|
||||
'text': 'Home Comparisons',
|
||||
'icon': 'home'
|
||||
}
|
||||
];
|
||||
|
||||
if (false && beestat.has_early_access() === true) {
|
||||
pages = [
|
||||
{
|
||||
'layer': 'dashboard',
|
||||
'text': 'Dashboard',
|
||||
'icon': 'tablet_dashboard'
|
||||
},
|
||||
{
|
||||
'layer': 'sensors',
|
||||
'text': 'Sensors',
|
||||
'icon': 'signal_variant'
|
||||
},
|
||||
{
|
||||
'layer': 'home_comparisons',
|
||||
'text': 'Comparisons',
|
||||
'icon': 'home_group'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
pages = [
|
||||
{
|
||||
'layer': 'dashboard',
|
||||
'text': 'Dashboard',
|
||||
'icon': 'tablet_dashboard'
|
||||
},
|
||||
{
|
||||
'layer': 'home_comparisons',
|
||||
'text': 'Home Comparisons',
|
||||
'icon': 'home_group'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
var gutter = beestat.style.size.gutter;
|
||||
|
||||
@ -77,7 +98,7 @@ beestat.component.header.prototype.decorate_ = function(parent) {
|
||||
.set_icon(page.icon)
|
||||
.set_text_color(beestat.style.color.bluegray.dark);
|
||||
|
||||
if (beestat.width > 500) {
|
||||
if (beestat.width > 650) {
|
||||
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.
|
||||
*/
|
||||
beestat.component.modal.runtime_detail_custom = function() {
|
||||
beestat.component.modal.runtime_thermostat_detail_custom = function() {
|
||||
beestat.component.modal.apply(this, arguments);
|
||||
this.state_.runtime_detail_range_type = beestat.setting('runtime_detail_range_type');
|
||||
this.state_.runtime_detail_range_dynamic = beestat.setting('runtime_detail_range_dynamic');
|
||||
this.state_.runtime_detail_range_static_begin = beestat.setting('runtime_detail_range_static_begin');
|
||||
this.state_.runtime_detail_range_static_end = beestat.setting('runtime_detail_range_static_end');
|
||||
this.state_.runtime_thermostat_detail_range_type = beestat.setting('runtime_thermostat_detail_range_type');
|
||||
this.state_.runtime_thermostat_detail_range_dynamic = beestat.setting('runtime_thermostat_detail_range_dynamic');
|
||||
this.state_.runtime_thermostat_detail_range_static_begin = beestat.setting('runtime_thermostat_detail_range_static_begin');
|
||||
this.state_.runtime_thermostat_detail_range_static_end = beestat.setting('runtime_thermostat_detail_range_static_end');
|
||||
this.state_.error = {
|
||||
'max_range': false,
|
||||
'invalid_range_begin': false,
|
||||
@ -14,19 +14,19 @@ beestat.component.modal.runtime_detail_custom = function() {
|
||||
'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.
|
||||
*
|
||||
* @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.'));
|
||||
|
||||
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);
|
||||
} else {
|
||||
this.decorate_range_static_(parent);
|
||||
@ -40,7 +40,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_contents_ = fun
|
||||
*
|
||||
* @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 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_text_color('#fff')
|
||||
.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.bluegray.base
|
||||
)
|
||||
.set_text('Dynamic')
|
||||
.addEventListener('click', function() {
|
||||
self.state_.runtime_detail_range_type = 'dynamic';
|
||||
self.state_.runtime_thermostat_detail_range_type = 'dynamic';
|
||||
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_text_color('#fff')
|
||||
.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.bluegray.base
|
||||
)
|
||||
.set_text('Static')
|
||||
.addEventListener('click', function() {
|
||||
self.state_.runtime_detail_range_type = 'static';
|
||||
self.state_.runtime_thermostat_detail_range_type = 'static';
|
||||
self.rerender();
|
||||
}));
|
||||
|
||||
@ -86,11 +86,11 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_type_ = f
|
||||
*
|
||||
* @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 runtime_detail_static_range_begin;
|
||||
var runtime_detail_static_range_end;
|
||||
var runtime_thermostat_detail_static_range_begin;
|
||||
var runtime_thermostat_detail_static_range_end;
|
||||
|
||||
/**
|
||||
* 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 begin = moment.min(
|
||||
moment(runtime_detail_static_range_begin.get_value()),
|
||||
moment(runtime_detail_static_range_end.get_value())
|
||||
moment(runtime_thermostat_detail_static_range_begin.get_value()),
|
||||
moment(runtime_thermostat_detail_static_range_end.get_value())
|
||||
);
|
||||
|
||||
var end = moment.max(
|
||||
moment(runtime_detail_static_range_begin.get_value() + ' 00:00:00'),
|
||||
moment(runtime_detail_static_range_end.get_value() + ' 23:59:59')
|
||||
moment(runtime_thermostat_detail_static_range_begin.get_value() + ' 00:00:00'),
|
||||
moment(runtime_thermostat_detail_static_range_end.get_value() + ' 23:59:59')
|
||||
);
|
||||
|
||||
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({
|
||||
'width': 110,
|
||||
'text-align': 'center',
|
||||
@ -133,16 +133,16 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
||||
'maxlength': 10
|
||||
})
|
||||
.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());
|
||||
if (m.isValid() === true) {
|
||||
self.state_.error.invalid_range_begin = false;
|
||||
|
||||
var value = m.format('M/D/YYYY');
|
||||
|
||||
var diff = Math.abs(m.diff(moment(runtime_detail_static_range_end.get_value()), 'day')) + 1;
|
||||
var diff = Math.abs(m.diff(moment(runtime_thermostat_detail_static_range_end.get_value()), 'day')) + 1;
|
||||
if (diff > 30) {
|
||||
self.state_.error.max_range = true;
|
||||
} else {
|
||||
@ -151,16 +151,16 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
||||
|
||||
check_out_of_sync_range();
|
||||
|
||||
self.state_.runtime_detail_range_static_begin = value;
|
||||
self.state_.runtime_thermostat_detail_range_static_begin = value;
|
||||
self.rerender();
|
||||
} 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.rerender();
|
||||
}
|
||||
});
|
||||
|
||||
runtime_detail_static_range_end = new beestat.component.input.text()
|
||||
runtime_thermostat_detail_static_range_end = new beestat.component.input.text()
|
||||
.set_style({
|
||||
'width': 110,
|
||||
'text-align': 'center',
|
||||
@ -170,16 +170,16 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
||||
'maxlength': 10
|
||||
})
|
||||
.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());
|
||||
if (m.isValid() === true) {
|
||||
self.state_.error.invalid_range_end = false;
|
||||
|
||||
var value = m.format('M/D/YYYY');
|
||||
|
||||
var diff = Math.abs(m.diff(moment(runtime_detail_static_range_begin.get_value()), 'day')) + 1;
|
||||
var diff = Math.abs(m.diff(moment(runtime_thermostat_detail_static_range_begin.get_value()), 'day')) + 1;
|
||||
if (diff > 30) {
|
||||
self.state_.error.max_range = true;
|
||||
} else {
|
||||
@ -188,10 +188,10 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
||||
|
||||
check_out_of_sync_range();
|
||||
|
||||
self.state_.runtime_detail_range_static_end = value;
|
||||
self.state_.runtime_thermostat_detail_range_static_end = value;
|
||||
self.rerender();
|
||||
} 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.rerender();
|
||||
}
|
||||
@ -205,7 +205,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
||||
row.appendChild(column);
|
||||
|
||||
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);
|
||||
|
||||
span = $.createElement('span')
|
||||
@ -218,7 +218,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
||||
column.appendChild(span);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@ -227,10 +227,10 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_static_ =
|
||||
*
|
||||
* @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 runtime_detail_range_dynamic = new beestat.component.input.text()
|
||||
var runtime_thermostat_detail_range_dynamic = new beestat.component.input.text()
|
||||
.set_style({
|
||||
'width': 75,
|
||||
'text-align': 'center',
|
||||
@ -240,9 +240,9 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
||||
'maxlength': 2
|
||||
})
|
||||
.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);
|
||||
if (isNaN(value) === true || value === 0) {
|
||||
value = 1;
|
||||
@ -250,7 +250,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
||||
value = 30;
|
||||
}
|
||||
this.set_value(value);
|
||||
self.state_.runtime_detail_range_dynamic = value;
|
||||
self.state_.runtime_thermostat_detail_range_dynamic = value;
|
||||
});
|
||||
|
||||
var span;
|
||||
@ -261,7 +261,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
||||
row.appendChild(column);
|
||||
|
||||
span = $.createElement('span').style('display', 'inline-block');
|
||||
runtime_detail_range_dynamic.render(span);
|
||||
runtime_thermostat_detail_range_dynamic.render(span);
|
||||
column.appendChild(span);
|
||||
|
||||
span = $.createElement('span')
|
||||
@ -278,7 +278,7 @@ beestat.component.modal.runtime_detail_custom.prototype.decorate_range_dynamic_
|
||||
*
|
||||
* @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);
|
||||
if (this.state_.error.max_range === true) {
|
||||
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
|
||||
*/
|
||||
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';
|
||||
};
|
||||
|
||||
@ -309,7 +309,7 @@ beestat.component.modal.runtime_detail_custom.prototype.get_title_ = function()
|
||||
*
|
||||
* @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 cancel = new beestat.component.button()
|
||||
@ -344,19 +344,19 @@ beestat.component.modal.runtime_detail_custom.prototype.get_buttons_ = function(
|
||||
.set_background_hover_color()
|
||||
.removeEventListener('click');
|
||||
|
||||
if (moment(self.state_.runtime_detail_range_static_begin).isAfter(moment(self.state_.runtime_detail_range_static_end)) === true) {
|
||||
var temp = self.state_.runtime_detail_range_static_begin;
|
||||
self.state_.runtime_detail_range_static_begin = self.state_.runtime_detail_range_static_end;
|
||||
self.state_.runtime_detail_range_static_end = temp;
|
||||
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_thermostat_detail_range_static_begin;
|
||||
self.state_.runtime_thermostat_detail_range_static_begin = self.state_.runtime_thermostat_detail_range_static_end;
|
||||
self.state_.runtime_thermostat_detail_range_static_end = temp;
|
||||
}
|
||||
|
||||
beestat.cache.delete('runtime_thermostat');
|
||||
beestat.setting(
|
||||
{
|
||||
'runtime_detail_range_type': self.state_.runtime_detail_range_type,
|
||||
'runtime_detail_range_dynamic': self.state_.runtime_detail_range_dynamic,
|
||||
'runtime_detail_range_static_begin': self.state_.runtime_detail_range_static_begin,
|
||||
'runtime_detail_range_static_end': self.state_.runtime_detail_range_static_end
|
||||
'runtime_thermostat_detail_range_type': self.state_.runtime_thermostat_detail_range_type,
|
||||
'runtime_thermostat_detail_range_dynamic': self.state_.runtime_thermostat_detail_range_dynamic,
|
||||
'runtime_thermostat_detail_range_static_begin': self.state_.runtime_thermostat_detail_range_static_begin,
|
||||
'runtime_thermostat_detail_range_static_end': self.state_.runtime_thermostat_detail_range_static_end
|
||||
},
|
||||
undefined,
|
||||
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/dashboard.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
|
||||
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/my_home.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/cool.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/runtime_thermostat_summary.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/temperature_profiles.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_detail.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/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/icon.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/filter_info.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/weather.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([
|
||||
{
|
||||
'card': new beestat.component.card.runtime_detail(
|
||||
'card': new beestat.component.card.runtime_thermostat_detail(
|
||||
beestat.setting('thermostat_id')
|
||||
),
|
||||
'size': 12
|
||||
|
@ -204,7 +204,7 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
|
||||
*/
|
||||
new beestat.api()
|
||||
.add_call(
|
||||
'runtime_thermostat',
|
||||
'runtime',
|
||||
'sync',
|
||||
{
|
||||
'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