mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
Most of the changes for 1.4
This commit is contained in:
parent
22671bb7b5
commit
47bcf5d45b
102
README.md
102
README.md
@ -1,51 +1,51 @@
|
||||
<h1 align="center">Welcome to beestat! 👋</h1>
|
||||
<p align="center">
|
||||
<a href="https://github.com/beestat/app/issues" target="_blank"><img src="https://img.shields.io/github/issues/beestat/app.svg" /></a>
|
||||
<a href="https://github.com/beestat/app/issues?q=is%3Aissue+is%3Aclosed" target="_blank"><img src="https://img.shields.io/github/issues-closed/beestat/app.svg" /></a>
|
||||
<a href="https://github.com/beestat/app/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/github/license/beestat/app.svg" /></a>
|
||||
<a href="https://github.com/beestat/app/commits/master" target="_blank"><img src="https://img.shields.io/github/last-commit/beestat/app.svg" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://status.beestat.io" target="_blank"><img src="https://img.shields.io/uptimerobot/status/m782893860-419cc0327f06e1ed9af8cac6.svg" /></a>
|
||||
<a href="https://status.beestat.io" target="_blank"><img src="https://img.shields.io/uptimerobot/ratio/7/m782893860-419cc0327f06e1ed9af8cac6.svg" /></a>
|
||||
</p>
|
||||
|
||||
> Beestat connects with your thermostat and provides you with useful charts and analytics so that you can make informed decisions and see how the changes you make lower your energy footprint.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<a href="https://www.browserstack.com/" target="_blank"><img src="https://marker.io/vendor/img/logo/browserstack-logo.svg" height="32px"/></a><br/>
|
||||
I use BrowserStack to help me test and debug in devices that I do not have physical access to. They provide free access to their tools for open source projects.
|
||||
|
||||
<a href="https://smartystreets.com/" target="_blank"><img src="https://d79i1fxsrar4t.cloudfront.net/assets/img/company/brand/smartystreets.b24876d8.png" height="32px"/></a><br/>
|
||||
SmartyStreets powers real time address lookup and normalization. This gives beestat accurate location information for home comparisons. SmartyStreets provided a free year of US address lookups.
|
||||
|
||||
|
||||
## Demo
|
||||
|
||||
See a demo of the app at <a href="https://demo.beestat.io" target="_blank">demo.beestat.io</a>.
|
||||
|
||||
|
||||
## Run your own instance
|
||||
|
||||
This is possible but untested and unsupported. Clone the code, throw it on a web server, and see what happens. The most help I can offer at the moment is to rename `api/cora/setting.example.php` to `api/cora/setting.php` and use the comments to help configure the installation.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions, issues and feature requests are welcome.
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
**Jon Ziebell**
|
||||
|
||||
Hi! This is a passion project of mine and I'm thrilled to be able to share it with the world. I developed beestat from the ground up and have tons of ideas to grow this project further.
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.patreon.com/beestat" target="_blank"><img src="https://img.shields.io/badge/Support%20beestat-83+-lightgrey.svg?style=social&logo=patreon" /></a>
|
||||
<a href="https://twitter.com/beestat_io" target="_blank"><img src="https://img.shields.io/twitter/follow/beestat_io.svg?style=social" /></a>
|
||||
<a href="https://reddit.com/r/beestat" target="_blank"><img src="https://img.shields.io/reddit/subreddit-subscribers/beestat.svg?style=social" /></a>
|
||||
</p>
|
||||
<h1 align="center">Welcome to beestat! 👋</h1>
|
||||
<p align="center">
|
||||
<a href="https://github.com/beestat/app/issues" target="_blank"><img src="https://img.shields.io/github/issues/beestat/app.svg" /></a>
|
||||
<a href="https://github.com/beestat/app/issues?q=is%3Aissue+is%3Aclosed" target="_blank"><img src="https://img.shields.io/github/issues-closed/beestat/app.svg" /></a>
|
||||
<a href="https://github.com/beestat/app/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/github/license/beestat/app.svg" /></a>
|
||||
<a href="https://github.com/beestat/app/commits/master" target="_blank"><img src="https://img.shields.io/github/last-commit/beestat/app.svg" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://status.beestat.io" target="_blank"><img src="https://img.shields.io/uptimerobot/status/m782893860-419cc0327f06e1ed9af8cac6.svg" /></a>
|
||||
<a href="https://status.beestat.io" target="_blank"><img src="https://img.shields.io/uptimerobot/ratio/7/m782893860-419cc0327f06e1ed9af8cac6.svg" /></a>
|
||||
</p>
|
||||
|
||||
> Beestat connects with your thermostat and provides you with useful charts and analytics so that you can make informed decisions and see how the changes you make lower your energy footprint.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<a href="https://www.browserstack.com/" target="_blank"><img src="https://marker.io/vendor/img/logo/browserstack-logo.svg" height="32px"/></a><br/>
|
||||
I use BrowserStack to help me test and debug in devices that I do not have physical access to. They provide free access to their tools for open source projects.
|
||||
|
||||
<a href="https://smartystreets.com/" target="_blank"><img src="https://d79i1fxsrar4t.cloudfront.net/assets/img/company/brand/smartystreets.b24876d8.png" height="32px"/></a><br/>
|
||||
SmartyStreets powers real time address lookup and normalization. This gives beestat accurate location information for home comparisons. SmartyStreets provided a free year of US address lookups.
|
||||
|
||||
|
||||
## Demo
|
||||
|
||||
See a demo of the app at <a href="https://demo.beestat.io" target="_blank">demo.beestat.io</a>.
|
||||
|
||||
|
||||
## Run your own instance
|
||||
|
||||
This is possible but untested and unsupported. Clone the code, throw it on a web server, and see what happens. The most help I can offer at the moment is to rename `api/cora/setting.example.php` to `api/cora/setting.php` and use the comments to help configure the installation.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions, issues and feature requests are welcome.
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
**Jon Ziebell**
|
||||
|
||||
Hi! This is a passion project of mine and I'm thrilled to be able to share it with the world. I developed beestat from the ground up and have tons of ideas to grow this project further.
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.patreon.com/beestat" target="_blank"><img src="https://img.shields.io/badge/Support%20beestat-83+-lightgrey.svg?style=social&logo=patreon" /></a>
|
||||
<a href="https://twitter.com/beestat_io" target="_blank"><img src="https://img.shields.io/twitter/follow/beestat_io.svg?style=social" /></a>
|
||||
<a href="https://reddit.com/r/beestat" target="_blank"><img src="https://img.shields.io/reddit/subreddit-subscribers/beestat.svg?style=social" /></a>
|
||||
</p>
|
||||
|
@ -10,10 +10,6 @@ namespace cora;
|
||||
*/
|
||||
class api_log extends crud {
|
||||
|
||||
public static $converged = [];
|
||||
|
||||
public static $user_locked = true;
|
||||
|
||||
/**
|
||||
* Insert an item into the api_log resource. Force the IP to the request IP
|
||||
* and disallow overriding the timestamp.
|
||||
@ -24,8 +20,12 @@ class api_log extends crud {
|
||||
*/
|
||||
public function create($attributes) {
|
||||
$attributes['request_ip'] = ip2long($_SERVER['REMOTE_ADDR']);
|
||||
$attributes['user_id'] = $this->session->get_user_id();
|
||||
unset($attributes['request_timestamp']);
|
||||
return parent::create($attributes);
|
||||
|
||||
// Insert using the transactionless connection.
|
||||
$database = database::get_transactionless_instance();
|
||||
return $database->create($this->resource, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,20 +39,20 @@ class api_log extends crud {
|
||||
*/
|
||||
public function get_number_requests_since($request_ip, $timestamp) {
|
||||
$request_ip_escaped = $this->database->escape(ip2long($request_ip));
|
||||
$timestamp_escaped = $this->database->escape($timestamp);
|
||||
$timestamp_escaped = $this->database->escape(
|
||||
date('Y-m-d H:i:s', $timestamp)
|
||||
);
|
||||
|
||||
$query = '
|
||||
select
|
||||
count(*) as number_requests_since
|
||||
count(*) `number_requests_since`
|
||||
from
|
||||
api_log
|
||||
`api_log`
|
||||
where
|
||||
request_ip = ' . $request_ip_escaped . '
|
||||
and request_timestamp >= from_unixtime(' . $timestamp_escaped . ')
|
||||
`request_ip` = ' . $request_ip_escaped . '
|
||||
and `request_timestamp` >= ' . $timestamp_escaped . '
|
||||
';
|
||||
|
||||
// Getting the number of requests since a certain date is considered
|
||||
// overhead since it's only used for rate limiting. See "Important" note in
|
||||
// documentation.
|
||||
$result = $this->database->query($query);
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
|
@ -456,15 +456,18 @@ final class cora {
|
||||
*/
|
||||
private function is_over_rate_limit() {
|
||||
$requests_per_minute = $this->setting->get('requests_per_minute');
|
||||
|
||||
if($requests_per_minute === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$api_log_resource = new api_log();
|
||||
$requests_this_minute = $api_log_resource->get_number_requests_since(
|
||||
$_SERVER['REMOTE_ADDR'],
|
||||
time() - 60
|
||||
);
|
||||
return ($requests_this_minute >= $requests_per_minute);
|
||||
|
||||
return ($requests_this_minute > $requests_per_minute);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,14 +32,18 @@ final class database extends \mysqli {
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* The second singleton...use sparingly. Used when a second connection to
|
||||
* the database is needed to escape a transcation (ex: for writing tokens).
|
||||
* It can be used for writing logs as well but that would open up two
|
||||
* connections per API call which is bad.
|
||||
* A database singleton that does not use transactions.
|
||||
*
|
||||
* @var database
|
||||
*/
|
||||
private static $second_instance;
|
||||
private static $transactionless_instance;
|
||||
|
||||
/**
|
||||
* Whether or not to use transactions in this connection.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $use_transactions = true;
|
||||
|
||||
/**
|
||||
* Whether or not a transaction has been started. Used to make sure only one
|
||||
@ -150,6 +154,7 @@ final class database extends \mysqli {
|
||||
if(isset(self::$instance) === false) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
@ -158,11 +163,13 @@ final class database extends \mysqli {
|
||||
*
|
||||
* @return database A new database object or the already created one.
|
||||
*/
|
||||
public static function get_second_instance() {
|
||||
if(isset(self::$second_instance) === false) {
|
||||
self::$second_instance = new self();
|
||||
public static function get_transactionless_instance() {
|
||||
if(isset(self::$transactionless_instance) === false) {
|
||||
self::$transactionless_instance = new self();
|
||||
self::$transactionless_instance->disable_transactions();
|
||||
}
|
||||
return self::$second_instance;
|
||||
|
||||
return self::$transactionless_instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -239,7 +246,7 @@ final class database extends \mysqli {
|
||||
else if($value === false) {
|
||||
return '0';
|
||||
}
|
||||
else if(is_int($value) === true || ctype_digit($value) === true) {
|
||||
else if(is_int($value) === true) {
|
||||
return $value;
|
||||
}
|
||||
else {
|
||||
@ -354,7 +361,7 @@ final class database extends \mysqli {
|
||||
$query_type = substr(trim($query), 0, 6);
|
||||
if(
|
||||
in_array($query_type, ['insert', 'update', 'delete']) === true &&
|
||||
$this->setting->get('use_transactions') === true
|
||||
$this->use_transactions === true
|
||||
) {
|
||||
$this->start_transaction();
|
||||
}
|
||||
@ -397,11 +404,12 @@ final class database extends \mysqli {
|
||||
* include arrays if you want to search in() something.
|
||||
* @param array $columns The columns to return. If not specified, all
|
||||
* columns are returned.
|
||||
* @param mixed $order_by String or array of order_bys.
|
||||
*
|
||||
* @return array An array of the database rows with the specified columns.
|
||||
* Even a single result will still be returned in an array of size 1.
|
||||
*/
|
||||
public function read($resource, $attributes = [], $columns = []) {
|
||||
public function read($resource, $attributes = [], $columns = [], $order_by = []) {
|
||||
$table = $this->get_table($resource);
|
||||
|
||||
// Build the column listing.
|
||||
@ -424,10 +432,9 @@ final class database extends \mysqli {
|
||||
}
|
||||
|
||||
// Build the where clause.
|
||||
if(count($attributes) === 0) {
|
||||
if (count($attributes) === 0) {
|
||||
$where = '';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$where = ' where ' .
|
||||
implode(
|
||||
' and ',
|
||||
@ -439,9 +446,26 @@ final class database extends \mysqli {
|
||||
);
|
||||
}
|
||||
|
||||
if (is_array($order_by) === false) {
|
||||
$order_by = [$order_by];
|
||||
}
|
||||
|
||||
if (count($order_by) === 0) {
|
||||
$order_by = '';
|
||||
} else {
|
||||
$order_by = ' order by ' .
|
||||
implode(
|
||||
',',
|
||||
array_map(
|
||||
[$this, 'escape_identifier'],
|
||||
$order_by
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Put everything together and return the result.
|
||||
$query = 'select ' . $columns . ' from ' .
|
||||
$this->escape_identifier($table) . $where;
|
||||
$this->escape_identifier($table) . $where . $order_by;
|
||||
$result = $this->query($query);
|
||||
|
||||
/**
|
||||
@ -564,12 +588,15 @@ final class database extends \mysqli {
|
||||
*
|
||||
* @param string $resource The resource to update.
|
||||
* @param array $attributes The attributes to set.
|
||||
* @param array $return_mode Either "row" or "id". Specifying row will
|
||||
* return the newly created row (does a database read). Specifying id will
|
||||
* return just the ID of the created row.
|
||||
*
|
||||
* @throws \Exception If no attributes were specified.
|
||||
*
|
||||
* @return int The updated row.
|
||||
*/
|
||||
public function update($resource, $attributes) {
|
||||
public function update($resource, $attributes, $return_mode = 'row') {
|
||||
$table = $this->get_table($resource);
|
||||
|
||||
// TODO This will go away as soon as I switch to json type columns.
|
||||
@ -633,7 +660,12 @@ final class database extends \mysqli {
|
||||
|
||||
$this->query($query);
|
||||
|
||||
return $this->read($resource, $where_attributes)[0];
|
||||
if($return_mode === 'row') {
|
||||
return $this->read($resource, $where_attributes)[0];
|
||||
} else {
|
||||
return $id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -658,11 +690,14 @@ final class database extends \mysqli {
|
||||
* inserts.
|
||||
*
|
||||
* @param string $table The table to insert into.
|
||||
* @param array $attributes The attributes to set on the row
|
||||
* @param array $attributes The attributes to set on the row.
|
||||
* @param array $return_mode Either "row" or "id". Specifying row will
|
||||
* return the newly created row (does a database read). Specifying id will
|
||||
* return just the ID of the created row.
|
||||
*
|
||||
* @return int The primary key of the inserted row.
|
||||
*/
|
||||
public function create($resource, $attributes) {
|
||||
public function create($resource, $attributes, $return_mode = 'row') {
|
||||
$table = $this->get_table($resource);
|
||||
|
||||
// TODO This will go away as soon as I switch to json type columns.
|
||||
@ -695,10 +730,13 @@ final class database extends \mysqli {
|
||||
|
||||
$this->query($query);
|
||||
|
||||
$read_attributes = [];
|
||||
$read_attributes[$table . '_id'] = $this->insert_id;
|
||||
return $this->read($resource, $read_attributes)[0];
|
||||
// return $this->insert_id;
|
||||
if($return_mode === 'row') {
|
||||
$read_attributes = [];
|
||||
$read_attributes[$table . '_id'] = $this->insert_id;
|
||||
return $this->read($resource, $read_attributes)[0];
|
||||
} else {
|
||||
return $this->insert_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -783,16 +821,18 @@ final class database extends \mysqli {
|
||||
* @param int $time_zone_offset Offset in minutes.
|
||||
*/
|
||||
public function set_time_zone($time_zone_offset) {
|
||||
// $time_zone_offset = -360;
|
||||
$operator = $time_zone_offset < 0 ? '-' : '+';
|
||||
$time_zone_offset = abs($time_zone_offset);
|
||||
$offset_hours = floor($time_zone_offset / 60);
|
||||
$offset_minutes = $time_zone_offset % 60;
|
||||
// var_dump($offset_hours);
|
||||
// var_dump($offset_minutes);
|
||||
// var_dump('SET time_zone = "' . $operator . sprintf('%d', $offset_hours) . ':' . str_pad($offset_minutes, 2, STR_PAD_LEFT) . '"');
|
||||
// die();
|
||||
$this->query('SET time_zone = "' . $operator . sprintf('%d', $offset_hours) . ':' . str_pad($offset_minutes, 2, STR_PAD_LEFT) . '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable transactions for this connection.
|
||||
*/
|
||||
public function disable_transactions() {
|
||||
$this->use_transactions = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -178,16 +178,7 @@ final class setting {
|
||||
* The number of requests allowed in a single batch API call. Set to null
|
||||
* to disable.
|
||||
*/
|
||||
'batch_limit' => null,
|
||||
|
||||
/**
|
||||
* Whether or not to wrap each individual or batch API call in a single
|
||||
* transaction. When disabled, transactions are available but not used
|
||||
* automatically.
|
||||
*
|
||||
* This must be set to false for now.
|
||||
*/
|
||||
'use_transactions' => false
|
||||
'batch_limit' => null
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -227,8 +227,8 @@ class ecobee extends external_api {
|
||||
if (isset($response['status']) === true && $response['status']['code'] === 14) {
|
||||
// Authentication token has expired. Refresh your tokens.
|
||||
if ($auto_refresh_token === true) {
|
||||
$this->api('ecobee_token', 'refresh');
|
||||
return $this->ecobee_api($method, $endpoint, $arguments, false);
|
||||
$ecobee_token = $this->api('ecobee_token', 'refresh');
|
||||
return $this->ecobee_api($method, $endpoint, $arguments, false, $ecobee_token);
|
||||
}
|
||||
else {
|
||||
if($this::$log_mysql !== 'all') {
|
||||
|
@ -1,543 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* All of the raw thermostat data sits here. Many millions of rows.
|
||||
*
|
||||
* @author Jon Ziebell
|
||||
*/
|
||||
class ecobee_runtime_thermostat extends cora\crud {
|
||||
|
||||
public static $exposed = [
|
||||
'private' => [
|
||||
'get_recent_activity',
|
||||
'get_aggregate_runtime',
|
||||
'sync'
|
||||
],
|
||||
'public' => []
|
||||
];
|
||||
|
||||
public static $cache = [
|
||||
'sync' => 3600, // 1 Hour
|
||||
'get_recent_activity' => 300, // 5 Minutes
|
||||
'get_aggregate_runtime' => 3600, // 1 Hour
|
||||
];
|
||||
|
||||
public static $converged = [];
|
||||
|
||||
public static $user_locked = true;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
$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 = 'ecobee_runtime_thermostat->sync(' . $thermostat_id . ')';
|
||||
$this->database->get_lock($lock_name);
|
||||
|
||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||
|
||||
if($thermostat['sync_begin'] !== $thermostat['first_connected']) {
|
||||
$this->sync_backwards($thermostat_id);
|
||||
} else {
|
||||
$this->sync_forwards($thermostat_id);
|
||||
}
|
||||
|
||||
// TODO: 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' => 'ecobee_runtime_thermostat'
|
||||
]
|
||||
);
|
||||
|
||||
$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.');
|
||||
}
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
$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.
|
||||
do {
|
||||
$this->database->start_transaction();
|
||||
|
||||
$chunk_begin = strtotime('-1 week', $chunk_end);
|
||||
$chunk_begin = max($chunk_begin, $sync_begin);
|
||||
|
||||
$this->sync_($thermostat['ecobee_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'])
|
||||
)
|
||||
)
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
// 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.
|
||||
$sync_begin = strtotime($thermostat['sync_end']);
|
||||
$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['ecobee_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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the runtime report data for a specified thermostat.
|
||||
*
|
||||
* @param int $ecobee_thermostat_id
|
||||
* @param int $begin
|
||||
* @param int $end
|
||||
*/
|
||||
private function sync_($ecobee_thermostat_id, $begin, $end) {
|
||||
$ecobee_thermostat = $this->api('ecobee_thermostat', 'get', $ecobee_thermostat_id);
|
||||
|
||||
/**
|
||||
* TODO: There is some issue related to the sync where we can miss small
|
||||
* chunks of time if begin/end are the same day or something like that. It
|
||||
* seems to happen around UTC 00:00:00 so 7:00pm or so local time. This
|
||||
* happens to fix it by forcing sycing backwards by an extra day so that
|
||||
* chunk of time can't be missed. Need to properly fix...maybe next time I
|
||||
* take a pass at the syncing...
|
||||
*/
|
||||
if(date('Y-m-d', $begin) === date('Y-m-d', $end)) {
|
||||
$begin = strtotime('-1 day', $begin);
|
||||
}
|
||||
|
||||
$begin = date('Y-m-d', $begin);
|
||||
$end = date('Y-m-d', $end);
|
||||
|
||||
$columns = [
|
||||
'auxHeat1' => 'auxiliary_heat_1',
|
||||
'auxHeat2' => 'auxiliary_heat_2',
|
||||
'auxHeat3' => 'auxiliary_heat_3',
|
||||
'compCool1' => 'compressor_cool_1',
|
||||
'compCool2' => 'compressor_cool_2',
|
||||
'compHeat1' => 'compressor_heat_1',
|
||||
'compHeat2' => 'compressor_heat_2',
|
||||
'dehumidifier' => 'dehumidifier',
|
||||
'dmOffset' => 'demand_management_offset',
|
||||
'economizer' => 'economizer',
|
||||
'fan' => 'fan',
|
||||
'humidifier' => 'humidifier',
|
||||
'hvacMode' => 'hvac_mode',
|
||||
'outdoorHumidity' => 'outdoor_humidity',
|
||||
'outdoorTemp' => 'outdoor_temperature',
|
||||
'sky' => 'sky',
|
||||
'ventilator' => 'ventilator',
|
||||
'wind' => 'wind',
|
||||
'zoneAveTemp' => 'zone_average_temperature',
|
||||
'zoneCalendarEvent' => 'zone_calendar_event',
|
||||
'zoneClimate' => 'zone_climate',
|
||||
'zoneCoolTemp' => 'zone_cool_temperature',
|
||||
'zoneHeatTemp' => 'zone_heat_temperature',
|
||||
'zoneHumidity' => 'zone_humidity',
|
||||
'zoneHumidityHigh' => 'zone_humidity_high',
|
||||
'zoneHumidityLow' => 'zone_humidity_low',
|
||||
'zoneHvacMode' => 'zone_hvac_mode',
|
||||
'zoneOccupancy' => 'zone_occupancy'
|
||||
];
|
||||
|
||||
$response = $this->api(
|
||||
'ecobee',
|
||||
'ecobee_api',
|
||||
[
|
||||
'method' => 'GET',
|
||||
'endpoint' => 'runtimeReport',
|
||||
'arguments' => [
|
||||
'body' => json_encode([
|
||||
'selection' => [
|
||||
'selectionType' => 'thermostats',
|
||||
'selectionMatch' => $ecobee_thermostat['identifier']
|
||||
],
|
||||
'startDate' => $begin,
|
||||
'endDate' => $end,
|
||||
'columns' => implode(',', array_keys($columns)),
|
||||
'includeSensors' => false
|
||||
])
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
$time_zone_offset = $ecobee_thermostat['json_location']['timeZoneOffsetMinutes'];
|
||||
|
||||
foreach($response['reportList'][0]['rowList'] as $row) {
|
||||
// Prepare the row!
|
||||
$row = substr($row, 0, -1); // Strip the trailing comma,
|
||||
$row = explode(',', $row);
|
||||
$row = array_map('trim', $row);
|
||||
|
||||
// Date and time are first two columns of the returned data. It is
|
||||
// returned in thermostat time, so convert it to UTC first.
|
||||
list($date, $time) = array_splice($row, 0, 2);
|
||||
$timestamp = date(
|
||||
'Y-m-d H:i:s',
|
||||
strtotime(
|
||||
$date . ' ' . $time . ' ' . ($time_zone_offset < 0 ? '+' : '-') . abs($time_zone_offset) . ' minute'
|
||||
)
|
||||
);
|
||||
|
||||
$data = [
|
||||
'ecobee_thermostat_id' => $ecobee_thermostat_id,
|
||||
'timestamp' => $timestamp
|
||||
];
|
||||
|
||||
$i = 0;
|
||||
foreach($columns as $ecobee_key => $database_key) {
|
||||
$data[$database_key] = ($row[$i] === '' ? null : $row[$i]);
|
||||
$i++;
|
||||
}
|
||||
|
||||
$existing_rows = $this->read([
|
||||
'ecobee_thermostat_id' => $ecobee_thermostat_id,
|
||||
'timestamp' => $timestamp
|
||||
]);
|
||||
|
||||
if(count($existing_rows) > 0) {
|
||||
$data['ecobee_runtime_thermostat_id'] = $existing_rows[0]['ecobee_runtime_thermostat_id'];
|
||||
$this->update($data);
|
||||
}
|
||||
else {
|
||||
$this->create($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query thermostat data and aggregate the results.
|
||||
*
|
||||
* @param int $ecobee_thermostat_id Thermostat to get data for.
|
||||
* @param string $time_period day|week|month|year|all
|
||||
* @param string $group_by hour|day|week|month|year
|
||||
* @param int $time_count How many time periods to include.
|
||||
*
|
||||
* @return array The aggregate runtime data.
|
||||
*/
|
||||
public function get_aggregate_runtime($ecobee_thermostat_id, $time_period, $group_by, $time_count) {
|
||||
if(in_array($time_period, ['day', 'week', 'month', 'year', 'all']) === false) {
|
||||
throw new Exception('Invalid time period');
|
||||
}
|
||||
|
||||
$ecobee_thermostat = $this->api('ecobee_thermostat', 'get', $ecobee_thermostat_id);
|
||||
$this->database->set_time_zone($ecobee_thermostat['json_location']['timeZoneOffsetMinutes']);
|
||||
|
||||
$select = [];
|
||||
$group_by_order_by = [];
|
||||
switch($group_by) {
|
||||
case 'week':
|
||||
/**
|
||||
* Week is a special case. If grouping by week, month, year, you can
|
||||
* get one week listed twice if it spans across months. So the month
|
||||
* group by is undesirable in this case.
|
||||
*
|
||||
* The second argument of 3 to the yearweek() function will cause
|
||||
* MySQL to return the ISO 8601 week of the year.
|
||||
*
|
||||
* https://stackoverflow.com/a/11804076
|
||||
*/
|
||||
$select[] = 'yearweek(`timestamp`, 3) `yearweek`';
|
||||
$group_by_order_by[] = 'yearweek(`timestamp`, 3)';
|
||||
break;
|
||||
case 'day':
|
||||
$select[] = 'day(`timestamp`) `day`';
|
||||
$group_by_order_by[] = 'day(`timestamp`)';
|
||||
case 'month':
|
||||
$select[] = 'month(`timestamp`) `month`';
|
||||
$group_by_order_by[] = 'month(`timestamp`)';
|
||||
case 'year':
|
||||
$select[] = 'year(`timestamp`) `year`';
|
||||
$group_by_order_by[] = 'year(`timestamp`)';
|
||||
break;
|
||||
}
|
||||
$group_by_order_by = array_reverse($group_by_order_by);
|
||||
|
||||
/**
|
||||
* Determine the appropriate start date. See #139 for more info. Basically
|
||||
* this allows you to select "Past 2 months, grouped by month" and get
|
||||
* data starting at the first of the previous month until now, instead of
|
||||
* data starting 60 days ago which may include a third month.
|
||||
*/
|
||||
switch($time_period) {
|
||||
case 'week':
|
||||
$start = 'date_format(now() - interval ' . (intval($time_count) - 1) . ' ' . $time_period . ' - interval (date_format(now(), "%w") - 1) day, "%Y-%m-%d 00:00:00")';
|
||||
break;
|
||||
case 'day':
|
||||
$start = 'date_format(now() - interval ' . (intval($time_count) - 1) . ' ' . $time_period . ', "%Y-%m-%d 00:00:00")';
|
||||
break;
|
||||
case 'month':
|
||||
$start = 'date_format(now() - interval ' . (intval($time_count) - 1) . ' ' . $time_period . ', "%Y-%m-01 00:00:00")';
|
||||
break;
|
||||
case 'year':
|
||||
$start = 'date_format(now() - interval ' . (intval($time_count) - 1) . ' ' . $time_period . ', "%Y-01-01 00:00:00")';
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a smidge sloppy but it gets the job done. Basically need to
|
||||
* subtract all higher tier heat/cool modes from the lower ones to avoid
|
||||
* double-counting.
|
||||
*/
|
||||
$select[] = 'count(*) `count`';
|
||||
$select[] = 'cast(avg(`outdoor_temperature`) as decimal(4,1)) `average_outdoor_temperature`';
|
||||
$select[] = 'cast(min(`outdoor_temperature`) as decimal(4,1)) `min_outdoor_temperature`';
|
||||
$select[] = 'cast(max(`outdoor_temperature`) as decimal(4,1)) `max_outdoor_temperature`';
|
||||
$select[] = 'cast(avg(`zone_average_temperature`) as decimal(4,1)) `zone_average_temperature`';
|
||||
$select[] = 'cast(avg(`zone_heat_temperature`) as decimal(4,1)) `zone_heat_temperature`';
|
||||
$select[] = 'cast(avg(`zone_cool_temperature`) as decimal(4,1)) `zone_cool_temperature`';
|
||||
$select[] = 'cast(sum(greatest(0, (cast(`compressor_heat_1` as signed) - cast(`compressor_heat_2` as signed)))) as unsigned) `compressor_heat_1`';
|
||||
$select[] = 'cast(sum(`compressor_heat_2`) as unsigned) `compressor_heat_2`';
|
||||
$select[] = 'cast(sum(greatest(0, (cast(`auxiliary_heat_1` as signed) - cast(`auxiliary_heat_2` as signed) - cast(`auxiliary_heat_3` as signed)))) as unsigned) `auxiliary_heat_1`';
|
||||
$select[] = 'cast(sum(greatest(0, (cast(`auxiliary_heat_2` as signed) - cast(`auxiliary_heat_3` as signed)))) as unsigned) `auxiliary_heat_2`';
|
||||
$select[] = 'cast(sum(`auxiliary_heat_3`) as unsigned) `auxiliary_heat_3`';
|
||||
$select[] = 'cast(sum(greatest(0, (cast(`compressor_cool_1` as signed) - cast(`compressor_cool_2` as signed)))) as unsigned) `compressor_cool_1`';
|
||||
$select[] = 'cast(sum(`compressor_cool_2`) as unsigned) `compressor_cool_2`';
|
||||
|
||||
// The zone_average_temperature check is for if data exists in the table but
|
||||
// is otherwise likely to be all null (like the bad data from February
|
||||
// 2019).
|
||||
$query = '
|
||||
select ' .
|
||||
implode(',', $select) . ' ' . '
|
||||
from
|
||||
`ecobee_runtime_thermostat`
|
||||
where
|
||||
`user_id` = ' . $this->session->get_user_id() . '
|
||||
and `ecobee_thermostat_id` = "' . $this->database->escape($ecobee_thermostat_id) . '" ' .
|
||||
($time_period !== 'all' ? ('and `timestamp` >= ' . $start) : '') . '
|
||||
and `timestamp` <= now()
|
||||
and `zone_average_temperature` is not null
|
||||
';
|
||||
|
||||
if(count($group_by_order_by) > 0) {
|
||||
$query .= 'group by ' .
|
||||
implode(', ', $group_by_order_by) . '
|
||||
order by ' .
|
||||
implode(', ', $group_by_order_by);
|
||||
}
|
||||
|
||||
$result = $this->database->query($query);
|
||||
$return = [];
|
||||
while($row = $result->fetch_assoc()) {
|
||||
// Cast to floats for nice responses. The database normally handles this
|
||||
// in regular read operations.
|
||||
foreach(['average_outdoor_temperature', 'min_outdoor_temperature', 'max_outdoor_temperature', 'zone_average_temperature', 'zone_heat_temperature', 'zone_cool_temperature'] as $key) {
|
||||
if($row[$key] !== null) {
|
||||
$row[$key] = (float) $row[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($row['yearweek']) === true) {
|
||||
$row['year'] = (int) substr($row['yearweek'], 0, 4);
|
||||
$row['week'] = (int) substr($row['yearweek'], 4);
|
||||
unset($row['yearweek']);
|
||||
}
|
||||
|
||||
$return[] = $row;
|
||||
}
|
||||
|
||||
$this->database->set_time_zone(0);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent thermostat activity. Max range is 30 days.
|
||||
*
|
||||
* @param int $ecobee_thermostat_id Thermostat to get data for.
|
||||
* @param string $begin Begin date/time.
|
||||
* @param string $end End date/time.
|
||||
*
|
||||
* @return array The rows in the desired date range.
|
||||
*/
|
||||
public function get_recent_activity($ecobee_thermostat_id, $begin, $end) {
|
||||
$thermostat = $this->api(
|
||||
'thermostat',
|
||||
'get',
|
||||
[
|
||||
'attributes' => [
|
||||
'ecobee_thermostat_id' => $ecobee_thermostat_id
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
$ecobee_thermostat = $this->api('ecobee_thermostat', 'get', $thermostat['ecobee_thermostat_id']);
|
||||
$this->database->set_time_zone($ecobee_thermostat['json_location']['timeZoneOffsetMinutes']);
|
||||
|
||||
$offset = $ecobee_thermostat['json_location']['timeZoneOffsetMinutes'];
|
||||
$end = ($end === null ? (time() + ($offset * 60)) : strtotime($end));
|
||||
$begin = ($begin === null ? strtotime('-14 day', $end) : strtotime($begin));
|
||||
|
||||
if(($end - $begin) > 2592000) {
|
||||
throw new Exception('Date range exceeds maximum of 30 days.');
|
||||
}
|
||||
|
||||
$query = '
|
||||
select
|
||||
`ecobee_thermostat_id`,
|
||||
`ecobee_runtime_thermostat_id`,
|
||||
`timestamp`,
|
||||
|
||||
cast(greatest(0, (cast(`compressor_heat_1` as signed) - cast(`compressor_heat_2` as signed))) as unsigned) `compressor_heat_1`,
|
||||
`compressor_heat_2`,
|
||||
cast(greatest(0, (cast(`auxiliary_heat_1` as signed) - cast(`auxiliary_heat_2` as signed) - cast(`auxiliary_heat_3` as signed))) as unsigned) `auxiliary_heat_1`,
|
||||
cast(greatest(0, (cast(`auxiliary_heat_2` as signed) - cast(`auxiliary_heat_3` as signed))) as unsigned) `auxiliary_heat_2`,
|
||||
`auxiliary_heat_3`,
|
||||
cast(greatest(0, (cast(`compressor_cool_1` as signed) - cast(`compressor_cool_2` as signed))) as unsigned) `compressor_cool_1`,
|
||||
`compressor_cool_2`,
|
||||
|
||||
`fan`,
|
||||
`dehumidifier`,
|
||||
`economizer`,
|
||||
`humidifier`,
|
||||
`ventilator`,
|
||||
`hvac_mode`,
|
||||
`outdoor_temperature`,
|
||||
`zone_average_temperature`,
|
||||
`zone_heat_temperature`,
|
||||
`zone_cool_temperature`,
|
||||
`zone_humidity`,
|
||||
`outdoor_humidity`,
|
||||
`zone_calendar_event`,
|
||||
`zone_climate`
|
||||
from
|
||||
`ecobee_runtime_thermostat`
|
||||
where
|
||||
`user_id` = ' . $this->database->escape($this->session->get_user_id()) . '
|
||||
and `ecobee_thermostat_id` = ' . $this->database->escape($ecobee_thermostat_id) . '
|
||||
and `timestamp` >= ' . $this->database->escape(date('Y-m-d H:i:s', $begin)) . '
|
||||
and `timestamp` <= ' . $this->database->escape(date('Y-m-d H:i:s', $end)) . '
|
||||
order by
|
||||
timestamp
|
||||
';
|
||||
|
||||
$result = $this->database->query($query);
|
||||
|
||||
$return = [];
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$return[] = $row;
|
||||
}
|
||||
|
||||
$this->database->set_time_zone(0);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
@ -212,6 +212,7 @@ class ecobee_thermostat extends cora\crud {
|
||||
$attributes['filters'] = $this->get_filters($thermostat, $ecobee_thermostat);
|
||||
$attributes['json_alerts'] = $this->get_alerts($thermostat, $ecobee_thermostat);
|
||||
$attributes['weather'] = $this->get_weather($thermostat, $ecobee_thermostat);
|
||||
$attributes['time_zone'] = $this->get_time_zone($thermostat, $ecobee_thermostat);
|
||||
|
||||
$detected_system_type = $this->get_detected_system_type($thermostat, $ecobee_thermostat);
|
||||
if($thermostat['system_type'] === null) {
|
||||
@ -448,23 +449,23 @@ class ecobee_thermostat extends cora\crud {
|
||||
$supported_types = [
|
||||
'furnaceFilter' => [
|
||||
'key' => 'furnace',
|
||||
'sum_column' => 'fan'
|
||||
'sum_column' => 'sum_fan'
|
||||
],
|
||||
'humidifierFilter' => [
|
||||
'key' => 'humidifier',
|
||||
'sum_column' => 'humidifier'
|
||||
'sum_column' => 'sum_humidifier'
|
||||
],
|
||||
'dehumidifierFilter' => [
|
||||
'key' => 'dehumidifier',
|
||||
'sum_column' => 'dehumidifier'
|
||||
'sum_column' => 'sum_dehumidifier'
|
||||
],
|
||||
'ventilator' => [
|
||||
'key' => 'ventilator',
|
||||
'sum_column' => 'ventilator'
|
||||
'sum_column' => 'sum_ventilator'
|
||||
],
|
||||
'uvLamp' => [
|
||||
'key' => 'uv_lamp',
|
||||
'sum_column' => 'fan'
|
||||
'sum_column' => 'sum_fan'
|
||||
]
|
||||
];
|
||||
|
||||
@ -482,7 +483,7 @@ class ecobee_thermostat extends cora\crud {
|
||||
'life_units' => $notification['filterLifeUnits']
|
||||
];
|
||||
|
||||
$sums[] = 'sum(case when `timestamp` > "' . $notification['filterLastChanged'] . '" then `' . $sum_column . '` else 0 end) `' . $key . '`';
|
||||
$sums[] = 'sum(case when `date` > "' . $notification['filterLastChanged'] . '" then `' . $sum_column . '` else 0 end) `' . $key . '`';
|
||||
$min_timestamp = min($min_timestamp, strtotime($notification['filterLastChanged']));
|
||||
}
|
||||
}
|
||||
@ -493,11 +494,11 @@ class ecobee_thermostat extends cora\crud {
|
||||
select
|
||||
' . implode(',', $sums) . '
|
||||
from
|
||||
ecobee_runtime_thermostat
|
||||
runtime_thermostat_summary
|
||||
where
|
||||
`user_id` = "' . $this->session->get_user_id() . '"
|
||||
and `ecobee_thermostat_id` = "' . $ecobee_thermostat['ecobee_thermostat_id'] . '"
|
||||
and `timestamp` > "' . date('Y-m-d', $min_timestamp) . '"
|
||||
and `thermostat_id` = "' . $thermostat['thermostat_id'] . '"
|
||||
and `date` >= "' . date('Y-m-d', $min_timestamp) . '"
|
||||
';
|
||||
|
||||
$result = $this->database->query($query);
|
||||
@ -861,4 +862,31 @@ class ecobee_thermostat extends cora\crud {
|
||||
return $weather;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current time zone. It's usually set. If not set use the offset
|
||||
* minutes to find it. Worst case default to the most common time zone.
|
||||
*
|
||||
* @param array $thermostat
|
||||
* @param array $ecobee_thermostat
|
||||
*
|
||||
* @return The time zone.
|
||||
*/
|
||||
private function get_time_zone($thermostat, $ecobee_thermostat) {
|
||||
$time_zone = $ecobee_thermostat['json_location']['timeZone'];
|
||||
|
||||
if (in_array($time_zone, timezone_identifiers_list()) === true) {
|
||||
return $time_zone;
|
||||
} else if ($ecobee_thermostat['json_location']['timeZoneOffsetMinutes'] !== '') {
|
||||
$offset_seconds = $ecobee_thermostat['json_location']['timeZoneOffsetMinutes'] * 60;
|
||||
$time_zone = timezone_name_from_abbr('', $offset_seconds, 1);
|
||||
// Workaround for bug #44780
|
||||
if ($time_zone === false) {
|
||||
$time_zone = timezone_name_from_abbr('', $offset_seconds, 0);
|
||||
}
|
||||
return $time_zone;
|
||||
} else {
|
||||
return 'America/New_York';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class ecobee_token extends cora\crud {
|
||||
isset($response['access_token']) === false ||
|
||||
isset($response['refresh_token']) === false
|
||||
) {
|
||||
throw new Exception('Could not get first token.', 10001);
|
||||
throw new Exception('Could not get first token.', 10000);
|
||||
}
|
||||
|
||||
return [
|
||||
@ -57,23 +57,18 @@ class ecobee_token extends cora\crud {
|
||||
* so that no other API call can attempt to get a token at the same time.
|
||||
* This way if two API calls fire off to ecobee at the same time, then
|
||||
* return at the same time, then call token->refresh() at the same time,
|
||||
* only one can run and actually refresh at a time. If the second one runs
|
||||
* after that's fine as it will look up the token prior to refreshing.
|
||||
* only one can run and actually refresh at a time. If the transactionless
|
||||
* one runs after that's fine as it will look up the token prior to
|
||||
* refreshing.
|
||||
*
|
||||
* Also this creates a new database connection. If a token is written to the
|
||||
* database, then the transaction gets rolled back, the token will be
|
||||
* erased. I originally tried to avoid this by not using transactions except
|
||||
* when syncing, but there are enough sync errors that happen where this
|
||||
* causes a problem. The extra overhead of a second database connection
|
||||
* every now and then shouldn't be that bad.
|
||||
* @return array The new token.
|
||||
*/
|
||||
public function refresh() {
|
||||
$database = cora\database::get_second_instance();
|
||||
$database = cora\database::get_transactionless_instance();
|
||||
|
||||
$lock_name = 'ecobee_token->refresh(' . $this->session->get_user_id() . ')';
|
||||
$database->get_lock($lock_name, 3);
|
||||
|
||||
// $ecobee_tokens = $this->read();
|
||||
$ecobee_tokens = $database->read(
|
||||
'ecobee_token',
|
||||
[
|
||||
@ -81,7 +76,7 @@ class ecobee_token extends cora\crud {
|
||||
]
|
||||
);
|
||||
if(count($ecobee_tokens) === 0) {
|
||||
throw new Exception('Could not refresh ecobee token; no token found.', 10002);
|
||||
throw new Exception('Could not refresh ecobee token; no token found.', 10001);
|
||||
}
|
||||
$ecobee_token = $ecobee_tokens[0];
|
||||
|
||||
@ -104,10 +99,10 @@ class ecobee_token extends cora\crud {
|
||||
) {
|
||||
$this->delete($ecobee_token['ecobee_token_id']);
|
||||
$database->release_lock($lock_name);
|
||||
throw new Exception('Could not refresh ecobee token; ecobee returned no token.', 10003);
|
||||
throw new Exception('Could not refresh ecobee token; ecobee returned no token.', 10002);
|
||||
}
|
||||
|
||||
$database->update(
|
||||
$ecobee_token = $database->update(
|
||||
'ecobee_token',
|
||||
[
|
||||
'ecobee_token_id' => $ecobee_token['ecobee_token_id'],
|
||||
@ -118,6 +113,8 @@ class ecobee_token extends cora\crud {
|
||||
);
|
||||
|
||||
$database->release_lock($lock_name);
|
||||
|
||||
return $ecobee_token;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,11 +126,10 @@ class ecobee_token extends cora\crud {
|
||||
* @return int
|
||||
*/
|
||||
public function delete($id) {
|
||||
$database = cora\database::get_second_instance();
|
||||
$database = cora\database::get_transactionless_instance();
|
||||
|
||||
// Need to delete the token before logging out or else the delete fails.
|
||||
$return = $database->delete('ecobee_token', $id);
|
||||
// $return = parent::delete($id);
|
||||
|
||||
// Log out
|
||||
$this->api('user', 'log_out', ['all' => true]);
|
||||
|
@ -16,13 +16,20 @@ class external_api_log extends cora\crud {
|
||||
]
|
||||
];
|
||||
|
||||
public static $user_locked = true;
|
||||
/**
|
||||
* Insert an item into the log table using the transactionless database
|
||||
* connection.
|
||||
*
|
||||
* @param array $attributes The attributes to insert.
|
||||
*
|
||||
* @return int The ID of the inserted row.
|
||||
*/
|
||||
public function create($attributes) {
|
||||
$attributes['user_id'] = $this->session->get_user_id();
|
||||
|
||||
public function read($attributes = [], $columns = []) {
|
||||
throw new Exception('This method is not allowed.');
|
||||
// Insert using the transactionless connection.
|
||||
$database = cora\database::get_transactionless_instance();
|
||||
return $database->create($this->resource, $attributes, 'id');
|
||||
}
|
||||
|
||||
public function update($attributes) {
|
||||
throw new Exception('This method is not allowed.');
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Just a placeholder for now.
|
||||
*
|
||||
* @author Jon Ziebell
|
||||
*/
|
||||
class nest_sensor extends cora\crud {
|
||||
|
||||
public static $converged = [];
|
||||
|
||||
public static $user_locked = true;
|
||||
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Just a placeholder for now.
|
||||
*
|
||||
* @author Jon Ziebell
|
||||
*/
|
||||
class nest_thermostat extends cora\crud {
|
||||
|
||||
public static $converged = [];
|
||||
|
||||
public static $user_locked = true;
|
||||
|
||||
}
|
@ -41,7 +41,7 @@ class patreon_token extends cora\crud {
|
||||
isset($response['access_token']) === false ||
|
||||
isset($response['refresh_token']) === false
|
||||
) {
|
||||
throw new Exception('Could not get first token');
|
||||
throw new Exception('Could not get first token', 10100);
|
||||
}
|
||||
|
||||
$new_patreon_token = [
|
||||
@ -68,18 +68,12 @@ class patreon_token extends cora\crud {
|
||||
* so that no other API call can attempt to get a token at the same time.
|
||||
* This way if two API calls fire off to patreon at the same time, then
|
||||
* return at the same time, then call token->refresh() at the same time,
|
||||
* only one can run and actually refresh at a time. If the second one runs
|
||||
* after that's fine as it will look up the token prior to refreshing.
|
||||
*
|
||||
* Also this creates a new database connection. If a token is written to the
|
||||
* database, then the transaction gets rolled back, the token will be
|
||||
* erased. I originally tried to avoid this by not using transactions except
|
||||
* when syncing, but there are enough sync errors that happen where this
|
||||
* causes a problem. The extra overhead of a second database connection
|
||||
* every now and then shouldn't be that bad.
|
||||
* only one can run and actually refresh at a time. If the transactionless
|
||||
* one runs after that's fine as it will look up the token prior to
|
||||
* refreshing.
|
||||
*/
|
||||
public function refresh() {
|
||||
$database = cora\database::get_second_instance();
|
||||
$database = cora\database::get_transactionless_instance();
|
||||
|
||||
$lock_name = 'patreon_token->refresh(' . $this->session->get_user_id() . ')';
|
||||
$database->get_lock($lock_name, 3);
|
||||
@ -92,7 +86,7 @@ class patreon_token extends cora\crud {
|
||||
]
|
||||
);
|
||||
if(count($patreon_tokens) === 0) {
|
||||
throw new Exception('Could not refresh patreon token; no token found.', 10002);
|
||||
throw new Exception('Could not refresh patreon token; no token found.', 10101);
|
||||
}
|
||||
$patreon_token = $patreon_tokens[0];
|
||||
|
||||
@ -115,7 +109,7 @@ class patreon_token extends cora\crud {
|
||||
) {
|
||||
$this->delete($patreon_token['patreon_token_id']);
|
||||
$database->release_lock($lock_name);
|
||||
throw new Exception('Could not refresh patreon token; patreon returned no token.', 10003);
|
||||
throw new Exception('Could not refresh patreon token; patreon returned no token.', 10102);
|
||||
}
|
||||
|
||||
$database->update(
|
||||
@ -139,7 +133,7 @@ class patreon_token extends cora\crud {
|
||||
* @return int
|
||||
*/
|
||||
public function delete($id) {
|
||||
$database = database::get_second_instance();
|
||||
$database = database::get_transactionless_instance();
|
||||
$return = $database->delete('patreon_token', $id);
|
||||
return $return;
|
||||
}
|
||||
|
657
api/runtime_thermostat.php
Executable file
657
api/runtime_thermostat.php
Executable file
@ -0,0 +1,657 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* All of the raw thermostat data sits here. Many millions of rows.
|
||||
*
|
||||
* @author Jon Ziebell
|
||||
*/
|
||||
class runtime_thermostat extends cora\crud {
|
||||
|
||||
public static $exposed = [
|
||||
'private' => [
|
||||
'read',
|
||||
'sync'
|
||||
],
|
||||
'public' => []
|
||||
];
|
||||
|
||||
public static $cache = [
|
||||
// 'sync' => 900, // 15 Minutes
|
||||
// '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;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
$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) {
|
||||
$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(
|
||||
$columns_begin['date'] . ' ' . $columns_begin['time'],
|
||||
$thermostat['time_zone']
|
||||
),
|
||||
$this->get_utc_datetime(
|
||||
$columns_end['date'] . ' ' . $columns_end['time'],
|
||||
$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.
|
||||
*/
|
||||
if(
|
||||
$columns['hvacMode'] === null ||
|
||||
$columns['zoneAveTemp'] === null ||
|
||||
$columns['zoneHumidity'] === null ||
|
||||
$columns['outdoorTemp'] === null ||
|
||||
$columns['outdoorHumidity'] === null ||
|
||||
$columns['zoneCoolTemp'] === null ||
|
||||
$columns['zoneHeatTemp'] === 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'];
|
||||
|
||||
$data['fan'] = $columns['fan'];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$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);
|
||||
$date_time = new DateTime($local_datetime, $local_time_zone);
|
||||
$date_time->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
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
|
||||
* 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 = []) {
|
||||
$thermostats = $this->api('thermostat', 'read_id');
|
||||
|
||||
// Check for exceptions.
|
||||
if (isset($attributes['thermostat_id']) === false) {
|
||||
throw new \Exception('Missing required attribute: thermostat_id.', 10201);
|
||||
}
|
||||
|
||||
if (isset($attributes['timestamp']) === false) {
|
||||
throw new \Exception('Missing required attribute: timestamp.', 10202);
|
||||
}
|
||||
|
||||
if (isset($thermostats[$attributes['thermostat_id']]) === false) {
|
||||
throw new \Exception('Invalid thermostat_id.', 10203);
|
||||
}
|
||||
|
||||
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.', 10204);
|
||||
}
|
||||
}
|
||||
|
||||
$thermostat = $thermostats[$attributes['thermostat_id']];
|
||||
$max_range = 2592000; // 30 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 30 days.', 10205);
|
||||
}
|
||||
|
||||
// Read the data.
|
||||
$runtime_thermostats = $this->database->read(
|
||||
'runtime_thermostat',
|
||||
[
|
||||
'timestamp' => $attributes['timestamp'],
|
||||
'thermostat_id' => $attributes['thermostat_id']
|
||||
],
|
||||
[],
|
||||
'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
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
// Clean up the data just a bit.
|
||||
foreach ($runtime_thermostats as &$runtime_thermostat) {
|
||||
$runtime_thermostat['timestamp'] = date(
|
||||
'c',
|
||||
strtotime($runtime_thermostat['timestamp'])
|
||||
);
|
||||
|
||||
foreach([
|
||||
'indoor_temperature',
|
||||
'outdoor_temperature',
|
||||
'setpoint_cool',
|
||||
'setpoint_heat'
|
||||
] as $key) {
|
||||
if ($runtime_thermostat[$key] !== null) {
|
||||
$runtime_thermostat[$key] /= 10;
|
||||
}
|
||||
}
|
||||
|
||||
if ($runtime_thermostat['event_runtime_thermostat_text_id'] !== null) {
|
||||
$runtime_thermostat['event'] = $runtime_thermostat_texts[
|
||||
$runtime_thermostat['event_runtime_thermostat_text_id']
|
||||
]['value'];
|
||||
} else {
|
||||
$runtime_thermostat['event'] = null;
|
||||
}
|
||||
unset($runtime_thermostat['event_runtime_thermostat_text_id']);
|
||||
|
||||
if ($runtime_thermostat['climate_runtime_thermostat_text_id'] !== null) {
|
||||
$runtime_thermostat['climate'] = $runtime_thermostat_texts[
|
||||
$runtime_thermostat['climate_runtime_thermostat_text_id']
|
||||
]['value'];
|
||||
} else {
|
||||
$runtime_thermostat['climate'] = null;
|
||||
}
|
||||
unset($runtime_thermostat['climate_runtime_thermostat_text_id']);
|
||||
}
|
||||
|
||||
return $runtime_thermostats;
|
||||
}
|
||||
|
||||
}
|
148
api/runtime_thermostat_summary.php
Executable file
148
api/runtime_thermostat_summary.php
Executable file
@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Summary table for runtime_thermostat.
|
||||
*
|
||||
* @author Jon Ziebell
|
||||
*/
|
||||
class runtime_thermostat_summary extends cora\crud {
|
||||
|
||||
public static $exposed = [
|
||||
'private' => [
|
||||
'read_id',
|
||||
'sync'
|
||||
],
|
||||
'public' => []
|
||||
];
|
||||
|
||||
public static $cache = [
|
||||
// Can't have these on right now because beestat loads and fires this API
|
||||
// call off. Then it syncs all the data and calls these again to get updated
|
||||
// data...if they're cached that just returns empty. So need to be able to
|
||||
// bypass the cache or something to get that to work. Or just don't call the
|
||||
// API call to begin with on first load if there's nothing there.
|
||||
|
||||
// 'read' => 3600, // 1 hour
|
||||
// 'read_id' => 3600, // 1 hour
|
||||
];
|
||||
|
||||
/**
|
||||
* Read from the runtime_thermostat_summary table. Fixes temperature columns
|
||||
* to return as decimals.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param array $columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function read($attributes = [], $columns = []) {
|
||||
$runtime_thermostat_summaries = parent::read($attributes, $columns);
|
||||
|
||||
foreach($runtime_thermostat_summaries as &$runtime_thermostat_summary) {
|
||||
$runtime_thermostat_summary['avg_outdoor_temperature'] /= 10;
|
||||
$runtime_thermostat_summary['min_outdoor_temperature'] /= 10;
|
||||
$runtime_thermostat_summary['max_outdoor_temperature'] /= 10;
|
||||
$runtime_thermostat_summary['avg_indoor_temperature'] /= 10;
|
||||
}
|
||||
|
||||
return $runtime_thermostat_summaries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the runtime_thermostat_summary table.
|
||||
*
|
||||
* @param int $thermostat_id
|
||||
*/
|
||||
public function populate($thermostat_id) {
|
||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||
|
||||
$query = '
|
||||
select
|
||||
min(`date`) `min_date`,
|
||||
max(`date`) `max_date`
|
||||
from
|
||||
`runtime_thermostat_summary`
|
||||
where
|
||||
`user_id` = ' . $this->database->escape($this->session->get_user_id()) . '
|
||||
and `thermostat_id` = ' . $this->database->escape($thermostat_id) . '
|
||||
';
|
||||
$result = $this->database->query($query);
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
if($row['min_date'] === null || $row['max_date'] === null) {
|
||||
$start = 'now() - interval 10 year'; // Just grab everything
|
||||
} else {
|
||||
if(strtotime($row['min_date']) > strtotime($thermostat['sync_begin'])) {
|
||||
$start = '"' . date('Y-m-d 00:00:00', strtotime($thermostat['sync_begin'])) . '"';
|
||||
} else {
|
||||
$start = '"' . date('Y-m-d 00:00:00', strtotime($row['max_date'] . ' - 1 day')) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// Query takes a full second to run for my data which would add some amount of time for the sync...
|
||||
// Going to need to add a stop as well so only adding in relevant data points as the backwards sync runs
|
||||
//
|
||||
// TODO
|
||||
// timezone convert!
|
||||
|
||||
$query = '
|
||||
insert into
|
||||
`runtime_thermostat_summary`
|
||||
select
|
||||
null `runtime_summary_id`,
|
||||
`thermostat`.`user_id` `user_id`,
|
||||
`thermostat_id` `thermostat_id`,
|
||||
date(convert_tz(`timestamp`, "UTC", "' . $thermostat['time_zone'] . '")) `date`,
|
||||
count(*) `count`,
|
||||
sum(case when `compressor_mode` = "cool" then `compressor_1` else 0 end) `compressor_cool_1`,
|
||||
sum(case when `compressor_mode` = "cool" then `compressor_2` else 0 end) `compressor_cool_2`,
|
||||
sum(case when `compressor_mode` = "heat" then `compressor_1` else 0 end) `compressor_heat_1`,
|
||||
sum(case when `compressor_mode` = "heat" then `compressor_2` else 0 end) `compressor_heat_2`,
|
||||
sum(`auxiliary_heat_1`) `auxiliary_heat_1`,
|
||||
sum(`auxiliary_heat_2`) `auxiliary_heat_2`,
|
||||
sum(`fan`) `fan`,
|
||||
sum(case when `accessory_type` = "humidifier" then `accessory` else 0 end) `humidifier`,
|
||||
sum(case when `accessory_type` = "dehumidifier" then `accessory` else 0 end) `dehumidifier`,
|
||||
sum(case when `accessory_type` = "ventilator" then `accessory` else 0 end) `ventilator`,
|
||||
sum(case when `accessory_type` = "economizer" then `accessory` else 0 end) `economizer`,
|
||||
round(avg(`outdoor_temperature`)) `avg_outdoor_temperature`,
|
||||
round(avg(`outdoor_humidity`)) `avg_outdoor_humidity`,
|
||||
min(`outdoor_temperature`) `min_outdoor_temperature`,
|
||||
max(`outdoor_temperature`) `max_outdoor_temperature`,
|
||||
round(avg(`indoor_temperature`)) `avg_indoor_temperature`,
|
||||
round(avg(`indoor_humidity`)) `avg_indoor_humidity`,
|
||||
0 `deleted`
|
||||
from
|
||||
`runtime_thermostat`
|
||||
join
|
||||
`thermostat` using(`thermostat_id`)
|
||||
where
|
||||
convert_tz(`timestamp`, "UTC", "' . $thermostat['time_zone'] . '") > ' . $start . '
|
||||
and thermostat_id = ' . $thermostat['thermostat_id'] . '
|
||||
group by
|
||||
`thermostat_id`,
|
||||
date(convert_tz(`timestamp`, "UTC", "' . $thermostat['time_zone'] . '"))
|
||||
on duplicate key update
|
||||
`count` = values(`count`),
|
||||
`sum_compressor_cool_1` = values(`sum_compressor_cool_1`),
|
||||
`sum_compressor_cool_2` = values(`sum_compressor_cool_2`),
|
||||
`sum_compressor_heat_1` = values(`sum_compressor_heat_1`),
|
||||
`sum_compressor_heat_2` = values(`sum_compressor_heat_2`),
|
||||
`sum_auxiliary_heat_1` = values(`sum_auxiliary_heat_1`),
|
||||
`sum_auxiliary_heat_2` = values(`sum_auxiliary_heat_2`),
|
||||
`sum_fan` = values(`sum_fan`),
|
||||
`sum_humidifier` = values(`sum_humidifier`),
|
||||
`sum_dehumidifier` = values(`sum_dehumidifier`),
|
||||
`sum_ventilator` = values(`sum_ventilator`),
|
||||
`sum_economizer` = values(`sum_economizer`),
|
||||
`avg_outdoor_temperature` = values(`avg_outdoor_temperature`),
|
||||
`avg_outdoor_humidity` = values(`avg_outdoor_humidity`),
|
||||
`min_outdoor_temperature` = values(`min_outdoor_temperature`),
|
||||
`max_outdoor_temperature` = values(`max_outdoor_temperature`),
|
||||
`avg_indoor_temperature` = values(`avg_indoor_temperature`),
|
||||
`avg_indoor_humidity` = values(`avg_indoor_humidity`)
|
||||
';
|
||||
$this->database->query($query);
|
||||
}
|
||||
}
|
51
api/runtime_thermostat_text.php
Executable file
51
api/runtime_thermostat_text.php
Executable file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Text normalized out of the runtime table to reduce the amount of stored data.
|
||||
*
|
||||
* @author Jon Ziebell
|
||||
*/
|
||||
class runtime_thermostat_text extends cora\crud {
|
||||
|
||||
public static $user_locked = false;
|
||||
|
||||
public static $query_cache = [];
|
||||
|
||||
/**
|
||||
* Gets the row from the runtime_text table. If it does not exist, create it
|
||||
* and then return the row. Every time a query is run the results are
|
||||
* statically cached on this object. This is an optimization to reduce
|
||||
* database queries as the values requested are repetitive.
|
||||
*
|
||||
* @param string $value The text to look for.
|
||||
*
|
||||
* @return mixed The row in the runtime_text table. If $value is null/empty
|
||||
* string/all spaces null is returned.
|
||||
*/
|
||||
public function get_create($value) {
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look in the cache first to avoid the query.
|
||||
if (isset(self::$query_cache[$value]) === true) {
|
||||
return self::$query_cache[$value];
|
||||
}
|
||||
|
||||
$runtime_text = $this->get([
|
||||
'value' => $value
|
||||
]);
|
||||
|
||||
if ($runtime_text === null) {
|
||||
$runtime_text = $this->create([
|
||||
'value' => $value
|
||||
]);
|
||||
}
|
||||
|
||||
// Cache the result.
|
||||
self::$query_cache[$value] = $runtime_text;
|
||||
|
||||
return $runtime_text;
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
/**
|
||||
* Some functionality for generating and working with temperature profiles.
|
||||
* Per ecobee documentation: The values supplied for any given 5-minute
|
||||
* interval is the value at the start of the interval and is not an average.
|
||||
*
|
||||
* @author Jon Ziebell
|
||||
*/
|
||||
@ -20,22 +22,17 @@ class temperature_profile extends cora\api {
|
||||
* Generate a temperature profile for the specified thermostat.
|
||||
*
|
||||
* @param int $thermostat_id
|
||||
* @param string $begin Begin date (local time).
|
||||
* @param string $end End date (local time).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function generate($thermostat_id, $begin, $end) {
|
||||
public function generate($thermostat_id) {
|
||||
set_time_limit(0);
|
||||
|
||||
$save = ($begin === null && $end === null);
|
||||
|
||||
// Begin and end are dates, not timestamps. Force that.
|
||||
if($begin !== null) {
|
||||
$begin = date('Y-m-d 00:00:00', strtotime($begin));
|
||||
}
|
||||
if($end !== null) {
|
||||
$end = date('Y-m-d 23:59:59', strtotime($end));
|
||||
// Make sure the thermostat_id provided is one of yours since there's no
|
||||
// user_id security on the runtime_thermostat table.
|
||||
$thermostats = $this->api('thermostat', 'read_id');
|
||||
if (isset($thermostats[$thermostat_id]) === false) {
|
||||
throw new Exception('Invalid thermostat_id.', 10300);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +49,7 @@ class temperature_profile extends cora\api {
|
||||
*
|
||||
* For now this is set to 30m, which I feel is an appropriate requirement.
|
||||
* I am not factoring in any variables outside of temperature for now.
|
||||
* Note that 30m is a MINIMUM due to the zone_calendar_event logic that
|
||||
* Note that 30m is a MINIMUM due to the event_runtime_thermostat_text_id logic that
|
||||
* will go back in time by 30m to account for sensor changes if the
|
||||
* calendar event changes.
|
||||
*/
|
||||
@ -94,7 +91,7 @@ class temperature_profile extends cora\api {
|
||||
|
||||
/**
|
||||
* How far back to query for additional data. For example, when the
|
||||
* zone_calendar_event changes I pull data from 30m ago. If that data is
|
||||
* event_runtime_thermostat_text_id changes I pull data from 30m ago. If that data is
|
||||
* not available in the current runtime chunk, then it will fail. This
|
||||
* will make sure that data is always included.
|
||||
*/
|
||||
@ -110,23 +107,11 @@ class temperature_profile extends cora\api {
|
||||
|
||||
// Get some stuff
|
||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||
$ecobee_thermostat_id = $thermostat['ecobee_thermostat_id'];
|
||||
|
||||
$ecobee_thermostat = $this->api('ecobee_thermostat', 'get', $thermostat['ecobee_thermostat_id']);
|
||||
$this->database->set_time_zone($ecobee_thermostat['json_location']['timeZoneOffsetMinutes']);
|
||||
|
||||
// Figure out all the starting and ending times. Round begin/end to the
|
||||
// nearest 5 minutes to help with the looping later on.
|
||||
$offset = $ecobee_thermostat['json_location']['timeZoneOffsetMinutes'];
|
||||
$end_timestamp = ($end === null ? (time() + ($offset * 60)) : strtotime($end));
|
||||
$begin_timestamp = ($begin === null ? strtotime('-1 year', $end_timestamp) : strtotime($begin));
|
||||
|
||||
// Swap them if they are backwards.
|
||||
if($end_timestamp < $begin_timestamp) {
|
||||
$tmp = $end_timestamp;
|
||||
$end_timestamp = $begin_timestamp;
|
||||
$begin_timestamp = $tmp;
|
||||
}
|
||||
$end_timestamp = time();
|
||||
$begin_timestamp = strtotime('-1 year', $end_timestamp);
|
||||
|
||||
// Round to 5 minute intervals.
|
||||
$begin_timestamp = floor($begin_timestamp / 300) * 300;
|
||||
@ -144,9 +129,9 @@ class temperature_profile extends cora\api {
|
||||
);
|
||||
|
||||
// Get all of the relevant data
|
||||
$ecobee_thermostat_ids = [];
|
||||
$thermostat_ids = [];
|
||||
foreach($group_thermostats as $thermostat) {
|
||||
$ecobee_thermostat_ids[] = $thermostat['ecobee_thermostat_id'];
|
||||
$thermostat_ids[] = $thermostat['thermostat_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,12 +142,12 @@ class temperature_profile extends cora\api {
|
||||
*/
|
||||
$memory_limit = 16; // mb
|
||||
$memory_per_thermostat_per_day = 0.6; // mb
|
||||
$days = (int) floor($memory_limit / ($memory_per_thermostat_per_day * count($ecobee_thermostat_ids)));
|
||||
$days = (int) floor($memory_limit / ($memory_per_thermostat_per_day * count($thermostat_ids)));
|
||||
|
||||
$chunk_size = $days * 86400;
|
||||
|
||||
if($chunk_size === 0) {
|
||||
throw new Exception('Too many thermostats; cannot generate temperature profile.');
|
||||
throw new Exception('Too many thermostats; cannot generate temperature profile.', 10301);
|
||||
}
|
||||
|
||||
$current_timestamp = $begin_timestamp;
|
||||
@ -188,23 +173,20 @@ class temperature_profile extends cora\api {
|
||||
$query = '
|
||||
select
|
||||
`timestamp`,
|
||||
`ecobee_thermostat_id`,
|
||||
`zone_average_temperature`,
|
||||
`thermostat_id`,
|
||||
`indoor_temperature`,
|
||||
`outdoor_temperature`,
|
||||
`compressor_heat_1`,
|
||||
`compressor_heat_2`,
|
||||
`compressor_1`,
|
||||
`compressor_2`,
|
||||
`compressor_mode`,
|
||||
`auxiliary_heat_1`,
|
||||
`auxiliary_heat_2`,
|
||||
`auxiliary_heat_3`,
|
||||
`compressor_cool_1`,
|
||||
`compressor_cool_2`,
|
||||
`zone_calendar_event`,
|
||||
`zone_climate`
|
||||
`event_runtime_thermostat_text_id`,
|
||||
`climate_runtime_thermostat_text_id`
|
||||
from
|
||||
`ecobee_runtime_thermostat`
|
||||
`runtime_thermostat`
|
||||
where
|
||||
`user_id` = ' . $this->database->escape($this->session->get_user_id()) . '
|
||||
and `ecobee_thermostat_id` in (' . implode(',', $ecobee_thermostat_ids) . ')
|
||||
`thermostat_id` in (' . implode(',', $thermostat_ids) . ')
|
||||
and `timestamp` >= "' . date('Y-m-d H:i:s', ($current_timestamp - $max_lookback)) . '"
|
||||
and `timestamp` < "' . date('Y-m-d H:i:s', ($chunk_end_timestamp + $max_lookahead)) . '"
|
||||
';
|
||||
@ -216,44 +198,54 @@ class temperature_profile extends cora\api {
|
||||
$thermostat['system_type']['detected']['heat'] === 'compressor' ||
|
||||
$thermostat['system_type']['detected']['heat'] === 'geothermal'
|
||||
) {
|
||||
$row['heat'] = max(
|
||||
$row['compressor_heat_1'],
|
||||
$row['compressor_heat_2']
|
||||
);
|
||||
if($row['compressor_mode'] === 'heat') {
|
||||
$row['heat'] = max(
|
||||
$row['compressor_1'],
|
||||
$row['compressor_2']
|
||||
);
|
||||
} else {
|
||||
$row['heat'] = 0;
|
||||
}
|
||||
$row['auxiliary_heat'] = max(
|
||||
$row['auxiliary_heat_1'],
|
||||
$row['auxiliary_heat_2'],
|
||||
$row['auxiliary_heat_3']
|
||||
$row['auxiliary_heat_2']
|
||||
);
|
||||
} else {
|
||||
$row['heat'] = max(
|
||||
$row['auxiliary_heat_1'],
|
||||
$row['auxiliary_heat_2'],
|
||||
$row['auxiliary_heat_3']
|
||||
$row['auxiliary_heat_2']
|
||||
);
|
||||
$row['auxiliary_heat'] = 0;
|
||||
}
|
||||
|
||||
$row['cool'] = max(
|
||||
$row['compressor_cool_1'],
|
||||
$row['compressor_cool_2']
|
||||
);
|
||||
if($row['compressor_mode'] === 'cool') {
|
||||
$row['cool'] = max(
|
||||
$row['compressor_1'],
|
||||
$row['compressor_2']
|
||||
);
|
||||
} else {
|
||||
$row['cool'] = 0;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($row['timestamp']);
|
||||
if (isset($runtime[$timestamp]) === false) {
|
||||
$runtime[$timestamp] = [];
|
||||
}
|
||||
$runtime[$timestamp][$row['ecobee_thermostat_id']] = $row;
|
||||
$runtime[$timestamp][$row['thermostat_id']] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if(
|
||||
isset($runtime[$current_timestamp]) === true && // Had data for at least one thermostat
|
||||
isset($runtime[$current_timestamp][$ecobee_thermostat_id]) === true // Had data for the requested thermostat
|
||||
isset($runtime[$current_timestamp][$thermostat_id]) === true // Had data for the requested thermostat
|
||||
) {
|
||||
$current_runtime = $runtime[$current_timestamp][$ecobee_thermostat_id];
|
||||
$current_runtime = $runtime[$current_timestamp][$thermostat_id];
|
||||
if($current_runtime['outdoor_temperature'] !== null) {
|
||||
$current_runtime['outdoor_temperature'] = round($current_runtime['outdoor_temperature'] / $smoothing) * $smoothing;
|
||||
// Rounds to the nearest degree (because temperatures are stored in tenths).
|
||||
$current_runtime['outdoor_temperature'] = round($current_runtime['outdoor_temperature'] / 10) * 10;
|
||||
|
||||
// Applies further smoothing if required.
|
||||
$current_runtime['outdoor_temperature'] = round($current_runtime['outdoor_temperature'] / $smoothing) * $smoothing;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -263,7 +255,7 @@ class temperature_profile extends cora\api {
|
||||
$most_off = true;
|
||||
$all_off = true;
|
||||
if(
|
||||
count($runtime[$current_timestamp]) < count($ecobee_thermostat_ids)
|
||||
count($runtime[$current_timestamp]) < count($thermostat_ids)
|
||||
) {
|
||||
// If I didn't get data at this timestamp for all thermostats in the
|
||||
// group, all off can't be true.
|
||||
@ -271,20 +263,17 @@ class temperature_profile extends cora\api {
|
||||
$most_off = false;
|
||||
}
|
||||
else {
|
||||
foreach($runtime[$current_timestamp] as $runtime_ecobee_thermostat_id => $thermostat_runtime) {
|
||||
foreach($runtime[$current_timestamp] as $runtime_thermostat_id => $thermostat_runtime) {
|
||||
if(
|
||||
$thermostat_runtime['compressor_heat_1'] !== 0 ||
|
||||
$thermostat_runtime['compressor_heat_2'] !== 0 ||
|
||||
$thermostat_runtime['compressor_1'] !== 0 ||
|
||||
$thermostat_runtime['compressor_2'] !== 0 ||
|
||||
$thermostat_runtime['auxiliary_heat_1'] !== 0 ||
|
||||
$thermostat_runtime['auxiliary_heat_2'] !== 0 ||
|
||||
$thermostat_runtime['auxiliary_heat_3'] !== 0 ||
|
||||
$thermostat_runtime['compressor_cool_1'] !== 0 ||
|
||||
$thermostat_runtime['compressor_cool_2'] !== 0 ||
|
||||
$thermostat_runtime['outdoor_temperature'] === null ||
|
||||
$thermostat_runtime['zone_average_temperature'] === null ||
|
||||
$thermostat_runtime['indoor_temperature'] === null ||
|
||||
(
|
||||
// Wasn't syncing this until mid-November 2018. Just going with December to be safe.
|
||||
$thermostat_runtime['zone_climate'] === null &&
|
||||
$thermostat_runtime['climate_runtime_thermostat_text_id'] === null &&
|
||||
$current_timestamp > 1543640400
|
||||
)
|
||||
) {
|
||||
@ -297,7 +286,7 @@ class temperature_profile extends cora\api {
|
||||
// If everything _but_ the requested thermostat is off. This is
|
||||
// used for the heat/cool scores as I need to only gather samples
|
||||
// when everything else is off.
|
||||
if($runtime_ecobee_thermostat_id !== $ecobee_thermostat_id) {
|
||||
if($runtime_thermostat_id !== $thermostat_id) {
|
||||
$most_off = false;
|
||||
}
|
||||
}
|
||||
@ -383,8 +372,8 @@ class temperature_profile extends cora\api {
|
||||
isset($previous_runtime) === true &&
|
||||
(
|
||||
$current_runtime['outdoor_temperature'] !== $begin_runtime['heat']['outdoor_temperature'] ||
|
||||
$current_runtime['zone_calendar_event'] !== $begin_runtime['heat']['zone_calendar_event'] ||
|
||||
$current_runtime['zone_climate'] !== $begin_runtime['heat']['zone_climate'] ||
|
||||
$current_runtime['event_runtime_thermostat_text_id'] !== $begin_runtime['heat']['event_runtime_thermostat_text_id'] ||
|
||||
$current_runtime['climate_runtime_thermostat_text_id'] !== $begin_runtime['heat']['climate_runtime_thermostat_text_id'] ||
|
||||
$most_off === false
|
||||
)
|
||||
) ||
|
||||
@ -400,8 +389,8 @@ class temperature_profile extends cora\api {
|
||||
isset($previous_runtime) === true &&
|
||||
(
|
||||
$current_runtime['outdoor_temperature'] !== $begin_runtime['cool']['outdoor_temperature'] ||
|
||||
$current_runtime['zone_calendar_event'] !== $begin_runtime['cool']['zone_calendar_event'] ||
|
||||
$current_runtime['zone_climate'] !== $begin_runtime['cool']['zone_climate'] ||
|
||||
$current_runtime['event_runtime_thermostat_text_id'] !== $begin_runtime['cool']['event_runtime_thermostat_text_id'] ||
|
||||
$current_runtime['climate_runtime_thermostat_text_id'] !== $begin_runtime['cool']['climate_runtime_thermostat_text_id'] ||
|
||||
$most_off === false
|
||||
)
|
||||
) ||
|
||||
@ -417,8 +406,8 @@ class temperature_profile extends cora\api {
|
||||
isset($previous_runtime) === true &&
|
||||
(
|
||||
$current_runtime['outdoor_temperature'] !== $begin_runtime['resist']['outdoor_temperature'] ||
|
||||
$current_runtime['zone_calendar_event'] !== $begin_runtime['resist']['zone_calendar_event'] ||
|
||||
$current_runtime['zone_climate'] !== $begin_runtime['resist']['zone_climate'] ||
|
||||
$current_runtime['event_runtime_thermostat_text_id'] !== $begin_runtime['resist']['event_runtime_thermostat_text_id'] ||
|
||||
$current_runtime['climate_runtime_thermostat_text_id'] !== $begin_runtime['resist']['climate_runtime_thermostat_text_id'] ||
|
||||
$all_off === false
|
||||
)
|
||||
)
|
||||
@ -426,25 +415,25 @@ class temperature_profile extends cora\api {
|
||||
// By default the end sample is the previous sample (five minutes ago).
|
||||
$offset = $five_minutes;
|
||||
|
||||
// If zone_calendar_event or zone_climate changes, need to ignore data
|
||||
// If event_runtime_thermostat_text_id or climate_runtime_thermostat_text_id changes, need to ignore data
|
||||
// from the previous 30 minutes as there are sensors changing during
|
||||
// that time.
|
||||
if(
|
||||
$current_runtime['zone_calendar_event'] !== $begin_runtime[$sample_type]['zone_calendar_event'] ||
|
||||
$current_runtime['zone_climate'] !== $begin_runtime[$sample_type]['zone_climate']
|
||||
$current_runtime['event_runtime_thermostat_text_id'] !== $begin_runtime[$sample_type]['event_runtime_thermostat_text_id'] ||
|
||||
$current_runtime['climate_runtime_thermostat_text_id'] !== $begin_runtime[$sample_type]['climate_runtime_thermostat_text_id']
|
||||
) {
|
||||
$offset = $thirty_minutes;
|
||||
} else {
|
||||
// Start looking ahead into the next 30 minutes looking for changes
|
||||
// to zone_calendar_event and zone_climate.
|
||||
// to event_runtime_thermostat_text_id and climate_runtime_thermostat_text_id.
|
||||
$lookahead = $five_minutes;
|
||||
while($lookahead <= $thirty_minutes) {
|
||||
if(
|
||||
isset($runtime[$current_timestamp + $lookahead]) === true &&
|
||||
isset($runtime[$current_timestamp + $lookahead][$ecobee_thermostat_id]) === true &&
|
||||
isset($runtime[$current_timestamp + $lookahead][$thermostat_id]) === true &&
|
||||
(
|
||||
$runtime[$current_timestamp + $lookahead][$ecobee_thermostat_id]['zone_calendar_event'] !== $current_runtime['zone_calendar_event'] ||
|
||||
$runtime[$current_timestamp + $lookahead][$ecobee_thermostat_id]['zone_climate'] !== $current_runtime['zone_climate']
|
||||
$runtime[$current_timestamp + $lookahead][$thermostat_id]['event_runtime_thermostat_text_id'] !== $current_runtime['event_runtime_thermostat_text_id'] ||
|
||||
$runtime[$current_timestamp + $lookahead][$thermostat_id]['climate_runtime_thermostat_text_id'] !== $current_runtime['climate_runtime_thermostat_text_id']
|
||||
)
|
||||
) {
|
||||
$offset = ($thirty_minutes - $lookahead);
|
||||
@ -462,16 +451,16 @@ class temperature_profile extends cora\api {
|
||||
// to this.
|
||||
if(
|
||||
isset($runtime[$current_timestamp - $offset]) === true &&
|
||||
isset($runtime[$current_timestamp - $offset][$ecobee_thermostat_id]) === true &&
|
||||
isset($runtime[$current_timestamp - $offset][$thermostat_id]) === true &&
|
||||
($current_timestamp - $offset) > strtotime($begin_runtime[$sample_type]['timestamp'])
|
||||
) {
|
||||
$end_runtime = $runtime[$current_timestamp - $offset][$ecobee_thermostat_id];
|
||||
$end_runtime = $runtime[$current_timestamp - $offset][$thermostat_id];
|
||||
} else {
|
||||
$end_runtime = null;
|
||||
}
|
||||
|
||||
if($end_runtime !== null) {
|
||||
$delta = $end_runtime['zone_average_temperature'] - $begin_runtime[$sample_type]['zone_average_temperature'];
|
||||
$delta = $end_runtime['indoor_temperature'] - $begin_runtime[$sample_type]['indoor_temperature'];
|
||||
$duration = strtotime($end_runtime['timestamp']) - strtotime($begin_runtime[$sample_type]['timestamp']);
|
||||
|
||||
if($duration > 0) {
|
||||
@ -500,7 +489,7 @@ class temperature_profile extends cora\api {
|
||||
if(
|
||||
$heat_on_for === 0 ||
|
||||
$current_runtime['outdoor_temperature'] === null ||
|
||||
$current_runtime['zone_average_temperature'] === null ||
|
||||
$current_runtime['indoor_temperature'] === null ||
|
||||
$current_runtime['auxiliary_heat'] > 0
|
||||
) {
|
||||
unset($begin_runtime['heat']);
|
||||
@ -508,7 +497,7 @@ class temperature_profile extends cora\api {
|
||||
if(
|
||||
$cool_on_for === 0 ||
|
||||
$current_runtime['outdoor_temperature'] === null ||
|
||||
$current_runtime['zone_average_temperature'] === null
|
||||
$current_runtime['indoor_temperature'] === null
|
||||
) {
|
||||
unset($begin_runtime['cool']);
|
||||
}
|
||||
@ -603,7 +592,7 @@ class temperature_profile extends cora\api {
|
||||
|
||||
// Only actually save this profile to the thermostat if it was run with the
|
||||
// default settings (aka the last year). Anything else is not valid to save.
|
||||
if($save === true) {
|
||||
// if($save === true) {
|
||||
$this->api(
|
||||
'thermostat',
|
||||
'update',
|
||||
@ -614,7 +603,7 @@ class temperature_profile extends cora\api {
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
// }
|
||||
|
||||
$this->database->set_time_zone(0);
|
||||
|
||||
|
0
font/material_icon/material_icon.eot
Normal file → Executable file
0
font/material_icon/material_icon.eot
Normal file → Executable file
0
font/material_icon/material_icon.ttf
Normal file → Executable file
0
font/material_icon/material_icon.ttf
Normal file → Executable file
0
font/material_icon/material_icon.woff
Normal file → Executable file
0
font/material_icon/material_icon.woff
Normal file → Executable file
0
font/material_icon/material_icon.woff2
Normal file → Executable file
0
font/material_icon/material_icon.woff2
Normal file → Executable file
@ -15,49 +15,49 @@
|
||||
"extends": "eslint:all",
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"no-lonely-if": "off",
|
||||
"capitalized-comments": "off",
|
||||
"complexity": "off",
|
||||
"no-loop-func": "off",
|
||||
"consistent-this": ["error", "self"],
|
||||
"dot-location": ["error", "property"],
|
||||
"default-case": "off",
|
||||
"dot-location": ["error", "property"],
|
||||
"func-names": ["error", "never"],
|
||||
"function-paren-newline": "off",
|
||||
"guard-for-in": "off",
|
||||
"id-length": "off",
|
||||
"indent": ["error", 2],
|
||||
"init-declarations": "off",
|
||||
"linebreak-style": "off",
|
||||
"lines-around-comment": "off",
|
||||
"max-len": ["error", {"ignoreUrls": true, "ignoreStrings": true}],
|
||||
"max-lines": "off",
|
||||
"max-depth": "off",
|
||||
"max-statements": "off",
|
||||
"max-len": "off",
|
||||
"max-lines": "off",
|
||||
"max-lines-per-function": "off",
|
||||
"max-params": ["error", 5],
|
||||
"max-statements": "off",
|
||||
"multiline-ternary": "off",
|
||||
"new-cap": ["error", {"newIsCap": false}],
|
||||
"newline-after-var": "off",
|
||||
"no-extra-parens": "off",
|
||||
"no-lonely-if": "off",
|
||||
"no-loop-func": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-multiple-empty-lines": ["warn", {"max": 1, "maxEOF": 1, "maxBOF": 0}],
|
||||
"no-negated-condition": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-ternary": "off",
|
||||
"no-trailing-spaces": "warn",
|
||||
"no-undefined": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"object-curly-newline": "warn",
|
||||
"one-var": ["error", "never"],
|
||||
"padded-blocks": ["warn", {"blocks": "never", "classes": "never", "switches": "never"}],
|
||||
"quotes": ["error", "single"],
|
||||
"require-unicode-regexp": "off",
|
||||
"sort-keys": "off",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"strict": "off",
|
||||
"valid-jsdoc": ["error", {"requireReturn": false, "requireParamDescription": false}],
|
||||
"vars-on-top": "off",
|
||||
"guard-for-in": "off",
|
||||
"multiline-ternary": "off",
|
||||
"no-ternary": "off",
|
||||
"no-trailing-spaces": "warn",
|
||||
"object-curly-newline": "warn",
|
||||
"max-len": "warn",
|
||||
"no-negated-condition": "off",
|
||||
"max-lines-per-function": "off",
|
||||
"function-paren-newline": "off",
|
||||
|
||||
// Node.js and CommonJS
|
||||
"callback-return": "off",
|
||||
|
20
js/beestat/clone.js
Normal file → Executable file
20
js/beestat/clone.js
Normal file → Executable file
@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Performs a deep clone of a simple object.
|
||||
*
|
||||
* @param {Object} object The object to clone.
|
||||
*
|
||||
* @return {Object} The cloned object.
|
||||
*/
|
||||
beestat.clone = function(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
};
|
||||
/**
|
||||
* Performs a deep clone of a simple object.
|
||||
*
|
||||
* @param {Object} object The object to clone.
|
||||
*
|
||||
* @return {Object} The cloned object.
|
||||
*/
|
||||
beestat.clone = function(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
};
|
||||
|
24
js/beestat/get_sync_progress.js
Executable file
24
js/beestat/get_sync_progress.js
Executable file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Get the sync progress for a thermostat.
|
||||
*
|
||||
* @param {number} thermostat_id
|
||||
*
|
||||
* @return {number} A number between 0 and 100 inclusive.
|
||||
*/
|
||||
beestat.get_sync_progress = function(thermostat_id) {
|
||||
var thermostat = beestat.cache.thermostat[thermostat_id];
|
||||
|
||||
var current_sync_begin = moment.utc(thermostat.sync_begin);
|
||||
var current_sync_end = moment.utc(thermostat.sync_end);
|
||||
|
||||
var required_sync_begin = moment.max(
|
||||
moment(thermostat.first_connected),
|
||||
moment().subtract(1, 'year')
|
||||
);
|
||||
var required_sync_end = moment().subtract(1, 'hour');
|
||||
|
||||
var denominator = required_sync_end.diff(required_sync_begin, 'day');
|
||||
var numerator = current_sync_end.diff(current_sync_begin, 'day');
|
||||
|
||||
return Math.min(100, Math.round(numerator / denominator * 100)) || 0;
|
||||
};
|
18
js/beestat/highcharts.js
Executable file
18
js/beestat/highcharts.js
Executable file
File diff suppressed because one or more lines are too long
@ -14,14 +14,14 @@ beestat.setting = function(key, opt_value, opt_callback) {
|
||||
var defaults = {
|
||||
'recent_activity_time_period': 'day',
|
||||
'recent_activity_time_count': 3,
|
||||
'aggregate_runtime_time_period': 'month',
|
||||
'aggregate_runtime_time_count': 2,
|
||||
'aggregate_runtime_group_by': 'day',
|
||||
'aggregate_runtime_gap_fill': true,
|
||||
|
||||
'runtime_thermostat_summary_time_count': 0,
|
||||
'runtime_thermostat_summary_time_period': 'all',
|
||||
'runtime_thermostat_summary_group_by': 'month',
|
||||
'runtime_thermostat_summary_gap_fill': true,
|
||||
|
||||
'comparison_region': 'global',
|
||||
'comparison_property_type': 'similar',
|
||||
'comparison_period': 0,
|
||||
'comparison_period_custom': moment().format('M/D/YYYY')
|
||||
'comparison_property_type': 'similar'
|
||||
};
|
||||
|
||||
if (user.json_settings === null) {
|
||||
|
@ -84,14 +84,13 @@ beestat.style.font_size = {
|
||||
beestat.style.set = function(element, base_style, media_style) {
|
||||
element.style(base_style);
|
||||
|
||||
for(var media in media_style) {
|
||||
for (var media in media_style) {
|
||||
var media_query_list = window.matchMedia(media);
|
||||
|
||||
var handler = function(e) {
|
||||
if(e.matches === true) {
|
||||
if (e.matches === true) {
|
||||
element.style(media_style[e.media]);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
element.style(base_style);
|
||||
}
|
||||
};
|
||||
@ -109,137 +108,174 @@ beestat.style.set = function(element, base_style, media_style) {
|
||||
* @return {object} RGB
|
||||
*/
|
||||
beestat.style.hex_to_rgb = function(hex) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
var result = (/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i).exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
'r': parseInt(result[1], 16),
|
||||
'g': parseInt(result[2], 16),
|
||||
'b': parseInt(result[3], 16)
|
||||
} : null;
|
||||
};
|
||||
|
||||
// Can't put these in beestat.js because of the dependency issues with color.
|
||||
beestat.series = {
|
||||
'compressor_cool_1': {
|
||||
'name': 'Cool 1',
|
||||
'color': beestat.style.color.lightblue.light
|
||||
},
|
||||
'compressor_cool_2': {
|
||||
'name': 'Cool 2',
|
||||
'color': beestat.style.color.lightblue.base
|
||||
},
|
||||
'compressor_heat_1': {
|
||||
'name': 'Heat 1',
|
||||
'color': beestat.style.color.orange.light
|
||||
},
|
||||
'compressor_heat_2': {
|
||||
'name': 'Heat 2',
|
||||
'color': beestat.style.color.orange.dark
|
||||
},
|
||||
'auxiliary_heat_1': {
|
||||
'name': 'Aux',
|
||||
'color': beestat.style.color.red.dark
|
||||
},
|
||||
'auxiliary_heat_2': {
|
||||
'name': 'Aux 2',
|
||||
'color': beestat.style.color.red.dark
|
||||
},
|
||||
'auxiliary_heat_3': {
|
||||
'name': 'Aux 3',
|
||||
'color': beestat.style.color.red.dark
|
||||
},
|
||||
'fan': {
|
||||
'name': 'Fan',
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
beestat.series = {};
|
||||
|
||||
'dehumidifier': {
|
||||
'name': 'Dehumidifier',
|
||||
'color': beestat.style.color.gray.light
|
||||
},
|
||||
'economizer': {
|
||||
'name': 'Economizer',
|
||||
'color': beestat.style.color.gray.light
|
||||
},
|
||||
'humidifier': {
|
||||
'name': 'Humidifier',
|
||||
'color': beestat.style.color.gray.light
|
||||
},
|
||||
'ventilator': {
|
||||
'name': 'Ventilator',
|
||||
'color': beestat.style.color.gray.light
|
||||
},
|
||||
|
||||
'indoor_temperature': {
|
||||
'name': 'Indoor Temp',
|
||||
'color': beestat.style.color.gray.light
|
||||
},
|
||||
'outdoor_temperature': {
|
||||
'name': 'Outdoor Temp',
|
||||
'color': beestat.style.color.gray.light
|
||||
},
|
||||
'average_outdoor_temperature': {
|
||||
'name': 'Average Outdoor Temp',
|
||||
'color': beestat.style.color.gray.light
|
||||
},
|
||||
'min_max_outdoor_temperature': {
|
||||
'name': 'Min/Max Outdoor Temp',
|
||||
'color': '#000'
|
||||
},
|
||||
'setpoint_heat': {
|
||||
'name': 'Setpoint',
|
||||
'color': beestat.style.color.orange.light
|
||||
},
|
||||
'setpoint_cool': {
|
||||
'name': 'Setpoint',
|
||||
'color': beestat.style.color.lightblue.light
|
||||
},
|
||||
'indoor_humidity': {
|
||||
'name': 'Indoor Humidity',
|
||||
'color': beestat.style.color.bluegreen.base
|
||||
},
|
||||
'outdoor_humidity': {
|
||||
'name': 'Outdoor Humidity',
|
||||
'color': beestat.style.color.green.light
|
||||
},
|
||||
|
||||
'calendar_event_home': {
|
||||
'name': 'Home',
|
||||
'color': beestat.style.color.green.dark
|
||||
},
|
||||
'calendar_event_smarthome': {
|
||||
'name': 'Smart Home',
|
||||
'color': beestat.style.color.green.dark
|
||||
},
|
||||
'calendar_event_smartrecovery': {
|
||||
'name': 'Smart Recovery',
|
||||
'color': beestat.style.color.green.dark
|
||||
},
|
||||
'calendar_event_away': {
|
||||
'name': 'Away',
|
||||
'color': beestat.style.color.gray.dark
|
||||
},
|
||||
'calendar_event_smartaway': {
|
||||
'name': 'Smart Away',
|
||||
'color': beestat.style.color.gray.dark
|
||||
},
|
||||
'calendar_event_vacation': {
|
||||
'name': 'Vacation',
|
||||
'color': beestat.style.color.gray.dark
|
||||
},
|
||||
'calendar_event_sleep': {
|
||||
'name': 'Sleep',
|
||||
'color': beestat.style.color.purple.light
|
||||
},
|
||||
'calendar_event_hold': {
|
||||
'name': 'Hold',
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'calendar_event_quicksave': {
|
||||
'name': 'QuickSave',
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'calendar_event_other': {
|
||||
'name': 'Other',
|
||||
'color': beestat.style.color.gray.base
|
||||
}
|
||||
beestat.series.compressor_heat_1 = {
|
||||
'name': 'Heat 1',
|
||||
'color': beestat.style.color.orange.light
|
||||
};
|
||||
|
||||
beestat.series.compressor_heat_2 = {
|
||||
'name': 'Heat 2',
|
||||
'color': beestat.style.color.orange.dark
|
||||
};
|
||||
|
||||
beestat.series.auxiliary_heat_1 = {
|
||||
'name': 'Aux',
|
||||
'color': beestat.style.color.red.dark
|
||||
};
|
||||
|
||||
beestat.series.auxiliary_heat_2 = {
|
||||
'name': 'Aux 2',
|
||||
'color': beestat.style.color.red.dark
|
||||
};
|
||||
|
||||
beestat.series.compressor_cool_1 = {
|
||||
'name': 'Cool 1',
|
||||
'color': beestat.style.color.lightblue.light
|
||||
};
|
||||
|
||||
beestat.series.compressor_cool_2 = {
|
||||
'name': 'Cool 2',
|
||||
'color': beestat.style.color.lightblue.base
|
||||
};
|
||||
|
||||
beestat.series.fan = {
|
||||
'name': 'Fan',
|
||||
'color': beestat.style.color.gray.base
|
||||
};
|
||||
|
||||
beestat.series.humidifier = {
|
||||
'name': 'Humidifier',
|
||||
'color': beestat.style.color.gray.light
|
||||
};
|
||||
|
||||
beestat.series.dehumidifier = {
|
||||
'name': 'Dehumidifier',
|
||||
'color': beestat.style.color.gray.light
|
||||
};
|
||||
|
||||
beestat.series.economizer = {
|
||||
'name': 'Economizer',
|
||||
'color': beestat.style.color.gray.light
|
||||
};
|
||||
|
||||
beestat.series.ventilator = {
|
||||
'name': 'Ventilator',
|
||||
'color': beestat.style.color.gray.light
|
||||
};
|
||||
|
||||
beestat.series.indoor_temperature = {
|
||||
'name': 'Indoor Temp',
|
||||
'color': beestat.style.color.gray.light
|
||||
};
|
||||
|
||||
beestat.series.outdoor_temperature = {
|
||||
'name': 'Outdoor Temp',
|
||||
'color': beestat.style.color.gray.light
|
||||
};
|
||||
|
||||
beestat.series.indoor_humidity = {
|
||||
'name': 'Indoor Humidity',
|
||||
'color': beestat.style.color.bluegreen.base
|
||||
};
|
||||
|
||||
beestat.series.outdoor_humidity = {
|
||||
'name': 'Outdoor Humidity',
|
||||
'color': beestat.style.color.green.light
|
||||
};
|
||||
|
||||
beestat.series.setpoint_heat = {
|
||||
'name': 'Setpoint',
|
||||
'color': beestat.style.color.orange.light
|
||||
};
|
||||
|
||||
beestat.series.setpoint_cool = {
|
||||
'name': 'Setpoint',
|
||||
'color': beestat.style.color.lightblue.light
|
||||
};
|
||||
|
||||
// Runtime Summary
|
||||
beestat.series.sum_compressor_heat_1 = beestat.series.compressor_heat_1;
|
||||
beestat.series.sum_compressor_heat_2 = beestat.series.compressor_heat_2;
|
||||
beestat.series.sum_auxiliary_heat_1 = beestat.series.auxiliary_heat_1;
|
||||
beestat.series.sum_auxiliary_heat_2 = beestat.series.auxiliary_heat_2;
|
||||
beestat.series.sum_compressor_cool_1 = beestat.series.compressor_cool_1;
|
||||
beestat.series.sum_compressor_cool_2 = beestat.series.compressor_cool_2;
|
||||
beestat.series.sum_fan = beestat.series.fan;
|
||||
beestat.series.sum_humidifier = beestat.series.humidifier;
|
||||
beestat.series.sum_dehumidifier = beestat.series.dehumidifier;
|
||||
beestat.series.sum_economizer = beestat.series.economizer;
|
||||
beestat.series.sum_ventilator = beestat.series.ventilator;
|
||||
beestat.series.avg_indoor_temperature = beestat.series.indoor_temperature;
|
||||
beestat.series.avg_outdoor_temperature = beestat.series.outdoor_temperature;
|
||||
beestat.series.avg_indoor_humidity = beestat.series.indoor_humidity;
|
||||
beestat.series.avg_outdoor_humidity = beestat.series.outdoor_humidity;
|
||||
beestat.series.extreme_outdoor_temperature = {
|
||||
'name': 'Outdoor Temp Extremes',
|
||||
'color': beestat.style.color.gray.dark
|
||||
};
|
||||
|
||||
beestat.series.setpoint_cool = {
|
||||
'name': 'Setpoint',
|
||||
'color': beestat.style.color.lightblue.light
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_home = {
|
||||
'name': 'Home',
|
||||
'color': beestat.style.color.green.dark
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_smarthome = {
|
||||
'name': 'Smart Home',
|
||||
'color': beestat.style.color.green.dark
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_smartrecovery = {
|
||||
'name': 'Smart Recovery',
|
||||
'color': beestat.style.color.green.dark
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_away = {
|
||||
'name': 'Away',
|
||||
'color': beestat.style.color.gray.dark
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_smartaway = {
|
||||
'name': 'Smart Away',
|
||||
'color': beestat.style.color.gray.dark
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_vacation = {
|
||||
'name': 'Vacation',
|
||||
'color': beestat.style.color.gray.dark
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_sleep = {
|
||||
'name': 'Sleep',
|
||||
'color': beestat.style.color.purple.light
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_hold = {
|
||||
'name': 'Hold',
|
||||
'color': beestat.style.color.gray.base
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_quicksave = {
|
||||
'name': 'QuickSave',
|
||||
'color': beestat.style.color.gray.base
|
||||
};
|
||||
|
||||
beestat.series.calendar_event_other = {
|
||||
'name': 'Other',
|
||||
'color': beestat.style.color.gray.base
|
||||
};
|
||||
|
@ -3,6 +3,9 @@
|
||||
* defined settings. Updates the cache with the response which fires off the
|
||||
* event for anything bound to that data.
|
||||
*
|
||||
* TODO: This can probably be refactored a bit. The API call is now gone
|
||||
* because it's no longer possible to generate these on the fly as of 1.4.
|
||||
*
|
||||
* @param {Function} callback Optional callback to fire when the API call
|
||||
* completes.
|
||||
*/
|
||||
@ -12,76 +15,13 @@ beestat.generate_temperature_profile = function(callback) {
|
||||
thermostat.thermostat_group_id
|
||||
];
|
||||
|
||||
var comparison_period = beestat.setting('comparison_period');
|
||||
beestat.cache.set(
|
||||
'data.comparison_temperature_profile',
|
||||
thermostat_group.temperature_profile
|
||||
);
|
||||
|
||||
// Fire off the API call to get the temperature profile.
|
||||
var begin;
|
||||
var end;
|
||||
if (comparison_period === 'custom') {
|
||||
end = moment(beestat.setting('comparison_period_custom'));
|
||||
|
||||
// If you picked today just set these to null to utilize the API cache.
|
||||
if (end.isSame(moment(), 'date') === true) {
|
||||
begin = null;
|
||||
end = null;
|
||||
} else {
|
||||
begin = moment(beestat.setting('comparison_period_custom'));
|
||||
begin.subtract(1, 'year');
|
||||
|
||||
end = end.format('YYYY-MM-DD');
|
||||
begin = begin.format('YYYY-MM-DD');
|
||||
}
|
||||
} else if (comparison_period !== 0) {
|
||||
begin = moment();
|
||||
end = moment();
|
||||
|
||||
begin.subtract(comparison_period, 'month');
|
||||
begin.subtract(1, 'year');
|
||||
|
||||
end.subtract(comparison_period, 'month');
|
||||
|
||||
end = end.format('YYYY-MM-DD');
|
||||
begin = begin.format('YYYY-MM-DD');
|
||||
} else {
|
||||
// Set to null for "today" so the API cache gets used.
|
||||
begin = null;
|
||||
end = null;
|
||||
}
|
||||
|
||||
/*
|
||||
* If begin/end were null, just use what's already stored on the
|
||||
* thermostat_group. This will take advantage of the cached data for a week
|
||||
* and let the cron job update it as necessary.
|
||||
*/
|
||||
if (begin === null && end === null) {
|
||||
beestat.cache.set(
|
||||
'data.comparison_temperature_profile',
|
||||
thermostat_group.temperature_profile
|
||||
);
|
||||
|
||||
if (callback !== undefined) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
beestat.cache.delete('data.comparison_temperature_profile');
|
||||
new beestat.api()
|
||||
.add_call(
|
||||
'thermostat_group',
|
||||
'generate_temperature_profile',
|
||||
{
|
||||
'thermostat_group_id': thermostat.thermostat_group_id,
|
||||
'begin': begin,
|
||||
'end': end
|
||||
}
|
||||
)
|
||||
.set_callback(function(data) {
|
||||
beestat.cache.set('data.comparison_temperature_profile', data);
|
||||
|
||||
if (callback !== undefined) {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.send();
|
||||
if (callback !== undefined) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,797 +0,0 @@
|
||||
/**
|
||||
* Recent activity card. Shows a graph similar to what ecobee shows with the
|
||||
* runtime info for a recent period of time.
|
||||
*/
|
||||
beestat.component.card.aggregate_runtime = function() {
|
||||
var self = this;
|
||||
|
||||
/*
|
||||
* When a setting is changed clear all of the data. Then rerender which will
|
||||
* trigger the loading state.
|
||||
*
|
||||
* 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 setting_change_function = beestat.debounce(function() {
|
||||
beestat.cache.set('aggregate_runtime', []);
|
||||
self.rerender();
|
||||
}, 10);
|
||||
|
||||
beestat.dispatcher.addEventListener(
|
||||
[
|
||||
'setting.aggregate_runtime_time_count',
|
||||
'setting.aggregate_runtime_time_period',
|
||||
'setting.aggregate_runtime_group_by',
|
||||
'setting.aggregate_runtime_gap_fill'
|
||||
],
|
||||
setting_change_function
|
||||
);
|
||||
|
||||
beestat.component.card.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.card.aggregate_runtime, beestat.component.card);
|
||||
|
||||
beestat.component.card.aggregate_runtime.equipment_series = [
|
||||
'compressor_cool_1',
|
||||
'compressor_cool_2',
|
||||
'compressor_heat_1',
|
||||
'compressor_heat_2',
|
||||
'auxiliary_heat_1',
|
||||
'auxiliary_heat_2',
|
||||
'auxiliary_heat_3'
|
||||
];
|
||||
|
||||
beestat.component.card.aggregate_runtime.prototype.decorate_contents_ = function(parent) {
|
||||
var self = this;
|
||||
|
||||
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
|
||||
|
||||
this.chart_ = new beestat.component.chart();
|
||||
var series = this.get_series_();
|
||||
|
||||
this.chart_.options.chart.backgroundColor = beestat.style.color.bluegray.base;
|
||||
this.chart_.options.exporting.filename = thermostat.name + ' - Aggregate Runtime';
|
||||
this.chart_.options.exporting.chartOptions.title.text = this.get_title_();
|
||||
this.chart_.options.exporting.chartOptions.subtitle.text = this.get_subtitle_();
|
||||
|
||||
var current_day;
|
||||
var current_hour;
|
||||
var current_week;
|
||||
var current_month;
|
||||
var current_year;
|
||||
|
||||
this.chart_.options.xAxis = {
|
||||
'categories': series.x.chart_data,
|
||||
'lineColor': beestat.style.color.bluegray.light,
|
||||
'tickLength': 0,
|
||||
'labels': {
|
||||
'style': {
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'formatter': function() {
|
||||
var date_parts = this.value.match(/(?:h(\d+))?(?:d(\d+))?(?:w(\d+))?(?:m(\d+))?(?:y(\d+))?/);
|
||||
var hour = moment(date_parts[1], 'H').format('ha');
|
||||
var day = date_parts[2];
|
||||
var month = moment(date_parts[4], 'M').format('MMM');
|
||||
|
||||
var year;
|
||||
var week;
|
||||
if (beestat.setting('aggregate_runtime_group_by') === 'week') {
|
||||
// ISO 8601 week of the year.
|
||||
var yearweek_m = moment().isoWeek(date_parts[3])
|
||||
.year(date_parts[5])
|
||||
.day('Monday');
|
||||
week = yearweek_m.format('MMM D');
|
||||
year = yearweek_m.format('YYYY');
|
||||
} else {
|
||||
year = date_parts[5];
|
||||
}
|
||||
|
||||
var label_parts = [];
|
||||
switch (beestat.setting('aggregate_runtime_group_by')) {
|
||||
case 'year':
|
||||
label_parts.push(year);
|
||||
break;
|
||||
case 'month':
|
||||
label_parts.push(month);
|
||||
if (year !== current_year) {
|
||||
label_parts.push(year);
|
||||
}
|
||||
break;
|
||||
case 'week':
|
||||
if (week !== current_week) {
|
||||
label_parts.push(week);
|
||||
}
|
||||
if (year !== current_year) {
|
||||
label_parts.push(year);
|
||||
}
|
||||
break;
|
||||
case 'day':
|
||||
if (month !== current_month) {
|
||||
label_parts.push(month);
|
||||
}
|
||||
label_parts.push(day);
|
||||
if (year !== current_year) {
|
||||
label_parts.push(year);
|
||||
}
|
||||
break;
|
||||
case 'hour':
|
||||
if (month !== current_month) {
|
||||
label_parts.push(month);
|
||||
}
|
||||
if (day !== current_day) {
|
||||
label_parts.push(day);
|
||||
}
|
||||
if (year !== current_year) {
|
||||
label_parts.push(year);
|
||||
}
|
||||
label_parts.push(hour);
|
||||
break;
|
||||
}
|
||||
|
||||
current_hour = hour;
|
||||
current_day = day;
|
||||
current_week = week;
|
||||
current_month = month;
|
||||
current_year = year;
|
||||
|
||||
return label_parts.join(' ');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var y_max_hours;
|
||||
var tick_interval;
|
||||
switch (beestat.setting('aggregate_runtime_group_by')) {
|
||||
case 'year':
|
||||
y_max_hours = 8760;
|
||||
tick_interval = 2190;
|
||||
break;
|
||||
case 'month':
|
||||
y_max_hours = 672;
|
||||
tick_interval = 168;
|
||||
break;
|
||||
case 'week':
|
||||
y_max_hours = 168;
|
||||
tick_interval = 24;
|
||||
break;
|
||||
case 'day':
|
||||
y_max_hours = 24;
|
||||
tick_interval = 6;
|
||||
break;
|
||||
}
|
||||
|
||||
this.chart_.options.yAxis = [
|
||||
{
|
||||
'alignTicks': false,
|
||||
'min': 0,
|
||||
'softMax': y_max_hours,
|
||||
'tickInterval': tick_interval,
|
||||
'reversedStacks': false,
|
||||
'gridLineColor': beestat.style.color.bluegray.light,
|
||||
'gridLineDashStyle': 'longdash',
|
||||
'title': {
|
||||
'text': ''
|
||||
},
|
||||
'labels': {
|
||||
'style': {
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'formatter': function() {
|
||||
return this.value + 'h';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'alignTicks': false,
|
||||
'gridLineColor': null,
|
||||
'gridLineDashStyle': 'longdash',
|
||||
'opposite': true,
|
||||
'allowDecimals': false,
|
||||
'title': {
|
||||
'text': ''
|
||||
},
|
||||
'labels': {
|
||||
'style': {
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'formatter': function() {
|
||||
return this.value + thermostat.temperature_unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
this.chart_.options.tooltip = {
|
||||
'shared': true,
|
||||
'useHTML': true,
|
||||
'borderWidth': 0,
|
||||
'shadow': false,
|
||||
'backgroundColor': null,
|
||||
'followPointer': true,
|
||||
'crosshairs': {
|
||||
'width': 1,
|
||||
'zIndex': 100,
|
||||
'color': beestat.style.color.gray.light,
|
||||
'dashStyle': 'shortDot',
|
||||
'snap': false
|
||||
},
|
||||
'positioner': function(tooltip_width, tooltip_height, point) {
|
||||
return beestat.component.chart.tooltip_positioner(
|
||||
self.chart_.get_chart(),
|
||||
tooltip_width,
|
||||
tooltip_height,
|
||||
point
|
||||
);
|
||||
},
|
||||
'formatter': function() {
|
||||
var date_parts = this.x.match(/(?:h(\d+))?(?:d(\d+))?(?:w(\d+))?(?:m(\d+))?(?:y(\d+))?/);
|
||||
var hour = moment(date_parts[1], 'H').format('ha');
|
||||
var day = date_parts[2];
|
||||
var month = moment(date_parts[4], 'M').format('MMM');
|
||||
|
||||
var year;
|
||||
var week;
|
||||
if (beestat.setting('aggregate_runtime_group_by') === 'week') {
|
||||
// ISO 8601 week of the year.
|
||||
var yearweek_m = moment().isoWeek(date_parts[3])
|
||||
.year(date_parts[5])
|
||||
.day('Monday');
|
||||
week = yearweek_m.format('MMM D');
|
||||
year = yearweek_m.format('YYYY');
|
||||
} else {
|
||||
year = date_parts[5];
|
||||
}
|
||||
|
||||
var label_parts = [];
|
||||
switch (beestat.setting('aggregate_runtime_group_by')) {
|
||||
case 'year':
|
||||
label_parts.push(year);
|
||||
break;
|
||||
case 'month':
|
||||
label_parts.push(month);
|
||||
label_parts.push(year);
|
||||
break;
|
||||
case 'week':
|
||||
label_parts.push('Week of');
|
||||
label_parts.push(week + ',');
|
||||
label_parts.push(year);
|
||||
break;
|
||||
case 'day':
|
||||
label_parts.push(month);
|
||||
label_parts.push(day);
|
||||
break;
|
||||
case 'hour':
|
||||
label_parts.push(hour);
|
||||
break;
|
||||
}
|
||||
|
||||
var sections = [];
|
||||
var section = [];
|
||||
for (var series_code in series) {
|
||||
var value = series[series_code].data[this.x];
|
||||
|
||||
// Don't show in tooltip if there was no runtime to report.
|
||||
if (value === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (series_code) {
|
||||
case 'x':
|
||||
case 'min_max_outdoor_temperature':
|
||||
continue;
|
||||
break;
|
||||
case 'average_outdoor_temperature':
|
||||
value = beestat.temperature({
|
||||
'temperature': value,
|
||||
'convert': false,
|
||||
'units': true,
|
||||
'round': 0
|
||||
});
|
||||
|
||||
value += ' (';
|
||||
value += beestat.temperature({
|
||||
'temperature': series.min_max_outdoor_temperature.data[this.x].min,
|
||||
'convert': false,
|
||||
'units': true,
|
||||
'round': 0
|
||||
});
|
||||
value += ' to ';
|
||||
value += beestat.temperature({
|
||||
'temperature': series.min_max_outdoor_temperature.data[this.x].max,
|
||||
'convert': false,
|
||||
'units': true,
|
||||
'round': 0
|
||||
});
|
||||
value += ')';
|
||||
|
||||
break;
|
||||
default:
|
||||
value = beestat.time(value, 'hours');
|
||||
break;
|
||||
}
|
||||
|
||||
if (value !== null) {
|
||||
section.push({
|
||||
'label': beestat.series[series_code].name,
|
||||
'value': value,
|
||||
'color': beestat.series[series_code].color
|
||||
});
|
||||
}
|
||||
}
|
||||
sections.push(section);
|
||||
|
||||
return beestat.component.chart.tooltip_formatter(
|
||||
label_parts.join(' '),
|
||||
sections,
|
||||
150
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.chart_.options.series = [];
|
||||
|
||||
beestat.component.card.aggregate_runtime.equipment_series.forEach(function(series_code) {
|
||||
if (series[series_code].enabled === true) {
|
||||
self.chart_.options.series.push({
|
||||
'data': series[series_code].chart_data,
|
||||
'yAxis': 0,
|
||||
'groupPadding': 0,
|
||||
'name': beestat.series[series_code].name,
|
||||
'type': 'column',
|
||||
'color': beestat.series[series_code].color
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.chart_.options.series.push({
|
||||
'name': beestat.series.average_outdoor_temperature.name,
|
||||
'data': series.average_outdoor_temperature.chart_data,
|
||||
'color': beestat.series.average_outdoor_temperature.color,
|
||||
'type': 'spline',
|
||||
'yAxis': 1,
|
||||
'dashStyle': 'ShortDash',
|
||||
'lineWidth': 1,
|
||||
'zones': beestat.component.chart.get_outdoor_temperature_zones()
|
||||
});
|
||||
|
||||
this.chart_.options.series.push({
|
||||
'name': beestat.series.min_max_outdoor_temperature.name,
|
||||
'data': series.min_max_outdoor_temperature.chart_data,
|
||||
'color': beestat.series.min_max_outdoor_temperature.color,
|
||||
'type': 'areasplinerange',
|
||||
'yAxis': 1,
|
||||
'fillOpacity': 0.2,
|
||||
'lineWidth': 0,
|
||||
'visible': false
|
||||
});
|
||||
|
||||
this.chart_.render(parent);
|
||||
|
||||
/*
|
||||
* If the data is available, then get the data if we don't already have it
|
||||
* loaded. If the data is not available, poll until it becomes available.
|
||||
*/
|
||||
if (this.data_available_() === true) {
|
||||
if (beestat.cache.aggregate_runtime.length === 0) {
|
||||
this.get_data_();
|
||||
} else {
|
||||
this.hide_loading_();
|
||||
}
|
||||
} else {
|
||||
var poll_interval = 10000;
|
||||
|
||||
beestat.add_poll_interval(poll_interval);
|
||||
beestat.dispatcher.addEventListener('poll.aggregate_runtime_load', function() {
|
||||
if (self.data_available_() === true) {
|
||||
beestat.remove_poll_interval(poll_interval);
|
||||
beestat.dispatcher.removeEventListener('poll.aggregate_runtime_load');
|
||||
self.get_data_();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all of the series data.
|
||||
*
|
||||
* @return {object} The series data.
|
||||
*/
|
||||
beestat.component.card.aggregate_runtime.prototype.get_series_ = function() {
|
||||
// TODO: Auto-generate these where possible like I did in recent_activity
|
||||
var series = {
|
||||
'x': {
|
||||
'enabled': true,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'average_outdoor_temperature': {
|
||||
'enabled': true,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'min_max_outdoor_temperature': {
|
||||
'enabled': true,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'compressor_heat_1': {
|
||||
'enabled': false,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'compressor_heat_2': {
|
||||
'enabled': false,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'compressor_cool_1': {
|
||||
'enabled': false,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'compressor_cool_2': {
|
||||
'enabled': false,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'auxiliary_heat_1': {
|
||||
'enabled': false,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'auxiliary_heat_2': {
|
||||
'enabled': false,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'auxiliary_heat_3': {
|
||||
'enabled': false,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
}
|
||||
};
|
||||
|
||||
beestat.cache.aggregate_runtime.forEach(function(aggregate, i) {
|
||||
|
||||
/*
|
||||
* Generate a custom x value that I can use to build the custom axis for
|
||||
* later. I thought about sending a timestamp back from the API instead of
|
||||
* these discrete values but it's not possible due to the grouping. I could
|
||||
* try to convert this to a timestamp or moment value but I'll just have to
|
||||
* break it back out anyways so there's not much point to that.
|
||||
*/
|
||||
var x_parts = [];
|
||||
[
|
||||
'hour',
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year'
|
||||
].forEach(function(period) {
|
||||
if (aggregate[period] !== undefined) {
|
||||
x_parts.push(period[0] + aggregate[period]);
|
||||
}
|
||||
});
|
||||
var x = x_parts.join('');
|
||||
|
||||
series.x.chart_data.push(x);
|
||||
|
||||
/*
|
||||
* Used to estimate values when data is missing. These magic numbers are the
|
||||
* number of expected data points in a group when that group represents a
|
||||
* year, month, etc.
|
||||
*/
|
||||
var adjustment_factor;
|
||||
switch (beestat.setting('aggregate_runtime_group_by')) {
|
||||
case 'year':
|
||||
var year = x_parts[0].match(/\d+/)[0];
|
||||
var is_leap_year = moment(year, 'YYYY').isLeapYear();
|
||||
var days_in_year = is_leap_year === true ? 366 : 365;
|
||||
adjustment_factor = days_in_year * 288;
|
||||
break;
|
||||
case 'month':
|
||||
var month = x_parts[0].match(/\d+/)[0];
|
||||
var year = x_parts[1].match(/\d+/)[0];
|
||||
var days_in_month = moment(year + '-' + month, 'YYYY-MM').daysInMonth();
|
||||
adjustment_factor = days_in_month * 288;
|
||||
break;
|
||||
case 'week':
|
||||
adjustment_factor = 2016;
|
||||
break;
|
||||
case 'day':
|
||||
adjustment_factor = 288;
|
||||
break;
|
||||
case 'hour':
|
||||
adjustment_factor = 12;
|
||||
break;
|
||||
default:
|
||||
console.error('Adjustment factor not available.');
|
||||
break;
|
||||
}
|
||||
|
||||
beestat.component.card.aggregate_runtime.equipment_series.forEach(function(series_code) {
|
||||
var value = aggregate[series_code];
|
||||
|
||||
// Account for missing data in all but the last x value.
|
||||
if (
|
||||
beestat.setting('aggregate_runtime_gap_fill') === true &&
|
||||
i < (beestat.cache.aggregate_runtime.length - 1)
|
||||
) {
|
||||
value = value *
|
||||
adjustment_factor /
|
||||
aggregate.count;
|
||||
}
|
||||
|
||||
// The value (in hours).
|
||||
value /= 3600;
|
||||
|
||||
// Enable the series if it has data.
|
||||
if (value > 0) {
|
||||
series[series_code].enabled = true;
|
||||
}
|
||||
|
||||
series[series_code].chart_data.push([
|
||||
x,
|
||||
value
|
||||
]);
|
||||
series[series_code].data[x] = value;
|
||||
});
|
||||
|
||||
// Average outdoor temperature.
|
||||
var average_outdoor_temperature_value = beestat.temperature({
|
||||
'temperature': aggregate.average_outdoor_temperature
|
||||
});
|
||||
|
||||
series.average_outdoor_temperature.data[x] = average_outdoor_temperature_value;
|
||||
series.average_outdoor_temperature.chart_data.push([
|
||||
x,
|
||||
average_outdoor_temperature_value
|
||||
]);
|
||||
|
||||
// Min/max outdoor temperature.
|
||||
var min_outdoor_temperature_value = beestat.temperature({
|
||||
'temperature': aggregate.min_outdoor_temperature
|
||||
});
|
||||
var max_outdoor_temperature_value = beestat.temperature({
|
||||
'temperature': aggregate.max_outdoor_temperature
|
||||
});
|
||||
|
||||
series.min_max_outdoor_temperature.data[x] = {
|
||||
'min': min_outdoor_temperature_value,
|
||||
'max': max_outdoor_temperature_value
|
||||
};
|
||||
series.min_max_outdoor_temperature.chart_data.push([
|
||||
x,
|
||||
min_outdoor_temperature_value,
|
||||
max_outdoor_temperature_value
|
||||
]);
|
||||
});
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorate the menu
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.card.aggregate_runtime.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 2 Months')
|
||||
.set_icon('calendar_range')
|
||||
.set_callback(function() {
|
||||
if (
|
||||
beestat.setting('aggregate_runtime_time_count') !== 2 ||
|
||||
beestat.setting('aggregate_runtime_time_period') !== 'month' ||
|
||||
beestat.setting('aggregate_runtime_group_by') !== 'day'
|
||||
) {
|
||||
beestat.setting({
|
||||
'aggregate_runtime_time_count': 2,
|
||||
'aggregate_runtime_time_period': 'month',
|
||||
'aggregate_runtime_group_by': 'day'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Past 12 Months')
|
||||
.set_icon('calendar_range')
|
||||
.set_callback(function() {
|
||||
if (
|
||||
beestat.setting('aggregate_runtime_time_count') !== 12 ||
|
||||
beestat.setting('aggregate_runtime_time_period') !== 'month' ||
|
||||
beestat.setting('aggregate_runtime_group_by') !== 'week'
|
||||
) {
|
||||
beestat.setting({
|
||||
'aggregate_runtime_time_count': 12,
|
||||
'aggregate_runtime_time_period': 'month',
|
||||
'aggregate_runtime_group_by': 'week'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('All Time')
|
||||
.set_icon('calendar_range')
|
||||
.set_callback(function() {
|
||||
if (
|
||||
beestat.setting('aggregate_runtime_time_count') !== 0 ||
|
||||
beestat.setting('aggregate_runtime_time_period') !== 'all' ||
|
||||
beestat.setting('aggregate_runtime_group_by') !== 'month'
|
||||
) {
|
||||
beestat.setting({
|
||||
'aggregate_runtime_time_count': 0,
|
||||
'aggregate_runtime_time_period': 'all',
|
||||
'aggregate_runtime_group_by': 'month'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Custom')
|
||||
.set_icon('calendar_edit')
|
||||
.set_callback(function() {
|
||||
(new beestat.component.modal.aggregate_runtime_custom()).render();
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Download Chart')
|
||||
.set_icon('download')
|
||||
.set_callback(function() {
|
||||
self.chart_.get_chart().exportChartLocal();
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Reset Zoom')
|
||||
.set_icon('magnify_minus')
|
||||
.set_callback(function() {
|
||||
self.chart_.get_chart().zoomOut();
|
||||
}));
|
||||
|
||||
if (beestat.setting('aggregate_runtime_gap_fill') === true) {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Disable Gap-Fill')
|
||||
.set_icon('basket_unfill')
|
||||
.set_callback(function() {
|
||||
beestat.setting('aggregate_runtime_gap_fill', false);
|
||||
}));
|
||||
} else {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Enable Gap-Fill')
|
||||
.set_icon('basket_fill')
|
||||
.set_callback(function() {
|
||||
beestat.setting('aggregate_runtime_gap_fill', true);
|
||||
}));
|
||||
}
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Help')
|
||||
.set_icon('help_circle')
|
||||
.set_callback(function() {
|
||||
(new beestat.component.modal.help_aggregate_runtime()).render();
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the title of the card.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
beestat.component.card.aggregate_runtime.prototype.get_title_ = function() {
|
||||
return 'Aggregate Runtime';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the subtitle of the card.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
beestat.component.card.aggregate_runtime.prototype.get_subtitle_ = function() {
|
||||
var s = (beestat.setting('aggregate_runtime_time_count') > 1) ? 's' : '';
|
||||
|
||||
var string = '';
|
||||
|
||||
if (beestat.setting('aggregate_runtime_time_period') === 'all') {
|
||||
string = 'All time';
|
||||
} else {
|
||||
string = 'Past ' +
|
||||
beestat.setting('aggregate_runtime_time_count') +
|
||||
' ' +
|
||||
beestat.setting('aggregate_runtime_time_period') +
|
||||
s;
|
||||
}
|
||||
|
||||
string += ', ' +
|
||||
' grouped by ' +
|
||||
beestat.setting('aggregate_runtime_group_by');
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is aggregate runtime data available?
|
||||
*
|
||||
* @return {boolean} Whether or not enough data is currently available to
|
||||
* render this card.
|
||||
*/
|
||||
beestat.component.card.aggregate_runtime.prototype.data_available_ = function() {
|
||||
// Demo can juse grab whatever data is there.
|
||||
if (window.is_demo === true) {
|
||||
this.show_loading_('Loading Aggregate Runtime');
|
||||
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);
|
||||
|
||||
var required_sync_begin;
|
||||
if (beestat.setting('aggregate_runtime_time_period') === 'all') {
|
||||
required_sync_begin = moment(thermostat.first_connected);
|
||||
} else {
|
||||
required_sync_begin = moment().subtract(moment.duration(
|
||||
beestat.setting('aggregate_runtime_time_count'),
|
||||
beestat.setting('aggregate_runtime_time_period')
|
||||
));
|
||||
}
|
||||
required_sync_begin = moment.max(
|
||||
required_sync_begin,
|
||||
moment(thermostat.first_connected)
|
||||
);
|
||||
var required_sync_end = moment().subtract(1, 'hour');
|
||||
|
||||
// Percentage
|
||||
var denominator = required_sync_end.diff(required_sync_begin, 'day');
|
||||
var numerator_begin = moment.max(current_sync_begin, required_sync_begin);
|
||||
var numerator_end = moment.min(current_sync_end, required_sync_end);
|
||||
var numerator = numerator_end.diff(numerator_begin, 'day');
|
||||
var percentage = numerator / denominator * 100;
|
||||
if (isNaN(percentage) === true || percentage < 0) {
|
||||
percentage = 0;
|
||||
}
|
||||
|
||||
if (percentage >= 95) {
|
||||
this.show_loading_('Loading Aggregate Runtime');
|
||||
} else {
|
||||
this.show_loading_('Syncing Data (' +
|
||||
Math.round(percentage) +
|
||||
'%)');
|
||||
}
|
||||
|
||||
return (
|
||||
current_sync_begin.isSameOrBefore(required_sync_begin) &&
|
||||
current_sync_end.isSameOrAfter(required_sync_end)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the data needed to render this card.
|
||||
*/
|
||||
beestat.component.card.aggregate_runtime.prototype.get_data_ = function() {
|
||||
var self = this;
|
||||
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
|
||||
|
||||
new beestat.api()
|
||||
.add_call(
|
||||
'ecobee_runtime_thermostat',
|
||||
'get_aggregate_runtime',
|
||||
{
|
||||
'ecobee_thermostat_id': thermostat.ecobee_thermostat_id,
|
||||
'time_period': beestat.setting('aggregate_runtime_time_period'),
|
||||
'group_by': beestat.setting('aggregate_runtime_group_by'),
|
||||
'time_count': beestat.setting('aggregate_runtime_time_count')
|
||||
}
|
||||
)
|
||||
.set_callback(function(response) {
|
||||
beestat.cache.set('aggregate_runtime', response);
|
||||
self.rerender();
|
||||
})
|
||||
.send();
|
||||
};
|
@ -4,8 +4,10 @@
|
||||
beestat.component.card.comparison_settings = function() {
|
||||
var self = this;
|
||||
|
||||
// If the thermostat_group changes that means the property_type could change
|
||||
// and thus need to rerender.
|
||||
/*
|
||||
* If the thermostat_group changes that means the property_type could change
|
||||
* and thus need to rerender.
|
||||
*/
|
||||
beestat.dispatcher.addEventListener('cache.thermostat_group', function() {
|
||||
self.rerender();
|
||||
});
|
||||
@ -31,17 +33,6 @@ beestat.component.card.comparison_settings.prototype.decorate_contents_ = functi
|
||||
row = $.createElement('div').addClass('row');
|
||||
parent.appendChild(row);
|
||||
|
||||
var column_date = $.createElement('div').addClass([
|
||||
'column',
|
||||
'column_12'
|
||||
]);
|
||||
row.appendChild(column_date);
|
||||
this.decorate_date_(column_date);
|
||||
|
||||
// Row
|
||||
row = $.createElement('div').addClass('row');
|
||||
parent.appendChild(row);
|
||||
|
||||
var column_region = $.createElement('div').addClass([
|
||||
'column',
|
||||
'column_4'
|
||||
@ -95,129 +86,6 @@ beestat.component.card.comparison_settings.prototype.decorate_contents_ = functi
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorate the date options.
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.card.comparison_settings.prototype.decorate_date_ = function(parent) {
|
||||
var self = this;
|
||||
|
||||
(new beestat.component.title('Date')).render(parent);
|
||||
|
||||
var periods = [
|
||||
{
|
||||
'value': 0,
|
||||
'text': 'Today'
|
||||
},
|
||||
{
|
||||
'value': 1,
|
||||
'text': '1 Month Ago'
|
||||
},
|
||||
{
|
||||
'value': 6,
|
||||
'text': '6 Months Ago'
|
||||
},
|
||||
{
|
||||
'value': 12,
|
||||
'text': '12 Months Ago'
|
||||
},
|
||||
{
|
||||
'value': 'custom',
|
||||
'text': 'Custom'
|
||||
}
|
||||
];
|
||||
|
||||
var current_comparison_period = beestat.setting('comparison_period');
|
||||
|
||||
var color = beestat.style.color.lightblue.base;
|
||||
|
||||
var input = new beestat.component.input.text()
|
||||
.set_style({
|
||||
'width': 110,
|
||||
'text-align': 'center',
|
||||
'border-bottom': '2px solid ' + color
|
||||
// 'background-color': color
|
||||
})
|
||||
.set_attribute({
|
||||
'maxlength': 10
|
||||
})
|
||||
.set_icon('calendar')
|
||||
.set_value(beestat.setting('comparison_period_custom'));
|
||||
|
||||
var button_group = new beestat.component.button_group();
|
||||
periods.forEach(function(period) {
|
||||
var button = new beestat.component.button()
|
||||
.set_background_hover_color(color)
|
||||
.set_text_color('#fff')
|
||||
.set_text(period.text);
|
||||
|
||||
if (current_comparison_period === period.value) {
|
||||
button.set_background_color(color);
|
||||
} else {
|
||||
button
|
||||
.set_background_color(beestat.style.color.bluegray.light)
|
||||
.addEventListener('click', function() {
|
||||
// Update the setting
|
||||
beestat.setting('comparison_period', period.value);
|
||||
|
||||
// Update the input to reflect the actual date.
|
||||
input.set_value(period.value);
|
||||
|
||||
// Rerender real quick to change the selected button
|
||||
self.rerender();
|
||||
|
||||
// Open up the loading window.
|
||||
if (period.value === 'custom') {
|
||||
self.show_loading_('Calculating Score for ' + beestat.setting('comparison_period_custom'));
|
||||
} else {
|
||||
self.show_loading_('Calculating Score for ' + period.text);
|
||||
}
|
||||
|
||||
if (period.value === 'custom') {
|
||||
self.focus_input_ = true;
|
||||
// self.rerender();
|
||||
|
||||
beestat.generate_temperature_profile(function() {
|
||||
// Rerender to get rid of the loader.
|
||||
self.rerender();
|
||||
});
|
||||
} else {
|
||||
beestat.generate_temperature_profile(function() {
|
||||
// Rerender to get rid of the loader.
|
||||
self.rerender();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
button_group.add_button(button);
|
||||
});
|
||||
button_group.render(parent);
|
||||
|
||||
if (current_comparison_period === 'custom') {
|
||||
var input_container = $.createElement('div')
|
||||
.style('margin-top', beestat.style.size.gutter);
|
||||
parent.appendChild(input_container);
|
||||
input.render(input_container);
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
var m = moment(input.get_value());
|
||||
if (m.isValid() === false) {
|
||||
beestat.error('That\'s not a valid date. Most any proper date format will work here.');
|
||||
} else if (m.isAfter()) {
|
||||
beestat.error('That\'s in the future. Fresh out of flux capacitors over here, sorry.');
|
||||
} else if (m.isSame(moment(beestat.setting('comparison_period_custom')), 'date') === false) {
|
||||
beestat.setting('comparison_period_custom', input.get_value());
|
||||
beestat.generate_temperature_profile(function() {
|
||||
// Rerender to get rid of the loader.
|
||||
self.rerender();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorate the region options.
|
||||
*
|
||||
@ -295,9 +163,9 @@ beestat.component.card.comparison_settings.prototype.decorate_property_ = functi
|
||||
if (thermostat_group.property_structure_type !== null) {
|
||||
property_types.push({
|
||||
'value': 'same_structure',
|
||||
'text': 'Type: '
|
||||
+ thermostat_group.property_structure_type.charAt(0).toUpperCase()
|
||||
+ thermostat_group.property_structure_type.slice(1)
|
||||
'text': 'Type: ' +
|
||||
thermostat_group.property_structure_type.charAt(0).toUpperCase() +
|
||||
thermostat_group.property_structure_type.slice(1)
|
||||
});
|
||||
}
|
||||
|
||||
@ -410,34 +278,15 @@ beestat.component.card.comparison_settings.prototype.decorate_top_right_ = funct
|
||||
* @return {boolean} Whether or not all of the data has been loaded.
|
||||
*/
|
||||
beestat.component.card.comparison_settings.prototype.data_available_ = function() {
|
||||
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
|
||||
var sync_progress = beestat.get_sync_progress(beestat.setting('thermostat_id'));
|
||||
|
||||
var current_sync_begin = moment.utc(thermostat.sync_begin);
|
||||
var current_sync_end = moment.utc(thermostat.sync_end);
|
||||
|
||||
var required_sync_begin = moment(thermostat.first_connected);
|
||||
var required_sync_end = moment().subtract(1, 'hour');
|
||||
|
||||
// Percentage
|
||||
var denominator = required_sync_end.diff(required_sync_begin, 'day');
|
||||
var numerator_begin = moment.max(current_sync_begin, required_sync_begin);
|
||||
var numerator_end = moment.min(current_sync_end, required_sync_end);
|
||||
var numerator = numerator_end.diff(numerator_begin, 'day');
|
||||
var percentage = numerator / denominator * 100;
|
||||
if (isNaN(percentage) === true || percentage < 0) {
|
||||
percentage = 0;
|
||||
}
|
||||
|
||||
if (percentage >= 95) {
|
||||
if (sync_progress >= 95) {
|
||||
this.show_loading_('Calculating Scores');
|
||||
} else {
|
||||
this.show_loading_('Syncing Data (' +
|
||||
Math.round(percentage) +
|
||||
Math.round(sync_progress) +
|
||||
'%)');
|
||||
}
|
||||
|
||||
return (
|
||||
current_sync_begin.isSameOrBefore(required_sync_begin) &&
|
||||
current_sync_end.isSameOrAfter(required_sync_end)
|
||||
);
|
||||
return sync_progress === 100;
|
||||
};
|
||||
|
209
js/component/card/recent_activity.js
Normal file → Executable file
209
js/component/card/recent_activity.js
Normal file → Executable file
@ -14,7 +14,6 @@ beestat.component.card.recent_activity.optional_series = [
|
||||
'compressor_cool_2',
|
||||
'auxiliary_heat_1',
|
||||
'auxiliary_heat_2',
|
||||
'auxiliary_heat_3',
|
||||
'fan',
|
||||
'dehumidifier',
|
||||
'economizer',
|
||||
@ -183,40 +182,40 @@ beestat.component.card.recent_activity.prototype.decorate_contents_ = function(p
|
||||
var sections = [];
|
||||
|
||||
// HVAC Mode
|
||||
var hvac_mode;
|
||||
var hvac_mode_color;
|
||||
var system_mode;
|
||||
var system_mode_color;
|
||||
|
||||
switch (series.hvac_mode.data[self.x]) {
|
||||
switch (series.system_mode.data[self.x]) {
|
||||
case 'auto':
|
||||
hvac_mode = 'Auto';
|
||||
hvac_mode_color = beestat.style.color.gray.base;
|
||||
system_mode = 'Auto';
|
||||
system_mode_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'heat':
|
||||
hvac_mode = 'Heat';
|
||||
hvac_mode_color = beestat.series.compressor_heat_1.color;
|
||||
system_mode = 'Heat';
|
||||
system_mode_color = beestat.series.compressor_heat_1.color;
|
||||
break;
|
||||
case 'cool':
|
||||
hvac_mode = 'Cool';
|
||||
hvac_mode_color = beestat.series.compressor_cool_1.color;
|
||||
system_mode = 'Cool';
|
||||
system_mode_color = beestat.series.compressor_cool_1.color;
|
||||
break;
|
||||
case 'off':
|
||||
hvac_mode = 'Off';
|
||||
hvac_mode_color = beestat.style.color.gray.base;
|
||||
system_mode = 'Off';
|
||||
system_mode_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'auxHeatOnly':
|
||||
hvac_mode = 'Aux';
|
||||
hvac_mode_color = beestat.series.auxiliary_heat_1.color;
|
||||
case 'auxiliary_heat':
|
||||
system_mode = 'Aux';
|
||||
system_mode_color = beestat.series.auxiliary_heat_1.color;
|
||||
break;
|
||||
}
|
||||
|
||||
var section_1 = [];
|
||||
sections.push(section_1);
|
||||
|
||||
if (hvac_mode !== undefined) {
|
||||
if (system_mode !== undefined) {
|
||||
section_1.push({
|
||||
'label': 'Mode',
|
||||
'value': hvac_mode,
|
||||
'color': hvac_mode_color
|
||||
'value': system_mode,
|
||||
'color': system_mode_color
|
||||
});
|
||||
}
|
||||
|
||||
@ -263,7 +262,7 @@ beestat.component.card.recent_activity.prototype.decorate_contents_ = function(p
|
||||
return;
|
||||
}
|
||||
|
||||
switch (series.hvac_mode.data[self.x]) {
|
||||
switch (series.system_mode.data[self.x]) {
|
||||
case 'heat':
|
||||
if (series.setpoint_heat.data[self.x] === null) {
|
||||
return;
|
||||
@ -451,8 +450,7 @@ beestat.component.card.recent_activity.prototype.decorate_contents_ = function(p
|
||||
|
||||
[
|
||||
'auxiliary_heat_1',
|
||||
'auxiliary_heat_2',
|
||||
'auxiliary_heat_3'
|
||||
'auxiliary_heat_2'
|
||||
].forEach(function(equipment) {
|
||||
if (series[equipment].enabled === true) {
|
||||
self.chart_.options.series.push({
|
||||
@ -585,7 +583,7 @@ beestat.component.card.recent_activity.prototype.decorate_contents_ = function(p
|
||||
this.chart_.options.series.push({
|
||||
'color': beestat.series.outdoor_temperature.color,
|
||||
'data': series.outdoor_temperature.chart_data,
|
||||
'zones': beestat.component.chart.get_outdoor_temperature_zones(),
|
||||
// 'zones': beestat.component.chart.get_outdoor_temperature_zones(),
|
||||
'yAxis': 0,
|
||||
'name': beestat.series.outdoor_temperature.name,
|
||||
'marker': {
|
||||
@ -638,7 +636,7 @@ beestat.component.card.recent_activity.prototype.decorate_contents_ = function(p
|
||||
* loaded. If the data is not available, poll until it becomes available.
|
||||
*/
|
||||
if (this.data_available_() === true) {
|
||||
if (beestat.cache.ecobee_runtime_thermostat.length === 0) {
|
||||
if (beestat.cache.runtime_thermostat.length === 0) {
|
||||
this.get_data_();
|
||||
} else {
|
||||
this.hide_loading_();
|
||||
@ -808,7 +806,7 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
},
|
||||
'hvac_mode': {
|
||||
'system_mode': {
|
||||
'enabled': true,
|
||||
'chart_data': [],
|
||||
'data': {}
|
||||
@ -857,12 +855,43 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
*/
|
||||
var durations = {};
|
||||
|
||||
beestat.cache.ecobee_runtime_thermostat.forEach(function(ecobee_runtime_thermostat, i) {
|
||||
if (ecobee_runtime_thermostat.ecobee_thermostat_id !== thermostat.ecobee_thermostat_id) {
|
||||
return;
|
||||
beestat.cache.runtime_thermostat.forEach(function(runtime_thermostat, i) {
|
||||
// if (runtime_thermostat.ecobee_thermostat_id !== thermostat.ecobee_thermostat_id) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
|
||||
if (runtime_thermostat.compressor_mode === 'heat') {
|
||||
runtime_thermostat.compressor_heat_1 = runtime_thermostat.compressor_1;
|
||||
runtime_thermostat.compressor_heat_2 = runtime_thermostat.compressor_2;
|
||||
runtime_thermostat.compressor_cool_1 = 0;
|
||||
runtime_thermostat.compressor_cool_2 = 0;
|
||||
} else if (runtime_thermostat.compressor_mode === 'cool') {
|
||||
runtime_thermostat.compressor_heat_1 = 0;
|
||||
runtime_thermostat.compressor_heat_2 = 0;
|
||||
runtime_thermostat.compressor_cool_1 = runtime_thermostat.compressor_1;
|
||||
runtime_thermostat.compressor_cool_2 = runtime_thermostat.compressor_2;
|
||||
} else if (runtime_thermostat.compressor_mode === 'off') {
|
||||
runtime_thermostat.compressor_heat_1 = 0;
|
||||
runtime_thermostat.compressor_heat_2 = 0;
|
||||
runtime_thermostat.compressor_cool_1 = 0;
|
||||
runtime_thermostat.compressor_cool_2 = 0;
|
||||
} else {
|
||||
runtime_thermostat.compressor_heat_1 = null;
|
||||
runtime_thermostat.compressor_heat_2 = null;
|
||||
runtime_thermostat.compressor_cool_1 = null;
|
||||
runtime_thermostat.compressor_cool_2 = null;
|
||||
}
|
||||
|
||||
var x = moment(ecobee_runtime_thermostat.timestamp).valueOf();
|
||||
runtime_thermostat.humidifier = 0;
|
||||
runtime_thermostat.dehumidifier = 0;
|
||||
runtime_thermostat.ventilator = 0;
|
||||
runtime_thermostat.economizer = 0;
|
||||
|
||||
// The string includes +00:00 as the UTC offset but moment knows what time
|
||||
// zone my PC is in...or at least it has a guess. This means that beestat
|
||||
// graphs can now show up in local time instead of thermostat time.
|
||||
var x = moment(runtime_thermostat.timestamp).valueOf();
|
||||
if (x < min_x) {
|
||||
return;
|
||||
}
|
||||
@ -870,9 +899,9 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
series.x.chart_data.push(x);
|
||||
|
||||
var original_durations = {};
|
||||
if (ecobee_runtime_thermostat.compressor_heat_2 > 0) {
|
||||
original_durations.compressor_heat_1 = ecobee_runtime_thermostat.compressor_heat_1;
|
||||
ecobee_runtime_thermostat.compressor_heat_1 = ecobee_runtime_thermostat.compressor_heat_2;
|
||||
if (runtime_thermostat.compressor_heat_2 > 0) {
|
||||
original_durations.compressor_heat_1 = runtime_thermostat.compressor_heat_1;
|
||||
runtime_thermostat.compressor_heat_1 = runtime_thermostat.compressor_heat_2;
|
||||
}
|
||||
// TODO DO THIS FOR AUX
|
||||
// TODO DO THIS FOR COOL
|
||||
@ -883,12 +912,12 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
}
|
||||
|
||||
// if (series_code === 'compressor_heat_1') {
|
||||
// ecobee_runtime_thermostat
|
||||
// runtime_thermostat
|
||||
// }
|
||||
|
||||
if (
|
||||
ecobee_runtime_thermostat[series_code] !== null &&
|
||||
ecobee_runtime_thermostat[series_code] > 0
|
||||
runtime_thermostat[series_code] !== null &&
|
||||
runtime_thermostat[series_code] > 0
|
||||
) {
|
||||
var value;
|
||||
switch (series_code) {
|
||||
@ -915,10 +944,10 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
|
||||
var duration = original_durations[series_code] !== undefined
|
||||
? original_durations[series_code]
|
||||
: ecobee_runtime_thermostat[series_code];
|
||||
: runtime_thermostat[series_code];
|
||||
|
||||
durations[series_code][durations[series_code].length - 1].seconds += duration;
|
||||
// durations[series_code][durations[series_code].length - 1].seconds += ecobee_runtime_thermostat[series_code];
|
||||
// durations[series_code][durations[series_code].length - 1].seconds += runtime_thermostat[series_code];
|
||||
series[series_code].durations[x] = durations[series_code][durations[series_code].length - 1];
|
||||
} else {
|
||||
series[series_code].chart_data.push([
|
||||
@ -1002,7 +1031,7 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
* Display a fixed schedule in demo mode.
|
||||
*/
|
||||
if (window.is_demo === true) {
|
||||
var m = moment(ecobee_runtime_thermostat.timestamp);
|
||||
var m = moment(runtime_thermostat.timestamp);
|
||||
|
||||
// Moment and ecobee use different indexes for the days of the week
|
||||
var day_of_week_index = (m.day() + 6) % 7;
|
||||
@ -1018,33 +1047,33 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
|
||||
this_calendar_event = 'calendar_event_' + ecobee_thermostat.json_program.schedule[day_of_week_index][chunk_of_day_index];
|
||||
} else {
|
||||
if (ecobee_runtime_thermostat.zone_calendar_event === null) {
|
||||
if (ecobee_runtime_thermostat.zone_climate === null) {
|
||||
if (runtime_thermostat.event === null) {
|
||||
if (runtime_thermostat.climate === null) {
|
||||
this_calendar_event = 'calendar_event_other';
|
||||
} else {
|
||||
this_calendar_event = 'calendar_event_' + ecobee_runtime_thermostat.zone_climate.toLowerCase();
|
||||
this_calendar_event = 'calendar_event_' + runtime_thermostat.climate.toLowerCase();
|
||||
}
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/SmartRecovery/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/SmartRecovery/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_smartrecovery';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^home$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^home$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_home';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^away$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^away$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_away';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^smarthome$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^smarthome$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_smarthome';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^smartaway$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^smartaway$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_smartaway';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^auto$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^auto$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_hold';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^today$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^today$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_hold';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^hold$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^hold$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_hold';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^vacation$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^vacation$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_vacation';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/(\S\S\S\s\d+\s\d\d\d\d)|(\d{12})/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/(\S\S\S\s\d+\s\d\d\d\d)|(\d{12})/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_vacation';
|
||||
} else if (ecobee_runtime_thermostat.zone_calendar_event.match(/^quicksave$/i) !== null) {
|
||||
} else if (runtime_thermostat.event.match(/^quicksave$/i) !== null) {
|
||||
this_calendar_event = 'calendar_event_quicksave';
|
||||
} else {
|
||||
this_calendar_event = 'calendar_event_other';
|
||||
@ -1066,7 +1095,7 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
};
|
||||
|
||||
beestat.series[this_calendar_event] = {
|
||||
'name': ecobee_runtime_thermostat.zone_climate,
|
||||
'name': runtime_thermostat.climate,
|
||||
'color': beestat.style.color.bluegreen.base
|
||||
};
|
||||
}
|
||||
@ -1091,13 +1120,13 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
|
||||
/*
|
||||
* HVAC Mode. This isn't graphed but it's available for the tooltip.
|
||||
* series.hvac_mode.chart_data.push([x, ecobee_runtime_thermostat.hvac_mode]);
|
||||
* series.system_mode.chart_data.push([x, runtime_thermostat.system_mode]);
|
||||
*/
|
||||
series.hvac_mode.data[x] = ecobee_runtime_thermostat.hvac_mode;
|
||||
series.system_mode.data[x] = runtime_thermostat.system_mode;
|
||||
|
||||
// Setpoints
|
||||
var setpoint_value_heat = beestat.temperature({'temperature': ecobee_runtime_thermostat.zone_heat_temperature});
|
||||
var setpoint_value_cool = beestat.temperature({'temperature': ecobee_runtime_thermostat.zone_cool_temperature});
|
||||
var setpoint_value_heat = beestat.temperature({'temperature': runtime_thermostat.setpoint_heat});
|
||||
var setpoint_value_cool = beestat.temperature({'temperature': runtime_thermostat.setpoint_cool});
|
||||
|
||||
// NOTE: At one point I was also factoring in your heat/cool differential
|
||||
// plus the extra degree offset ecobee adds when you are "away". That made
|
||||
@ -1105,10 +1134,10 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
// be confusing.
|
||||
|
||||
if (
|
||||
ecobee_runtime_thermostat.hvac_mode === 'auto' ||
|
||||
ecobee_runtime_thermostat.hvac_mode === 'heat' ||
|
||||
ecobee_runtime_thermostat.hvac_mode === 'auxHeatOnly' ||
|
||||
ecobee_runtime_thermostat.hvac_mode === null // Need this for the explicit null to remove from the graph.
|
||||
runtime_thermostat.system_mode === 'auto' ||
|
||||
runtime_thermostat.system_mode === 'heat' ||
|
||||
runtime_thermostat.system_mode === 'auxiliary_heat' ||
|
||||
runtime_thermostat.system_mode === null // Need this for the explicit null to remove from the graph.
|
||||
) {
|
||||
series.setpoint_heat.data[x] = setpoint_value_heat;
|
||||
series.setpoint_heat.chart_data.push([
|
||||
@ -1134,9 +1163,9 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
}
|
||||
|
||||
if (
|
||||
ecobee_runtime_thermostat.hvac_mode === 'auto' ||
|
||||
ecobee_runtime_thermostat.hvac_mode === 'cool' ||
|
||||
ecobee_runtime_thermostat.hvac_mode === null // Need this for the explicit null to remove from the graph.
|
||||
runtime_thermostat.system_mode === 'auto' ||
|
||||
runtime_thermostat.system_mode === 'cool' ||
|
||||
runtime_thermostat.system_mode === null // Need this for the explicit null to remove from the graph.
|
||||
) {
|
||||
series.setpoint_cool.data[x] = setpoint_value_cool;
|
||||
series.setpoint_cool.chart_data.push([
|
||||
@ -1162,7 +1191,7 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
}
|
||||
|
||||
// Indoor temperature
|
||||
var indoor_temperature_value = beestat.temperature(ecobee_runtime_thermostat.zone_average_temperature);
|
||||
var indoor_temperature_value = beestat.temperature(runtime_thermostat.indoor_temperature);
|
||||
series.indoor_temperature.data[x] = indoor_temperature_value;
|
||||
|
||||
/*
|
||||
@ -1181,10 +1210,10 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
) ||
|
||||
indoor_temperature_value === null ||
|
||||
(
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1].zone_average_temperature === null
|
||||
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.runtime_thermostat[i + 1].indoor_temperature === null
|
||||
) ||
|
||||
i === (beestat.cache.ecobee_runtime_thermostat.length - 1)
|
||||
i === (beestat.cache.runtime_thermostat.length - 1)
|
||||
) {
|
||||
series.indoor_temperature.enabled = true;
|
||||
series.indoor_temperature.chart_data.push([
|
||||
@ -1199,7 +1228,7 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
}
|
||||
|
||||
// Outdoor temperature
|
||||
var outdoor_temperature_value = beestat.temperature(ecobee_runtime_thermostat.outdoor_temperature);
|
||||
var outdoor_temperature_value = beestat.temperature(runtime_thermostat.outdoor_temperature);
|
||||
series.outdoor_temperature.data[x] = outdoor_temperature_value;
|
||||
|
||||
/*
|
||||
@ -1218,10 +1247,10 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
) ||
|
||||
outdoor_temperature_value === null ||
|
||||
(
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1].outdoor_temperature === null
|
||||
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.runtime_thermostat[i + 1].outdoor_temperature === null
|
||||
) ||
|
||||
i === (beestat.cache.ecobee_runtime_thermostat.length - 1)
|
||||
i === (beestat.cache.runtime_thermostat.length - 1)
|
||||
) {
|
||||
series.outdoor_temperature.enabled = true;
|
||||
series.outdoor_temperature.chart_data.push([
|
||||
@ -1237,9 +1266,9 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
|
||||
// Indoor humidity
|
||||
var indoor_humidity_value;
|
||||
if (ecobee_runtime_thermostat.zone_humidity !== null) {
|
||||
if (runtime_thermostat.indoor_humidity !== null) {
|
||||
indoor_humidity_value = parseInt(
|
||||
ecobee_runtime_thermostat.zone_humidity,
|
||||
runtime_thermostat.indoor_humidity,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
@ -1263,10 +1292,10 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
) ||
|
||||
indoor_humidity_value === null ||
|
||||
(
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1].zone_humidity === null
|
||||
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.runtime_thermostat[i + 1].indoor_humidity === null
|
||||
) ||
|
||||
i === (beestat.cache.ecobee_runtime_thermostat.length - 1)
|
||||
i === (beestat.cache.runtime_thermostat.length - 1)
|
||||
) {
|
||||
series.indoor_humidity.enabled = true;
|
||||
series.indoor_humidity.chart_data.push([
|
||||
@ -1277,9 +1306,9 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
|
||||
// Outdoor humidity
|
||||
var outdoor_humidity_value;
|
||||
if (ecobee_runtime_thermostat.outdoor_humidity !== null) {
|
||||
if (runtime_thermostat.outdoor_humidity !== null) {
|
||||
outdoor_humidity_value = parseInt(
|
||||
ecobee_runtime_thermostat.outdoor_humidity,
|
||||
runtime_thermostat.outdoor_humidity,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
@ -1303,10 +1332,10 @@ beestat.component.card.recent_activity.prototype.get_series_ = function() {
|
||||
) ||
|
||||
outdoor_humidity_value === null ||
|
||||
(
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.ecobee_runtime_thermostat[i + 1].outdoor_humidity === null
|
||||
beestat.cache.runtime_thermostat[i + 1] !== undefined &&
|
||||
beestat.cache.runtime_thermostat[i + 1].outdoor_humidity === null
|
||||
) ||
|
||||
i === (beestat.cache.ecobee_runtime_thermostat.length - 1)
|
||||
i === (beestat.cache.runtime_thermostat.length - 1)
|
||||
) {
|
||||
series.outdoor_humidity.enabled = true;
|
||||
series.outdoor_humidity.chart_data.push([
|
||||
@ -1387,16 +1416,22 @@ beestat.component.card.recent_activity.prototype.get_data_ = function() {
|
||||
|
||||
new beestat.api()
|
||||
.add_call(
|
||||
'ecobee_runtime_thermostat',
|
||||
'get_recent_activity',
|
||||
'runtime_thermostat',
|
||||
'read',
|
||||
{
|
||||
'ecobee_thermostat_id': thermostat.ecobee_thermostat_id,
|
||||
'begin': null,
|
||||
'end': null
|
||||
'attributes': {
|
||||
'thermostat_id': thermostat.thermostat_id,
|
||||
'timestamp': {
|
||||
'value': moment()
|
||||
.subtract(7, 'd')
|
||||
.format('YYYY-MM-DD'),
|
||||
'operator': '>'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.set_callback(function(response) {
|
||||
beestat.cache.set('ecobee_runtime_thermostat', response);
|
||||
beestat.cache.set('runtime_thermostat', response);
|
||||
self.rerender();
|
||||
})
|
||||
.send();
|
||||
|
592
js/component/card/runtime_thermostat_summary.js
Executable file
592
js/component/card/runtime_thermostat_summary.js
Executable file
@ -0,0 +1,592 @@
|
||||
/**
|
||||
* Runtime summary card. Compare to the ecobee weather impact chart.
|
||||
*
|
||||
* @param {number} thermostat_id The thermostat_id this card is displaying
|
||||
* data for.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary = function(thermostat_id) {
|
||||
var self = this;
|
||||
|
||||
this.thermostat_id_ = thermostat_id;
|
||||
|
||||
/*
|
||||
* Initialize a variable to store when the card was first loaded to guess how
|
||||
* long the sync will take to complete.
|
||||
*/
|
||||
this.sync_begin_m_ = moment();
|
||||
this.sync_begin_progress_ = beestat.get_sync_progress(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_thermostat_summary_time_count',
|
||||
'setting.runtime_thermostat_summary_time_period',
|
||||
'setting.runtime_thermostat_summary_group_by',
|
||||
'setting.runtime_thermostat_summary_gap_fill',
|
||||
'cache.runtime_thermostat_summary'
|
||||
],
|
||||
change_function
|
||||
);
|
||||
|
||||
beestat.component.card.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.card.runtime_thermostat_summary, beestat.component.card);
|
||||
|
||||
/**
|
||||
* Decorate.
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.decorate_contents_ = function(parent) {
|
||||
var data = this.get_data_();
|
||||
this.chart_ = new beestat.component.chart2.runtime_thermostat_summary(
|
||||
this.thermostat_id_,
|
||||
data
|
||||
);
|
||||
this.chart_.render(parent);
|
||||
|
||||
var sync_progress = beestat.get_sync_progress(this.thermostat_id_);
|
||||
|
||||
if (sync_progress < 100) {
|
||||
var time_taken = moment.duration(moment().diff(this.sync_begin_m_));
|
||||
var percent_taken = sync_progress - this.sync_begin_progress_;
|
||||
var percent_per_second = percent_taken / time_taken.asSeconds();
|
||||
|
||||
var time_remain = (100 - sync_progress) / percent_per_second;
|
||||
|
||||
var string_remain;
|
||||
if (time_remain === Infinity) {
|
||||
string_remain = 'A few minutes';
|
||||
} else {
|
||||
if (time_remain > 59) {
|
||||
string_remain = Math.round(time_remain / 60) + 'm ';
|
||||
} else {
|
||||
string_remain = Math.round(time_remain) + 's';
|
||||
}
|
||||
}
|
||||
|
||||
this.show_loading_('Syncing Thermostat Summary (' + sync_progress + '%)<br/>' + string_remain + ' remaining');
|
||||
setTimeout(function() {
|
||||
var api = new beestat.api();
|
||||
api.add_call(
|
||||
'runtime_thermostat_summary',
|
||||
'read_id',
|
||||
{},
|
||||
'runtime_thermostat_summary'
|
||||
);
|
||||
|
||||
api.add_call(
|
||||
'thermostat',
|
||||
'read_id',
|
||||
{
|
||||
'attributes': {
|
||||
'inactive': 0
|
||||
}
|
||||
},
|
||||
'thermostat'
|
||||
);
|
||||
|
||||
api.set_callback(function(response) {
|
||||
beestat.cache.set('thermostat', response.thermostat);
|
||||
beestat.cache.set('runtime_thermostat_summary', response.runtime_thermostat_summary);
|
||||
});
|
||||
|
||||
api.send();
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all of the series data.
|
||||
*
|
||||
* @return {object} The series data.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.get_data_ = function() {
|
||||
var data = {
|
||||
'x': [],
|
||||
'series': {},
|
||||
'metadata': {
|
||||
'series': {}
|
||||
}
|
||||
};
|
||||
|
||||
[
|
||||
'sum_compressor_cool_1',
|
||||
'sum_compressor_cool_2',
|
||||
'sum_compressor_heat_1',
|
||||
'sum_compressor_heat_2',
|
||||
'sum_auxiliary_heat_1',
|
||||
'sum_auxiliary_heat_2',
|
||||
'sum_fan',
|
||||
'sum_humidifier',
|
||||
'sum_dehumidifier',
|
||||
'sum_ventilator',
|
||||
'sum_economizer',
|
||||
'avg_outdoor_temperature',
|
||||
'avg_outdoor_humidity',
|
||||
'min_outdoor_temperature',
|
||||
'max_outdoor_temperature',
|
||||
'extreme_outdoor_temperature',
|
||||
'avg_indoor_temperature',
|
||||
'avg_indoor_humidity'
|
||||
].forEach(function(series_code) {
|
||||
data.series[series_code] = [];
|
||||
data.metadata.series[series_code] = {
|
||||
'active': false
|
||||
};
|
||||
});
|
||||
|
||||
var buckets = this.get_buckets_();
|
||||
|
||||
if (buckets === null) {
|
||||
return data;
|
||||
}
|
||||
|
||||
var begin_m;
|
||||
if (beestat.setting('runtime_thermostat_summary_time_period') === 'all') {
|
||||
begin_m = moment(
|
||||
beestat.cache.thermostat[this.thermostat_id_].first_connected
|
||||
);
|
||||
} else {
|
||||
var time_periods = [
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year'
|
||||
];
|
||||
|
||||
/**
|
||||
* See #145. This makes the date range more intuitive when the group by
|
||||
* duration is less than the time period you select.
|
||||
*/
|
||||
var subtract;
|
||||
if (
|
||||
time_periods.indexOf(beestat.setting('runtime_thermostat_summary_group_by')) <
|
||||
time_periods.indexOf(beestat.setting('runtime_thermostat_summary_time_period'))
|
||||
) {
|
||||
subtract = 0;
|
||||
} else {
|
||||
subtract = 1;
|
||||
}
|
||||
|
||||
begin_m = moment()
|
||||
.subtract(
|
||||
(beestat.setting('runtime_thermostat_summary_time_count') - subtract),
|
||||
beestat.setting('runtime_thermostat_summary_time_period')
|
||||
)
|
||||
.startOf(
|
||||
beestat.setting('runtime_thermostat_summary_group_by') === 'week'
|
||||
? 'isoweek'
|
||||
: beestat.setting('runtime_thermostat_summary_group_by')
|
||||
);
|
||||
}
|
||||
var end_m = moment();
|
||||
|
||||
var current_m = begin_m;
|
||||
while (current_m.isSameOrAfter(end_m) === false) {
|
||||
var next_m = current_m
|
||||
.clone()
|
||||
.add(1, beestat.setting('runtime_thermostat_summary_group_by'));
|
||||
|
||||
var bucket_key = this.get_bucket_key_(
|
||||
current_m,
|
||||
beestat.setting('runtime_thermostat_summary_group_by')
|
||||
);
|
||||
|
||||
var bucket = buckets[bucket_key];
|
||||
|
||||
if (bucket !== undefined) {
|
||||
data.x.push(current_m.clone());
|
||||
|
||||
for (var key in data.series) {
|
||||
if (key === 'extreme_outdoor_temperature') {
|
||||
// Outdoor temperature extremes
|
||||
if (
|
||||
bucket !== undefined &&
|
||||
bucket.min_outdoor_temperature !== null &&
|
||||
bucket.max_outdoor_temperature !== null
|
||||
) {
|
||||
data.series.extreme_outdoor_temperature.push([
|
||||
current_m.clone(),
|
||||
bucket.min_outdoor_temperature,
|
||||
bucket.max_outdoor_temperature
|
||||
]);
|
||||
data.metadata.series[key].active = true;
|
||||
} else {
|
||||
data.series.extreme_outdoor_temperature.push(null);
|
||||
}
|
||||
} else {
|
||||
var value = bucket !== undefined ? bucket[key] : null;
|
||||
|
||||
/*
|
||||
* If Gap-fill is on, and it's a Gap-fillable value, and it's not the
|
||||
* last bucket, gap-fill it.
|
||||
*/
|
||||
if (
|
||||
beestat.setting('runtime_thermostat_summary_gap_fill') === true &&
|
||||
key.substring(0, 3) === 'sum' &&
|
||||
next_m.isSameOrAfter(end_m) === false
|
||||
) {
|
||||
value = this.gap_fill_(
|
||||
value,
|
||||
bucket.count,
|
||||
beestat.setting('runtime_thermostat_summary_group_by'),
|
||||
bucket_key
|
||||
);
|
||||
}
|
||||
|
||||
data.series[key].push(value);
|
||||
|
||||
data.metadata.series[key].active = data.metadata.series[key].active || (value > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current_m.add(1, beestat.setting('runtime_thermostat_summary_group_by'));
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Just calls a couple of helper functions to get the buckets.
|
||||
*
|
||||
* @return {object} The buckets.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.get_buckets_ = function() {
|
||||
if (beestat.cache.runtime_thermostat_summary.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.get_buckets_combined_(this.get_buckets_group_());
|
||||
};
|
||||
|
||||
/**
|
||||
* Combine all the runtime_thermostat_summary rows into one row per
|
||||
* day/week/month/year. Each bucket key has an array of values, not a sum,
|
||||
* average, etc.
|
||||
*
|
||||
* @return {object} The buckets.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.get_buckets_group_ = function() {
|
||||
var buckets = {};
|
||||
|
||||
for (var runtime_thermostat_summary_id in beestat.cache.runtime_thermostat_summary) {
|
||||
var runtime_thermostat_summary = beestat.cache.runtime_thermostat_summary[
|
||||
runtime_thermostat_summary_id
|
||||
];
|
||||
if (runtime_thermostat_summary.thermostat_id === this.thermostat_id_) {
|
||||
var bucket_key = this.get_bucket_key_(
|
||||
moment.utc(runtime_thermostat_summary.date),
|
||||
beestat.setting('runtime_thermostat_summary_group_by')
|
||||
);
|
||||
|
||||
if (buckets[bucket_key] === undefined) {
|
||||
buckets[bucket_key] = {
|
||||
'count': [],
|
||||
'sum_compressor_cool_1': [],
|
||||
'sum_compressor_cool_2': [],
|
||||
'sum_compressor_heat_1': [],
|
||||
'sum_compressor_heat_2': [],
|
||||
'sum_auxiliary_heat_1': [],
|
||||
'sum_auxiliary_heat_2': [],
|
||||
'sum_fan': [],
|
||||
'sum_humidifier': [],
|
||||
'sum_dehumidifier': [],
|
||||
'sum_ventilator': [],
|
||||
'sum_economizer': [],
|
||||
'avg_outdoor_temperature': [],
|
||||
'avg_outdoor_humidity': [],
|
||||
'min_outdoor_temperature': [],
|
||||
'max_outdoor_temperature': [],
|
||||
'avg_indoor_temperature': [],
|
||||
'avg_indoor_humidity': []
|
||||
};
|
||||
}
|
||||
|
||||
for (var key in buckets[bucket_key]) {
|
||||
buckets[bucket_key][key].push(runtime_thermostat_summary[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buckets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the key for a bucket from a date and a grouping.
|
||||
*
|
||||
* @param {moment} date_m
|
||||
* @param {string} group_by day|week|month|year
|
||||
*
|
||||
* @return {string} The bucket key.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.get_bucket_key_ = function(date_m, group_by) {
|
||||
var bucket_key;
|
||||
|
||||
switch (group_by) {
|
||||
case 'day':
|
||||
bucket_key = date_m.format('YYYY-DDDD');
|
||||
break;
|
||||
case 'week':
|
||||
bucket_key = date_m.format('YYYY-WW');
|
||||
break;
|
||||
case 'month':
|
||||
bucket_key = date_m.format('YYYY-MM');
|
||||
break;
|
||||
case 'year':
|
||||
bucket_key = date_m.format('YYYY');
|
||||
break;
|
||||
}
|
||||
|
||||
return bucket_key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combine the individual array values in each bucket key by getting the sum,
|
||||
* average, min, max, etc.
|
||||
*
|
||||
* @param {object} buckets The buckets.
|
||||
*
|
||||
* @return {object} The combined buckets.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.get_buckets_combined_ = function(buckets) {
|
||||
for (var bucket_key in buckets) {
|
||||
var bucket = buckets[bucket_key];
|
||||
|
||||
bucket.count = bucket.count.reduce(function(accumulator, current_value) {
|
||||
return accumulator + current_value;
|
||||
}, 0);
|
||||
|
||||
for (var key in buckets[bucket_key]) {
|
||||
switch (key.substring(0, 3)) {
|
||||
case 'avg':
|
||||
var sum = bucket[key].reduce(function(accumulator, current_value) {
|
||||
return accumulator + current_value;
|
||||
}, 0);
|
||||
|
||||
bucket[key] = sum / bucket[key].length;
|
||||
|
||||
if (key.substring(key.length - 11) === 'temperature') {
|
||||
bucket[key] = Math.round(bucket[key] * 10) / 10;
|
||||
} else {
|
||||
bucket[key] = Math.round(bucket[key]);
|
||||
}
|
||||
break;
|
||||
case 'min':
|
||||
bucket[key] = Math.min.apply(null, bucket[key]);
|
||||
break;
|
||||
case 'max':
|
||||
bucket[key] = Math.max.apply(null, bucket[key]);
|
||||
break;
|
||||
case 'sum':
|
||||
bucket[key] = bucket[key].reduce(function(accumulator, current_value) {
|
||||
return accumulator + current_value;
|
||||
}, 0);
|
||||
|
||||
/*
|
||||
* This is a really good spot for Gap-fill to happen but it doesn't work
|
||||
* here because there's no order to the buckets so I can't ignore the
|
||||
* last bucket.
|
||||
*/
|
||||
|
||||
// Convert seconds to hours.
|
||||
bucket[key] /= 3600;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buckets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to account for missing data based on how much is missing from the series.
|
||||
*
|
||||
* @param {number} value The sum to gap-fill.
|
||||
* @param {number} count The number of values in the sum.
|
||||
* @param {string} group_by How the data is grouped.
|
||||
* @param {string} bucket_key Which group this is in.
|
||||
*
|
||||
* @return {number} The gap-filled sum.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.gap_fill_ = function(value, count, group_by, bucket_key) {
|
||||
var adjustment_factor;
|
||||
var year;
|
||||
var month;
|
||||
switch (group_by) {
|
||||
case 'year':
|
||||
year = bucket_key;
|
||||
var is_leap_year = moment(year, 'YYYY').isLeapYear();
|
||||
var days_in_year = is_leap_year === true ? 366 : 365;
|
||||
adjustment_factor = days_in_year * 288;
|
||||
break;
|
||||
case 'month':
|
||||
year = bucket_key.substring(0, 4);
|
||||
month = bucket_key.substring(5, 7);
|
||||
var days_in_month = moment(year + '-' + month, 'YYYY-MM').daysInMonth();
|
||||
adjustment_factor = days_in_month * 288;
|
||||
break;
|
||||
case 'week':
|
||||
adjustment_factor = 2016;
|
||||
break;
|
||||
case 'day':
|
||||
adjustment_factor = 288;
|
||||
break;
|
||||
}
|
||||
|
||||
return value * adjustment_factor / count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the title of the card.
|
||||
*
|
||||
* @return {string} The title.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.get_title_ = function() {
|
||||
return 'Runtime Summary';
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorate the menu
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.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 3 Months')
|
||||
.set_icon('calendar_range')
|
||||
.set_callback(function() {
|
||||
if (
|
||||
beestat.setting('runtime_thermostat_summary_time_count') !== 3 ||
|
||||
beestat.setting('runtime_thermostat_summary_time_period') !== 'month' ||
|
||||
beestat.setting('runtime_thermostat_summary_group_by') !== 'day'
|
||||
) {
|
||||
beestat.setting({
|
||||
'runtime_thermostat_summary_time_count': 3,
|
||||
'runtime_thermostat_summary_time_period': 'month',
|
||||
'runtime_thermostat_summary_group_by': 'day'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Past 12 Months')
|
||||
.set_icon('calendar_range')
|
||||
.set_callback(function() {
|
||||
if (
|
||||
beestat.setting('runtime_thermostat_summary_time_count') !== 12 ||
|
||||
beestat.setting('runtime_thermostat_summary_time_period') !== 'month' ||
|
||||
beestat.setting('runtime_thermostat_summary_group_by') !== 'week'
|
||||
) {
|
||||
beestat.setting({
|
||||
'runtime_thermostat_summary_time_count': 12,
|
||||
'runtime_thermostat_summary_time_period': 'month',
|
||||
'runtime_thermostat_summary_group_by': 'week'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('All Time')
|
||||
.set_icon('calendar_range')
|
||||
.set_callback(function() {
|
||||
if (
|
||||
beestat.setting('runtime_thermostat_summary_time_count') !== 0 ||
|
||||
beestat.setting('runtime_thermostat_summary_time_period') !== 'all' ||
|
||||
beestat.setting('runtime_thermostat_summary_group_by') !== 'month'
|
||||
) {
|
||||
beestat.setting({
|
||||
'runtime_thermostat_summary_time_count': 0,
|
||||
'runtime_thermostat_summary_time_period': 'all',
|
||||
'runtime_thermostat_summary_group_by': 'month'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Custom')
|
||||
.set_icon('calendar_edit')
|
||||
.set_callback(function() {
|
||||
(new beestat.component.modal.runtime_thermostat_summary_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_thermostat_summary_gap_fill') === true) {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Disable Gap-Fill')
|
||||
.set_icon('basket_unfill')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_thermostat_summary_gap_fill', false);
|
||||
}));
|
||||
} else {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Enable Gap-Fill')
|
||||
.set_icon('basket_fill')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_thermostat_summary_gap_fill', true);
|
||||
}));
|
||||
}
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Help')
|
||||
.set_icon('help_circle')
|
||||
.set_callback(function() {
|
||||
(new beestat.component.modal.help_runtime_thermostat_summary()).render();
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the subtitle of the card.
|
||||
*
|
||||
* @return {string} The subtitle.
|
||||
*/
|
||||
beestat.component.card.runtime_thermostat_summary.prototype.get_subtitle_ = function() {
|
||||
var s = (beestat.setting('runtime_thermostat_summary_time_count') > 1) ? 's' : '';
|
||||
|
||||
var string = '';
|
||||
|
||||
if (beestat.setting('runtime_thermostat_summary_time_period') === 'all') {
|
||||
string = 'All time';
|
||||
} else {
|
||||
string = 'Past ' +
|
||||
beestat.setting('runtime_thermostat_summary_time_count') +
|
||||
' ' +
|
||||
beestat.setting('runtime_thermostat_summary_time_period') +
|
||||
s;
|
||||
}
|
||||
|
||||
string += ', ' +
|
||||
' grouped by ' +
|
||||
beestat.setting('runtime_thermostat_summary_group_by');
|
||||
|
||||
return string;
|
||||
};
|
@ -53,9 +53,9 @@ beestat.component.card.temperature_profiles.prototype.decorate_contents_ = funct
|
||||
// Convert the data to Celsius if necessary
|
||||
var deltas_converted = {};
|
||||
for (var key in profile.deltas) {
|
||||
deltas_converted[beestat.temperature({'temperature': key})] =
|
||||
deltas_converted[beestat.temperature({'temperature': (key / 10)})] =
|
||||
beestat.temperature({
|
||||
'temperature': profile.deltas[key],
|
||||
'temperature': (profile.deltas[key] / 10),
|
||||
'delta': true,
|
||||
'round': 3
|
||||
});
|
||||
@ -150,7 +150,16 @@ beestat.component.card.temperature_profiles.prototype.decorate_contents_ = funct
|
||||
'formatter': function() {
|
||||
return this.value + thermostat.temperature_unit;
|
||||
}
|
||||
}
|
||||
},
|
||||
'plotLines': [
|
||||
{
|
||||
'color': beestat.series.outdoor_temperature.color,
|
||||
'dashStyle': 'ShortDash',
|
||||
'width': 1,
|
||||
'value': beestat.temperature(thermostat.weather.temperature),
|
||||
'zIndex': 2
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.chart_.options.yAxis = [
|
||||
|
311
js/component/chart/runtime_thermostat_summary.js
Executable file
311
js/component/chart/runtime_thermostat_summary.js
Executable file
@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Runtime thermostat summary chart.
|
||||
*
|
||||
* @param {number} thermostat_id The thermostat_id this chart is showing data
|
||||
* for.
|
||||
* @param {object} data The chart data.
|
||||
*/
|
||||
beestat.component.chart2.runtime_thermostat_summary = function(thermostat_id, data) {
|
||||
beestat.component.chart2.apply(this, arguments);
|
||||
this.thermostat_id_ = thermostat_id;
|
||||
this.data_ = data;
|
||||
};
|
||||
beestat.extend(beestat.component.chart2.runtime_thermostat_summary, beestat.component.chart2);
|
||||
|
||||
/**
|
||||
* Override for get_options_xAxis_labels_formatter_.
|
||||
*
|
||||
* @return {Function} xAxis labels formatter.
|
||||
*/
|
||||
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_xAxis_labels_formatter_ = function() {
|
||||
var current_month;
|
||||
var current_year;
|
||||
|
||||
return function() {
|
||||
if (this.isFirst === true) {
|
||||
current_month = null;
|
||||
current_year = null;
|
||||
}
|
||||
var label_parts = [];
|
||||
|
||||
var day = this.value.format('D');
|
||||
var week = this.value.clone().startOf('isoweek')
|
||||
.format('MMM D');
|
||||
var month = this.value.format('MMM');
|
||||
var year = this.value.format('YYYY');
|
||||
|
||||
switch (beestat.setting('runtime_thermostat_summary_group_by')) {
|
||||
case 'year':
|
||||
label_parts.push(year);
|
||||
break;
|
||||
case 'month':
|
||||
label_parts.push(month);
|
||||
if (year !== current_year) {
|
||||
label_parts.push(year);
|
||||
}
|
||||
break;
|
||||
case 'week':
|
||||
label_parts.push(week);
|
||||
if (year !== current_year) {
|
||||
label_parts.push(year);
|
||||
}
|
||||
break;
|
||||
case 'day':
|
||||
if (month !== current_month) {
|
||||
label_parts.push(month);
|
||||
}
|
||||
label_parts.push(day);
|
||||
if (year !== current_year) {
|
||||
label_parts.push(year);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
current_month = month;
|
||||
current_year = year;
|
||||
|
||||
return label_parts.join(' ');
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Override for get_options_series_.
|
||||
*
|
||||
* @return {Array} All of the series to display on the chart.
|
||||
*/
|
||||
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_series_ = function() {
|
||||
var self = this;
|
||||
var series = [];
|
||||
|
||||
[
|
||||
'sum_compressor_cool_1',
|
||||
'sum_compressor_cool_2',
|
||||
'sum_compressor_heat_1',
|
||||
'sum_compressor_heat_2',
|
||||
'sum_auxiliary_heat_1',
|
||||
'sum_auxiliary_heat_2'
|
||||
].forEach(function(series_code) {
|
||||
if (self.data_.metadata.series[series_code].active === true) {
|
||||
series.push({
|
||||
'name': series_code,
|
||||
'data': self.data_.series[series_code],
|
||||
'color': beestat.series[series_code].color,
|
||||
'yAxis': 0,
|
||||
'groupPadding': 0,
|
||||
'type': 'column'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (self.data_.metadata.series.avg_outdoor_temperature.active === true) {
|
||||
series.push({
|
||||
'name': 'avg_outdoor_temperature',
|
||||
'data': this.data_.series.avg_outdoor_temperature,
|
||||
'color': beestat.series.avg_outdoor_temperature.color,
|
||||
'yAxis': 1,
|
||||
'type': 'spline',
|
||||
'dashStyle': 'ShortDash',
|
||||
'lineWidth': 1
|
||||
});
|
||||
}
|
||||
|
||||
if (self.data_.metadata.series.extreme_outdoor_temperature.active === true) {
|
||||
series.push({
|
||||
'name': 'extreme_outdoor_temperature',
|
||||
'data': this.data_.series.extreme_outdoor_temperature,
|
||||
'color': beestat.series.extreme_outdoor_temperature.color,
|
||||
'type': 'areasplinerange',
|
||||
'yAxis': 1,
|
||||
'fillOpacity': 0.2,
|
||||
'lineWidth': 0,
|
||||
'visible': false
|
||||
});
|
||||
}
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
/**
|
||||
* Override for get_options_yAxis_.
|
||||
*
|
||||
* @return {Array} The y-axis options.
|
||||
*/
|
||||
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_yAxis_ = function() {
|
||||
var self = this;
|
||||
|
||||
var y_max_hours;
|
||||
var tick_interval;
|
||||
switch (beestat.setting('runtime_thermostat_summary_group_by')) {
|
||||
case 'year':
|
||||
y_max_hours = 8760;
|
||||
tick_interval = 2190;
|
||||
break;
|
||||
case 'month':
|
||||
y_max_hours = 672;
|
||||
tick_interval = 168;
|
||||
break;
|
||||
case 'week':
|
||||
y_max_hours = 168;
|
||||
tick_interval = 24;
|
||||
break;
|
||||
case 'day':
|
||||
y_max_hours = 24;
|
||||
tick_interval = 6;
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
'alignTicks': false,
|
||||
'min': 0,
|
||||
'softMax': y_max_hours,
|
||||
'tickInterval': tick_interval,
|
||||
'reversedStacks': false,
|
||||
'gridLineColor': beestat.style.color.bluegray.light,
|
||||
'gridLineDashStyle': 'longdash',
|
||||
'title': {
|
||||
'text': ''
|
||||
},
|
||||
'labels': {
|
||||
'style': {
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'formatter': function() {
|
||||
return this.value + 'h';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'alignTicks': false,
|
||||
'gridLineColor': null,
|
||||
'gridLineDashStyle': 'longdash',
|
||||
'opposite': true,
|
||||
'allowDecimals': false,
|
||||
'title': {
|
||||
'text': ''
|
||||
},
|
||||
'labels': {
|
||||
'style': {
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'formatter': function() {
|
||||
return this.value + beestat.cache.thermostat[self.thermostat_id_].temperature_unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Override for get_options_tooltip_formatter_.
|
||||
*
|
||||
* @return {Function} The tooltip formatter.
|
||||
*/
|
||||
beestat.component.chart2.runtime_thermostat_summary.prototype.get_options_tooltip_formatter_ = function() {
|
||||
var self = this;
|
||||
|
||||
return function() {
|
||||
var sections = [];
|
||||
var groups = {
|
||||
'heat': [],
|
||||
'cool': [],
|
||||
'other': []
|
||||
};
|
||||
|
||||
var values = {};
|
||||
this.points.forEach(function(point) {
|
||||
if (point.series.name === 'extreme_outdoor_temperature') {
|
||||
values.min_outdoor_temperature = point.point.low;
|
||||
values.max_outdoor_temperature = point.point.high;
|
||||
} else {
|
||||
values[point.series.name] = point.y;
|
||||
}
|
||||
});
|
||||
|
||||
this.points.forEach(function(point) {
|
||||
var label;
|
||||
var value;
|
||||
var color;
|
||||
switch (point.series.name) {
|
||||
case 'extreme_outdoor_temperature':
|
||||
label = beestat.series.extreme_outdoor_temperature.name;
|
||||
color = point.series.color;
|
||||
if (
|
||||
values.min_outdoor_temperature !== undefined &&
|
||||
values.max_outdoor_temperature !== undefined
|
||||
) {
|
||||
value = beestat.temperature({
|
||||
'temperature': values.min_outdoor_temperature,
|
||||
'convert': false,
|
||||
'units': true,
|
||||
'round': 0
|
||||
});
|
||||
value += ' to ';
|
||||
value += beestat.temperature({
|
||||
'temperature': values.max_outdoor_temperature,
|
||||
'convert': false,
|
||||
'units': true,
|
||||
'round': 0
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'avg_outdoor_temperature':
|
||||
label = beestat.series.avg_outdoor_temperature.name;
|
||||
color = point.series.color;
|
||||
value = beestat.temperature({
|
||||
'temperature': values.avg_outdoor_temperature,
|
||||
'convert': false,
|
||||
'units': true,
|
||||
'round': 0
|
||||
});
|
||||
break;
|
||||
default:
|
||||
label = beestat.series[point.series.name].name;
|
||||
value = beestat.time(values[point.series.name], 'hours');
|
||||
color = point.series.color;
|
||||
break;
|
||||
}
|
||||
|
||||
var group;
|
||||
if (point.series.name.indexOf('heat') !== -1) {
|
||||
group = 'heat';
|
||||
} else if (point.series.name.indexOf('cool') !== -1) {
|
||||
group = 'cool';
|
||||
} else {
|
||||
group = 'other';
|
||||
}
|
||||
|
||||
groups[group].push({
|
||||
'label': label,
|
||||
'value': value,
|
||||
'color': color
|
||||
});
|
||||
});
|
||||
|
||||
sections.push(groups.heat);
|
||||
sections.push(groups.cool);
|
||||
sections.push(groups.other);
|
||||
|
||||
var title;
|
||||
switch (beestat.setting('runtime_thermostat_summary_group_by')) {
|
||||
case 'year':
|
||||
title = this.x.format('YYYY');
|
||||
break;
|
||||
case 'month':
|
||||
title = this.x.format('MMM YYYY');
|
||||
break;
|
||||
case 'week':
|
||||
title = 'Week of ' + this.x.clone().startOf('isoweek')
|
||||
.format('MMM Do, YYYY');
|
||||
break;
|
||||
case 'day':
|
||||
title = this.x.format('MMM Do');
|
||||
break;
|
||||
}
|
||||
|
||||
return self.tooltip_formatter_helper_(
|
||||
title,
|
||||
sections
|
||||
);
|
||||
};
|
||||
};
|
403
js/component/chart2.js
Executable file
403
js/component/chart2.js
Executable file
@ -0,0 +1,403 @@
|
||||
/**
|
||||
* A chart. Mostly just a wrapper for the Highcharts stuff so the defaults
|
||||
* don't have to be set every single time.
|
||||
*/
|
||||
beestat.component.chart2 = function() {
|
||||
var self = this;
|
||||
|
||||
this.addEventListener('render', function() {
|
||||
self.chart_.reflow();
|
||||
});
|
||||
|
||||
beestat.component.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.chart2, beestat.component);
|
||||
|
||||
beestat.component.chart2.prototype.rerender_on_breakpoint_ = false;
|
||||
|
||||
/**
|
||||
* Decorate. Calls all the option getters and renders the chart.
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.chart2.prototype.decorate_ = function(parent) {
|
||||
var options = {};
|
||||
|
||||
options.credits = this.get_options_credits_();
|
||||
options.exporting = this.get_options_exporting_();
|
||||
options.chart = this.get_options_chart_();
|
||||
options.title = this.get_options_title_();
|
||||
options.subtitle = this.get_options_subtitle_();
|
||||
options.legend = this.get_options_legend_();
|
||||
options.plotOptions = this.get_options_plotOptions_();
|
||||
options.xAxis = this.get_options_xAxis_();
|
||||
options.yAxis = this.get_options_yAxis_();
|
||||
options.series = this.get_options_series_();
|
||||
options.tooltip = this.get_options_tooltip_();
|
||||
|
||||
options.chart.renderTo = parent[0];
|
||||
|
||||
this.chart_ = Highcharts.chart(options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the chart zoom level all the way out.
|
||||
*/
|
||||
beestat.component.chart2.prototype.reset_zoom = function() {
|
||||
this.chart_.zoomOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* Export the chart to a PNG.
|
||||
*/
|
||||
beestat.component.chart2.prototype.export = function() {
|
||||
this.chart_.exportChartLocal();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the legend options.
|
||||
*
|
||||
* @return {object} The legend options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_legend_ = function() {
|
||||
return {
|
||||
'itemStyle': {
|
||||
'color': '#ecf0f1',
|
||||
'font-weight': '500'
|
||||
},
|
||||
'itemHoverStyle': {
|
||||
'color': '#bdc3c7'
|
||||
},
|
||||
'itemHiddenStyle': {
|
||||
'color': '#7f8c8d'
|
||||
},
|
||||
'labelFormatter': this.get_options_legend_labelFormatter_()
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the legend labelFormatter options.
|
||||
*
|
||||
* @return {Function} The legend labelFormatter options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_legend_labelFormatter_ = function() {
|
||||
return function() {
|
||||
return beestat.series[this.name].name;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the plotOptions.
|
||||
*
|
||||
* @return {object} The plotOptions.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_plotOptions_ = function() {
|
||||
return {
|
||||
'series': {
|
||||
'animation': false,
|
||||
'marker': {
|
||||
'enabled': false
|
||||
},
|
||||
'states': {
|
||||
'hover': {
|
||||
'enabled': false
|
||||
},
|
||||
'inactive': {
|
||||
'opacity': 1
|
||||
}
|
||||
}
|
||||
},
|
||||
'column': {
|
||||
'pointPadding': 0,
|
||||
'borderWidth': 0,
|
||||
'stacking': 'normal',
|
||||
'dataLabels': {
|
||||
'enabled': false
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the title options.
|
||||
*
|
||||
* @return {object} The title options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_title_ = function() {
|
||||
return {
|
||||
'text': null
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the subtitle options
|
||||
*
|
||||
* @return {object} The subtitle options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_subtitle_ = function() {
|
||||
return {
|
||||
'text': null
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the chart options.
|
||||
*
|
||||
* @return {object} The chart options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_chart_ = function() {
|
||||
return {
|
||||
'style': {
|
||||
'fontFamily': 'Montserrat'
|
||||
},
|
||||
'spacing': [
|
||||
beestat.style.size.gutter,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
'zoomType': 'x',
|
||||
'panning': true,
|
||||
'panKey': 'ctrl',
|
||||
'backgroundColor': beestat.style.color.bluegray.base,
|
||||
'resetZoomButton': {
|
||||
'theme': {
|
||||
'display': 'none'
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the export options.
|
||||
*
|
||||
* @return {object} The export options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_exporting_ = function() {
|
||||
return {
|
||||
'enabled': false,
|
||||
'sourceWidth': 980,
|
||||
'scale': 1,
|
||||
'filename': 'beestat',
|
||||
'chartOptions': {
|
||||
'credits': {
|
||||
'text': 'beestat.io'
|
||||
},
|
||||
'title': {
|
||||
'align': 'left',
|
||||
'text': null,
|
||||
'margin': beestat.style.size.gutter,
|
||||
'style': {
|
||||
'color': '#fff',
|
||||
'font-weight': beestat.style.font_weight.bold,
|
||||
'font-size': beestat.style.font_size.large
|
||||
}
|
||||
},
|
||||
'subtitle': {
|
||||
'align': 'left',
|
||||
'text': null,
|
||||
'style': {
|
||||
'color': '#fff',
|
||||
'font-weight': beestat.style.font_weight.light,
|
||||
'font-size': beestat.style.font_size.normal
|
||||
}
|
||||
},
|
||||
'chart': {
|
||||
'style': {
|
||||
'fontFamily': 'Montserrat, Helvetica, Sans-Serif'
|
||||
},
|
||||
'spacing': [
|
||||
beestat.style.size.gutter,
|
||||
beestat.style.size.gutter,
|
||||
beestat.style.size.gutter,
|
||||
beestat.style.size.gutter
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the credits options.
|
||||
*
|
||||
* @return {boolean} The credits options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_credits_ = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the xAxis options.
|
||||
*
|
||||
* @return {object} The xAxis options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_xAxis_ = function() {
|
||||
return {
|
||||
'categories': this.data_.x,
|
||||
'lineColor': beestat.style.color.bluegray.light,
|
||||
'tickLength': 0,
|
||||
'labels': {
|
||||
'style': {
|
||||
'color': beestat.style.color.gray.base
|
||||
},
|
||||
'formatter': this.get_options_xAxis_labels_formatter_()
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the xAxis label formatter options. Needs to be overridden.
|
||||
*
|
||||
* @return {object} The xAxis label formatter options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_xAxis_labels_formatter_ = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the yAxis label formatter options. Needs to be overridden.
|
||||
*
|
||||
* @return {object} The yAxis label formatter options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_yAxis_ = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the series options. Needs to be overridden.
|
||||
*
|
||||
* @return {object} The series options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_series_ = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the tooltip options.
|
||||
*
|
||||
* @return {object} The tooltip options.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_tooltip_ = function() {
|
||||
return {
|
||||
'shared': true,
|
||||
'useHTML': true,
|
||||
'borderWidth': 0,
|
||||
'shadow': false,
|
||||
'backgroundColor': null,
|
||||
'followPointer': true,
|
||||
'crosshairs': {
|
||||
'width': 1,
|
||||
'zIndex': 100,
|
||||
'color': beestat.style.color.gray.light,
|
||||
'dashStyle': 'shortDot',
|
||||
'snap': false
|
||||
},
|
||||
'positioner': this.get_options_tooltip_positioner_(),
|
||||
'formatter': this.get_options_tooltip_formatter_()
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the tooltip formatter. Needs to be overridden.
|
||||
*
|
||||
* @return {Function} The tooltip formatter.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_tooltip_formatter_ = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the tooltip positioner. Makes sure the tooltip is positioned nicely.
|
||||
*
|
||||
* @return {Function} The tooltip positioner.
|
||||
*/
|
||||
beestat.component.chart2.prototype.get_options_tooltip_positioner_ = function() {
|
||||
var self = this;
|
||||
return function(tooltip_width, tooltip_height, point) {
|
||||
var plot_width = self.chart_.plotWidth;
|
||||
|
||||
var fits_on_left = (point.plotX - tooltip_width) > 0;
|
||||
var fits_on_right = (point.plotX + tooltip_width) < plot_width;
|
||||
|
||||
var x;
|
||||
var y = 60;
|
||||
if (fits_on_left === true) {
|
||||
x = point.plotX - tooltip_width + self.chart_.plotLeft;
|
||||
} else if (fits_on_right === true) {
|
||||
x = point.plotX + self.chart_.plotLeft;
|
||||
} else {
|
||||
x = self.chart_.plotLeft;
|
||||
}
|
||||
|
||||
return {
|
||||
'x': x,
|
||||
'y': y
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the HTML needed to render a tooltip.
|
||||
*
|
||||
* @param {string} title The tooltip title.
|
||||
* @param {array} sections Data inside the tooltip.
|
||||
*
|
||||
* @return {string} The tooltip HTML.
|
||||
*/
|
||||
beestat.component.chart2.prototype.tooltip_formatter_helper_ = function(title, sections) {
|
||||
var tooltip = $.createElement('div')
|
||||
.style({
|
||||
'background-color': beestat.style.color.bluegray.dark,
|
||||
'padding': beestat.style.size.gutter / 2
|
||||
});
|
||||
|
||||
var title_div = $.createElement('div')
|
||||
.style({
|
||||
'font-weight': beestat.style.font_weight.bold,
|
||||
'font-size': beestat.style.font_size.large,
|
||||
'margin-bottom': beestat.style.size.gutter / 4,
|
||||
'color': beestat.style.color.gray.light
|
||||
})
|
||||
.innerText(title);
|
||||
tooltip.appendChild(title_div);
|
||||
|
||||
var table = $.createElement('table')
|
||||
.setAttribute({
|
||||
'cellpadding': '0',
|
||||
'cellspacing': '0'
|
||||
});
|
||||
tooltip.appendChild(table);
|
||||
|
||||
sections.forEach(function(section, i) {
|
||||
if (section.length > 0) {
|
||||
section.forEach(function(item) {
|
||||
var tr = $.createElement('tr').style('color', item.color);
|
||||
table.appendChild(tr);
|
||||
|
||||
var td_label = $.createElement('td')
|
||||
.style({
|
||||
'font-weight': beestat.style.font_weight.bold
|
||||
})
|
||||
.innerText(item.label);
|
||||
tr.appendChild(td_label);
|
||||
|
||||
var td_value = $.createElement('td').innerText(item.value)
|
||||
.style({
|
||||
'padding-left': beestat.style.size.gutter / 4
|
||||
});
|
||||
tr.appendChild(td_value);
|
||||
});
|
||||
|
||||
if (i < sections.length) {
|
||||
var spacer_tr = $.createElement('tr');
|
||||
table.appendChild(spacer_tr);
|
||||
|
||||
var spacer_td = $.createElement('td')
|
||||
.style('padding-bottom', beestat.style.size.gutter / 4);
|
||||
spacer_tr.appendChild(spacer_td);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tooltip[0].outerHTML;
|
||||
};
|
@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Loading bar
|
||||
* Loading thing.
|
||||
*
|
||||
* @param {string} text Optional text to display with the loading thing.
|
||||
*/
|
||||
beestat.component.loading = function(text) {
|
||||
this.text_ = text;
|
||||
|
@ -14,9 +14,6 @@ beestat.extend(beestat.component.modal.help_comparison_settings, beestat.compone
|
||||
beestat.component.modal.help_comparison_settings.prototype.decorate_contents_ = function(parent) {
|
||||
parent.appendChild($.createElement('p').innerText('Comparison settings allow you to customize how your home is compared to the homes of other beestat users. All thermostats at the same physical address are compared together.'));
|
||||
|
||||
(new beestat.component.title('Date')).render(parent);
|
||||
parent.appendChild($.createElement('p').innerText('This is the date your home\'s score is calculated on. Make some energy-saving improvements lately? Set this date back a few months and see what difference they made. Note that even though you\'re looking at your home in the past, beestat always compares to all other homes in the present.'));
|
||||
|
||||
(new beestat.component.title('Region')).render(parent);
|
||||
parent.appendChild($.createElement('p').innerText('Compare your home to other homes within 250 miles (400km) or expand this to all homes globally.'));
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Help for the aggregate runtime card.
|
||||
*/
|
||||
beestat.component.modal.help_aggregate_runtime = function() {
|
||||
beestat.component.modal.help_runtime_thermostat_summary = function() {
|
||||
beestat.component.modal.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.modal.help_aggregate_runtime, beestat.component.modal);
|
||||
beestat.extend(beestat.component.modal.help_runtime_thermostat_summary, beestat.component.modal);
|
||||
|
||||
beestat.component.modal.help_aggregate_runtime.prototype.decorate_contents_ = function(parent) {
|
||||
beestat.component.modal.help_runtime_thermostat_summary.prototype.decorate_contents_ = function(parent) {
|
||||
parent.appendChild($.createElement('p').innerHTML('View HVAC usage trends over large periods of time. This can help you identify problems or visualize effeciency gains from new equipment, insulation, etc. Compare to the Home IQ Weather Impact chart.'));
|
||||
parent.appendChild($.createElement('p').innerHTML('If you have Gap Fill enabled (on by default), this data may not match the ecobee website exactly. Ecobee displays total runtime as stored, while beestat will intelligently fill in missing data to produce a more accurate result.'));
|
||||
|
||||
@ -31,25 +31,9 @@ beestat.component.modal.help_aggregate_runtime.prototype.decorate_contents_ = fu
|
||||
td = $.createElement('td');
|
||||
td.setAttribute('valign', 'top');
|
||||
tr.appendChild(td);
|
||||
td.innerHTML('Ecobee purged weather data prior to April 2018 as a result of switching weather providers. If you joined beestat after this happened you will not have access to this historical data.');
|
||||
|
||||
tr = $.createElement('tr');
|
||||
table.appendChild(tr);
|
||||
|
||||
td = $.createElement('td');
|
||||
td.setAttribute('valign', 'top');
|
||||
tr.appendChild(td);
|
||||
|
||||
(new beestat.component.icon('information')
|
||||
.set_color(beestat.style.color.blue.base)
|
||||
).render(td);
|
||||
|
||||
td = $.createElement('td');
|
||||
td.setAttribute('valign', 'top');
|
||||
tr.appendChild(td);
|
||||
td.innerHTML('Ecobee typically purges data after about a year. Beestat currently stores all historical data even though ecobee does not.');
|
||||
td.innerHTML('Ecobee typically purges data after about a year. Beestat stores all historical data even though ecobee does not.');
|
||||
};
|
||||
|
||||
beestat.component.modal.help_aggregate_runtime.prototype.get_title_ = function() {
|
||||
return 'Aggregate Runtime - Help';
|
||||
beestat.component.modal.help_runtime_thermostat_summary.prototype.get_title_ = function() {
|
||||
return 'Runtime Summary - Help';
|
||||
};
|
@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Custom date range for the aggregate runtime chart.
|
||||
*/
|
||||
beestat.component.modal.aggregate_runtime_custom = function() {
|
||||
beestat.component.modal.runtime_thermostat_summary_custom = function() {
|
||||
beestat.component.modal.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.modal.aggregate_runtime_custom, beestat.component.modal);
|
||||
beestat.extend(beestat.component.modal.runtime_thermostat_summary_custom, beestat.component.modal);
|
||||
|
||||
beestat.component.modal.aggregate_runtime_custom.prototype.decorate_contents_ = function(parent) {
|
||||
beestat.component.modal.runtime_thermostat_summary_custom.prototype.decorate_contents_ = function(parent) {
|
||||
var self = this;
|
||||
|
||||
parent.appendChild($.createElement('p').innerHTML('Choose a custom range to display on the Aggregate Runtime chart.'));
|
||||
@ -22,26 +22,26 @@ beestat.component.modal.aggregate_runtime_custom.prototype.decorate_contents_ =
|
||||
'maxlength': 10
|
||||
})
|
||||
.set_icon('pound')
|
||||
.set_value(beestat.setting('aggregate_runtime_time_count'));
|
||||
.set_value(beestat.setting('runtime_thermostat_summary_time_count'));
|
||||
|
||||
self.state_.aggregate_runtime_time_count =
|
||||
beestat.setting('aggregate_runtime_time_count');
|
||||
self.state_.runtime_thermostat_summary_time_count =
|
||||
beestat.setting('runtime_thermostat_summary_time_count');
|
||||
|
||||
time_count.addEventListener('blur', function() {
|
||||
self.state_.aggregate_runtime_time_count =
|
||||
self.state_.runtime_thermostat_summary_time_count =
|
||||
parseInt(this.get_value(), 10) || 1;
|
||||
});
|
||||
|
||||
// Button groups
|
||||
var options = {
|
||||
'aggregate_runtime_time_period': [
|
||||
'runtime_thermostat_summary_time_period': [
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year',
|
||||
'all'
|
||||
],
|
||||
'aggregate_runtime_group_by': [
|
||||
'runtime_thermostat_summary_group_by': [
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
@ -57,13 +57,13 @@ beestat.component.modal.aggregate_runtime_custom.prototype.decorate_contents_ =
|
||||
|
||||
let button_group = new beestat.component.button_group();
|
||||
options[key].forEach(function(value) {
|
||||
let text = value.replace('aggregate_runtime_', '')
|
||||
let text = value.replace('runtime_thermostat_summary_', '')
|
||||
.charAt(0)
|
||||
.toUpperCase() +
|
||||
value.slice(1) +
|
||||
(
|
||||
(
|
||||
key === 'aggregate_runtime_time_period' &&
|
||||
key === 'runtime_thermostat_summary_time_period' &&
|
||||
value !== 'all'
|
||||
) ? 's' : ''
|
||||
);
|
||||
@ -73,12 +73,12 @@ beestat.component.modal.aggregate_runtime_custom.prototype.decorate_contents_ =
|
||||
.set_text_color('#fff')
|
||||
.set_text(text)
|
||||
.addEventListener('click', function() {
|
||||
if (key === 'aggregate_runtime_time_period') {
|
||||
if (key === 'runtime_thermostat_summary_time_period') {
|
||||
if (value === 'all') {
|
||||
time_count.set_value('∞').disable();
|
||||
} else if (time_count.get_value() === '∞') {
|
||||
time_count
|
||||
.set_value(self.state_.aggregate_runtime_time_count || '1')
|
||||
.set_value(self.state_.runtime_thermostat_summary_time_count || '1')
|
||||
.enable();
|
||||
time_count.dispatchEvent('blur');
|
||||
}
|
||||
@ -98,7 +98,7 @@ beestat.component.modal.aggregate_runtime_custom.prototype.decorate_contents_ =
|
||||
|
||||
if (current_type === value) {
|
||||
if (
|
||||
key === 'aggregate_runtime_time_period' &&
|
||||
key === 'runtime_thermostat_summary_time_period' &&
|
||||
value === 'all'
|
||||
) {
|
||||
time_count.set_value('∞').disable();
|
||||
@ -128,13 +128,13 @@ beestat.component.modal.aggregate_runtime_custom.prototype.decorate_contents_ =
|
||||
time_count.render(column);
|
||||
column = $.createElement('div').addClass(['column column_10']);
|
||||
row.appendChild(column);
|
||||
button_groups.aggregate_runtime_time_period.render(column);
|
||||
button_groups.runtime_thermostat_summary_time_period.render(column);
|
||||
(new beestat.component.title('Group By')).render(parent);
|
||||
row = $.createElement('div').addClass('row');
|
||||
parent.appendChild(row);
|
||||
column = $.createElement('div').addClass(['column column_12']);
|
||||
row.appendChild(column);
|
||||
button_groups.aggregate_runtime_group_by.render(column);
|
||||
button_groups.runtime_thermostat_summary_group_by.render(column);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -142,7 +142,7 @@ beestat.component.modal.aggregate_runtime_custom.prototype.decorate_contents_ =
|
||||
*
|
||||
* @return {string} Title
|
||||
*/
|
||||
beestat.component.modal.aggregate_runtime_custom.prototype.get_title_ = function() {
|
||||
beestat.component.modal.runtime_thermostat_summary_custom.prototype.get_title_ = function() {
|
||||
return 'Aggregate Runtime - Custom Range';
|
||||
};
|
||||
|
||||
@ -151,7 +151,7 @@ beestat.component.modal.aggregate_runtime_custom.prototype.get_title_ = function
|
||||
*
|
||||
* @return {[beestat.component.button]} The buttons.
|
||||
*/
|
||||
beestat.component.modal.aggregate_runtime_custom.prototype.get_buttons_ = function() {
|
||||
beestat.component.modal.runtime_thermostat_summary_custom.prototype.get_buttons_ = function() {
|
||||
var self = this;
|
||||
|
||||
var cancel = new beestat.component.button()
|
||||
@ -176,14 +176,14 @@ beestat.component.modal.aggregate_runtime_custom.prototype.get_buttons_ = functi
|
||||
|
||||
beestat.setting(
|
||||
{
|
||||
'aggregate_runtime_time_count':
|
||||
self.state_.aggregate_runtime_time_period === 'all'
|
||||
'runtime_thermostat_summary_time_count':
|
||||
self.state_.runtime_thermostat_summary_time_period === 'all'
|
||||
? 0
|
||||
: self.state_.aggregate_runtime_time_count,
|
||||
'aggregate_runtime_time_period':
|
||||
self.state_.aggregate_runtime_time_period,
|
||||
'aggregate_runtime_group_by':
|
||||
self.state_.aggregate_runtime_group_by
|
||||
: self.state_.runtime_thermostat_summary_time_count,
|
||||
'runtime_thermostat_summary_time_period':
|
||||
self.state_.runtime_thermostat_summary_time_period,
|
||||
'runtime_thermostat_summary_group_by':
|
||||
self.state_.runtime_thermostat_summary_group_by
|
||||
},
|
||||
undefined,
|
||||
function() {
|
490
js/component/modal/weather.js
Normal file → Executable file
490
js/component/modal/weather.js
Normal file → Executable file
@ -1,245 +1,245 @@
|
||||
/**
|
||||
* Current weather.
|
||||
*/
|
||||
beestat.component.modal.weather = function() {
|
||||
var self = this;
|
||||
|
||||
beestat.dispatcher.addEventListener(
|
||||
'cache.thermostat',
|
||||
function() {
|
||||
self.rerender();
|
||||
}
|
||||
);
|
||||
|
||||
beestat.component.modal.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.modal.weather, beestat.component.modal);
|
||||
|
||||
beestat.component.modal.weather.prototype.decorate_contents_ = function(parent) {
|
||||
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
|
||||
|
||||
var icon;
|
||||
var icon_color;
|
||||
switch (thermostat.weather.condition) {
|
||||
case 'sunny':
|
||||
icon = 'weather_sunny';
|
||||
icon_color = beestat.style.color.yellow.base;
|
||||
break;
|
||||
case 'few_clouds':
|
||||
case 'partly_cloudy':
|
||||
icon = 'weather_partly_cloudy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'mostly_cloudy':
|
||||
case 'overcast':
|
||||
icon = 'weather_cloudy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'drizzle':
|
||||
case 'rain':
|
||||
case 'showers':
|
||||
icon = 'weather_pouring';
|
||||
icon_color = beestat.style.color.blue.light;
|
||||
break;
|
||||
case 'freezing_rain':
|
||||
case 'hail':
|
||||
case 'pellets':
|
||||
icon_color = beestat.style.color.lightblue.base;
|
||||
icon = 'weather_hail';
|
||||
break;
|
||||
case 'snow':
|
||||
case 'flurries':
|
||||
case 'freezing_snow':
|
||||
icon_color = beestat.style.color.lightblue.light;
|
||||
icon = 'weather_snowy';
|
||||
break;
|
||||
case 'blizzard':
|
||||
icon = 'weather_snowy_heavy';
|
||||
icon_color = beestat.style.color.lightblue.light;
|
||||
break;
|
||||
case 'thunderstorm':
|
||||
icon = 'weather_lightning_rainy';
|
||||
icon_color = beestat.style.color.red.base;
|
||||
break;
|
||||
case 'windy':
|
||||
icon = 'weather_windy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'tornado':
|
||||
icon = 'weather_tornado';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'fog':
|
||||
icon = 'weather_fog';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'haze':
|
||||
case 'smoke':
|
||||
case 'dust':
|
||||
icon = 'weather_hazy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
default:
|
||||
icon = 'cloud_question';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
}
|
||||
|
||||
var condition = thermostat.weather.condition.replace('_', ' ');
|
||||
condition = condition.charAt(0).toUpperCase() + condition.slice(1);
|
||||
|
||||
var tr;
|
||||
var td;
|
||||
|
||||
var table = $.createElement('table');
|
||||
|
||||
tr = $.createElement('tr');
|
||||
table.appendChild(tr);
|
||||
|
||||
td = $.createElement('td')
|
||||
.setAttribute('rowspan', '2')
|
||||
.style({
|
||||
'padding-right': beestat.style.size.gutter
|
||||
});
|
||||
(new beestat.component.icon(icon))
|
||||
.set_size(64)
|
||||
.set_color(icon_color)
|
||||
.render(td);
|
||||
tr.appendChild(td);
|
||||
|
||||
td = $.createElement('td');
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(
|
||||
beestat.temperature({
|
||||
'round': 0,
|
||||
'units': true,
|
||||
'temperature': thermostat.weather.temperature
|
||||
})
|
||||
)
|
||||
.style({
|
||||
'font-size': '24px'
|
||||
})
|
||||
);
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(condition)
|
||||
.style({
|
||||
'font-size': '18px',
|
||||
'padding-left': (beestat.style.size.gutter / 2)
|
||||
})
|
||||
);
|
||||
tr.appendChild(td);
|
||||
|
||||
tr = $.createElement('tr').style('color', beestat.style.color.gray.base);
|
||||
table.appendChild(tr);
|
||||
|
||||
td = $.createElement('td');
|
||||
// Low
|
||||
td.appendChild($.createElement('span').innerText('Low: '));
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(
|
||||
beestat.temperature({
|
||||
'round': 0,
|
||||
'units': false,
|
||||
'temperature': thermostat.weather.temperature_low
|
||||
})
|
||||
)
|
||||
.style({
|
||||
'padding-right': (beestat.style.size.gutter / 2)
|
||||
})
|
||||
);
|
||||
// High
|
||||
td.appendChild($.createElement('span').innerText('High: '));
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(
|
||||
beestat.temperature({
|
||||
'round': 0,
|
||||
'units': false,
|
||||
'temperature': thermostat.weather.temperature_high
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
tr.appendChild(td);
|
||||
|
||||
parent.appendChild(table);
|
||||
|
||||
var container = $.createElement('div')
|
||||
.style({
|
||||
'display': 'grid',
|
||||
'grid-template-columns': 'repeat(auto-fill, minmax(120px, 1fr))',
|
||||
'margin': '0 0 16px -16px'
|
||||
});
|
||||
parent.appendChild(container);
|
||||
|
||||
var bearings = [
|
||||
'N',
|
||||
'NNE',
|
||||
'NE',
|
||||
'ENE',
|
||||
'E',
|
||||
'ESE',
|
||||
'SE',
|
||||
'SSE',
|
||||
'S',
|
||||
'SSW',
|
||||
'SW',
|
||||
'WSW',
|
||||
'W',
|
||||
'WNW',
|
||||
'NW',
|
||||
'NNW'
|
||||
];
|
||||
|
||||
var fields = [
|
||||
{
|
||||
'name': 'Humidity',
|
||||
'value': thermostat.weather.humidity_relative + '%'
|
||||
},
|
||||
{
|
||||
'name': 'Dew Point',
|
||||
'value': beestat.temperature({
|
||||
'round': 0,
|
||||
'units': true,
|
||||
'temperature': thermostat.weather.dew_point
|
||||
})
|
||||
},
|
||||
{
|
||||
'name': 'Wind',
|
||||
'value': thermostat.weather.wind_speed === 0
|
||||
? '0mph'
|
||||
: thermostat.weather.wind_speed + 'mph ' + bearings[Math.floor(((thermostat.weather.wind_bearing / 22.5) + 0.5) % 16)]
|
||||
},
|
||||
{
|
||||
'name': 'Pressure',
|
||||
'value': thermostat.weather.barometric_pressure + 'mb'
|
||||
},
|
||||
{
|
||||
'name': 'Station',
|
||||
'value': thermostat.weather.station
|
||||
}
|
||||
];
|
||||
|
||||
fields.forEach(function(field) {
|
||||
var div = $.createElement('div')
|
||||
.style({
|
||||
'padding': '16px 0 0 16px'
|
||||
});
|
||||
container.appendChild(div);
|
||||
|
||||
div.appendChild($.createElement('div')
|
||||
.style({
|
||||
'font-weight': beestat.style.font_weight.bold,
|
||||
'margin-bottom': (beestat.style.size.gutter / 4)
|
||||
})
|
||||
.innerHTML(field.name));
|
||||
div.appendChild($.createElement('div').innerHTML(field.value));
|
||||
});
|
||||
};
|
||||
|
||||
beestat.component.modal.weather.prototype.get_title_ = function() {
|
||||
return 'Weather';
|
||||
};
|
||||
/**
|
||||
* Current weather.
|
||||
*/
|
||||
beestat.component.modal.weather = function() {
|
||||
var self = this;
|
||||
|
||||
beestat.dispatcher.addEventListener(
|
||||
'cache.thermostat',
|
||||
function() {
|
||||
self.rerender();
|
||||
}
|
||||
);
|
||||
|
||||
beestat.component.modal.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.modal.weather, beestat.component.modal);
|
||||
|
||||
beestat.component.modal.weather.prototype.decorate_contents_ = function(parent) {
|
||||
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
|
||||
|
||||
var icon;
|
||||
var icon_color;
|
||||
switch (thermostat.weather.condition) {
|
||||
case 'sunny':
|
||||
icon = 'weather_sunny';
|
||||
icon_color = beestat.style.color.yellow.base;
|
||||
break;
|
||||
case 'few_clouds':
|
||||
case 'partly_cloudy':
|
||||
icon = 'weather_partly_cloudy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'mostly_cloudy':
|
||||
case 'overcast':
|
||||
icon = 'weather_cloudy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'drizzle':
|
||||
case 'rain':
|
||||
case 'showers':
|
||||
icon = 'weather_pouring';
|
||||
icon_color = beestat.style.color.blue.light;
|
||||
break;
|
||||
case 'freezing_rain':
|
||||
case 'hail':
|
||||
case 'pellets':
|
||||
icon_color = beestat.style.color.lightblue.base;
|
||||
icon = 'weather_hail';
|
||||
break;
|
||||
case 'snow':
|
||||
case 'flurries':
|
||||
case 'freezing_snow':
|
||||
icon_color = beestat.style.color.lightblue.light;
|
||||
icon = 'weather_snowy';
|
||||
break;
|
||||
case 'blizzard':
|
||||
icon = 'weather_snowy_heavy';
|
||||
icon_color = beestat.style.color.lightblue.light;
|
||||
break;
|
||||
case 'thunderstorm':
|
||||
icon = 'weather_lightning_rainy';
|
||||
icon_color = beestat.style.color.red.base;
|
||||
break;
|
||||
case 'windy':
|
||||
icon = 'weather_windy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'tornado':
|
||||
icon = 'weather_tornado';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'fog':
|
||||
icon = 'weather_fog';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
case 'haze':
|
||||
case 'smoke':
|
||||
case 'dust':
|
||||
icon = 'weather_hazy';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
default:
|
||||
icon = 'cloud_question';
|
||||
icon_color = beestat.style.color.gray.base;
|
||||
break;
|
||||
}
|
||||
|
||||
var condition = thermostat.weather.condition.replace('_', ' ');
|
||||
condition = condition.charAt(0).toUpperCase() + condition.slice(1);
|
||||
|
||||
var tr;
|
||||
var td;
|
||||
|
||||
var table = $.createElement('table');
|
||||
|
||||
tr = $.createElement('tr');
|
||||
table.appendChild(tr);
|
||||
|
||||
td = $.createElement('td')
|
||||
.setAttribute('rowspan', '2')
|
||||
.style({
|
||||
'padding-right': beestat.style.size.gutter
|
||||
});
|
||||
(new beestat.component.icon(icon))
|
||||
.set_size(64)
|
||||
.set_color(icon_color)
|
||||
.render(td);
|
||||
tr.appendChild(td);
|
||||
|
||||
td = $.createElement('td');
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(
|
||||
beestat.temperature({
|
||||
'round': 0,
|
||||
'units': true,
|
||||
'temperature': thermostat.weather.temperature
|
||||
})
|
||||
)
|
||||
.style({
|
||||
'font-size': '24px'
|
||||
})
|
||||
);
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(condition)
|
||||
.style({
|
||||
'font-size': '18px',
|
||||
'padding-left': (beestat.style.size.gutter / 2)
|
||||
})
|
||||
);
|
||||
tr.appendChild(td);
|
||||
|
||||
tr = $.createElement('tr').style('color', beestat.style.color.gray.base);
|
||||
table.appendChild(tr);
|
||||
|
||||
td = $.createElement('td');
|
||||
// Low
|
||||
td.appendChild($.createElement('span').innerText('Low: '));
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(
|
||||
beestat.temperature({
|
||||
'round': 0,
|
||||
'units': false,
|
||||
'temperature': thermostat.weather.temperature_low
|
||||
})
|
||||
)
|
||||
.style({
|
||||
'padding-right': (beestat.style.size.gutter / 2)
|
||||
})
|
||||
);
|
||||
// High
|
||||
td.appendChild($.createElement('span').innerText('High: '));
|
||||
td.appendChild(
|
||||
$.createElement('span')
|
||||
.innerText(
|
||||
beestat.temperature({
|
||||
'round': 0,
|
||||
'units': false,
|
||||
'temperature': thermostat.weather.temperature_high
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
tr.appendChild(td);
|
||||
|
||||
parent.appendChild(table);
|
||||
|
||||
var container = $.createElement('div')
|
||||
.style({
|
||||
'display': 'grid',
|
||||
'grid-template-columns': 'repeat(auto-fill, minmax(120px, 1fr))',
|
||||
'margin': '0 0 16px -16px'
|
||||
});
|
||||
parent.appendChild(container);
|
||||
|
||||
var bearings = [
|
||||
'N',
|
||||
'NNE',
|
||||
'NE',
|
||||
'ENE',
|
||||
'E',
|
||||
'ESE',
|
||||
'SE',
|
||||
'SSE',
|
||||
'S',
|
||||
'SSW',
|
||||
'SW',
|
||||
'WSW',
|
||||
'W',
|
||||
'WNW',
|
||||
'NW',
|
||||
'NNW'
|
||||
];
|
||||
|
||||
var fields = [
|
||||
{
|
||||
'name': 'Humidity',
|
||||
'value': thermostat.weather.humidity_relative + '%'
|
||||
},
|
||||
{
|
||||
'name': 'Dew Point',
|
||||
'value': beestat.temperature({
|
||||
'round': 0,
|
||||
'units': true,
|
||||
'temperature': thermostat.weather.dew_point
|
||||
})
|
||||
},
|
||||
{
|
||||
'name': 'Wind',
|
||||
'value': thermostat.weather.wind_speed === 0
|
||||
? '0mph'
|
||||
: thermostat.weather.wind_speed + 'mph ' + bearings[Math.floor(((thermostat.weather.wind_bearing / 22.5) + 0.5) % 16)]
|
||||
},
|
||||
{
|
||||
'name': 'Pressure',
|
||||
'value': thermostat.weather.barometric_pressure + 'mb'
|
||||
},
|
||||
{
|
||||
'name': 'Station',
|
||||
'value': thermostat.weather.station
|
||||
}
|
||||
];
|
||||
|
||||
fields.forEach(function(field) {
|
||||
var div = $.createElement('div')
|
||||
.style({
|
||||
'padding': '16px 0 0 16px'
|
||||
});
|
||||
container.appendChild(div);
|
||||
|
||||
div.appendChild($.createElement('div')
|
||||
.style({
|
||||
'font-weight': beestat.style.font_weight.bold,
|
||||
'margin-bottom': (beestat.style.size.gutter / 4)
|
||||
})
|
||||
.innerHTML(field.name));
|
||||
div.appendChild($.createElement('div').innerHTML(field.value));
|
||||
});
|
||||
};
|
||||
|
||||
beestat.component.modal.weather.prototype.get_title_ = function() {
|
||||
return 'Weather';
|
||||
};
|
||||
|
10
js/js.php
Normal file → Executable file
10
js/js.php
Normal file → Executable file
@ -29,6 +29,8 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/beestat/poll.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/google_analytics.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/thermostat_group.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/highcharts.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/beestat/get_sync_progress.js"></script>' . PHP_EOL;
|
||||
|
||||
// Layer
|
||||
echo '<script src="/js/layer.js"></script>' . PHP_EOL;
|
||||
@ -40,7 +42,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/alert.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/aggregate_runtime.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/runtime_thermostat_summary.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/alerts.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/comparison_settings.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/comparison_issue.js"></script>' . PHP_EOL;
|
||||
@ -57,6 +59,8 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component/card/system.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/temperature_profiles.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart2.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_thermostat_summary.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/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;
|
||||
@ -65,13 +69,13 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component/menu.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/menu_item.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/aggregate_runtime_custom.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/runtime_thermostat_summary_custom.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/announcements.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/change_system_type.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/change_thermostat.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/error.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/filter_info.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/help_aggregate_runtime.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/help_runtime_thermostat_summary.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/help_alerts.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/help_home_efficiency.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/help_my_home.js"></script>' . PHP_EOL;
|
||||
|
@ -85,7 +85,9 @@ beestat.layer.dashboard.prototype.decorate_ = function(parent) {
|
||||
]);
|
||||
cards.push([
|
||||
{
|
||||
'card': new beestat.component.card.aggregate_runtime(),
|
||||
'card': new beestat.component.card.runtime_thermostat_summary(
|
||||
beestat.setting('thermostat_id')
|
||||
),
|
||||
'size': 12
|
||||
}
|
||||
]);
|
||||
|
@ -37,11 +37,11 @@ beestat.layer.home_comparisons.prototype.decorate_ = function(parent) {
|
||||
cards.push([
|
||||
{
|
||||
'card': new beestat.component.card.comparison_settings(),
|
||||
'size': 8
|
||||
'size': 6
|
||||
},
|
||||
{
|
||||
'card': new beestat.component.card.my_home(),
|
||||
'size': 4
|
||||
'size': 6
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -120,6 +120,13 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
|
||||
'announcement'
|
||||
);
|
||||
|
||||
api.add_call(
|
||||
'runtime_thermostat_summary',
|
||||
'read_id',
|
||||
{},
|
||||
'runtime_thermostat_summary'
|
||||
);
|
||||
|
||||
api.set_callback(function(response) {
|
||||
beestat.cache.set('user', response.user);
|
||||
|
||||
@ -145,8 +152,8 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
|
||||
beestat.cache.set('ecobee_sensor', response.ecobee_sensor);
|
||||
beestat.cache.set('address', response.address);
|
||||
beestat.cache.set('announcement', response.announcement);
|
||||
beestat.cache.set('ecobee_runtime_thermostat', []);
|
||||
beestat.cache.set('aggregate_runtime', []);
|
||||
beestat.cache.set('runtime_thermostat', []);
|
||||
beestat.cache.set('runtime_thermostat_summary', response.runtime_thermostat_summary);
|
||||
|
||||
// Set the active thermostat_id if this is your first time visiting.
|
||||
if (beestat.setting('thermostat_id') === undefined) {
|
||||
@ -170,24 +177,22 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
|
||||
|
||||
// Rename series if only one stage is available.
|
||||
if (ecobee_thermostat.json_settings.coolStages === 1) {
|
||||
beestat.series.compressor_cool_1.name = 'Cool';
|
||||
beestat.series.sum_compressor_cool_1.name = 'Cool';
|
||||
}
|
||||
if (ecobee_thermostat.json_settings.heatStages === 1) {
|
||||
beestat.series.compressor_heat_1.name = 'Heat';
|
||||
beestat.series.sum_compressor_heat_1.name = 'Heat';
|
||||
}
|
||||
|
||||
// Fix some other stuff for non-heat-pump.
|
||||
if (ecobee_thermostat.json_settings.hasHeatPump === false) {
|
||||
beestat.series.auxiliary_heat_1.name =
|
||||
beestat.series.compressor_heat_1.name;
|
||||
beestat.series.sum_compressor_heat_1.name;
|
||||
beestat.series.auxiliary_heat_1.color =
|
||||
beestat.series.compressor_heat_1.color;
|
||||
beestat.series.auxiliary_heat_2.name =
|
||||
beestat.series.sum_compressor_heat_1.color;
|
||||
beestat.series.sum_auxiliary_heat_2.name =
|
||||
beestat.series.compressor_heat_2.name;
|
||||
beestat.series.auxiliary_heat_2.color =
|
||||
beestat.series.sum_auxiliary_heat_2.color =
|
||||
beestat.series.compressor_heat_2.color;
|
||||
beestat.series.auxiliary_heat_3.name = 'Heat 3';
|
||||
beestat.series.auxiliary_heat_3.color = '#d35400';
|
||||
}
|
||||
|
||||
/*
|
||||
@ -196,7 +201,7 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
|
||||
*/
|
||||
new beestat.api()
|
||||
.add_call(
|
||||
'ecobee_runtime_thermostat',
|
||||
'runtime_thermostat',
|
||||
'sync',
|
||||
{
|
||||
'thermostat_id': thermostat.thermostat_id
|
||||
|
0
js/lib/highcharts/highcharts.js
Normal file → Executable file
0
js/lib/highcharts/highcharts.js
Normal file → Executable file
Loading…
x
Reference in New Issue
Block a user