13 |
14 | > 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.
15 |
16 |
17 | ## Demo
18 |
19 | See a demo of the app at demo.beestat.io.
20 |
21 |
22 | ## Run your own instance
23 |
24 | See Self-Hosting Instructions.
25 |
26 |
27 | ## Contributing
28 |
29 | Contributions, issues and feature requests are welcome.
30 |
31 |
32 | ## Author
33 |
34 | **Jon Ziebell**
35 |
36 | 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.
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | To report a security vulnerability, please email contact@beestat.io.
6 |
--------------------------------------------------------------------------------
/api/announcement.php:
--------------------------------------------------------------------------------
1 | [],
15 | 'public' => [
16 | 'read_id'
17 | ]
18 | ];
19 |
20 | public static $user_locked = false;
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/api/cora/api.php:
--------------------------------------------------------------------------------
1 | resource = get_class($this);
57 | $class_parts = explode('\\', $this->resource);
58 | $this->table = end($class_parts);
59 | $this->database = database::get_instance();
60 | $this->request = request::get_instance();
61 | $this->setting = setting::get_instance();
62 | $this->session = session::get_instance();
63 | }
64 |
65 | /**
66 | * Shortcut method for doing API calls within the API. This will create an
67 | * instance of the resource you want and call the method you want with the
68 | * arguments you want.
69 | *
70 | * @param string $resource The resource to use.
71 | * @param string $method The method to call.
72 | * @param mixed $arguments The arguments to send. If not an array then
73 | * assumes a single argument.
74 | *
75 | * @return mixed
76 | */
77 | public function api($resource, $method, $arguments = []) {
78 | if(is_array($arguments) === false) {
79 | $arguments = [$arguments];
80 | }
81 |
82 | $resource_instance = new $resource();
83 | return call_user_func_array([$resource_instance, $method], $arguments);
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/api/cora/api_log.php:
--------------------------------------------------------------------------------
1 | database->escape(ip2long($ip_address));
24 | $timestamp_escaped = $this->database->escape(
25 | date('Y-m-d H:i:s', $timestamp)
26 | );
27 |
28 | $query = '
29 | select
30 | count(*) `number_requests_since`
31 | from
32 | `api_log`
33 | where
34 | `ip_address` = ' . $ip_address_escaped . '
35 | and `timestamp` >= ' . $timestamp_escaped . '
36 | ';
37 |
38 | $result = $this->database->query($query);
39 | $row = $result->fetch_assoc();
40 |
41 | return $row['number_requests_since'];
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/api/cora/api_user.php:
--------------------------------------------------------------------------------
1 | reportable = $reportable;
19 | $this->extra_info = $extra_info;
20 | $this->rollback = $rollback;
21 | return parent::__construct($message, $code, null);
22 | }
23 |
24 | public function getReportable() {
25 | return $this->reportable;
26 | }
27 |
28 | public function getExtraInfo() {
29 | return $this->extra_info;
30 | }
31 |
32 | public function getRollback() {
33 | return $this->rollback;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/api/ecobee_api_cache.php:
--------------------------------------------------------------------------------
1 | get('beestat_root_uri') . 'api/?resource=ecobee&method=initialize&arguments=' . json_encode($arguments) . '&api_key=' . $setting->get('ecobee_api_key_local'));
28 |
29 | die();
30 |
--------------------------------------------------------------------------------
/api/external_api_cache.php:
--------------------------------------------------------------------------------
1 | session->get_user_id();
19 | $this->request->queue_database_action($this->resource, 'create', $attributes);
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/api/floor_plan.php:
--------------------------------------------------------------------------------
1 | [
12 | 'read_id',
13 | 'update',
14 | 'create',
15 | 'delete'
16 | ],
17 | 'public' => []
18 | ];
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/api/index.php:
--------------------------------------------------------------------------------
1 | process($data);
48 |
49 | // Useful function
50 | function array_median($array) {
51 | $count = count($array);
52 |
53 | if($count === 0) {
54 | return null;
55 | }
56 |
57 | $middle = floor($count / 2);
58 | sort($array, SORT_NUMERIC);
59 | $median = $array[$middle]; // assume an odd # of items
60 | // Handle the even case by averaging the middle 2 items
61 | if ($count % 2 == 0) {
62 | $median = ($median + $array[$middle - 1]) / 2;
63 | }
64 | return $median;
65 | }
66 |
67 | // Useful function
68 | function array_mean($array) {
69 | $count = count($array);
70 |
71 | if($count === 0) {
72 | return null;
73 | }
74 |
75 | return array_sum($array) / $count;
76 | }
77 |
78 | // Useful function
79 | function array_standard_deviation($array) {
80 | $count = count($array);
81 |
82 | if ($count === 0) {
83 | return null;
84 | }
85 |
86 | $mean = array_mean($array);
87 |
88 | $variance = 0;
89 | foreach($array as $i) {
90 | $variance += pow(($i - $mean), 2);
91 | }
92 |
93 | return round(sqrt($variance / $count), 2);
94 | }
95 |
96 | /**
97 | * Convert a local datetime string to a UTC datetime string.
98 | *
99 | * @param string $local_datetime Local datetime string.
100 | * @param string $local_time_zone The local time zone to convert from.
101 | *
102 | * @return string The UTC datetime string.
103 | */
104 | function get_utc_datetime($local_datetime, $local_time_zone, $format = 'Y-m-d H:i:s') {
105 | $local_time_zone = new DateTimeZone($local_time_zone);
106 | $utc_time_zone = new DateTimeZone('UTC');
107 | $date_time = new DateTime($local_datetime, $local_time_zone);
108 | $date_time->setTimezone($utc_time_zone);
109 |
110 | return $date_time->format($format);
111 | }
112 |
113 | /**
114 | * Convert a UTC datetime string to a local datetime string.
115 | *
116 | * @param string $utc_datetime Local datetime string.
117 | * @param string $local_time_zone The local time zone to convert from.
118 | *
119 | * @return string The local datetime string.
120 | */
121 | function get_local_datetime($utc_datetime, $local_time_zone, $format = 'Y-m-d H:i:s') {
122 | $local_time_zone = new DateTimeZone($local_time_zone);
123 | $utc_time_zone = new DateTimeZone('UTC');
124 | $date_time = new DateTime($utc_datetime, $utc_time_zone);
125 | $date_time->setTimezone($local_time_zone);
126 |
127 | return $date_time->format($format);
128 | }
129 |
--------------------------------------------------------------------------------
/api/mailgun.php:
--------------------------------------------------------------------------------
1 | [],
17 | 'public' => [
18 | 'subscribe',
19 | 'unsubscribe'
20 | ]
21 | ];
22 |
23 | /**
24 | * Send an API call off to mailgun
25 | *
26 | * @param string $method HTTP Method.
27 | * @param string $endpoint API Endpoint.
28 | * @param array $data API request data.
29 | *
30 | * @throws Exception If mailgun did not return valid JSON.
31 | *
32 | * @return array The mailgun response.
33 | */
34 | private function mailgun_api($method, $endpoint, $data) {
35 | $curl_response = $this->curl([
36 | 'url' => $this->setting->get('mailgun_base_url') . $endpoint,
37 | 'post_fields' => $data,
38 | 'method' => $method,
39 | 'header' => [
40 | 'Authorization: Basic ' . base64_encode('api:' . $this->setting->get('mailgun_api_key')),
41 | 'Content-Type: multipart/form-data'
42 | ]
43 | ]);
44 |
45 | $response = json_decode($curl_response, true);
46 |
47 | if ($response === null) {
48 | throw new cora\exception('Invalid JSON', 10600);
49 | }
50 |
51 | return $response;
52 | }
53 |
54 | /**
55 | * Subscribe to the mailing list.
56 | *
57 | * @param string $email_address The email address to subscribe.
58 | *
59 | * @throws exception If the subscribe failed.
60 | *
61 | * @return array Subscriber info.
62 | */
63 | public function subscribe($email_address) {
64 | $method = 'POST';
65 |
66 | $email_address = trim(strtolower($email_address));
67 | $endpoint = 'lists/' . $this->setting->get('mailgun_newsletter') . '/members';
68 |
69 | $data = [
70 | 'address' => $email_address,
71 | 'subscribed' => 'yes',
72 | 'upsert' => 'yes'
73 | ];
74 |
75 | $response = $this->mailgun_api($method, $endpoint, $data);
76 |
77 | if (
78 | isset($response['member']) &&
79 | isset($response['member']['subscribed']) &&
80 | $response['member']['subscribed'] === true
81 | ) {
82 | return $response['member'];
83 | } else {
84 | throw new cora\exception('Failed to subscribe.', 10601);
85 | }
86 | }
87 |
88 | /**
89 | * Unsubscribe from the mailing list.
90 | *
91 | * @param string $email_address The email address to unsubscribe.
92 | *
93 | * @throws exception If the unsubscribe failed.
94 | *
95 | * @return array Subscriber info.
96 | */
97 | public function unsubscribe($email_address) {
98 | $method = 'POST';
99 |
100 | $email_address = trim(strtolower($email_address));
101 | $endpoint = 'lists/' . $this->setting->get('mailgun_newsletter') . '/members';
102 |
103 | $data = [
104 | 'address' => $email_address,
105 | 'subscribed' => 'no',
106 | 'upsert' => 'yes'
107 | ];
108 |
109 | $response = $this->mailgun_api($method, $endpoint, $data);
110 |
111 | if (
112 | isset($response['member']) &&
113 | isset($response['member']['subscribed']) &&
114 | $response['member']['subscribed'] === false
115 | ) {
116 | return $response['member'];
117 | } else {
118 | throw new cora\exception('Failed to unsubscribe.', 10602);
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/api/mailgun_api_cache.php:
--------------------------------------------------------------------------------
1 | get('beestat_root_uri') . 'api/?resource=patreon&method=initialize&arguments=' . json_encode($arguments) . '&api_key=' . $setting->get('patreon_api_key_local'));
22 |
23 | die();
24 |
--------------------------------------------------------------------------------
/api/runtime_thermostat_text.php:
--------------------------------------------------------------------------------
1 | get([
36 | 'value' => $value
37 | ]);
38 |
39 | if ($runtime_text === null) {
40 | $runtime_text = $this->create([
41 | 'value' => $value
42 | ]);
43 | }
44 |
45 | // Cache the result.
46 | self::$query_cache[$value] = $runtime_text;
47 |
48 | return $runtime_text;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/api/sensor.php:
--------------------------------------------------------------------------------
1 | [
12 | 'read_id',
13 | 'sync'
14 | ],
15 | 'public' => []
16 | ];
17 |
18 | public static $cache = [
19 | 'sync' => 180 // 3 Minutes
20 | ];
21 |
22 | /**
23 | * Normal read_id, but filter out unsupported sensor types.
24 | *
25 | * @param array $attributes
26 | * @param array $columns
27 | *
28 | * @return array
29 | */
30 | public function read_id($attributes = [], $columns = []) {
31 | $sensors = parent::read_id($attributes, $columns);
32 |
33 | $return = [];
34 | foreach($sensors as $sensor) {
35 | if (
36 | in_array(
37 | $sensor['type'],
38 | ['ecobee3_remote_sensor', 'thermostat']
39 | ) === true
40 | ) {
41 | $return[$sensor['sensor_id']] = $sensor;
42 | } else if (
43 | in_array(
44 | $sensor['type'],
45 | ['monitor_sensor', 'control_sensor']
46 | ) === true
47 | ) {
48 | /**
49 | * Support for these sensor types is rudimentary. It's enough to get
50 | * temperature but some of these legacy sensors split a single sensor
51 | * up into multiple sensors. For example, thermostat temperature,
52 | * humidity, and motion are three different sensors. Newer sensors
53 | * show up as a single sensor with more than one capability. To make
54 | * sense of this I would have to do a good deal of merging etc in the
55 | * GUI.
56 | *
57 | * I don't really think it's worth the clutter or the time investment.
58 | */
59 | foreach($sensor['capability'] as $capability) {
60 | if($capability['type'] === 'temperature') {
61 | $return[$sensor['sensor_id']] = $sensor;
62 | }
63 | }
64 | }
65 | }
66 |
67 | return $return;
68 | }
69 |
70 | /**
71 | * Sync all sensors for the current user. If we fail to get a lock, fail
72 | * silently (catch the exception) and just return false.
73 | *
74 | * @return boolean true if the sync ran, false if not.
75 | */
76 | public function sync() {
77 | // Skip this for the demo
78 | if($this->setting->is_demo() === true) {
79 | return;
80 | }
81 |
82 | try {
83 | $lock_name = 'sensor->sync(' . $this->session->get_user_id() . ')';
84 | $this->database->get_lock($lock_name);
85 |
86 | $this->api('ecobee_sensor', 'sync');
87 |
88 | $this->api(
89 | 'user',
90 | 'update_sync_status',
91 | [
92 | 'key' => 'sensor'
93 | ]
94 | );
95 |
96 | $this->database->release_lock($lock_name);
97 | } catch(cora\exception $e) {
98 | return false;
99 | }
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/api/smarty_streets.php:
--------------------------------------------------------------------------------
1 | setting->get('smarty_streets_auth_id') === null ||
36 | $this->setting->get('smarty_streets_auth_token') === null
37 | ) {
38 | return null;
39 | }
40 |
41 | // Smarty has a different endpoint for USA vs International.
42 | if ($country === 'USA') {
43 | $url = 'https://us-street.api.smartystreets.com/street-address';
44 | $url .= '?' . http_build_query([
45 | 'street' => $address_string,
46 | 'auth-id' => $this->setting->get('smarty_streets_auth_id'),
47 | 'auth-token' => $this->setting->get('smarty_streets_auth_token')
48 | ]);
49 | } else {
50 | $url = 'https://international-street.api.smartystreets.com/verify';
51 | $url .= '?' . http_build_query([
52 | 'freeform' => $address_string,
53 | 'country' => $country,
54 | 'geocode' => 'true',
55 | 'auth-id' => $this->setting->get('smarty_streets_auth_id'),
56 | 'auth-token' => $this->setting->get('smarty_streets_auth_token')
57 | ]);
58 | }
59 |
60 | $curl_response = $this->curl([
61 | 'url' => $url
62 | ]);
63 |
64 | $response = json_decode($curl_response, true);
65 |
66 | if ($response === null || count($response) === 0) {
67 | return null;
68 | } else {
69 | // Smarty doesn't return this but I want it.
70 | if($country === 'USA') {
71 | $response[0]['components']['country_iso_3'] = 'USA';
72 | }
73 | return $response[0];
74 | }
75 | }
76 |
77 | /**
78 | * Generate a cache key from a URL. Just hashes it.
79 | *
80 | * @param array $arguments
81 | *
82 | * @return string
83 | */
84 | protected function generate_cache_key($arguments) {
85 | return sha1($arguments['url']);
86 | }
87 |
88 | /**
89 | * Determine whether or not a request should be cached. For this, cache
90 | * valid JSON responses with status code 200.
91 | *
92 | * @param array $arguments
93 | * @param string $curl_response
94 | * @param array $curl_info
95 | *
96 | * @return boolean
97 | */
98 | protected function should_cache($arguments, $curl_response, $curl_info) {
99 | return (
100 | $curl_info['http_code'] === 200 &&
101 | json_decode($curl_response, true) !== null
102 | );
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/api/smarty_streets_api_cache.php:
--------------------------------------------------------------------------------
1 | [],
12 | 'public' => []
13 | ];
14 |
15 | protected static $log_mysql = 'all';
16 |
17 | protected static $cache = false;
18 | protected static $cache_for = null;
19 |
20 | /**
21 | * Send an API call off to Stripe
22 | *
23 | * @param string $method HTTP Method.
24 | * @param string $endpoint API Endpoint.
25 | * @param array $data API request data.
26 | *
27 | * @throws Exception If Stripe did not return valid JSON.
28 | *
29 | * @return array The Stripe response.
30 | */
31 | public function stripe_api($method, $endpoint, $data) {
32 | $curl_response = $this->curl([
33 | 'url' => $this->setting->get('stripe_base_url') . $endpoint,
34 | 'post_fields' => http_build_query($data),
35 | 'method' => $method,
36 | 'header' => [
37 | 'Authorization: Basic ' . base64_encode($this->setting->get('stripe_secret_key') . ':'),
38 | 'Content-Type: application/x-www-form-urlencoded'
39 | ]
40 | ]);
41 |
42 | $response = json_decode($curl_response, true);
43 |
44 | if ($response === null) {
45 | throw new cora\exception('Invalid JSON', 10900);
46 | }
47 |
48 | return $response;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/api/stripe_api_cache.php:
--------------------------------------------------------------------------------
1 | [
12 | 'read_id'
13 | ],
14 | 'public' => []
15 | ];
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/api/stripe_payment_link.php:
--------------------------------------------------------------------------------
1 | [],
12 | 'public' => [
13 | 'open'
14 | ]
15 | ];
16 |
17 | public static $user_locked = false;
18 |
19 | /**
20 | * Get a stripe_payment_link for the specified attributes. If none exists,
21 | * create.
22 | *
23 | * @param array $attributes
24 | *
25 | * @return array
26 | */
27 | public function get($attributes) {
28 | $stripe_payment_link = parent::get([
29 | 'amount' => $attributes['amount'],
30 | 'currency' => $attributes['currency'],
31 | 'interval' => $attributes['interval']
32 | ]);
33 |
34 | if($stripe_payment_link === null) {
35 | $price = $this->api(
36 | 'stripe',
37 | 'stripe_api',
38 | [
39 | 'method' => 'POST',
40 | 'endpoint' => 'prices',
41 | 'data' => [
42 | 'product' => $this->setting->get('stripe_product_id'),
43 | 'unit_amount' => $attributes['amount'],
44 | 'currency' => $attributes['currency'],
45 | 'recurring[interval]' => $attributes['interval']
46 | ]
47 | ]
48 | );
49 |
50 | $payment_link = $this->api(
51 | 'stripe',
52 | 'stripe_api',
53 | [
54 | 'method' => 'POST',
55 | 'endpoint' => 'payment_links',
56 | 'data' => [
57 | 'line_items[0][price]' => $price['id'],
58 | 'line_items[0][quantity]' => '1'
59 | ]
60 | ]
61 | );
62 |
63 | return $this->create([
64 | 'amount' => $attributes['amount'],
65 | 'currency' => $attributes['currency'],
66 | 'interval' => $attributes['interval'],
67 | 'url' => $payment_link['url']
68 | ]);
69 | } else {
70 | return $stripe_payment_link;
71 | }
72 | }
73 |
74 | /**
75 | * Open a Stripe link. This exists because in JS it would be a popup to run
76 | * an API call to get the link, then do window.open after. This lets you
77 | * just do a window.open directly to this endpoint.
78 | *
79 | * @param array $attributes
80 | */
81 | public function open($attributes) {
82 | $stripe_payment_link = $this->get($attributes);
83 |
84 | $url = $stripe_payment_link['url'] .
85 | '?prefilled_email=' . $attributes['prefilled_email'] .
86 | '&client_reference_id=' . $attributes['client_reference_id'];
87 |
88 | header('Location: ' . $url);
89 | die();
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/favicon.ico
--------------------------------------------------------------------------------
/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/favicon.png
--------------------------------------------------------------------------------
/favicon_apple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/favicon_apple.png
--------------------------------------------------------------------------------
/font/material_icon/material_icon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/material_icon/material_icon.eot
--------------------------------------------------------------------------------
/font/material_icon/material_icon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/material_icon/material_icon.ttf
--------------------------------------------------------------------------------
/font/material_icon/material_icon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/material_icon/material_icon.woff
--------------------------------------------------------------------------------
/font/material_icon/material_icon.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/material_icon/material_icon.woff2
--------------------------------------------------------------------------------
/font/montserrat/montserrat_100.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_100.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_100.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_100.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_100.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_100.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_100.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_100.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_200.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_200.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_200.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_200.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_200.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_200.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_200.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_200.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_300.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_300.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_300.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_300.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_300.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_300.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_300.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_300.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_400.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_400.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_400.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_400.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_400.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_500.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_500.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_500.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_500.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_500.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_500.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_500.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_600.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_600.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_600.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_600.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_600.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_600.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_600.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_700.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_700.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_700.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_700.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_700.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_700.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_700.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_800.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_800.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_800.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_800.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_800.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_800.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_800.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_800.woff
--------------------------------------------------------------------------------
/font/montserrat/montserrat_900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_900.eot
--------------------------------------------------------------------------------
/font/montserrat/montserrat_900.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_900.otf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_900.ttf
--------------------------------------------------------------------------------
/font/montserrat/montserrat_900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/font/montserrat/montserrat_900.woff
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/logo.png
--------------------------------------------------------------------------------
/img/merchandise/sticker_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/merchandise/sticker_logo.png
--------------------------------------------------------------------------------
/img/merchandise/sticker_logo_text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/merchandise/sticker_logo_text.png
--------------------------------------------------------------------------------
/img/visualize/skybox/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/visualize/skybox/back.png
--------------------------------------------------------------------------------
/img/visualize/skybox/down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/visualize/skybox/down.png
--------------------------------------------------------------------------------
/img/visualize/skybox/front.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/visualize/skybox/front.png
--------------------------------------------------------------------------------
/img/visualize/skybox/left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/visualize/skybox/left.png
--------------------------------------------------------------------------------
/img/visualize/skybox/right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/visualize/skybox/right.png
--------------------------------------------------------------------------------
/img/visualize/skybox/up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/visualize/skybox/up.png
--------------------------------------------------------------------------------
/img/visualize/stripe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beestat/app/bf0e9a82728c1efedc10706c9360d4e44d165f0c/img/visualize/stripe.png
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | is_demo() === true) {
8 | setcookie(
9 | 'session_key',
10 | 'd31d3ef451fe65885928e5e1bdf4af321f702009',
11 | 4294967295,
12 | '/',
13 | 'demo.beestat.io',
14 | $setting->get('force_ssl'),
15 | true
16 | );
17 |
18 | // Just so I can make some simpler assumptions in app.php since the
19 | // superglobal is not updated when calling setcookie().
20 | $_COOKIE['session_key'] = 'd31d3ef451fe65885928e5e1bdf4af321f702009';
21 | }
22 |
23 | // If you're not logged in, just take you directly to the ecobee login page.
24 | if(isset($_COOKIE['session_key']) === false) {
25 | header('Location: https://' . $_SERVER['HTTP_HOST'] . '/api/?resource=ecobee&method=authorize&arguments={}&api_key=' . $setting->get('beestat_api_key_local'));
26 | die();
27 | }
28 |
29 | ?>
30 |
31 |
32 |
33 |
34 | beestat
35 |
36 |
37 |
38 |
39 |
40 |
41 | is_demo() === false) {
43 | echo '';
44 | echo '';
45 | }
46 | ?>
47 | get('commit') . '">';
50 | echo '';
51 |
52 | require 'js/js.php';
53 | ?>
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/js/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "globals": {
7 | "rocket": true,
8 | "$": true,
9 | "beestat": true,
10 | "moment": true,
11 | "Highcharts": true,
12 | "Sentry": true,
13 | "THREE": true,
14 | "SunCalc": true,
15 | "ClipperLib": true,
16 | "polylabel": true
17 | },
18 | "extends": "eslint:all",
19 | "rules": {
20 | "camelcase": "off",
21 | "capitalized-comments": "off",
22 | "complexity": "off",
23 | "consistent-this": ["error", "self"],
24 | "default-case": "off",
25 | "dot-location": ["error", "property"],
26 | "func-names": ["error", "never"],
27 | "function-call-argument-newline": "off",
28 | "function-paren-newline": "off",
29 | "guard-for-in": "off",
30 | "id-length": "off",
31 | "indent": ["error", 2],
32 | "init-declarations": "off",
33 | "linebreak-style": "off",
34 | "lines-around-comment": "off",
35 | "max-depth": "off",
36 | "max-len": "off",
37 | "max-lines": "off",
38 | "max-lines-per-function": "off",
39 | "max-params": ["error", 5],
40 | "max-statements": "off",
41 | "multiline-ternary": "off",
42 | "new-cap": ["error", {"newIsCap": false}],
43 | "newline-after-var": "off",
44 | "no-continue": "off",
45 | "no-extra-parens": "off",
46 | "no-lonely-if": "off",
47 | "no-loop-func": "off",
48 | "no-magic-numbers": "off",
49 | "no-multiple-empty-lines": ["warn", {"max": 1, "maxEOF": 1, "maxBOF": 0}],
50 | "no-negated-condition": "off",
51 | "no-plusplus": "off",
52 | "no-ternary": "off",
53 | "no-trailing-spaces": "warn",
54 | "no-undefined": "off",
55 | "no-underscore-dangle": "off",
56 | "object-curly-newline": "warn",
57 | "one-var": ["error", "never"],
58 | "padded-blocks": ["warn", {"blocks": "never", "classes": "never", "switches": "never"}],
59 | "quotes": ["error", "single"],
60 | "require-unicode-regexp": "off",
61 | "sort-keys": "off",
62 | "space-before-function-paren": ["error", "never"],
63 | "strict": "off",
64 | "valid-jsdoc": ["error", {"requireReturn": false, "requireParamDescription": false, "requireReturnDescription": false}],
65 | "vars-on-top": "off",
66 | "operator-assignment": "off",
67 | "no-else-return": "off",
68 | "prefer-object-spread": "off",
69 |
70 | // Node.js and CommonJS
71 | "callback-return": "off",
72 | "global-require": "off",
73 | "handle-callback-err": "off",
74 | "no-mixed-requires": "off",
75 | "no-new-require": "off",
76 | "no-path-concat": "off",
77 | "no-process-env": "off",
78 | "no-process-exit": "off",
79 | "no-restricted-modules": "off",
80 | "no-sync": "off",
81 |
82 | // ES6
83 | "arrow-body-style": "off",
84 | "arrow-parens": "off",
85 | "arrow-spacing": "off",
86 | "constructor-super": "off",
87 | "generator-star-spacing": "off",
88 | "no-class-assign": "off",
89 | "no-confusing-arrow": "off",
90 | "no-const-assign": "off",
91 | "no-dupe-class-members": "off",
92 | "no-duplicate-imports": "off",
93 | "no-new-symbol": "off",
94 | "no-restricted-imports": "off",
95 | "no-this-before-super": "off",
96 | "no-useless-computed-key": "off",
97 | "no-useless-constructor": "off",
98 | "no-useless-rename": "off",
99 | "no-var": "off",
100 | "object-shorthand": "off",
101 | "prefer-arrow-callback": "off",
102 | "prefer-const": "off",
103 | "prefer-destructuring": "off",
104 | "prefer-numeric-literals": "off",
105 | "prefer-rest-params": "off",
106 | "prefer-spread": "off",
107 | "prefer-template": "off",
108 | "require-yield": "off",
109 | "rest-spread-spacing": "off",
110 | "sort-imports": "off",
111 | "symbol-description": "off",
112 | "template-curly-spacing": "off",
113 | "yield-star-spacing": "off"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/js/beestat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Top-level namespace.
3 | */
4 | var beestat = {};
5 |
6 | beestat.ecobee_thermostat_models = {
7 | 'apolloEms': 'ecobee4 EMS',
8 | 'apolloSmart': 'ecobee4',
9 | 'athenaEms': 'ecobee3 EMS',
10 | 'athenaSmart': 'ecobee3',
11 | 'corSmart': 'Côr',
12 | 'idtEms': 'Smart EMS',
13 | 'idtSmart': 'Smart',
14 | 'nikeEms': 'ecobee3 lite EMS',
15 | 'nikeSmart': 'ecobee3 lite',
16 | 'siEms': 'Smart Si EMS',
17 | 'siSmart': 'Smart Si',
18 | 'vulcanSmart': 'Smart Thermostat',
19 | 'aresSmart': 'Smart Thermostat Premium',
20 | 'artemisSmart': 'Smart Thermostat Enhanced',
21 | 'attisPro': 'Smart Thermostat Lite',
22 | 'attisRetail': 'Smart Thermostat Essential'
23 | };
24 |
25 | /**
26 | * Get a default value for an argument if it is not currently set.
27 | *
28 | * @param {mixed} argument The argument to check.
29 | * @param {mixed} default_value The value to use if argument is undefined.
30 | *
31 | * @return {mixed}
32 | */
33 | beestat.default_value = function(argument, default_value) {
34 | return (argument === undefined) ? default_value : argument;
35 | };
36 |
37 | // Register service worker
38 | if ('serviceWorker' in navigator) {
39 | window.addEventListener('load', function() {
40 | navigator.serviceWorker.register('/service_worker.js').then(function(registration) {
41 |
42 | /*
43 | * Registration was successful
44 | * console.log('ServiceWorker registration successful with scope: ', registration.scope);
45 | */
46 | }, function(error) {
47 |
48 | /*
49 | * registration failed :(
50 | * console.log('ServiceWorker registration failed: ', err);
51 | */
52 | });
53 | });
54 | }
55 |
56 | /**
57 | * Dispatch the resize event every now and then.
58 | */
59 | window.addEventListener('resize', rocket.throttle(100, function() {
60 | beestat.dispatcher.dispatchEvent('resize');
61 | }));
62 |
63 | // First run
64 | var $ = rocket.extend(rocket.$, rocket);
65 | $.ready(function() {
66 | moment.suppressDeprecationWarnings = true;
67 | if (window.environment === 'live') {
68 | Sentry.init({
69 | 'release': window.commit,
70 | 'dsn': 'https://af9fd2cf6cda49dcb93dcaf02fe39fc6@sentry.io/3736982',
71 | 'ignoreErrors': ['window.webkit.messageHandlers'],
72 | 'integrations': [
73 | Sentry.replayIntegration({
74 | maskAllText: false,
75 | blockAllMedia: false,
76 | }),
77 | ],
78 | 'replaysSessionSampleRate': 0.01, // 1%
79 | 'replaysOnErrorSampleRate': 1.0, // 100%
80 | });
81 | }
82 | (new beestat.layer.load()).render();
83 | });
84 |
--------------------------------------------------------------------------------
/js/beestat/address.js:
--------------------------------------------------------------------------------
1 | beestat.address = {};
2 |
3 | /**
4 | * Get the parts of an address in two separate lines.
5 | *
6 | * @param {number} address_id
7 | *
8 | * @return {array}
9 | */
10 | beestat.address.get_lines = function(address_id) {
11 | const address = beestat.cache.address[address_id];
12 |
13 | if (address.normalized === null) {
14 | return null;
15 | }
16 |
17 | // US Address
18 | if (address.normalized.components.country_iso_3 === 'USA') {
19 | return [
20 | address.normalized.delivery_line_1,
21 | [
22 | address.normalized.components.city_name,
23 | address.normalized.components.state_abbreviation + ',',
24 | address.normalized.components.zipcode
25 | ].join(' ')
26 | ];
27 | } else {
28 | return [
29 | address.normalized.address1,
30 | address.normalized.address2
31 | ];
32 | }
33 | };
34 |
35 | /**
36 | * Get whether or not this address was validated and thus has address
37 | * components/metadata, is geocoded, etc.
38 | *
39 | * @param {number} address_id
40 | *
41 | * @return {boolean}
42 | */
43 | beestat.address.is_valid = function(address_id) {
44 | const address = beestat.cache.address[address_id];
45 | return address.normalized !== null;
46 | };
47 |
--------------------------------------------------------------------------------
/js/beestat/affiliate.js:
--------------------------------------------------------------------------------
1 | beestat.affiliate = {};
2 |
3 | beestat.affiliate.links = {
4 | 'bosch_glm20': {
5 | 'USA': 'https://amzn.to/3P3z2Ea',
6 | 'CAN': 'https://amzn.to/3RieCZx'
7 | },
8 | 'ecobee_smart_thermostat_premium': {
9 | 'USA': 'https://amzn.to/3A7vv3S',
10 | 'CAN': 'https://amzn.to/3R0spV0'
11 | },
12 | 'ecobee_smart_sensor_2_pack': {
13 | 'USA': 'https://amzn.to/3SprUVB',
14 | 'CAN': 'https://amzn.to/3pSh8tR'
15 | }
16 | };
17 |
18 | /**
19 | * Link getter for affiliate links in the currently active thermostat's
20 | * country. Defaults to USA.
21 | *
22 | * @param {string} type
23 | *
24 | * @return {string}
25 | */
26 | beestat.affiliate.get_link = function(type) {
27 | const thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
28 | let country_iso_3 = 'USA';
29 | if (thermostat.address_id !== null) {
30 | const address = beestat.cache.address[thermostat.address_id];
31 | if (beestat.address.is_valid(address.address_id) === true) {
32 | country_iso_3 = address.normalized.components.country_iso_3;
33 | }
34 | }
35 | return beestat.affiliate.links[type][country_iso_3] ||
36 | beestat.affiliate.links[type].USA;
37 | };
38 |
--------------------------------------------------------------------------------
/js/beestat/area.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Format a area in a number of different ways.
3 | *
4 | * @param {object} args Instructions on how to format:
5 | * area (required) - area to work with
6 | * output_area_unit (optional, default ft) - Output area unit; default matches setting.
7 | * convert (optional, default true) - Whether or not to convert to Celcius if necessary
8 | * round (optional, default 0) - Number of decimal points to round to
9 | * units (optional, default false) - Whether or not to include units in the result
10 | * type (optional, default number) - Type of value to return (string|number)
11 | *
12 | * @return {string} The formatted area.
13 | */
14 | beestat.area = function(args) {
15 | // Allow passing a single argument of area for convenience.
16 | if (typeof args !== 'object' || args === null) {
17 | args = {
18 | 'area': args
19 | };
20 | }
21 |
22 | var input_area_unit = beestat.default_value(
23 | args.input_area_unit,
24 | 'ft²'
25 | );
26 | var output_area_unit = beestat.default_value(
27 | args.output_area_unit,
28 | beestat.setting('units.area')
29 | );
30 | var round = beestat.default_value(args.round, 0);
31 | var units = beestat.default_value(args.units, false);
32 | var type = beestat.default_value(args.type, 'number');
33 |
34 | var area = parseFloat(args.area);
35 |
36 | // Check for invalid values.
37 | if (isNaN(area) === true || isFinite(area) === false) {
38 | return null;
39 | }
40 |
41 | const conversion_factors = {
42 | 'in²': {
43 | 'ft²': 0.00694444,
44 | 'm²': 0.00064516
45 | },
46 | 'ft²': {
47 | 'in²': 144,
48 | 'm²': 0.092903
49 | }
50 | };
51 |
52 | // Convert if necessary and asked for.
53 | if (input_area_unit !== output_area_unit) {
54 | area *= conversion_factors[input_area_unit][output_area_unit];
55 | }
56 |
57 | /*
58 | * Get to the appropriate number of decimal points. This will turn the number
59 | * into a string. Then do a couple silly operations to fix -0.02 from showing
60 | * up as -0.0 in string form.
61 | */
62 | area = area.toFixed(round);
63 | area = parseFloat(area);
64 | area = area.toFixed(round);
65 |
66 | /*
67 | * Convert the previous string back to a number if requested. Format matters
68 | * because HighCharts doesn't accept strings in some cases.
69 | */
70 | if (type === 'number' && units === false) {
71 | area = Number(area);
72 | }
73 |
74 | // Append units if asked for.
75 | if (units === true) {
76 | area = Number(area).toLocaleString() + ' ' + output_area_unit;
77 | }
78 |
79 | return area;
80 | };
81 |
--------------------------------------------------------------------------------
/js/beestat/cache.js:
--------------------------------------------------------------------------------
1 | beestat.cache = {
2 | 'data': {}
3 | };
4 |
5 | /**
6 | * Overwrite a cache key with new data. Dispatches an event when done.
7 | *
8 | * @param {string} key The cache key to update.
9 | * @param {object} value The data to be placed in that key.
10 | * @param {boolean} dispatch Whether or not to dispatch the event. Default
11 | * true.
12 | */
13 | beestat.cache.set = function(key, value, dispatch) {
14 | if (key.substring(0, 5) === 'data.') {
15 | beestat.cache.data[key.substring(5)] = value;
16 | } else {
17 | beestat.cache[key] = value;
18 | }
19 |
20 | if (dispatch !== false) {
21 | beestat.dispatcher.dispatchEvent('cache.' + key);
22 | }
23 | };
24 |
25 | /**
26 | * Delete data from the cache. Dispatches an event when done.
27 | *
28 | * @param {string} key The cache key to delete.
29 | * @param {boolean} dispatch Whether or not to dispatch the event. Default
30 | * true.
31 | */
32 | beestat.cache.delete = function(key, dispatch) {
33 | if (key.substring(0, 5) === 'data.') {
34 | delete beestat.cache.data[key.substring(5)];
35 | } else {
36 | delete beestat.cache[key];
37 | }
38 |
39 | if (dispatch !== false) {
40 | beestat.dispatcher.dispatchEvent('cache.' + key);
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/js/beestat/clone.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Performs a deep clone of a simple object.
3 | *
4 | * @param {Object} object The object to clone.
5 | *
6 | * @return {Object} The cloned object.
7 | */
8 | beestat.clone = function(object) {
9 | return JSON.parse(JSON.stringify(object));
10 | };
11 |
--------------------------------------------------------------------------------
/js/beestat/comparisons.js:
--------------------------------------------------------------------------------
1 | beestat.comparisons = {};
2 |
3 | /**
4 | * Based on the comparison settings chosen in the GUI, get the proper broken
5 | * out comparison attributes needed to make an API call.
6 | *
7 | * @return {object} The comparison attributes.
8 | */
9 | beestat.comparisons.get_attributes = function() {
10 | var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
11 | var address = beestat.cache.address[thermostat.address_id];
12 |
13 | var attributes = {};
14 |
15 | if (beestat.setting('comparison_property_type') === 'similar') {
16 | // Match structure type exactly.
17 | if (thermostat.property.structure_type !== null) {
18 | attributes.property_structure_type =
19 | thermostat.property.structure_type;
20 | }
21 |
22 | // Always a 10 year age delta on both sides.
23 | if (thermostat.property.age !== null) {
24 | var property_age_delta = 10;
25 | var min_property_age = Math.max(
26 | 0,
27 | thermostat.property.age - property_age_delta
28 | );
29 | var max_property_age = thermostat.property.age + property_age_delta;
30 | attributes.property_age = {
31 | 'operator': 'between',
32 | 'value': [
33 | min_property_age,
34 | max_property_age
35 | ]
36 | };
37 | }
38 |
39 | // Always a 1000ft² size delta on both sides (total 2000 ft²).
40 | if (thermostat.property.square_feet !== null) {
41 | var property_square_feet_delta = 1000;
42 | var min_property_square_feet = Math.max(
43 | 0,
44 | thermostat.property.square_feet - property_square_feet_delta
45 | );
46 | var max_property_square_feet =
47 | thermostat.property.square_feet +
48 | property_square_feet_delta;
49 | attributes.property_square_feet = {
50 | 'operator': 'between',
51 | 'value': [
52 | min_property_square_feet,
53 | max_property_square_feet
54 | ]
55 | };
56 | }
57 |
58 | /*
59 | * If 0 or 1 stories, then 1 story, else just more than one story.
60 | * Apartments ignore this.
61 | */
62 | if (
63 | thermostat.property.stories !== null &&
64 | thermostat.property.structure_type !== 'apartment'
65 | ) {
66 | if (thermostat.property.stories < 2) {
67 | attributes.property_stories = thermostat.property.stories;
68 | } else {
69 | attributes.property_stories = {
70 | 'operator': '>=',
71 | 'value': thermostat.property.stories
72 | };
73 | }
74 | }
75 | } else if (beestat.setting('comparison_property_type') === 'same_structure') {
76 | // Match structure type exactly.
77 | if (thermostat.property.structure_type !== null) {
78 | attributes.property_structure_type =
79 | thermostat.property.structure_type;
80 | }
81 | }
82 |
83 | if (
84 | beestat.address.is_valid(address.address_id) === true &&
85 | beestat.setting('comparison_region') !== 'global'
86 | ) {
87 | attributes.radius = {
88 | 'operator': '<',
89 | 'value': 250
90 | };
91 | }
92 |
93 | return attributes;
94 | };
95 |
--------------------------------------------------------------------------------
/js/beestat/crypto.js:
--------------------------------------------------------------------------------
1 | // Polyfill for this. Ran into a user with Safari 15.2
2 | // https://github.com/ungap/random-uuid/blob/main/index.js
3 | if (typeof crypto === 'undefined')
4 | var crypto = require('crypto');
5 |
6 | if (!('randomUUID' in crypto))
7 | // https://stackoverflow.com/a/2117523/2800218
8 | // LICENSE: https://creativecommons.org/licenses/by-sa/4.0/legalcode
9 | crypto.randomUUID = function randomUUID() {
10 | return (
11 | [1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,
12 | c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/js/beestat/date.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Format a date.
3 | *
4 | * @param {object} args Instructions on how to format:
5 | * date (required) - Temperature to work with
6 | *
7 | * @return {string} The formatted date.
8 | */
9 | beestat.date = function(args) {
10 | // Allow passing a single argument of date for convenience.
11 | if (typeof args !== 'object' || args === null) {
12 | args = {
13 | 'date': args
14 | };
15 | }
16 |
17 | const m = moment(args.date);
18 | if (
19 | args.date !== undefined &&
20 | m.isValid() === true
21 | ) {
22 | return m.format(beestat.setting('date_format'));
23 | } else {
24 | return '';
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/js/beestat/debounce.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns a function, that, as long as it continues to be invoked, will not
3 | * be triggered.
4 | *
5 | * @param {Function} func The function to call.
6 | * @param {number} wait The function will be called after it stops being
7 | * called for N milliseconds.
8 | * @param {boolean} immediate Trigger the function on the leading edge,
9 | * instead of the trailing.
10 | *
11 | * @link https://davidwalsh.name/javascript-debounce-function
12 | *
13 | * @return {Function} The debounced function.
14 | */
15 | beestat.debounce = function(func, wait, immediate) {
16 | var timeout;
17 | return function() {
18 | var self = this;
19 | var args = arguments;
20 | var later = function() {
21 | timeout = null;
22 | if (!immediate) {
23 | func.apply(self, args);
24 | }
25 | };
26 | var callNow = immediate && !timeout;
27 | window.clearTimeout(timeout);
28 | timeout = window.setTimeout(later, wait);
29 | if (callNow) {
30 | func.apply(self, args);
31 | }
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/js/beestat/dispatcher.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Simple global event dispatcher. Anything can use this to dispatch events by
3 | * calling beestat.dispatcher.dispatchEvent('{{event_name}}') which anything
4 | * else can be listening for.
5 | */
6 | beestat.dispatcher_ = function() {
7 | // Class only exists so it can extend rocket.EventTarget.
8 | };
9 | beestat.extend(beestat.dispatcher_, rocket.EventTarget);
10 | beestat.dispatcher = new beestat.dispatcher_();
11 |
12 | /**
13 | * Do the normal event listener stuff. Extends the rocket version just a bit
14 | * to allow passing multiple event types at once for brevity.
15 | *
16 | * @param {string|array} type The event type or an array of event types.
17 | * @param {Function} listener Event Listener.
18 | *
19 | * @return {beestat.dispatcher_} this.
20 | */
21 | beestat.dispatcher_.prototype.addEventListener = function(type, listener) {
22 | if (typeof type === 'object') {
23 | for (var i = 0; i < type.length; i++) {
24 | rocket.EventTarget.prototype.addEventListener.apply(this, [
25 | type[i],
26 | listener
27 | ]);
28 | }
29 | } else {
30 | rocket.EventTarget.prototype.addEventListener.apply(this, arguments);
31 | }
32 | return this;
33 | };
34 |
--------------------------------------------------------------------------------
/js/beestat/distance.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Format a distance in a number of different ways.
3 | *
4 | * @param {object} args Instructions on how to format:
5 | * distance (required) - distance to work with
6 | * output_distance_unit (optional, default ft) - Output distance unit; default matches setting.
7 | * convert (optional, default true) - Whether or not to convert to Celcius if necessary
8 | * round (optional, default 1) - Number of decimal points to round to
9 | * units (optional, default false) - Whether or not to include units in the result
10 | * type (optional, default number) - Type of value to return (string|number)
11 | *
12 | * @return {string} The formatted distance.
13 | */
14 | beestat.distance = function(args) {
15 | // Allow passing a single argument of distance for convenience.
16 | if (typeof args !== 'object' || args === null) {
17 | args = {
18 | 'distance': args
19 | };
20 | }
21 |
22 | var input_distance_unit = beestat.default_value(
23 | args.input_distance_unit,
24 | 'in'
25 | );
26 | var output_distance_unit = beestat.default_value(
27 | args.output_distance_unit,
28 | beestat.setting('units.distance')
29 | );
30 | var round = beestat.default_value(args.round, 1);
31 | var units = beestat.default_value(args.units, false);
32 | var type = beestat.default_value(args.type, 'number');
33 |
34 | var distance = parseFloat(args.distance);
35 |
36 | // Check for invalid values.
37 | if (isNaN(distance) === true || isFinite(distance) === false) {
38 | return null;
39 | }
40 |
41 | const conversion_factors = {
42 | 'in': {
43 | 'ft': 0.0833,
44 | 'm': 0.0254
45 | },
46 | 'm': {
47 | 'in': 39.3701,
48 | 'ft': 3.28084
49 | },
50 | 'ft': {
51 | 'm': 0.3048,
52 | 'in': 12
53 | }
54 | };
55 |
56 | // Convert if necessary and asked for.
57 | if (input_distance_unit !== output_distance_unit) {
58 | distance *= conversion_factors[input_distance_unit][output_distance_unit];
59 | }
60 |
61 | /*
62 | * Get to the appropriate number of decimal points. This will turn the number
63 | * into a string. Then do a couple silly operations to fix -0.02 from showing
64 | * up as -0.0 in string form.
65 | */
66 | distance = distance.toFixed(round);
67 | distance = parseFloat(distance);
68 | distance = distance.toFixed(round);
69 |
70 | /*
71 | * Convert the previous string back to a number if requested. Format matters
72 | * because HighCharts doesn't accept strings in some cases.
73 | */
74 | if (type === 'number' && units === false) {
75 | distance = Number(distance);
76 | }
77 |
78 | // Append units if asked for.
79 | if (units === true) {
80 | distance = Number(distance).toLocaleString() + ' ' + output_distance_unit;
81 | }
82 |
83 | return distance;
84 | };
85 |
--------------------------------------------------------------------------------
/js/beestat/ecobee.js:
--------------------------------------------------------------------------------
1 | beestat.ecobee = {};
2 |
3 | /**
4 | * Check to see if ecobee is down. If so, render the footer component.
5 | */
6 | beestat.ecobee.notify_if_down = function() {
7 | // Turning this off to review and/or deprecate.
8 | return;
9 |
10 | if (
11 | beestat.cache !== undefined &&
12 | beestat.cache.thermostat !== undefined &&
13 | beestat.user.get() !== undefined
14 | ) {
15 | var last_update = moment.utc(beestat.user.get().sync_status.thermostat);
16 | var down = last_update.isBefore(moment().subtract(15, 'minutes'));
17 |
18 | if (beestat.ecobee.down_notification_ === undefined) {
19 | beestat.ecobee.down_notification_ = new beestat.component.down_notification();
20 | }
21 |
22 | if (
23 | down === true &&
24 | window.is_demo === false
25 | ) {
26 | beestat.ecobee.down_notification_.render($('body'));
27 | } else {
28 | beestat.ecobee.down_notification_.dispose();
29 | }
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/js/beestat/error.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Pop up a modal error message.
3 | *
4 | * @param {string} message The human-readable message.
5 | * @param {string} detail Technical error details.
6 | */
7 | beestat.error = function(message, detail) {
8 | var exception_modal = new beestat.component.modal.error();
9 | exception_modal.set_message(message);
10 | exception_modal.set_detail(detail);
11 | exception_modal.render();
12 | };
13 |
--------------------------------------------------------------------------------
/js/beestat/extend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Extends one class with another.
3 | *
4 | * @link https://oli.me.uk/2013/06/01/prototypical-inheritance-done-right/
5 | *
6 | * @param {Function} destination The class that should be inheriting things.
7 | * @param {Function} source The parent class that should be inherited from.
8 | *
9 | * @return {Object} The prototype of the parent.
10 | */
11 | beestat.extend = function(destination, source) {
12 | destination.prototype = Object.create(source.prototype);
13 | destination.prototype.constructor = destination;
14 |
15 | return source.prototype;
16 | };
17 |
--------------------------------------------------------------------------------
/js/beestat/math.js:
--------------------------------------------------------------------------------
1 | beestat.math = {};
2 |
3 | /**
4 | * Get a linear trendline from a set of data.
5 | *
6 | * @param {Object} data The data; at least two points required.
7 | *
8 | * @return {Object} The slope and intercept of the trendline.
9 | */
10 | beestat.math.get_linear_trendline = function(data) {
11 | // Requires at least two points.
12 | if (Object.keys(data).length < 2) {
13 | return null;
14 | }
15 |
16 | var sum_x = 0;
17 | var sum_y = 0;
18 | var sum_xy = 0;
19 | var sum_x_squared = 0;
20 | var n = 0;
21 |
22 | for (var x in data) {
23 | x = parseFloat(x);
24 | var y = parseFloat(data[x]);
25 |
26 | sum_x += x;
27 | sum_y += y;
28 | sum_xy += (x * y);
29 | sum_x_squared += Math.pow(x, 2);
30 | n++;
31 | }
32 |
33 | var slope = ((n * sum_xy) - (sum_x * sum_y)) /
34 | ((n * sum_x_squared) - (Math.pow(sum_x, 2)));
35 | var intercept = ((sum_y) - (slope * sum_x)) / (n);
36 |
37 | return {
38 | 'slope': slope,
39 | 'intercept': intercept
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/js/beestat/platform.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Determine what platform the app is being accessed from. Defaults to
3 | * "desktop" if "android" or "ios" are not specified in the browser query
4 | * string.
5 | *
6 | * @return {string} The platform.
7 | */
8 | beestat.platform = function() {
9 | const platform = new URLSearchParams(window.location.search).get('platform');
10 |
11 | switch (platform) {
12 | case 'android':
13 | case 'ios':
14 | return platform;
15 | }
16 |
17 | return 'browser';
18 | };
19 |
--------------------------------------------------------------------------------
/js/beestat/poll.js:
--------------------------------------------------------------------------------
1 | beestat.enable_poll = function() {
2 | window.clearTimeout(beestat.poll_timeout);
3 | beestat.poll_timeout = window.setTimeout(
4 | beestat.poll,
5 | 60000 * 5
6 | );
7 | };
8 |
9 | beestat.enable_poll_watcher = function() {
10 | window.clearTimeout(beestat.poll_watcher_timeout);
11 | beestat.poll_watcher_timeout = window.setTimeout(
12 | beestat.poll_watcher,
13 | 1000
14 | );
15 | };
16 |
17 | /**
18 | * Check every second for when the last successful poll was. Used for when the
19 | * app is sent to the background and the polling stops to ensure an update is
20 | * run immediately.
21 | */
22 | beestat.poll_watcher = function() {
23 | if (
24 | beestat.poll_last !== undefined &&
25 | beestat.poll_last.isBefore(moment().subtract(6, 'minute')) === true
26 | ) {
27 | window.clearTimeout(beestat.poll_timeout);
28 | beestat.poll();
29 | }
30 |
31 | beestat.enable_poll_watcher();
32 | };
33 |
34 | /**
35 | * Poll the database for changes and update the cache.
36 | */
37 | beestat.poll = function() {
38 | beestat.poll_last = moment();
39 |
40 | var api = new beestat.api();
41 |
42 | api.add_call(
43 | 'thermostat',
44 | 'sync',
45 | {},
46 | 'thermostat_sync'
47 | );
48 |
49 | api.add_call(
50 | 'sensor',
51 | 'sync',
52 | {},
53 | 'sensor_sync'
54 | );
55 |
56 | api.add_call(
57 | 'user',
58 | 'read_id',
59 | {},
60 | 'user'
61 | );
62 |
63 | api.add_call(
64 | 'thermostat',
65 | 'read_id',
66 | {
67 | 'attributes': {
68 | 'inactive': 0
69 | }
70 | },
71 | 'thermostat'
72 | );
73 |
74 | api.add_call(
75 | 'sensor',
76 | 'read_id',
77 | {
78 | 'attributes': {
79 | 'inactive': 0
80 | }
81 | },
82 | 'sensor'
83 | );
84 |
85 | api.add_call(
86 | 'ecobee_thermostat',
87 | 'read_id',
88 | {
89 | 'attributes': {
90 | 'inactive': 0
91 | }
92 | },
93 | 'ecobee_thermostat'
94 | );
95 |
96 | api.add_call(
97 | 'ecobee_sensor',
98 | 'read_id',
99 | {
100 | 'attributes': {
101 | 'inactive': 0
102 | }
103 | },
104 | 'ecobee_sensor'
105 | );
106 |
107 | api.set_callback(function(response) {
108 | beestat.cache.set('user', response.user);
109 | beestat.cache.set('thermostat', response.thermostat);
110 | beestat.cache.set('sensor', response.sensor);
111 | beestat.cache.set('ecobee_thermostat', response.ecobee_thermostat);
112 | beestat.cache.set('ecobee_sensor', response.ecobee_sensor);
113 |
114 | beestat.enable_poll();
115 |
116 | beestat.ecobee.notify_if_down();
117 | });
118 |
119 | api.send();
120 |
121 | /**
122 | * Send this every poll but don't specifically do anything with the
123 | * response. The caching won't allow it to send every time, but it should at
124 | * least keep up.
125 | */
126 | new beestat.api()
127 | .add_call(
128 | 'runtime',
129 | 'sync',
130 | {
131 | 'thermostat_id': beestat.setting('thermostat_id')
132 | }
133 | )
134 | .set_callback(function(response, from_cache) {
135 | if (from_cache === false) {
136 | // Delete this cached data so the charts update.
137 | beestat.cache.delete('data.runtime_thermostat_detail__runtime_thermostat');
138 | beestat.cache.delete('data.runtime_sensor_detail__runtime_thermostat');
139 | beestat.cache.delete('data.runtime_sensor_detail__runtime_sensor');
140 | beestat.cache.delete('data.air_quality_detail__runtime_thermostat');
141 | beestat.cache.delete('data.air_quality_detail__runtime_sensor');
142 | beestat.cache.delete('data.three_d__runtime_sensor');
143 | beestat.cache.delete('data.three_d__runtime_thermostat');
144 | }
145 | })
146 | .send();
147 | };
148 |
--------------------------------------------------------------------------------
/js/beestat/requestor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * If you want some data from the API in the cache this is the preferred way
3 | * to get it there. It will queue requests and if two things make the same API
4 | * call it will collapse them into a single API call.
5 | *
6 | * This is helpful for de-duplicating API calls if two cards need the same data.
7 | */
8 | beestat.requestor = {};
9 |
10 | beestat.requestor.requested_api_calls_ = [];
11 |
12 | beestat.requestor.sending_ = false;
13 |
14 | beestat.requestor.timeout_ = undefined;
15 |
16 | /**
17 | * Adds the requested API calls to the request stack, then waits 100ms for any
18 | * more to be added before executing them.
19 | *
20 | * @param {array} api_calls The API calls to request.
21 | */
22 | beestat.requestor.request = function(api_calls) {
23 | // Clear the timeout that was set to run the pending API calls.
24 | window.clearTimeout(beestat.requestor.timeout_);
25 |
26 | api_calls.forEach(function(api_call) {
27 | beestat.requestor.requested_api_calls_.push(api_call);
28 | });
29 |
30 | /**
31 | * If we aren't already sending, queue up the next API call to go in 100ms.
32 | * If we are actively sending, the next API call will get queued up after
33 | * it's done.
34 | */
35 | if (beestat.requestor.sending_ === false) {
36 | beestat.requestor.timeout_ = window.setTimeout(beestat.requestor.send, 3000);
37 | }
38 | };
39 |
40 | /**
41 | * Send all of the pending API calls.
42 | */
43 | beestat.requestor.send = function() {
44 | beestat.requestor.sending_ = true;
45 |
46 | const api = new beestat.api();
47 |
48 | // Force a batch API call to make the response handling simpler.
49 | api.force_batch();
50 |
51 | beestat.requestor.requested_api_calls_.forEach(function(requested_api_call) {
52 | api.add_call(
53 | requested_api_call.resource,
54 | requested_api_call.method,
55 | requested_api_call.arguments
56 | );
57 | });
58 |
59 | api.set_callback(function(response) {
60 | beestat.requestor.callback(response, api);
61 | });
62 |
63 | api.send();
64 | };
65 |
66 | beestat.requestor.callback = function(response, api) {
67 | /**
68 | * Data from the API calls is first merged into a holding object so it can
69 | * be merged into the cache in a single call.
70 | */
71 | const data = {};
72 |
73 | // Remove sent API calls from the request stack.
74 | api.get_api_calls().forEach(function(sent_api_call, i) {
75 | if (data[sent_api_call.resource] === undefined) {
76 | data[sent_api_call.resource] = {};
77 | }
78 |
79 | console.info('Performance might be better with concat');
80 | Object.assign(data[sent_api_call.resource], response[i]);
81 |
82 | /**
83 | * Remove API call sfrom the requested_api_calls array that have now been
84 | * sent.
85 | */
86 | let j = beestat.requestor.requested_api_calls_.length;
87 | while (j--) {
88 | if (
89 | sent_api_call.resource === beestat.requestor.requested_api_calls_[j].resource &&
90 | sent_api_call.method === beestat.requestor.requested_api_calls_[j].method &&
91 | sent_api_call.arguments === JSON.stringify(beestat.requestor.requested_api_calls_[j].arguments)
92 | ) {
93 | beestat.requestor.requested_api_calls_.splice(j, 1);
94 | }
95 | }
96 | });
97 |
98 | // Update the cache
99 | for (const key in data) {
100 | beestat.cache.set(key, data[key]);
101 | }
102 |
103 | beestat.requestor.sending_ = false;
104 |
105 | /**
106 | * If there are any API calls left to send, queue them up now. These would
107 | * have been added between when the API call started and finished.
108 | */
109 | if (beestat.requestor.requested_api_calls_.length > 0) {
110 | beestat.requestor.timeout_ = window.setTimeout(beestat.requestor.send, 3000);
111 | }
112 | };
113 |
--------------------------------------------------------------------------------
/js/beestat/temperature.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Format a temperature in a number of different ways. Default settings will
3 | * return a number converted to Celcius if necessary and rounded to one decimal
4 | * place.
5 | *
6 | * @param {object} args Instructions on how to format:
7 | * temperature (required) - Temperature to work with
8 | * input_temperature_unit (optional, default °F) - Input temperature unit
9 | * output_temperature_unit (optional, default current setting) - Output temperature unit; default matches setting.
10 | * convert (optional, default true) - Whether or not to convert to Celcius if necessary
11 | * delta (optional, default false) - Whether or not the convert action is for a delta instead of a normal value
12 | * round (optional, default 1) - Number of decimal points to round to
13 | * units (optional, default false) - Whether or not to include units in the result
14 | * type (optional, default number) - Type of value to return (string|number)
15 | *
16 | * @return {string} The formatted temperature.
17 | */
18 | beestat.temperature = function(args) {
19 | // Allow passing a single argument of temperature for convenience.
20 | if (typeof args !== 'object' || args === null) {
21 | args = {
22 | 'temperature': args
23 | };
24 | }
25 |
26 | var input_temperature_unit = beestat.default_value(
27 | args.input_temperature_unit,
28 | '°F'
29 | );
30 | var output_temperature_unit = beestat.default_value(
31 | args.output_temperature_unit,
32 | beestat.setting('units.temperature')
33 | );
34 | var delta = beestat.default_value(args.delta, false);
35 | var round = beestat.default_value(args.round, 1);
36 | var units = beestat.default_value(args.units, false);
37 | var type = beestat.default_value(args.type, 'number');
38 |
39 | var temperature = parseFloat(args.temperature);
40 |
41 | // Check for invalid values.
42 | if (isNaN(temperature) === true || isFinite(temperature) === false) {
43 | return null;
44 | }
45 |
46 | // Convert to Celcius if necessary and asked for.
47 | if (input_temperature_unit !== output_temperature_unit) {
48 | if (input_temperature_unit === '°F') {
49 | if (delta === true) {
50 | temperature *= (5 / 9);
51 | } else {
52 | temperature = (temperature - 32) * (5 / 9);
53 | }
54 | } else if (input_temperature_unit === '°C') {
55 | if (delta === true) {
56 | temperature *= (9 / 5);
57 | } else {
58 | temperature = (temperature * (9 / 5)) + 32;
59 | }
60 | }
61 | }
62 |
63 | /*
64 | * Get to the appropriate number of decimal points. This will turn the number
65 | * into a string. Then do a couple silly operations to fix -0.02 from showing
66 | * up as -0.0 in string form.
67 | */
68 | temperature = temperature.toFixed(round);
69 | temperature = parseFloat(temperature);
70 | temperature = temperature.toFixed(round);
71 |
72 | /*
73 | * Convert the previous string back to a number if requested. Format matters
74 | * because HighCharts doesn't accept strings in some cases.
75 | */
76 | if (type === 'number' && units === false) {
77 | temperature = Number(temperature);
78 | }
79 |
80 | // Append units if asked for.
81 | if (units === true) {
82 | temperature += output_temperature_unit;
83 | }
84 |
85 | return temperature;
86 | };
87 |
--------------------------------------------------------------------------------
/js/beestat/text_dimensions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Get the dimensions of a text string.
3 | *
4 | * @param {string} text
5 | * @param {number} font_size
6 | * @param {number} font_weight
7 | *
8 | * @return {number}
9 | */
10 | beestat.text_dimensions = function(text, font_size, font_weight) {
11 | const div = document.createElement('div');
12 | div.style.fontSize = font_size + 'px';
13 | div.style.fontWeight = font_weight;
14 | div.style.position = 'absolute';
15 | div.style.left = -1000;
16 | div.style.top = -1000;
17 |
18 | div.textContent = text;
19 |
20 | document.body.appendChild(div);
21 |
22 | const bounding_box = div.getBoundingClientRect();
23 |
24 | document.body.removeChild(div);
25 |
26 | return {
27 | 'width': bounding_box.width,
28 | 'height': bounding_box.height
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/js/beestat/time.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Get a nice resresentation of a time duration.
3 | *
4 | * @param {number} seconds
5 | * @param {string} opt_unit Any unit that moment supports when creating
6 | * durations. If left out defaults to seconds.
7 | *
8 | * @return {string} A humanized duration string.
9 | */
10 | beestat.time = function(seconds, opt_unit) {
11 | var duration = moment.duration(seconds, opt_unit || 'seconds');
12 |
13 | var hours = Math.floor(duration.asHours());
14 | var minutes = duration.get('minutes');
15 |
16 | return hours + 'h ' + minutes + 'm';
17 | };
18 |
--------------------------------------------------------------------------------
/js/beestat/user.js:
--------------------------------------------------------------------------------
1 | beestat.user = {};
2 |
3 | /**
4 | * Determine whether or not the current user is an active Patron.
5 | *
6 | * @return {boolean}
7 | */
8 | beestat.user.patreon_is_active = function() {
9 | const user = beestat.user.get();
10 | return (
11 | user.patreon_status !== null &&
12 | user.patreon_status.patron_status === 'active_patron'
13 | );
14 | };
15 |
16 | /**
17 | * Determine whether or not the current user is an active Stripe giver.
18 | *
19 | * @return {boolean}
20 | */
21 | beestat.user.stripe_is_active = function() {
22 | const stripe_events = Object.values(beestat.cache.stripe_event);
23 | for (let i = 0; i < stripe_events.length; i++) {
24 | if (
25 | stripe_events[i].type === 'invoice.paid' &&
26 | // This is a bug. It counts anyone who has contributed ever via stripe as a supporter.
27 | moment.unix(stripe_events[i].data.period_end).isAfter(moment()) === false
28 | ) {
29 | return true;
30 | }
31 | }
32 |
33 | return false;
34 | };
35 |
36 | /**
37 | * Determine whether or not the current user is an active contributor.
38 | *
39 | * @return {boolean}
40 | */
41 | beestat.user.contribution_is_active = function() {
42 | return beestat.user.patreon_is_active() === true ||
43 | beestat.user.stripe_is_active() === true;
44 | };
45 |
46 | /**
47 | * Is the user connected to Patreon.
48 | *
49 | * @return {boolean} true if yes, false if no.
50 | */
51 | beestat.user.patreon_is_connected = function() {
52 | return beestat.user.get().patreon_status !== null;
53 | };
54 |
55 | /**
56 | * Whether or not the current user gets access to early release features.
57 | *
58 | * @return {boolean}
59 | */
60 | beestat.user.has_early_access = function() {
61 | const user = beestat.user.get();
62 | return user.user_id === 1 ||
63 | beestat.user.contribution_is_active() === true;
64 | };
65 |
66 | /**
67 | * Get the current user.
68 | *
69 | * @return {object} The current user.
70 | */
71 | beestat.user.get = function() {
72 | const user_id = Object.keys(beestat.cache.user)[0];
73 | return beestat.cache.user[user_id];
74 | };
75 |
--------------------------------------------------------------------------------
/js/component.js:
--------------------------------------------------------------------------------
1 | beestat.component = function() {
2 | const self = this;
3 |
4 | this.rendered_ = false;
5 |
6 | // Give every component a state object to use for storing data.
7 | this.state_ = {};
8 |
9 | this.layer_ = beestat.current_layer;
10 |
11 | if (this.rerender_on_resize_ === true) {
12 | beestat.dispatcher.addEventListener('resize', function() {
13 | self.rerender();
14 | });
15 | }
16 | };
17 | beestat.extend(beestat.component, rocket.EventTarget);
18 |
19 | /**
20 | * First put everything in a container, then append the new container. This
21 | * prevents the child from having to worry about multiple redraws since they
22 | * aren't doing anything directly on the body.
23 | *
24 | * @param {rocket.Elements} parent
25 | *
26 | * @return {beestat.component} This
27 | */
28 | beestat.component.prototype.render = function(parent) {
29 | if (this.rendered_ === false) {
30 | var self = this;
31 |
32 | if (parent !== undefined) {
33 | this.component_container_ = $.createElement('div');
34 | Object.assign(this.component_container_[0].style, Object.assign(
35 | {
36 | 'position': 'relative'
37 | },
38 | this.style_
39 | ));
40 | this.decorate_(this.component_container_);
41 | parent.appendChild(this.component_container_);
42 | } else {
43 | this.decorate_();
44 | }
45 |
46 | // The element should now exist on the DOM.
47 | window.setTimeout(function() {
48 | self.dispatchEvent('render');
49 | }, 0);
50 |
51 | // The render function was called.
52 | this.rendered_ = true;
53 | }
54 |
55 | return this;
56 | };
57 |
58 | /**
59 | * First put everything in a container, then append the new container. This
60 | * prevents the child from having to worry about multiple redraws since they
61 | * aren't doing anything directly on the body.
62 | *
63 | * @return {beestat.component} This
64 | */
65 | beestat.component.prototype.rerender = function() {
66 | if (this.rendered_ === true) {
67 | this.rendered_ = false;
68 |
69 | var new_container = $.createElement('div')
70 | .style('position', 'relative');
71 | this.decorate_(new_container);
72 | this.component_container_
73 | .parentNode().replaceChild(new_container, this.component_container_);
74 | this.component_container_ = new_container;
75 |
76 | var self = this;
77 | window.setTimeout(function() {
78 | self.dispatchEvent('render');
79 | }, 0);
80 |
81 | this.rendered_ = true;
82 | }
83 | return this;
84 | };
85 |
86 | /**
87 | * Remove this component from the page.
88 | */
89 | beestat.component.prototype.dispose = function() {
90 | if (this.rendered_ === true) {
91 | var child = this.component_container_;
92 | var parent = child.parentNode();
93 | parent.removeChild(child);
94 | this.rendered_ = false;
95 | }
96 | };
97 |
98 | beestat.component.prototype.decorate_ = function() {
99 | // Left for the subclass to implement.
100 | };
101 |
102 | /**
103 | * Add custom styling to a component container. Mostly useful for when a
104 | * component needs margins, etc applied depending on the context.
105 | *
106 | * @param {object} style
107 | *
108 | * @return {beestat.component} This
109 | */
110 | beestat.component.prototype.style = function(style) {
111 | this.style_ = style;
112 | return this.rerender();
113 | };
114 |
--------------------------------------------------------------------------------
/js/component/card/air_quality_not_supported.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Air Quality Not Supported
3 | */
4 | beestat.component.card.air_quality_not_supported = function() {
5 | beestat.component.card.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.card.air_quality_not_supported, beestat.component.card);
8 |
9 | /**
10 | * Decorate
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.card.air_quality_not_supported.prototype.decorate_contents_ = function(parent) {
15 | parent.style('background', beestat.style.color.blue.light);
16 | parent.appendChild($.createElement('p').innerText('Access to Air Quality information requires a compatible thermostat. Support beestat by buying through this affiliate link.'));
17 |
18 | new beestat.component.tile()
19 | .set_icon('open_in_new')
20 | .set_text([
21 | 'Ecobee Smart Thermostat Premium',
22 | 'Amazon Affiliate'
23 | ])
24 | .set_size('large')
25 | .set_background_color(beestat.style.color.green.dark)
26 | .set_background_hover_color(beestat.style.color.green.light)
27 | .addEventListener('click', function() {
28 | window.open(beestat.affiliate.get_link('ecobee_smart_thermostat_premium'));
29 | })
30 | .render(parent);
31 | };
32 |
33 | /**
34 | * Get the title of the card.
35 | *
36 | * @return {string} The title.
37 | */
38 | beestat.component.card.air_quality_not_supported.prototype.get_title_ = function() {
39 | return 'Unsupported Thermostat';
40 | };
41 |
--------------------------------------------------------------------------------
/js/component/card/contribute_benefits.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Contribute benefits.
3 | */
4 | beestat.component.card.contribute_benefits = function() {
5 | beestat.component.card.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.card.contribute_benefits, beestat.component.card);
8 |
9 | /**
10 | * Decorate.
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.card.contribute_benefits.prototype.decorate_contents_ = function(parent) {
15 | const p = document.createElement('p');
16 | p.innerText = 'In addition to satisfaction of supporting a great project, you\'ll get:';
17 | parent.appendChild(p);
18 |
19 | const benefit_container = document.createElement('div');
20 | Object.assign(benefit_container.style, {
21 | 'background': beestat.style.color.bluegray.dark,
22 | 'padding': `${beestat.style.size.gutter}px`
23 | });
24 | parent.appendChild(benefit_container);
25 |
26 | const benefits = [
27 | 'Early access to new features',
28 | 'Private Discord membership',
29 | 'More frequent data syncing',
30 | 'Removed contribute banner'
31 | ];
32 | benefits.forEach(function(benefit) {
33 | new beestat.component.tile()
34 | .set_shadow(false)
35 | .set_text_color(beestat.style.color.yellow.base)
36 | .set_icon('octagram')
37 | .set_text(benefit)
38 | .style({
39 | 'margin-bottom': `${beestat.style.size.gutter / 2}px`
40 | })
41 | .render($(benefit_container));
42 | });
43 |
44 | new beestat.component.tile()
45 | .set_shadow(false)
46 | .set_text_color(beestat.style.color.red.base)
47 | .set_icon('heart')
48 | .set_text('My unending gratitude')
49 | .render($(benefit_container));
50 | };
51 |
52 | /**
53 | * Get the title of the card.
54 | *
55 | * @return {string} The title.
56 | */
57 | beestat.component.card.contribute_benefits.prototype.get_title_ = function() {
58 | return 'Benefits';
59 | };
60 |
--------------------------------------------------------------------------------
/js/component/card/contribute_reminder.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Green banner asking people for money. $_$
3 | */
4 | beestat.component.card.contribute_reminder = function() {
5 | var self = this;
6 |
7 | beestat.dispatcher.addEventListener([
8 | 'cache.user',
9 | 'setting.contribute_reminder_hide_until'
10 | ], function() {
11 | self.rerender();
12 | });
13 |
14 | beestat.component.card.apply(this, arguments);
15 | };
16 | beestat.extend(beestat.component.card.contribute_reminder, beestat.component.card);
17 |
18 | beestat.component.card.contribute_reminder.prototype.decorate_contents_ = function(parent) {
19 | var self = this;
20 |
21 | // Don't render anything if the user is an active Patron.
22 | if (beestat.component.card.contribute_reminder.should_show() === false) {
23 | window.setTimeout(function() {
24 | self.dispose();
25 | }, 0);
26 | return;
27 | }
28 |
29 | parent.style('background', beestat.style.color.green.base);
30 |
31 | new beestat.component.tile()
32 | .set_icon('heart')
33 | .set_size('large')
34 | .set_text([
35 | 'Support this project!',
36 | 'Your contribution matters'
37 | ])
38 | .set_background_color(beestat.style.color.green.dark)
39 | .set_background_hover_color(beestat.style.color.green.light)
40 | .addEventListener('click', function() {
41 | new beestat.layer.contribute().render();
42 | })
43 | .render(parent);
44 | };
45 |
46 | /**
47 | * Get the title of the card.
48 | *
49 | * @return {string} The title.
50 | */
51 | beestat.component.card.contribute_reminder.prototype.get_title_ = function() {
52 | return 'Enjoy beestat?';
53 | };
54 |
55 | /**
56 | * Decorate the close button.
57 | *
58 | * @param {rocket.Elements} parent
59 | */
60 | beestat.component.card.contribute_reminder.prototype.decorate_top_right_ = function(parent) {
61 | new beestat.component.tile()
62 | .set_type('pill')
63 | .set_shadow(false)
64 | .set_icon('close')
65 | .set_text_color('#fff')
66 | .set_background_hover_color(beestat.style.color.green.light)
67 | .addEventListener('click', function() {
68 | (new beestat.component.modal.enjoy_beestat()).render();
69 | })
70 | .render(parent);
71 | };
72 |
73 | /**
74 | * Determine whether or not this card should be shown.
75 | *
76 | * @return {boolean} Whether or not to show the card.
77 | */
78 | beestat.component.card.contribute_reminder.should_show = function() {
79 | if (
80 | beestat.user.contribution_is_active() === true ||
81 | window.is_demo === true ||
82 | (
83 | beestat.setting('contribute_reminder_hide_until') !== undefined &&
84 | moment.utc(beestat.setting('contribute_reminder_hide_until')).isAfter(moment.utc())
85 | )
86 | ) {
87 | return false;
88 | }
89 |
90 | return true;
91 | };
92 |
--------------------------------------------------------------------------------
/js/component/card/demo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Make sure people know they're in the demo.
3 | */
4 | beestat.component.card.demo = function() {
5 | beestat.component.card.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.card.demo, beestat.component.card);
8 |
9 | beestat.component.card.demo.prototype.decorate_contents_ = function(parent) {
10 | parent.style('background', beestat.style.color.lightblue.base);
11 |
12 | parent.appendChild($.createElement('p').innerText('This is a demo of beestat; it works exactly like the real thing. Changes you make will not be saved.'));
13 | };
14 |
--------------------------------------------------------------------------------
/js/component/card/early_access.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Early access
3 | */
4 | beestat.component.card.early_access = function() {
5 | beestat.component.card.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.card.early_access, beestat.component.card);
8 |
9 | /**
10 | * Decorate
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.card.early_access.prototype.decorate_contents_ = function(parent) {
15 | parent.style('background', beestat.style.color.green.base);
16 | parent.appendChild($.createElement('p').innerText('Welcome to the early access release for Visualize. Currently this is limited to the floor plan builder. The 3D view with sensor data visualizations is still a work in progress.'));
17 | };
18 |
--------------------------------------------------------------------------------
/js/component/card/footer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helpful footer stuff.
3 | */
4 | beestat.component.card.footer = function() {
5 | beestat.component.card.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.card.footer, beestat.component.card);
8 |
9 | beestat.component.card.footer.prototype.box_shadow_ = false;
10 |
11 | beestat.component.card.footer.prototype.decorate_contents_ = function(parent) {
12 | parent.style('background', beestat.style.color.bluegray.light);
13 |
14 | var footer = $.createElement('div')
15 | .style({
16 | 'text-align': 'center'
17 | });
18 | parent.appendChild(footer);
19 |
20 | var footer_links = $.createElement('div');
21 | footer.appendChild(footer_links);
22 |
23 | footer_links.appendChild(
24 | $.createElement('a')
25 | .setAttribute('href', 'https://doc.beestat.io/')
26 | .setAttribute('target', '_blank')
27 | .innerText('Help')
28 | );
29 | footer_links.appendChild($.createElement('span').innerText(' • '));
30 |
31 | footer_links.appendChild(
32 | $.createElement('a')
33 | .setAttribute('href', 'https://community.beestat.io/')
34 | .setAttribute('target', '_blank')
35 | .innerText('Feedback')
36 | );
37 | footer_links.appendChild($.createElement('span').innerText(' • '));
38 |
39 | footer_links.appendChild(
40 | $.createElement('a')
41 | .setAttribute('href', 'https://beestat.io/privacy/')
42 | .setAttribute('target', '_blank')
43 | .innerText('Privacy')
44 | );
45 | footer_links.appendChild($.createElement('span').innerText(' • '));
46 |
47 | footer_links.appendChild(
48 | $.createElement('a')
49 | .innerText('Newsletter')
50 | .addEventListener('click', function() {
51 | (new beestat.component.modal.newsletter()).render();
52 | })
53 | );
54 | footer_links.appendChild($.createElement('span').innerText(' • '));
55 |
56 | footer_links.appendChild(
57 | $.createElement('a')
58 | .setAttribute('href', 'mailto:contact@beestat.io')
59 | .innerText('Contact')
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/js/component/card/rate_app_reminder.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Banner asking people to rate and review the app.
3 | */
4 | beestat.component.card.rate_app_reminder = function() {
5 | const self = this;
6 |
7 | beestat.dispatcher.addEventListener(
8 | 'setting.ui.rate_app_reminder_hide_until',
9 | function() {
10 | self.rerender();
11 | }
12 | );
13 |
14 | beestat.component.card.apply(this, arguments);
15 | };
16 | beestat.extend(beestat.component.card.rate_app_reminder, beestat.component.card);
17 |
18 | /**
19 | * Decorate
20 | *
21 | * @param {rocket.Elements} parent
22 | */
23 | beestat.component.card.rate_app_reminder.prototype.decorate_contents_ = function(parent) {
24 | const self = this;
25 |
26 | // Don't render anything if the user dismissed this card.
27 | if (beestat.component.card.rate_app_reminder.should_show() === false) {
28 | window.setTimeout(function() {
29 | self.dispose();
30 | }, 0);
31 | return;
32 | }
33 |
34 | parent.style('background', beestat.style.color.bluegray.base);
35 |
36 | let icon;
37 | let store_name;
38 | let store_url;
39 |
40 | if (beestat.platform() === 'ios') {
41 | icon = 'apple';
42 | store_name = 'the App Store';
43 | store_url = 'https://apps.apple.com/us/app/beestat/id6469190206?platform=ipad';
44 | } else if (beestat.platform() === 'android') {
45 | icon = 'google_play';
46 | store_name = 'Google Play';
47 | store_url = 'https://play.google.com/store/apps/details?id=io.beestat';
48 | } else {
49 | throw new Error('Unsupported platform.');
50 | }
51 |
52 | new beestat.component.tile()
53 | .set_icon(icon)
54 | .set_size('large')
55 | .set_text(
56 | 'Rate now on ' + store_name
57 | )
58 | .set_background_color(beestat.style.color.green.dark)
59 | .set_background_hover_color(beestat.style.color.green.light)
60 | .addEventListener('click', function() {
61 | beestat.setting(
62 | 'ui.rate_app_reminder_hide_until',
63 | moment().utc()
64 | .add(1000, 'year')
65 | .format('YYYY-MM-DD HH:mm:ss')
66 | );
67 | window.open(store_url);
68 | })
69 | .render(parent);
70 | };
71 |
72 | /**
73 | * Get the title of the card.
74 | *
75 | * @return {string} The title.
76 | */
77 | beestat.component.card.rate_app_reminder.prototype.get_title_ = function() {
78 | return 'Like the app? Leave a rating or review!';
79 | };
80 |
81 | /**
82 | * Decorate the close button.
83 | *
84 | * @param {rocket.Elements} parent
85 | */
86 | beestat.component.card.rate_app_reminder.prototype.decorate_top_right_ = function(parent) {
87 | new beestat.component.tile()
88 | .set_type('pill')
89 | .set_shadow(false)
90 | .set_icon('close')
91 | .set_text_color('#fff')
92 | .set_background_hover_color(beestat.style.color.bluegray.light)
93 | .addEventListener('click', function() {
94 | beestat.setting(
95 | 'ui.rate_app_reminder_hide_until',
96 | moment().utc()
97 | .add(1000, 'year')
98 | .format('YYYY-MM-DD HH:mm:ss')
99 | );
100 | })
101 | .render(parent);
102 | };
103 |
104 | /**
105 | * Determine whether or not this card should be shown.
106 | *
107 | * @return {boolean} Whether or not to show the card.
108 | */
109 | beestat.component.card.rate_app_reminder.should_show = function() {
110 | return (
111 | (
112 | beestat.platform() === 'android' ||
113 | beestat.platform() === 'ios'
114 | ) &&
115 | beestat.setting('meta.opens.' + beestat.platform()) > 10 &&
116 | (
117 | beestat.setting('ui.rate_app_reminder_hide_until') === undefined ||
118 | moment.utc(beestat.setting('ui.rate_app_reminder_hide_until')).isBefore(moment.utc())
119 | )
120 | );
121 | };
122 |
--------------------------------------------------------------------------------
/js/component/card/rookstack_survey_notification.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Blue banner asking people to check out a research opportunity.
3 | */
4 | beestat.component.card.rookstack_survey_notification = function() {
5 | var self = this;
6 |
7 | beestat.dispatcher.addEventListener([
8 | 'setting.display_2024_equipment_sizing_study_rookstack'
9 | ], function() {
10 | self.rerender();
11 | });
12 |
13 | beestat.component.card.apply(this, arguments);
14 | };
15 | beestat.extend(beestat.component.card.rookstack_survey_notification, beestat.component.card);
16 |
17 | beestat.component.card.rookstack_survey_notification.prototype.decorate_contents_ = function(parent) {
18 | var self = this;
19 |
20 | // Don't render anything if the user is an active Patron.
21 | if (beestat.component.card.rookstack_survey_notification.should_show() === false) {
22 | window.setTimeout(function() {
23 | self.dispose();
24 | }, 0);
25 | return;
26 | }
27 |
28 | parent.style('background', beestat.style.color.blue.base);
29 |
30 |
31 | new beestat.component.tile()
32 | .set_icon('microscope')
33 | .set_size('large')
34 | .set_text([
35 | 'I am interested in participating',
36 | 'Learn more'
37 | ])
38 | .set_background_color(beestat.style.color.blue.dark)
39 | .set_background_hover_color(beestat.style.color.blue.light)
40 | .addEventListener('click', function() {
41 | beestat.setting('clicked_2024_equipment_sizing_study_rookstack', moment().utc().format('YYYY-MM-DD HH:mm:ss'));
42 | window.open('https://docs.google.com/presentation/d/1OY8RR6hMeL86ODH5LdxfrZ_es7nnRDq3cG-1qIfS7XI/present#slide=id.p', '_blank');
43 | })
44 | .render(parent);
45 | };
46 |
47 | /**
48 | * Get the title of the card.
49 | *
50 | * @return {string} The title.
51 | */
52 | beestat.component.card.rookstack_survey_notification.prototype.get_title_ = function() {
53 | return 'Research Opportunity';
54 | };
55 |
56 | /**
57 | * Get the subtitle of the card.
58 | *
59 | * @return {string} The subtitle.
60 | */
61 | beestat.component.card.rookstack_survey_notification.prototype.get_subtitle_ = function() {
62 | return 'Purdue University is looking for participants in a U.S. Department of Energy funded study that aims to develop and Artificial Intelligence solution to residential heating and cooling equipment sizing.';
63 | };
64 |
65 | /**
66 | * Decorate the close button.
67 | *
68 | * @param {rocket.Elements} parent
69 | */
70 | beestat.component.card.rookstack_survey_notification.prototype.decorate_top_right_ = function(parent) {
71 | new beestat.component.tile()
72 | .set_type('pill')
73 | .set_shadow(false)
74 | .set_icon('close')
75 | .set_text_color('#fff')
76 | .set_background_hover_color(beestat.style.color.blue.light)
77 | .addEventListener('click', function() {
78 | beestat.setting('display_2024_equipment_sizing_study_rookstack', false);
79 | })
80 | .render(parent);
81 | };
82 |
83 | /**
84 | * Determine whether or not this card should be shown.
85 | *
86 | * @return {boolean} Whether or not to show the card.
87 | */
88 | beestat.component.card.rookstack_survey_notification.should_show = function() {
89 | return beestat.setting('display_2024_equipment_sizing_study_rookstack') === true;
90 | };
91 |
--------------------------------------------------------------------------------
/js/component/card/visualize_affiliate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Visualize intro.
3 | *
4 | * @param {number} thermostat_id
5 | */
6 | beestat.component.card.visualize_affiliate = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.card.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.card.visualize_affiliate, beestat.component.card);
12 |
13 | /**
14 | * Decorate.
15 | *
16 | * @param {rocket.Elements} parent
17 | */
18 | beestat.component.card.visualize_affiliate.prototype.decorate_contents_ = function(parent) {
19 | const tile_group = new beestat.component.tile_group();
20 |
21 | tile_group.add_tile(new beestat.component.tile()
22 | .set_icon('open_in_new')
23 | .set_text([
24 | 'SmartSensor 2 Pack',
25 | 'Amazon Affiliate'
26 | ])
27 | .set_size('large')
28 | .set_background_color(beestat.style.color.green.dark)
29 | .set_background_hover_color(beestat.style.color.green.light)
30 | .addEventListener('click', function() {
31 | window.open(beestat.affiliate.get_link('ecobee_smart_sensor_2_pack'));
32 | })
33 | );
34 |
35 | tile_group.render(parent);
36 | };
37 |
38 | /**
39 | * Decorate the close button.
40 | *
41 | * @param {rocket.Elements} parent
42 | */
43 | beestat.component.card.visualize_affiliate.prototype.decorate_top_right_ = function(parent) {
44 | new beestat.component.tile()
45 | .set_type('pill')
46 | .set_shadow(false)
47 | .set_icon('close')
48 | .set_text_color('#fff')
49 | .set_background_hover_color('rgba(255, 255, 255, 0.1')
50 | .addEventListener('click', function() {
51 | beestat.setting('visualize.hide_affiliate', true);
52 | })
53 | .render(parent);
54 | };
55 |
56 | /**
57 | * Get the title of the card.
58 | *
59 | * @return {string} The title.
60 | */
61 | beestat.component.card.visualize_affiliate.prototype.get_title_ = function() {
62 | return 'Need more sensors?';
63 | };
64 |
--------------------------------------------------------------------------------
/js/component/card/visualize_intro.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Visualize intro.
3 | *
4 | * @param {number} thermostat_id
5 | */
6 | beestat.component.card.visualize_intro = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.card.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.card.visualize_intro, beestat.component.card);
12 |
13 | /**
14 | * Decorate.
15 | *
16 | * @param {rocket.Elements} parent
17 | */
18 | beestat.component.card.visualize_intro.prototype.decorate_contents_ = function(parent) {
19 | const self = this;
20 |
21 | const center_container = document.createElement('div');
22 | center_container.style.textAlign = 'center';
23 | parent.appendChild(center_container);
24 |
25 | new beestat.component.tile()
26 | .set_icon('plus')
27 | .set_text([
28 | 'Get started now!',
29 | 'Create my first floor plan'
30 | ])
31 | .set_size('large')
32 | .set_background_color(beestat.style.color.green.dark)
33 | .set_background_hover_color(beestat.style.color.green.light)
34 | .render($(center_container))
35 | .addEventListener('click', function() {
36 | new beestat.component.modal.create_floor_plan(
37 | self.thermostat_id_
38 | ).render();
39 | });
40 | };
41 |
42 | /**
43 | * Get the title of the card.
44 | *
45 | * @return {string} The title.
46 | */
47 | beestat.component.card.visualize_intro.prototype.get_title_ = function() {
48 | return 'Visualize';
49 | };
50 |
--------------------------------------------------------------------------------
/js/component/card/visualize_video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Visualize video.
3 | */
4 | beestat.component.card.visualize_video = function() {
5 | beestat.component.card.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.card.visualize_video, beestat.component.card);
8 |
9 | /**
10 | * Decorate.
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.card.visualize_video.prototype.decorate_ = function(parent) {
15 | const container = document.createElement('div');
16 | /**
17 | * The 16:9 aspect ratio corresponds to a height that is 56.25% of the width.
18 | * https://www.ankursheel.com/blog/full-width-you-tube-video-embed
19 | */
20 | Object.assign(container.style, {
21 | 'position': 'relative',
22 | 'padding-bottom': '56.25%',
23 | 'height': '0'
24 | });
25 | parent.appendChild(container);
26 |
27 | const iframe = document.createElement('iframe');
28 | Object.assign(iframe.style, {
29 | 'position': 'absolute',
30 | 'top': '0',
31 | 'left': '0',
32 | 'width': '100%',
33 | 'height': '100%'
34 | });
35 | iframe.setAttribute('src', 'https://player.vimeo.com/video/751478276?h=584bebb57b');
36 | iframe.setAttribute('frameborder', '0');
37 | iframe.setAttribute('allow', 'autoplay; fullscreen; picture-in-picture');
38 | iframe.setAttribute('allowfullscreen', 'allowfullscreen');
39 | container.appendChild(iframe);
40 | };
41 |
--------------------------------------------------------------------------------
/js/component/chart/runtime_thermostat_detail_equipment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime thermostat detail equipment chart.
3 | *
4 | * @param {object} data The chart data.
5 | */
6 | beestat.component.chart.runtime_thermostat_detail_equipment = function(data) {
7 | this.data_ = data;
8 |
9 | beestat.component.chart.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.chart.runtime_thermostat_detail_equipment, beestat.component.chart);
12 |
13 | /**
14 | * Override for get_options_xAxis_labels_formatter_.
15 | *
16 | * @return {Function} xAxis labels formatter.
17 | */
18 | beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_xAxis_labels_formatter_ = function() {
19 | return function() {
20 | return null;
21 | };
22 | };
23 |
24 | /**
25 | * Override for get_options_series_.
26 | *
27 | * @return {Array} All of the series to display on the chart.
28 | */
29 | beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_series_ = function() {
30 | var self = this;
31 | var series = [];
32 |
33 | [
34 | 'calendar_event_smartrecovery',
35 | 'calendar_event_home',
36 | 'calendar_event_away',
37 | 'calendar_event_sleep',
38 | 'calendar_event_smarthome',
39 | 'calendar_event_smartaway',
40 | 'calendar_event_hold',
41 | 'calendar_event_vacation',
42 | 'calendar_event_quicksave',
43 | 'calendar_event_door_window_open',
44 | 'calendar_event_other',
45 | 'calendar_event_custom',
46 | 'compressor_heat_1',
47 | 'compressor_heat_2',
48 | 'auxiliary_heat_1',
49 | 'auxiliary_heat_2',
50 | 'compressor_cool_1',
51 | 'compressor_cool_2',
52 | 'fan',
53 | 'humidifier',
54 | 'dehumidifier',
55 | 'ventilator',
56 | 'economizer'
57 | ].forEach(function(series_code) {
58 | if (self.data_.metadata.series[series_code].active === true) {
59 | var line_width;
60 | if (
61 | series_code.includes('heat') === true ||
62 | series_code.includes('cool') === true
63 | ) {
64 | line_width = 12;
65 | } else {
66 | line_width = 6;
67 | }
68 |
69 | series.push({
70 | 'name': series_code,
71 | 'data': self.data_.series[series_code],
72 | 'color': beestat.series[series_code].color,
73 | 'yAxis': 0,
74 | 'type': 'line',
75 | 'lineWidth': line_width,
76 | 'linecap': 'square',
77 | 'className': 'crisp_edges'
78 | });
79 | }
80 | });
81 |
82 | return series;
83 | };
84 |
85 | /**
86 | * Override for get_options_yAxis_.
87 | *
88 | * @return {Array} The y-axis options.
89 | */
90 | beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_yAxis_ = function() {
91 | return [
92 | {
93 | 'min': 0,
94 | 'max': 44,
95 |
96 | // Keeps the chart from ending on a multiple of whatever the tick interval gets set to.
97 | 'endOnTick': false,
98 |
99 | 'reversed': true,
100 | 'gridLineWidth': 0,
101 | 'title': {'text': null},
102 | 'labels': {'enabled': false}
103 | }
104 | ];
105 | };
106 |
107 | /**
108 | * Get the height of the chart.
109 | *
110 | * @return {number} The height of the chart.
111 | */
112 | beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_chart_height_ = function() {
113 | return 44;
114 | };
115 |
116 | /**
117 | * Get the legend enabled options.
118 | *
119 | * @return {Function} The legend enabled options.
120 | */
121 | beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_legend_enabled_ = function() {
122 | return false;
123 | };
124 |
125 | /**
126 | * Get the left margin for the chart.
127 | *
128 | * @return {number} The left margin for the chart.
129 | */
130 | beestat.component.chart.runtime_thermostat_detail_equipment.prototype.get_options_chart_marginLeft_ = function() {
131 | return 45;
132 | };
133 |
--------------------------------------------------------------------------------
/js/component/down_notification.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Ecobee is down!
3 | */
4 | beestat.component.down_notification = function() {
5 | beestat.component.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.down_notification, beestat.component);
8 |
9 | /**
10 | * Decorate a floating banner at the bottom of the page.
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.down_notification.prototype.decorate_ = function(parent) {
15 | var div = $.createElement('div');
16 | div.style({
17 | 'position': 'fixed',
18 | 'bottom': '0px',
19 | 'left': '0px',
20 | 'width': '100%',
21 | 'text-align': 'center',
22 | 'padding-left': beestat.style.size.gutter,
23 | 'padding-right': beestat.style.size.gutter,
24 | 'background': beestat.style.color.red.dark
25 | });
26 |
27 | var last_update = moment.utc(beestat.user.get().sync_status.thermostat).local()
28 | .format('h:mm a');
29 | div.appendChild($.createElement('p').innerText('Ecobee seems to be down. Your data will update as soon as possible. Last update was at ' + last_update + '.'));
30 |
31 | parent.appendChild(div);
32 | };
33 |
--------------------------------------------------------------------------------
/js/component/input.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Input parent class.
3 | */
4 | beestat.component.input = function() {
5 | this.uuid_ = window.crypto.randomUUID();
6 |
7 | beestat.component.apply(this, arguments);
8 | };
9 | beestat.extend(beestat.component.input, beestat.component);
10 |
11 | /**
12 | * Focus an input.
13 | *
14 | * @return {beestat.component.input} This.
15 | */
16 | beestat.component.input.prototype.focus = function() {
17 | this.input_.focus();
18 | this.input_.setSelectionRange(0, this.input_.value.length);
19 |
20 | return this;
21 | };
22 |
23 | /**
24 | * Enable or disable an input.
25 | *
26 | * @param {boolean} enabled Whether or not the input should be enabled.
27 | *
28 | * @return {beestat.component.input} This.
29 | */
30 | beestat.component.input.prototype.set_enabled = function(enabled) {
31 | this.input_.disabled = !enabled;
32 |
33 | if (this.rendered_ === true) {
34 | this.rerender();
35 | }
36 |
37 | return this;
38 | };
39 |
40 | /**
41 | * Generic setter that sets a key to a value, rerenders if necessary.
42 | *
43 | * @param {string} key
44 | * @param {string} value
45 | *
46 | * @return {beestat.component.input} This.
47 | */
48 | beestat.component.input.prototype.set_ = function(key, value) {
49 | this[key + '_'] = value;
50 |
51 | if (this.rendered_ === true) {
52 | this.rerender();
53 | }
54 |
55 | return this;
56 | };
57 |
58 | /**
59 | * Set the requirements for this input to be valid.
60 | *
61 | * @param {object} requirements
62 | *
63 | * @return {beestat.component.input.text} This.
64 | */
65 | beestat.component.input.prototype.set_requirements = function(requirements) {
66 | this.requirements_ = requirements;
67 |
68 | return this;
69 | };
70 |
71 | /**
72 | * Check whether or not this input meets the requirements.
73 | *
74 | * @return {boolean} Whether or not this input meets the requirements.
75 | */
76 | beestat.component.input.prototype.meets_requirements = function() {
77 | if (this.requirements_ !== undefined) {
78 | switch (this.requirements_.type) {
79 | case 'integer':
80 | this.requirements_.regexp = /^-?\d+$/;
81 | break;
82 | case 'decimal':
83 | this.requirements_.regexp = /^-?\d*(?:\.\d+)?$/;
84 | break;
85 | }
86 |
87 | if (
88 | this.requirements_.required === true &&
89 | this.get_value() === undefined
90 | ) {
91 | return false;
92 | }
93 |
94 | if (
95 | this.requirements_.min_value !== undefined &&
96 | this.get_value() < this.requirements_.min_value
97 | ) {
98 | return false;
99 | }
100 |
101 | if (
102 | this.requirements_.max_value !== undefined &&
103 | this.get_value() > this.requirements_.max_value
104 | ) {
105 | return false;
106 | }
107 |
108 | if (
109 | this.get_value() !== undefined &&
110 | this.requirements_.regexp !== undefined &&
111 | this.requirements_.regexp.test(this.get_value()) === false
112 | ) {
113 | return false;
114 | }
115 |
116 | if (
117 | this.get_value() !== undefined &&
118 | this.requirements_.type === 'date' &&
119 | moment(this.get_value()).isValid() === false
120 | ) {
121 | return false;
122 | }
123 | }
124 |
125 | return true;
126 | };
127 |
--------------------------------------------------------------------------------
/js/component/input/checkbox.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Checkbox input.
3 | */
4 | beestat.component.input.checkbox = function() {
5 | const self = this;
6 |
7 | this.input_ = document.createElement('input');
8 | this.input_.setAttribute('type', 'checkbox');
9 |
10 | this.input_.addEventListener('change', function() {
11 | self.dispatchEvent('change');
12 | });
13 |
14 | beestat.component.input.apply(this, arguments);
15 | };
16 | beestat.extend(beestat.component.input.checkbox, beestat.component.input);
17 |
18 | /**
19 | * Decorate
20 | *
21 | * @param {rocket.Elements} parent
22 | */
23 | beestat.component.input.checkbox.prototype.decorate_ = function(parent) {
24 | const self = this;
25 |
26 | const div = document.createElement('div');
27 | div.className = 'checkbox';
28 |
29 | this.input_.setAttribute('id', this.uuid_);
30 |
31 | div.appendChild(this.input_);
32 |
33 | const label = document.createElement('label');
34 | label.setAttribute('for', this.uuid_);
35 | div.appendChild(label);
36 |
37 | const span = document.createElement('span');
38 | span.style.cursor = 'pointer';
39 | span.style.paddingLeft = (beestat.style.size.gutter / 4) + 'px';
40 | span.innerText = this.label_;
41 | span.addEventListener('click', function() {
42 | self.input_.click();
43 | });
44 | div.appendChild(span);
45 |
46 | parent.appendChild(div);
47 | };
48 |
49 | /**
50 | * Set whether or not this checkbox is selected.
51 | *
52 | * @param {boolean} checked
53 | *
54 | * @return {beestat.component.input.checkbox} This.
55 | */
56 | beestat.component.input.checkbox.prototype.set_checked = function(checked) {
57 | this.input_.checked = checked;
58 |
59 | return this;
60 | };
61 |
62 | /**
63 | * Get whether or not this checkbox is selected.
64 | *
65 | * @return {string} Whether or not this checkbox is selected.
66 | */
67 | beestat.component.input.checkbox.prototype.get_checked = function() {
68 | return this.input_.checked;
69 | };
70 |
71 | /**
72 | * Set the checkbox label.
73 | *
74 | * @param {string} label
75 | *
76 | * @return {beestat.component.input.checkbox} This.
77 | */
78 | beestat.component.input.checkbox.prototype.set_label = function(label) {
79 | this.label_ = label;
80 |
81 | if (this.rendered_ === true) {
82 | this.rerender();
83 | }
84 |
85 | return this;
86 | };
87 |
--------------------------------------------------------------------------------
/js/component/input/radio.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Radio input.
3 | */
4 | beestat.component.input.radio = function() {
5 | const self = this;
6 |
7 | this.input_ = document.createElement('input');
8 | this.input_.setAttribute('type', 'radio');
9 |
10 | this.input_.addEventListener('change', function() {
11 | self.dispatchEvent('change');
12 | });
13 |
14 | beestat.component.input.apply(this, arguments);
15 | };
16 | beestat.extend(beestat.component.input.radio, beestat.component.input);
17 |
18 | /**
19 | * Decorate
20 | *
21 | * @param {rocket.Elements} parent
22 | */
23 | beestat.component.input.radio.prototype.decorate_ = function(parent) {
24 | const self = this;
25 |
26 | const div = document.createElement('div');
27 | div.className = 'radio';
28 |
29 | this.input_.setAttribute('id', this.uuid_);
30 | this.input_.setAttribute('name', this.name_);
31 |
32 | div.appendChild(this.input_);
33 |
34 | const label = document.createElement('label');
35 | label.setAttribute('for', this.uuid_);
36 | div.appendChild(label);
37 |
38 | const span = document.createElement('span');
39 | span.style.cursor = 'pointer';
40 | span.style.paddingLeft = (beestat.style.size.gutter / 4) + 'px';
41 | span.innerText = this.label_;
42 | span.addEventListener('click', function() {
43 | self.input_.click();
44 | });
45 | div.appendChild(span);
46 |
47 | parent.appendChild(div);
48 | };
49 |
50 | /**
51 | * Set the value of the radio button that is returned when calling
52 | * get_value().
53 | *
54 | * @param {string} value
55 | *
56 | * @return {beestat.component.input.radio} This.
57 | */
58 | beestat.component.input.radio.prototype.set_value = function(value) {
59 | this.value_ = value;
60 |
61 | return this;
62 | };
63 |
64 | /**
65 | * Get the value of the radio button.
66 | *
67 | * @return {string} The value in the input field.
68 | */
69 | beestat.component.input.radio.prototype.get_value = function() {
70 | return this.value_;
71 | };
72 |
73 | /**
74 | * Set whether or not this radio is selected.
75 | *
76 | * @param {boolean} checked
77 | *
78 | * @return {beestat.component.input.radio} This.
79 | */
80 | beestat.component.input.radio.prototype.set_checked = function(checked) {
81 | this.input_.checked = checked;
82 |
83 | return this;
84 | };
85 |
86 | /**
87 | * Get whether or not this radio is selected.
88 | *
89 | * @return {string} Whether or not this radio is selected.
90 | */
91 | beestat.component.input.radio.prototype.get_checked = function() {
92 | return this.input_.checked;
93 | };
94 |
95 | /**
96 | * Set the radio label.
97 | *
98 | * @param {string} label
99 | *
100 | * @return {beestat.component.input.radio} This.
101 | */
102 | beestat.component.input.radio.prototype.set_label = function(label) {
103 | this.label_ = label;
104 |
105 | if (this.rendered_ === true) {
106 | this.rerender();
107 | }
108 |
109 | return this;
110 | };
111 |
112 | /**
113 | * Set the radio name. Required to group radio elements together.
114 | *
115 | * @param {string} name
116 | *
117 | * @return {beestat.component.input.radio} This.
118 | */
119 | beestat.component.input.radio.prototype.set_name = function(name) {
120 | this.name_ = name;
121 |
122 | if (this.rendered_ === true) {
123 | this.rerender();
124 | }
125 |
126 | return this;
127 | };
128 |
--------------------------------------------------------------------------------
/js/component/input/range.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Range input.
3 | */
4 | beestat.component.input.range = function() {
5 | const self = this;
6 |
7 | this.input_ = document.createElement('input');
8 | this.input_.setAttribute('type', 'range');
9 |
10 | this.input_.addEventListener('change', function() {
11 | self.dispatchEvent('change');
12 | });
13 |
14 | this.input_.addEventListener('input', function() {
15 | self.dispatchEvent('input');
16 | });
17 |
18 | beestat.component.input.apply(this, arguments);
19 | };
20 | beestat.extend(beestat.component.input.range, beestat.component.input);
21 |
22 | /**
23 | * Decorate
24 | *
25 | * @param {rocket.Elements} parent
26 | */
27 | beestat.component.input.range.prototype.decorate_ = function(parent) {
28 | this.input_.style.width = '100%';
29 |
30 | parent.appendChild(this.input_);
31 | };
32 |
33 | /**
34 | * Set the value in the range field. Do not rerender; it's unnecessary.
35 | *
36 | * @param {string} value
37 | *
38 | * @return {beestat.component.input.range} This.
39 | */
40 | beestat.component.input.range.prototype.set_value = function(value) {
41 | this.input_.value = value;
42 |
43 | this.dispatchEvent('change');
44 |
45 | return this;
46 | };
47 |
48 | /**
49 | * Get the value of the input.
50 | *
51 | * @return {string}
52 | */
53 | beestat.component.input.range.prototype.get_value = function() {
54 | return this.input_.value;
55 | };
56 |
57 | /**
58 | * Set the min value of the range input.
59 | *
60 | * @param {string} min
61 | *
62 | * @return {beestat.component.input.range} This.
63 | */
64 | beestat.component.input.range.prototype.set_min = function(min) {
65 | this.input_.setAttribute('min', min);
66 |
67 | return this;
68 | };
69 |
70 | /**
71 | * Set the max value of the range input.
72 | *
73 | * @param {string} max
74 | *
75 | * @return {beestat.component.input.range} This.
76 | */
77 | beestat.component.input.range.prototype.set_max = function(max) {
78 | this.input_.setAttribute('max', max);
79 |
80 | return this;
81 | };
82 |
83 | /**
84 | * Set the background value of the range input.
85 | *
86 | * @param {string} background
87 | *
88 | * @return {beestat.component.input.range} This.
89 | */
90 | beestat.component.input.range.prototype.set_background = function(background) {
91 | this.input_.style.setProperty('--background', background);
92 |
93 | return this;
94 | };
95 |
--------------------------------------------------------------------------------
/js/component/layout.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Takes a bunch of rows/columns and lays them out nicely on the page.
3 | *
4 | * @param {Array} rows
5 | */
6 | beestat.component.layout = function(rows) {
7 | this.rows_ = rows;
8 | beestat.component.apply(this, arguments);
9 | };
10 | beestat.extend(beestat.component.layout, beestat.component);
11 |
12 | /**
13 | * Decorate. Not much thinking to be done here; all the grid layout stuff is
14 | * built in CSS.
15 | *
16 | * @param {rocket.Elements} parent
17 | */
18 | beestat.component.layout.prototype.decorate_ = function(parent) {
19 | this.rows_.forEach(function(row) {
20 | var row_element = $.createElement('div').addClass('row');
21 | parent.appendChild(row_element);
22 |
23 | // Create the columns
24 | row.forEach(function(column) {
25 | var column_element = $.createElement('div')
26 | .addClass('column')
27 | .addClass('column_' + column.size);
28 | row_element.appendChild(column_element);
29 |
30 | column.card.render(column_element);
31 | });
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/js/component/loading.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Loading thing.
3 | *
4 | * @param {string} text Optional text to display with the loading thing.
5 | */
6 | beestat.component.loading = function(text) {
7 | this.text_ = text;
8 | beestat.component.apply(this, arguments);
9 | };
10 | beestat.extend(beestat.component.loading, beestat.component);
11 |
12 | beestat.component.loading.prototype.decorate_ = function(parent) {
13 | if (this.text_ !== undefined) {
14 | this.text_block_ = $.createElement('div')
15 | .style({
16 | 'margin-bottom': beestat.style.size.gutter,
17 | 'color': beestat.style.color.yellow.base,
18 | 'font-weight': beestat.style.font_weight.bold
19 | })
20 | .innerHTML(this.text_);
21 |
22 | parent.appendChild(this.text_block_);
23 | }
24 |
25 | var loading_wrapper = $.createElement('div').addClass('loading_wrapper');
26 | parent.appendChild(loading_wrapper);
27 |
28 | loading_wrapper.appendChild($.createElement('div').addClass('loading_1'));
29 | loading_wrapper.appendChild($.createElement('div').addClass('loading_2'));
30 | };
31 |
32 | /**
33 | * Set the text of the loading container. If you call this after it's rendered
34 | * it will change the existing text. It will not add text to a loader that was
35 | * rendered without text, though.
36 | *
37 | * @param {string} text
38 | */
39 | beestat.component.loading.prototype.set_text = function(text) {
40 | this.text_ = text;
41 | this.text_block_.innerHTML(this.text_);
42 | };
43 |
--------------------------------------------------------------------------------
/js/component/logo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Logo
3 | *
4 | * @param {number} height The height of the logo
5 | */
6 | beestat.component.logo = function(height) {
7 | this.height_ = height || 48;
8 | beestat.component.apply(this, arguments);
9 | };
10 | beestat.extend(beestat.component.logo, beestat.component);
11 |
12 | /**
13 | * Decorate
14 | *
15 | * @param {rocket.Elements} parent
16 | */
17 | beestat.component.logo.prototype.decorate_ = function(parent) {
18 | const logo = $.createElement('img')
19 | .setAttribute('src', 'img/logo.png')
20 | .style({
21 | 'height': this.height_ + 'px'
22 | });
23 | parent.appendChild(logo);
24 | };
25 |
--------------------------------------------------------------------------------
/js/component/metric/balance_point.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Balance point metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.balance_point = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.balance_point, beestat.component.metric);
12 |
13 | beestat.component.metric.balance_point.prototype.parent_metric_name_ = 'balance_point';
14 |
15 | beestat.component.metric.balance_point.prototype.is_temperature_ = true;
16 |
17 | /**
18 | * Get the units for this metric.
19 | *
20 | * @return {string} The units for this metric.
21 | */
22 | beestat.component.metric.balance_point.prototype.get_units_ = function() {
23 | return beestat.setting('units.temperature');
24 | };
25 |
26 | /**
27 | * Get the title of this metric.
28 | *
29 | * @return {string} The title of this metric.
30 | */
31 | beestat.component.metric.balance_point.prototype.get_title_ = function() {
32 | return beestat.series['compressor_' + this.child_metric_name_].name;
33 | };
34 |
35 | /**
36 | * Get the color of this metric.
37 | *
38 | * @return {string} The color of this metric.
39 | */
40 | beestat.component.metric.balance_point.prototype.get_color_ = function() {
41 | return beestat.series['compressor_' + this.child_metric_name_].color;
42 | };
43 |
44 |
--------------------------------------------------------------------------------
/js/component/metric/balance_point/heat_1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Balance Point for Heat Stage 1
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.balance_point.heat_1 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.balance_point.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.balance_point.heat_1, beestat.component.metric.balance_point);
12 |
13 | beestat.component.metric.balance_point.heat_1.prototype.child_metric_name_ = 'heat_1';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.balance_point.heat_1.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/balance_point/heat_2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Balance Point for Heat Stage 2
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.balance_point.heat_2 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.balance_point.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.balance_point.heat_2, beestat.component.metric.balance_point);
12 |
13 | beestat.component.metric.balance_point.heat_2.prototype.child_metric_name_ = 'heat_2';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.balance_point.heat_2.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/balance_point/resist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Balance Point for Resist
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.balance_point.resist = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.balance_point.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.balance_point.resist, beestat.component.metric.balance_point);
12 |
13 | beestat.component.metric.balance_point.resist.prototype.child_metric_name_ = 'resist';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.balance_point.resist.prototype.get_icon_ = function() {
21 | return 'resistor';
22 | };
23 |
24 | /**
25 | * Get the title of this metric.
26 | *
27 | * @return {string} The title of this metric.
28 | */
29 | beestat.component.metric.balance_point.resist.prototype.get_title_ = function() {
30 | return 'Resist';
31 | };
32 |
33 | /**
34 | * Get the color of this metric.
35 | *
36 | * @return {string} The color of this metric.
37 | */
38 | beestat.component.metric.balance_point.resist.prototype.get_color_ = function() {
39 | return beestat.style.color.gray.dark;
40 | };
41 |
--------------------------------------------------------------------------------
/js/component/metric/property.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Property metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.property = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.property, beestat.component.metric);
12 |
13 | beestat.component.metric.property.prototype.parent_metric_name_ = 'property';
14 |
15 | /**
16 | * Get the title of this metric.
17 | *
18 | * @return {string} The title of this metric.
19 | */
20 | beestat.component.metric.property.prototype.get_title_ = function() {
21 | return this.child_metric_name_.charAt(0).toUpperCase() + this.child_metric_name_.slice(1);
22 | };
23 |
24 | /**
25 | * Get the color of this metric.
26 | *
27 | * @return {string} The color of this metric.
28 | */
29 | beestat.component.metric.property.prototype.get_color_ = function() {
30 | return beestat.style.color.purple.base;
31 | };
32 |
33 |
--------------------------------------------------------------------------------
/js/component/metric/property/age.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Property age metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.property.age = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.property.age, beestat.component.metric.property);
12 |
13 | beestat.component.metric.property.age.prototype.child_metric_name_ = 'age';
14 |
15 | /**
16 | * Get the units for this metric.
17 | *
18 | * @return {string} The units for this metric.
19 | */
20 | beestat.component.metric.property.age.prototype.get_units_ = function() {
21 | return 'y';
22 | };
23 |
24 | /**
25 | * Get the title of this metric.
26 | *
27 | * @return {string} The title of this metric.
28 | */
29 | beestat.component.metric.property.age.prototype.get_title_ = function() {
30 | return 'Age';
31 | };
32 |
33 | /**
34 | * Get the icon of this metric.
35 | *
36 | * @return {string} The icon of this metric.
37 | */
38 | beestat.component.metric.property.age.prototype.get_icon_ = function() {
39 | return 'clock_outline';
40 | };
41 |
42 | /**
43 | * Get max cutoff. This is used to set the chart min to max(median - 2 *
44 | * stddev, max cutoff).
45 | *
46 | * @return {object} The cutoff value.
47 | */
48 | beestat.component.metric.property.age.prototype.get_cutoff_min_ = function() {
49 | return 0;
50 | };
51 |
--------------------------------------------------------------------------------
/js/component/metric/property/square_feet.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Property square feet metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.property.square_feet = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.property.square_feet, beestat.component.metric.property);
12 |
13 | beestat.component.metric.property.square_feet.prototype.child_metric_name_ = 'square_feet';
14 |
15 | beestat.component.metric.property.square_feet.prototype.is_area_ = true;
16 |
17 | /**
18 | * Get the units for this metric.
19 | *
20 | * @return {string} The units for this metric.
21 | */
22 | beestat.component.metric.property.square_feet.prototype.get_units_ = function() {
23 | return beestat.setting('units.area');
24 | };
25 |
26 | /**
27 | * Get the a formatter function that applies a transformation to the value.
28 | *
29 | * @return {mixed} A function that formats the string.
30 | */
31 | beestat.component.metric.property.square_feet.prototype.get_formatter_ = function() {
32 | return function(value) {
33 | return beestat.area({
34 | 'area': value,
35 | 'units': true
36 | });
37 | };
38 | };
39 |
40 | /**
41 | * Get the title of this metric.
42 | *
43 | * @return {string} The title of this metric.
44 | */
45 | beestat.component.metric.property.square_feet.prototype.get_title_ = function() {
46 | return 'Area';
47 | };
48 |
49 | /**
50 | * Get the icon of this metric.
51 | *
52 | * @return {string} The icon of this metric.
53 | */
54 | beestat.component.metric.property.square_feet.prototype.get_icon_ = function() {
55 | return 'view_quilt';
56 | };
57 |
58 | /**
59 | * Get max cutoff. This is used to set the chart min to max(median - 2 *
60 | * stddev, max cutoff).
61 | *
62 | * @return {object} The cutoff value.
63 | */
64 | beestat.component.metric.property.square_feet.prototype.get_cutoff_min_ = function() {
65 | return beestat.setting('units.area') === 'ft²' ? 500 : 50;
66 | };
67 |
68 | /**
69 | * Get the counting interval for the histogram.
70 | *
71 | * @return {number} The interval.
72 | */
73 | beestat.component.metric.property.square_feet.prototype.get_interval_ = function() {
74 | return beestat.setting('units.area') === 'ft²' ? 500 : 50;
75 | };
76 |
--------------------------------------------------------------------------------
/js/component/metric/runtime_per_degree_day.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime per heating degree day metric.
3 | *
4 | * @param {number} thermostat_id The thermostat group.
5 | */
6 | beestat.component.metric.runtime_per_degree_day = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.runtime_per_degree_day, beestat.component.metric);
12 |
13 | beestat.component.metric.runtime_per_degree_day.prototype.parent_metric_name_ = 'runtime_per_degree_day';
14 |
15 | /**
16 | * Get the units for this metric.
17 | *
18 | * @return {string} The units for this metric.
19 | */
20 | beestat.component.metric.runtime_per_degree_day.prototype.get_units_ = function() {
21 | return 'm';
22 | };
23 |
24 | /**
25 | * Get the title of this metric.
26 | *
27 | * @return {string} The title of this metric.
28 | */
29 | beestat.component.metric.runtime_per_degree_day.prototype.get_title_ = function() {
30 | return beestat.series['compressor_' + this.child_metric_name_].name;
31 | };
32 |
33 | /**
34 | * Get the color of this metric.
35 | *
36 | * @return {string} The color of this metric.
37 | */
38 | beestat.component.metric.runtime_per_degree_day.prototype.get_color_ = function() {
39 | return beestat.series['compressor_' + this.child_metric_name_].color;
40 | };
41 |
42 | /**
43 | * Get max cutoff. This is used to set the chart min to max(median - 2 *
44 | * stddev, max cutoff).
45 | *
46 | * @return {object} The cutoff value.
47 | */
48 | beestat.component.metric.runtime_per_degree_day.prototype.get_cutoff_min_ = function() {
49 | return 0;
50 | };
51 |
--------------------------------------------------------------------------------
/js/component/metric/runtime_per_degree_day/auxiliary_heat_1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime / HDD for Auxiliary Heat Stage 1
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_1 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.runtime_per_degree_day.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.runtime_per_degree_day.auxiliary_heat_1, beestat.component.metric.runtime_per_degree_day);
12 |
13 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_1.prototype.child_metric_name_ = 'auxiliary_heat_1';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_1.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
24 | /**
25 | * Get the title of this metric.
26 | *
27 | * @return {string} The title of this metric.
28 | */
29 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_1.prototype.get_title_ = function() {
30 | return beestat.series[this.child_metric_name_].name;
31 | };
32 |
33 | /**
34 | * Get the color of this metric.
35 | *
36 | * @return {string} The color of this metric.
37 | */
38 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_1.prototype.get_color_ = function() {
39 | return beestat.series[this.child_metric_name_].color;
40 | };
41 |
--------------------------------------------------------------------------------
/js/component/metric/runtime_per_degree_day/auxiliary_heat_2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime / HDD for Auxiliary Heat Stage 2
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_2 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.runtime_per_degree_day.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.runtime_per_degree_day.auxiliary_heat_2, beestat.component.metric.runtime_per_degree_day);
12 |
13 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_2.prototype.child_metric_name_ = 'auxiliary_heat_2';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_2.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
24 | /**
25 | * Get the title of this metric.
26 | *
27 | * @return {string} The title of this metric.
28 | */
29 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_2.prototype.get_title_ = function() {
30 | return beestat.series[this.child_metric_name_].name;
31 | };
32 |
33 | /**
34 | * Get the color of this metric.
35 | *
36 | * @return {string} The color of this metric.
37 | */
38 | beestat.component.metric.runtime_per_degree_day.auxiliary_heat_2.prototype.get_color_ = function() {
39 | return beestat.series[this.child_metric_name_].color;
40 | };
41 |
--------------------------------------------------------------------------------
/js/component/metric/runtime_per_degree_day/cool_1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime / CDD for Cool Stage 1
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.runtime_per_degree_day.cool_1 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.runtime_per_degree_day.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.runtime_per_degree_day.cool_1, beestat.component.metric.runtime_per_degree_day);
12 |
13 | beestat.component.metric.runtime_per_degree_day.cool_1.prototype.child_metric_name_ = 'cool_1';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.runtime_per_degree_day.cool_1.prototype.get_icon_ = function() {
21 | return 'snowflake';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/runtime_per_degree_day/cool_2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime / CDD for Cool Stage 2
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.runtime_per_degree_day.cool_2 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.runtime_per_degree_day.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.runtime_per_degree_day.cool_2, beestat.component.metric.runtime_per_degree_day);
12 |
13 | beestat.component.metric.runtime_per_degree_day.cool_2.prototype.child_metric_name_ = 'cool_2';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.runtime_per_degree_day.cool_2.prototype.get_icon_ = function() {
21 | return 'snowflake';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/runtime_per_degree_day/heat_1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime / HDD for Heat Stage 1
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.runtime_per_degree_day.heat_1 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.runtime_per_degree_day.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.runtime_per_degree_day.heat_1, beestat.component.metric.runtime_per_degree_day);
12 |
13 | beestat.component.metric.runtime_per_degree_day.heat_1.prototype.child_metric_name_ = 'heat_1';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.runtime_per_degree_day.heat_1.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/runtime_per_degree_day/heat_2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Runtime / HDD for Heat Stage 2
3 | *
4 | * @param {number} thermostat_id The thermostat ID.
5 | */
6 | beestat.component.metric.runtime_per_degree_day.heat_2 = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.runtime_per_degree_day.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.runtime_per_degree_day.heat_2, beestat.component.metric.runtime_per_degree_day);
12 |
13 | beestat.component.metric.runtime_per_degree_day.heat_2.prototype.child_metric_name_ = 'heat_2';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.runtime_per_degree_day.heat_2.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/setback.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Setback metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.setback = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.setback, beestat.component.metric);
12 |
13 | beestat.component.metric.setback.prototype.parent_metric_name_ = 'setback';
14 |
15 | beestat.component.metric.setback.prototype.is_temperature_ = true;
16 |
17 | beestat.component.metric.setback.prototype.is_temperature_delta_ = true;
18 |
19 | /**
20 | * Get the units for this metric.
21 | *
22 | * @return {string} The units for this metric.
23 | */
24 | beestat.component.metric.setback.prototype.get_units_ = function() {
25 | return beestat.setting('units.temperature');
26 | };
27 |
28 | /**
29 | * Get the title of this metric.
30 | *
31 | * @return {string} The title of this metric.
32 | */
33 | beestat.component.metric.setback.prototype.get_title_ = function() {
34 | return this.child_metric_name_.charAt(0).toUpperCase() + this.child_metric_name_.slice(1);
35 | };
36 |
37 | /**
38 | * Get the color of this metric.
39 | *
40 | * @return {string} The color of this metric.
41 | */
42 | beestat.component.metric.setback.prototype.get_color_ = function() {
43 | return beestat.series['compressor_' + this.child_metric_name_ + '_1'].color;
44 | };
45 |
46 | /**
47 | * Get max cutoff. This is used to set the chart min to max(median - 2 *
48 | * stddev, max cutoff).
49 | *
50 | * @return {object} The cutoff value.
51 | */
52 | beestat.component.metric.setback.prototype.get_cutoff_min_ = function() {
53 | return 0;
54 | };
55 |
--------------------------------------------------------------------------------
/js/component/metric/setback/cool.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Cool setback metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.setback.cool = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.setback.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.setback.cool, beestat.component.metric.setback);
12 |
13 | beestat.component.metric.setback.cool.prototype.child_metric_name_ = 'cool';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.setback.cool.prototype.get_icon_ = function() {
21 | return 'snowflake';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/setback/heat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Heat setback metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.setback.heat = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.setback.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.setback.heat, beestat.component.metric.setback);
12 |
13 | beestat.component.metric.setback.heat.prototype.child_metric_name_ = 'heat';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.setback.heat.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/setpoint.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Setpoint metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.setpoint = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.setpoint, beestat.component.metric);
12 |
13 | beestat.component.metric.setpoint.prototype.parent_metric_name_ = 'setpoint';
14 |
15 | beestat.component.metric.setpoint.prototype.is_temperature_ = true;
16 |
17 | /**
18 | * Get the units for this metric.
19 | *
20 | * @return {string} The units for this metric.
21 | */
22 | beestat.component.metric.setpoint.prototype.get_units_ = function() {
23 | return beestat.setting('units.temperature');
24 | };
25 |
26 | /**
27 | * Get the title of this metric.
28 | *
29 | * @return {string} The title of this metric.
30 | */
31 | beestat.component.metric.setpoint.prototype.get_title_ = function() {
32 | return this.child_metric_name_.charAt(0).toUpperCase() + this.child_metric_name_.slice(1);
33 | };
34 |
35 | /**
36 | * Get the color of this metric.
37 | *
38 | * @return {string} The color of this metric.
39 | */
40 | beestat.component.metric.setpoint.prototype.get_color_ = function() {
41 | return beestat.series['compressor_' + this.child_metric_name_ + '_1'].color;
42 | };
43 |
44 |
--------------------------------------------------------------------------------
/js/component/metric/setpoint/cool.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Cool setpoint metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.setpoint.cool = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.setpoint.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.setpoint.cool, beestat.component.metric.setpoint);
12 |
13 | beestat.component.metric.setpoint.cool.prototype.child_metric_name_ = 'cool';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.setpoint.cool.prototype.get_icon_ = function() {
21 | return 'snowflake';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/metric/setpoint/heat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Heat setpoint metric.
3 | *
4 | * @param {number} thermostat_id The thermostat.
5 | */
6 | beestat.component.metric.setpoint.heat = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.metric.setpoint.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.metric.setpoint.heat, beestat.component.metric.setpoint);
12 |
13 | beestat.component.metric.setpoint.heat.prototype.child_metric_name_ = 'heat';
14 |
15 | /**
16 | * Get the icon of this metric.
17 | *
18 | * @return {string} The icon of this metric.
19 | */
20 | beestat.component.metric.setpoint.heat.prototype.get_icon_ = function() {
21 | return 'fire';
22 | };
23 |
--------------------------------------------------------------------------------
/js/component/modal/announcements.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Announcements
3 | */
4 | beestat.component.modal.announcements = function() {
5 | beestat.component.modal.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.modal.announcements, beestat.component.modal);
8 |
9 | /**
10 | * Decorate
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.modal.announcements.prototype.decorate_contents_ = function(parent) {
15 | var announcements = $.values(beestat.cache.announcement).reverse();
16 |
17 | if (announcements.length === 0) {
18 | parent.appendChild($.createElement('p').innerText('No recent announcements! :)'));
19 | } else {
20 | announcements.forEach(function(announcement) {
21 | parent.appendChild($.createElement('div').style({
22 | 'border-bottom': '1px solid #eee',
23 | 'margin-left': (beestat.style.size.gutter * -1) + 'px',
24 | 'margin-right': (beestat.style.size.gutter * -1) + 'px',
25 | 'margin-top': (beestat.style.size.gutter) + 'px',
26 | 'margin-bottom': (beestat.style.size.gutter) + 'px'
27 | }));
28 |
29 | var icon = new beestat.component.icon(announcement.icon)
30 | .set_text(announcement.title +
31 | ' • ' +
32 | moment.utc(announcement.created_at).fromNow());
33 |
34 | icon.render(parent);
35 |
36 | beestat.dispatcher.dispatchEvent('view_announcements');
37 |
38 | parent.appendChild($.createElement('p').innerHTML(announcement.text));
39 | });
40 |
41 | beestat.setting(
42 | 'last_read_announcement_id',
43 | announcements[0].announcement_id
44 | );
45 | }
46 | };
47 |
48 | /**
49 | * Get the title.
50 | *
51 | * @return {string} The title.
52 | */
53 | beestat.component.modal.announcements.prototype.get_title_ = function() {
54 | return 'Announcements';
55 | };
56 |
--------------------------------------------------------------------------------
/js/component/modal/change_floor_plan.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Change floor plan
3 | */
4 | beestat.component.modal.change_floor_plan = function() {
5 | beestat.component.modal.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.modal.change_floor_plan, beestat.component.modal);
8 |
9 | /**
10 | * Decorate
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.modal.change_floor_plan.prototype.decorate_contents_ = function(parent) {
15 | const self = this;
16 |
17 | const p = document.createElement('p');
18 | p.innerText = 'You have multiple floor plans; which one would you like to view?';
19 | parent.appendChild(p);
20 |
21 | const grid = document.createElement('div');
22 | grid.style.display = 'grid';
23 | grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(150px, 1fr))';
24 | grid.style.columnGap = beestat.style.size.gutter + 'px';
25 | grid.style.rowGap = beestat.style.size.gutter + 'px';
26 | parent.appendChild(grid);
27 |
28 | var sorted_floor_plans = $.values(beestat.cache.floor_plan)
29 | .sort(function(a, b) {
30 | return a.name > b.name;
31 | });
32 |
33 | let div;
34 | sorted_floor_plans.forEach(function(floor_plan) {
35 | div = document.createElement('div');
36 | grid.appendChild(div);
37 |
38 | const tile = new beestat.component.tile.floor_plan(floor_plan.floor_plan_id)
39 | .set_text_color('#fff')
40 | .set_display('block');
41 |
42 | if (floor_plan.floor_plan_id === beestat.setting('visualize.floor_plan_id')) {
43 | tile.set_background_color(beestat.style.color.lightblue.base);
44 | } else {
45 | tile
46 | .set_background_color(beestat.style.color.bluegray.base)
47 | .set_background_hover_color(beestat.style.color.lightblue.base)
48 | .addEventListener('click', function() {
49 | beestat.setting('visualize.floor_plan_id', floor_plan.floor_plan_id);
50 | self.dispose();
51 | });
52 | }
53 |
54 | tile.render($(div));
55 | });
56 | };
57 |
58 | /**
59 | * Get title.
60 | *
61 | * @return {string} Title.
62 | */
63 | beestat.component.modal.change_floor_plan.prototype.get_title_ = function() {
64 | return 'Change Floor Plan';
65 | };
66 |
67 |
--------------------------------------------------------------------------------
/js/component/modal/change_thermostat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Change thermostat
3 | */
4 | beestat.component.modal.change_thermostat = function() {
5 | beestat.component.modal.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.modal.change_thermostat, beestat.component.modal);
8 |
9 | /**
10 | * Decorate
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.modal.change_thermostat.prototype.decorate_contents_ = function(parent) {
15 | const p = document.createElement('p');
16 | p.innerText = 'You have multiple thermostats; which one would you like to view?';
17 | parent.appendChild(p);
18 |
19 | const grid = document.createElement('div');
20 | grid.style.display = 'grid';
21 | grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(150px, 1fr))';
22 | grid.style.columnGap = beestat.style.size.gutter + 'px';
23 | grid.style.rowGap = beestat.style.size.gutter + 'px';
24 | parent.appendChild(grid);
25 |
26 | var sorted_thermostats = $.values(beestat.cache.thermostat)
27 | .sort(function(a, b) {
28 | return a.name > b.name;
29 | });
30 |
31 | let div;
32 | sorted_thermostats.forEach(function(thermostat) {
33 | div = document.createElement('div');
34 | grid.appendChild(div);
35 |
36 | const tile = new beestat.component.tile.thermostat(thermostat.thermostat_id)
37 | .set_size('large')
38 | .set_text_color('#fff')
39 | .set_display('block');
40 |
41 | if (thermostat.thermostat_id === beestat.setting('thermostat_id')) {
42 | tile.set_background_color(beestat.style.color.lightblue.base);
43 | } else {
44 | tile
45 | .set_background_color(beestat.style.color.bluegray.base)
46 | .set_background_hover_color(beestat.style.color.lightblue.base)
47 | .addEventListener('click', function() {
48 | beestat.setting('thermostat_id', thermostat.thermostat_id, function() {
49 | window.location.reload();
50 | });
51 | });
52 | }
53 |
54 | tile.render($(div));
55 | });
56 | };
57 |
58 | /**
59 | * Get title.
60 | *
61 | * @return {string} Title.
62 | */
63 | beestat.component.modal.change_thermostat.prototype.get_title_ = function() {
64 | return 'Change Thermostat';
65 | };
66 |
--------------------------------------------------------------------------------
/js/component/modal/delete_floor_plan.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Delete a floor plan.
3 | *
4 | * @param {number} floor_plan_id
5 | */
6 | beestat.component.modal.delete_floor_plan = function(floor_plan_id) {
7 | this.floor_plan_id_ = floor_plan_id;
8 |
9 | beestat.component.modal.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.modal.delete_floor_plan, beestat.component.modal);
12 |
13 | /**
14 | * Decorate
15 | *
16 | * @param {rocket.Elements} parent
17 | */
18 | beestat.component.modal.delete_floor_plan.prototype.decorate_contents_ = function(parent) {
19 | const p = document.createElement('p');
20 | p.innerText = 'Are you sure you want to delete this floor plan?';
21 | parent.appendChild(p);
22 |
23 | new beestat.component.tile.floor_plan(this.floor_plan_id_)
24 | .set_background_color(beestat.style.color.bluegray.base)
25 | .set_text_color('#fff')
26 | .render(parent);
27 | };
28 |
29 | /**
30 | * Get title.
31 | *
32 | * @return {string} The title.
33 | */
34 | beestat.component.modal.delete_floor_plan.prototype.get_title_ = function() {
35 | return 'Delete Floor Plan';
36 | };
37 |
38 | /**
39 | * Get the buttons that go on the bottom of this modal.
40 | *
41 | * @return {[beestat.component.button]} The buttons.
42 | */
43 | beestat.component.modal.delete_floor_plan.prototype.get_buttons_ = function() {
44 | const self = this;
45 |
46 | const cancel_button = new beestat.component.tile()
47 | .set_background_color('#fff')
48 | .set_text_color(beestat.style.color.gray.base)
49 | .set_text_hover_color(beestat.style.color.red.base)
50 | .set_shadow(false)
51 | .set_text('Cancel')
52 | .addEventListener('click', function() {
53 | self.dispose();
54 | });
55 |
56 | const delete_button = new beestat.component.tile()
57 | .set_background_color(beestat.style.color.red.base)
58 | .set_background_hover_color(beestat.style.color.red.light)
59 | .set_text_color('#fff')
60 | .set_text('Delete Floor Plan')
61 | .addEventListener('click', function() {
62 | this
63 | .set_background_color(beestat.style.color.gray.base)
64 | .set_background_hover_color()
65 | .removeEventListener('click');
66 |
67 | new beestat.api()
68 | .add_call(
69 | 'floor_plan',
70 | 'delete',
71 | {
72 | 'id': self.floor_plan_id_
73 | },
74 | 'delete_floor_plan'
75 | )
76 | .add_call(
77 | 'floor_plan',
78 | 'read_id',
79 | {},
80 | 'floor_plan'
81 | )
82 | .set_callback(function(response) {
83 | self.dispose();
84 |
85 | if (Object.keys(response.floor_plan).length > 0) {
86 | beestat.setting('visualize.floor_plan_id', Object.values(response.floor_plan)[0].floor_plan_id);
87 | } else {
88 | beestat.setting('visualize.floor_plan_id', null);
89 | }
90 |
91 | beestat.cache.set('floor_plan', response.floor_plan);
92 | })
93 | .send();
94 | });
95 |
96 | return [
97 | cancel_button,
98 | delete_button
99 | ];
100 | };
101 |
--------------------------------------------------------------------------------
/js/component/modal/enjoy_beestat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Options for hiding the contribute reminder.
3 | */
4 | beestat.component.modal.enjoy_beestat = function() {
5 | beestat.component.modal.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.modal.enjoy_beestat, beestat.component.modal);
8 |
9 | /**
10 | * Decorate
11 | *
12 | * @param {rocket.Elements} parent
13 | */
14 | beestat.component.modal.enjoy_beestat.prototype.decorate_contents_ = function(parent) {
15 | parent.appendChild($.createElement('p').innerHTML('Beestat is completely free to use and does not run ads or sell your data. If you want to help, consider supporting the project. Among other benefits, it will hide this banner permanently.'));
16 | parent.appendChild($.createElement('p').innerHTML('If you prefer not to give, you can hide this banner. Please keep using and enjoying beestat! :)'));
17 | };
18 |
19 | /**
20 | * Get the title.
21 | *
22 | * @return {string} The title.
23 | */
24 | beestat.component.modal.enjoy_beestat.prototype.get_title_ = function() {
25 | return 'Enjoy beestat?';
26 | };
27 |
28 | /**
29 | * Hide the contribute reminder for some amount of time.
30 | *
31 | * @param {number} amount How long.
32 | * @param {string} unit The unit (day, month, etc).
33 | */
34 | beestat.component.modal.enjoy_beestat.prototype.hide_contribute_reminder_ = function(amount, unit) {
35 | beestat.setting(
36 | 'contribute_reminder_hide_until',
37 | moment().utc()
38 | .add(amount, unit)
39 | .format('YYYY-MM-DD HH:mm:ss')
40 | );
41 | };
42 |
43 | /**
44 | * Get the buttons on the modal.
45 | *
46 | * @return {[beestat.component.button]} The buttons.
47 | */
48 | beestat.component.modal.enjoy_beestat.prototype.get_buttons_ = function() {
49 | var self = this;
50 |
51 | var hide = new beestat.component.tile()
52 | .set_background_color('#fff')
53 | .set_shadow(false)
54 | .set_text_color(beestat.style.color.gray.base)
55 | .set_text_hover_color(beestat.style.color.bluegray.base)
56 | .set_text('Hide for one month')
57 | .addEventListener('click', function() {
58 | self.hide_contribute_reminder_(1, 'month');
59 | self.dispose();
60 | });
61 |
62 | var link = new beestat.component.tile()
63 | .set_text('Support beestat')
64 | .set_icon('heart')
65 | .set_background_color(beestat.style.color.green.base)
66 | .set_background_hover_color(beestat.style.color.green.light)
67 | .set_text_color('#fff')
68 | .addEventListener('click', function() {
69 | new beestat.layer.contribute().render();
70 | });
71 |
72 | return [
73 | hide,
74 | link
75 | ];
76 | };
77 |
--------------------------------------------------------------------------------
/js/component/modal/error.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Error modal.
3 | */
4 | beestat.component.modal.error = function() {
5 | beestat.component.modal.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.modal.error, beestat.component.modal);
8 |
9 | beestat.component.modal.error.prototype.decorate_contents_ = function(parent) {
10 | parent.appendChild($.createElement('p').innerHTML(this.message_));
11 |
12 | if (this.detail_ !== undefined) {
13 | parent.appendChild($.createElement('p').innerHTML('Sorry about that! This error has been logged and will be investigated and appropriately punished. Please reach out to contact@beestat.io if it persists.'));
14 | parent.appendChild($.createElement('p')
15 | .style({
16 | 'padding': beestat.style.size.gutter / 2,
17 | 'background': beestat.style.color.bluegray.dark,
18 | 'color': beestat.style.color.gray.light,
19 | 'font-family': 'Courier New, Monospace',
20 | 'max-height': '200px',
21 | 'overflow-y': 'auto',
22 | 'font-size': beestat.style.font_size.normal,
23 | 'white-space': 'pre'
24 | })
25 | .innerHTML(this.detail_));
26 | }
27 | };
28 |
29 | beestat.component.modal.error.prototype.set_message = function(message) {
30 | this.message_ = message;
31 | };
32 |
33 | beestat.component.modal.error.prototype.set_detail = function(detail) {
34 | this.detail_ = detail;
35 | };
36 |
37 | beestat.component.modal.error.prototype.get_title_ = function() {
38 | var titles = [
39 | 'Looks like you broke it again.',
40 | 'Yep, it\'s broken.',
41 | 'Something went wrong.',
42 | 'You have died of dysentery.',
43 | 'What a happy accident.',
44 | 'Witty title for an error.',
45 | 'Greedo shot first!',
46 | 'We can\'t all be winners.',
47 | 'Don\'t panic!',
48 | 'Hello. It\'s me.',
49 | '¯\\_(ツ)_/¯'
50 | ];
51 |
52 | return titles[Math.floor(Math.random() * titles.length)];
53 | };
54 |
--------------------------------------------------------------------------------
/js/component/modal/floor_plan_elevation_help.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Help for floor plan elevation.
3 | *
4 | * @param {number} floor_plan_id
5 | */
6 | beestat.component.modal.floor_plan_elevation_help = function() {
7 | beestat.component.modal.apply(this, arguments);
8 | };
9 | beestat.extend(beestat.component.modal.floor_plan_elevation_help, beestat.component.modal);
10 |
11 | /**
12 | * Decorate
13 | *
14 | * @param {rocket.Elements} parent
15 | */
16 | beestat.component.modal.floor_plan_elevation_help.prototype.decorate_contents_ = function(parent) {
17 | const p1 = document.createElement('p');
18 | p1.innerText = 'Whoops!';
19 | parent.appendChild(p1);
20 |
21 | const p2 = document.createElement('p');
22 | p2.innerText = 'Elevation should be the height of this floor or room relative to the ground outside your home. For example, your first floor elevation should typically be 0, and your second floor elevation would be the height of your first floor ceilings.';
23 | parent.appendChild(p2);
24 |
25 | const p3 = document.createElement('p');
26 | p3.innerText = 'All rooms inherit the elevation of their floor, but can be overridden for complex floor plans.';
27 | parent.appendChild(p3);
28 | };
29 |
30 | /**
31 | * Get title.
32 | *
33 | * @return {string} The title.
34 | */
35 | beestat.component.modal.floor_plan_elevation_help.prototype.get_title_ = function() {
36 | return 'Elevation';
37 | };
38 |
39 | /**
40 | * Get the buttons that go on the bottom of this modal.
41 | *
42 | * @return {[beestat.component.button]} The buttons.
43 | */
44 | beestat.component.modal.floor_plan_elevation_help.prototype.get_buttons_ = function() {
45 | var self = this;
46 |
47 | var ok = new beestat.component.tile()
48 | .set_background_color(beestat.style.color.green.base)
49 | .set_background_hover_color(beestat.style.color.green.light)
50 | .set_text_color('#fff')
51 | .set_text('Got it!')
52 | .addEventListener('click', function() {
53 | self.dispose();
54 | });
55 |
56 | return [ok];
57 | };
58 |
--------------------------------------------------------------------------------
/js/component/modal/temperature_profiles_info.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Temperature Profiles Details
3 | */
4 | beestat.component.modal.temperature_profiles_info = function() {
5 | beestat.component.modal.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.modal.temperature_profiles_info, beestat.component.modal);
8 |
9 | beestat.component.modal.temperature_profiles_info.prototype.decorate_contents_ = function(parent) {
10 | const thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
11 |
12 | const container = $.createElement('div')
13 | .style({
14 | 'display': 'grid',
15 | 'grid-template-columns': 'repeat(auto-fill, minmax(150px, 1fr))',
16 | 'margin': '0 0 16px -16px'
17 | });
18 | parent.appendChild(container);
19 |
20 | const fields = [];
21 |
22 | [
23 | 'heat_1',
24 | 'heat_2',
25 | 'auxiliary_heat_1',
26 | 'auxiliary_heat_2',
27 | 'cool_1',
28 | 'cool_2',
29 | 'resist'
30 | ].forEach(function(type) {
31 | if (thermostat.profile.temperature[type] !== null) {
32 | const profile = thermostat.profile.temperature[type];
33 |
34 | // Convert the data to Celsius if necessary
35 | const deltas_converted = {};
36 | for (let key in profile.deltas) {
37 | deltas_converted[beestat.temperature({'temperature': key})] =
38 | beestat.temperature({
39 | 'temperature': (profile.deltas[key]),
40 | 'delta': true,
41 | 'round': 3
42 | });
43 | }
44 |
45 | profile.deltas = deltas_converted;
46 | const linear_trendline = beestat.math.get_linear_trendline(profile.deltas);
47 |
48 | fields.push({
49 | 'name': beestat.series['indoor_' + type + '_delta'].name,
50 | 'value':
51 | 'Slope = ' +
52 | linear_trendline.slope.toFixed(4) +
53 | ' Intercept = ' +
54 | linear_trendline.intercept.toFixed(4) + beestat.setting('units.temperature')
55 | });
56 | }
57 | });
58 |
59 | fields.forEach(function(field) {
60 | var div = $.createElement('div')
61 | .style({
62 | 'padding': '16px 0 0 16px'
63 | });
64 | container.appendChild(div);
65 |
66 | div.appendChild($.createElement('div')
67 | .style({
68 | 'font-weight': beestat.style.font_weight.bold,
69 | 'margin-bottom': (beestat.style.size.gutter / 4)
70 | })
71 | .innerHTML(field.name));
72 | div.appendChild($.createElement('div').innerHTML(field.value));
73 | });
74 | };
75 |
76 | beestat.component.modal.temperature_profiles_info.prototype.get_title_ = function() {
77 | return 'Temperature Profiles Info';
78 | };
79 |
--------------------------------------------------------------------------------
/js/component/modal/thermostat_info.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Thermostat Details
3 | */
4 | beestat.component.modal.thermostat_info = function() {
5 | beestat.component.modal.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.component.modal.thermostat_info, beestat.component.modal);
8 |
9 | beestat.component.modal.thermostat_info.prototype.decorate_contents_ = function(parent) {
10 | var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
11 |
12 | var ecobee_thermostat = beestat.cache.ecobee_thermostat[
13 | thermostat.ecobee_thermostat_id
14 | ];
15 |
16 | var container = $.createElement('div')
17 | .style({
18 | 'display': 'grid',
19 | 'grid-template-columns': 'repeat(auto-fill, minmax(150px, 1fr))',
20 | 'margin': '0 0 16px -16px'
21 | });
22 | parent.appendChild(container);
23 |
24 | var fields = [
25 | {
26 | 'name': 'Model',
27 | 'value': beestat.ecobee_thermostat_models[ecobee_thermostat.model_number] || 'Unknown'
28 | },
29 | {
30 | 'name': 'Serial Number',
31 | 'value': ecobee_thermostat.identifier
32 | },
33 | {
34 | 'name': 'Firmware Revision',
35 | 'value': ecobee_thermostat.version.thermostatFirmwareVersion
36 | },
37 | {
38 | 'name': 'First Connected',
39 | 'value': moment.utc(ecobee_thermostat.runtime.firstConnected).local()
40 | .format('MMM Do, YYYY')
41 | }
42 | ];
43 |
44 | fields.forEach(function(field) {
45 | var div = $.createElement('div')
46 | .style({
47 | 'padding': '16px 0 0 16px'
48 | });
49 | container.appendChild(div);
50 |
51 | div.appendChild($.createElement('div')
52 | .style({
53 | 'font-weight': beestat.style.font_weight.bold,
54 | 'margin-bottom': (beestat.style.size.gutter / 4)
55 | })
56 | .innerHTML(field.name));
57 | div.appendChild($.createElement('div').innerHTML(field.value));
58 | });
59 | };
60 |
61 | beestat.component.modal.thermostat_info.prototype.get_title_ = function() {
62 | return 'Thermostat Info';
63 | };
64 |
--------------------------------------------------------------------------------
/js/component/radio_group.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A group of radio input elements.
3 | */
4 | beestat.component.radio_group = function() {
5 | this.radios_ = [];
6 | this.name_ = window.crypto.randomUUID();
7 | beestat.component.apply(this, arguments);
8 | };
9 | beestat.extend(beestat.component.radio_group, beestat.component);
10 |
11 | /**
12 | * Decorate
13 | *
14 | * @param {rocket.Elements} parent
15 | */
16 | beestat.component.radio_group.prototype.decorate_ = function(parent) {
17 | const self = this;
18 |
19 | // Outer container
20 | const container = document.createElement('div');
21 | if (this.arrangement_ === 'horizontal') {
22 | Object.assign(container.style, {
23 | 'display': 'flex',
24 | 'grid-gap': `${beestat.style.size.gutter}px`
25 | });
26 | }
27 | parent.appendChild(container);
28 |
29 | // Radios
30 | this.radios_.forEach(function(radio) {
31 | radio.set_name(self.name_);
32 |
33 | radio.addEventListener('change', function() {
34 | self.value_ = radio.get_value();
35 | self.dispatchEvent('change');
36 | });
37 |
38 | radio.render($(container));
39 | });
40 | };
41 |
42 | /**
43 | * Add a radio to this group.
44 | *
45 | * @param {beestat.component.radio} radio The radio to add.
46 | *
47 | * @return {beestat.component.radio_group}
48 | */
49 | beestat.component.radio_group.prototype.add_radio = function(radio) {
50 | this.radios_.push(radio);
51 | if (this.rendered_ === true) {
52 | this.rerender();
53 | }
54 | return this;
55 | };
56 |
57 | /**
58 | * Remove this component from the page. Disposes the radios first.
59 | */
60 | beestat.component.radio_group.prototype.dispose = function() {
61 | this.radios_.forEach(function(radio) {
62 | radio.dispose();
63 | });
64 | beestat.component.prototype.dispose.apply(this, arguments);
65 | };
66 |
67 | /**
68 | * Get the selected radio button's value.
69 | *
70 | * @return {string} The value.
71 | */
72 | beestat.component.radio_group.prototype.get_value = function() {
73 | for (let i = 0; i < this.radios_.length; i++) {
74 | if (this.radios_[i].get_checked() === true) {
75 | return this.radios_[i].get_value();
76 | }
77 | }
78 |
79 | return null;
80 | };
81 |
82 | /**
83 | * Set the arrangement of the radio buttons in the group.
84 | *
85 | * @param {string} arrangement horizontal|vertical
86 | *
87 | * @return {beestat.component.radio_group}
88 | */
89 | beestat.component.radio_group.prototype.set_arrangement = function(arrangement) {
90 | this.arrangement_ = arrangement;
91 |
92 | return this;
93 | };
94 |
--------------------------------------------------------------------------------
/js/component/tile/floor_plan.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A tile representing a floor plan.
3 | *
4 | * @param {integer} floor_plan_id
5 | *
6 | */
7 | beestat.component.tile.floor_plan = function(floor_plan_id) {
8 | this.floor_plan_id_ = floor_plan_id;
9 |
10 | beestat.component.tile.apply(this, arguments);
11 | };
12 | beestat.extend(beestat.component.tile.floor_plan, beestat.component.tile);
13 |
14 | /**
15 | * Get the icon for this tile.
16 | *
17 | * @return {string} The icon.
18 | */
19 | beestat.component.tile.floor_plan.prototype.get_icon_ = function() {
20 | return 'floor_plan';
21 | };
22 |
23 | /**
24 | * Get the text for this tile.
25 | *
26 | * @return {string} The first line of text.
27 | */
28 | beestat.component.tile.floor_plan.prototype.get_text_ = function() {
29 | const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
30 |
31 | const line_2_parts = [];
32 | let floor_count = floor_plan.data.groups.length;
33 | line_2_parts.push(floor_count + (floor_count === 1 ? ' Floor' : ' Floors'));
34 | line_2_parts.push(
35 | beestat.area({
36 | 'input_area_unit': 'in²',
37 | 'area': beestat.floor_plan.get_area(this.floor_plan_id_),
38 | 'round': 0,
39 | 'units': true
40 | })
41 | );
42 |
43 | return [
44 | floor_plan.name,
45 | line_2_parts.join(' • ')
46 | ];
47 | };
48 |
49 | /**
50 | * Get the size of this tile.
51 | *
52 | * @return {string} The size of this tile.
53 | */
54 | beestat.component.tile.floor_plan.prototype.get_size_ = function() {
55 | return 'large';
56 | };
57 |
--------------------------------------------------------------------------------
/js/component/tile/floor_plan_group.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A tile representing a floor plan group.
3 | *
4 | * @param {object} floor_plan_group
5 | */
6 | beestat.component.tile.floor_plan_group = function(floor_plan_group) {
7 | this.floor_plan_group_ = floor_plan_group;
8 |
9 | beestat.component.tile.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.tile.floor_plan_group, beestat.component.tile);
12 |
13 | /**
14 | * Get the icon for this tile.
15 | *
16 | * @return {string} The icon.
17 | */
18 | beestat.component.tile.floor_plan_group.prototype.get_icon_ = function() {
19 | return 'layers';
20 | };
21 |
22 | /**
23 | * Get the text for this tile.
24 | *
25 | * @return {string} The first line of text.
26 | */
27 | beestat.component.tile.floor_plan_group.prototype.get_text_ = function() {
28 | const line_2_parts = [];
29 | let room_count = this.floor_plan_group_.rooms.length;
30 | line_2_parts.push(room_count + (room_count === 1 ? ' Room' : ' Rooms'));
31 | line_2_parts.push(
32 | beestat.area({
33 | 'input_area_unit': 'in²',
34 | 'area': beestat.floor_plan.get_area_group(this.floor_plan_group_),
35 | 'round': 0,
36 | 'units': true
37 | })
38 | );
39 |
40 | return [
41 | this.floor_plan_group_.name,
42 | line_2_parts.join(' • ')
43 | ];
44 | };
45 |
46 | /**
47 | * Get the size of this tile.
48 | *
49 | * @return {string} The size of this tile.
50 | */
51 | beestat.component.tile.floor_plan_group.prototype.get_size_ = function() {
52 | return 'large';
53 | };
54 |
--------------------------------------------------------------------------------
/js/component/tile/thermostat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A tile representing a thermostat.
3 | *
4 | * @param {number} thermostat_id
5 | */
6 | beestat.component.tile.thermostat = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.tile.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.tile.thermostat, beestat.component.tile);
12 |
13 | /**
14 | * Get the icon for this tile.
15 | *
16 | * @return {string} The icon.
17 | */
18 | beestat.component.tile.thermostat.prototype.get_icon_ = function() {
19 | return 'thermostat';
20 | };
21 |
22 | /**
23 | * Get the text for this tile.
24 | *
25 | * @return {string} The first line of text.
26 | */
27 | beestat.component.tile.thermostat.prototype.get_text_ = function() {
28 | const thermostat = beestat.cache.thermostat[this.thermostat_id_];
29 |
30 | const temperature = beestat.temperature({
31 | 'temperature': thermostat.temperature,
32 | 'round': 0,
33 | 'units': true
34 | });
35 |
36 | return [
37 | thermostat.name,
38 | temperature
39 | ];
40 | };
41 |
42 | /**
43 | * Get the size of this tile.
44 | *
45 | * @return {string} The size of this tile.
46 | */
47 | beestat.component.tile.thermostat.prototype.get_size_ = function() {
48 | return 'large';
49 | };
50 |
--------------------------------------------------------------------------------
/js/component/tile/thermostat/switcher.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A tile representing a thermostat for the quick switch.
3 | *
4 | * @param {integer} thermostat_id
5 | */
6 | beestat.component.tile.thermostat.switcher = function(thermostat_id) {
7 | this.thermostat_id_ = thermostat_id;
8 |
9 | beestat.component.tile.thermostat.apply(this, arguments);
10 | };
11 | beestat.extend(beestat.component.tile.thermostat.switcher, beestat.component.tile.thermostat);
12 |
13 | /**
14 | * Get the icon for this tile.
15 | *
16 | * @return {string} The icon.
17 | */
18 | beestat.component.tile.thermostat.switcher.prototype.get_icon_ = function() {
19 | return undefined;
20 | };
21 |
22 | /**
23 | * Get the text for this tile.
24 | *
25 | * @return {string} The first line of text.
26 | */
27 | beestat.component.tile.thermostat.switcher.prototype.get_text_ = function() {
28 | const thermostat = beestat.cache.thermostat[this.thermostat_id_];
29 |
30 | const temperature = beestat.temperature({
31 | 'temperature': thermostat.temperature,
32 | 'round': 0,
33 | 'units': true
34 | });
35 |
36 | return thermostat.name + ' • ' + temperature;
37 | };
38 |
39 | /**
40 | * Get the size of this tile.
41 | *
42 | * @return {string} The size of this tile.
43 | */
44 | beestat.component.tile.thermostat.switcher.prototype.get_size_ = function() {
45 | return 'medium';
46 | };
47 |
--------------------------------------------------------------------------------
/js/component/tile_group.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A group of tiles.
3 | */
4 | beestat.component.tile_group = function() {
5 | this.tiles_ = [];
6 | beestat.component.apply(this, arguments);
7 | };
8 | beestat.extend(beestat.component.tile_group, beestat.component);
9 |
10 | /**
11 | * Decorate
12 | *
13 | * @param {rocket.Elements} parent
14 | */
15 | beestat.component.tile_group.prototype.decorate_ = function(parent) {
16 | const flex = document.createElement('div');
17 | Object.assign(flex.style, {
18 | 'display': 'inline-flex',
19 | 'flex-wrap': 'wrap',
20 | 'grid-gap': `${beestat.style.size.gutter / 2}px`
21 | });
22 | parent.appendChild(flex);
23 |
24 | this.tiles_.forEach(function(tile) {
25 | tile.render($(flex));
26 | });
27 | };
28 |
29 | /**
30 | * Add a tile to this group.
31 | *
32 | * @param {beestat.component.tile} tile The tile to add.
33 | */
34 | beestat.component.tile_group.prototype.add_tile = function(tile) {
35 | this.tiles_.push(tile);
36 | if (this.rendered_ === true) {
37 | this.rerender();
38 | }
39 | };
40 |
41 | /**
42 | * Remove this component from the page. Disposes the tiles first.
43 | */
44 | beestat.component.tile_group.prototype.dispose = function() {
45 | this.tiles_.forEach(function(tile) {
46 | tile.dispose();
47 | });
48 | beestat.component.prototype.dispose.apply(this, arguments);
49 | };
50 |
--------------------------------------------------------------------------------
/js/component/title.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Simple bolded title text with a margin.
3 | *
4 | * @param {string} title The title.
5 | */
6 | beestat.component.title = function(title) {
7 | this.title_ = title;
8 | beestat.component.apply(this, arguments);
9 | };
10 | beestat.extend(beestat.component.title, beestat.component);
11 |
12 | /**
13 | * Decorate
14 | *
15 | * @param {rocket.Elements} parent
16 | */
17 | beestat.component.title.prototype.decorate_ = function(parent) {
18 | var title = $.createElement('div')
19 | .style({
20 | 'font-size': beestat.style.font_size.normal,
21 | 'font-weight': beestat.style.font_weight.bold,
22 | 'margin-bottom': (beestat.style.size.gutter / 2)
23 | })
24 | .innerText(this.title_);
25 | parent.appendChild(title);
26 | };
27 |
--------------------------------------------------------------------------------
/js/layer.js:
--------------------------------------------------------------------------------
1 | beestat.layer = function() {
2 | this.loaders_ = [];
3 | };
4 |
5 | /**
6 | * Render this layer onto the body. First put everything in a container, then
7 | * clear the body, then append the new container. This prevents the child
8 | * layers from having to worry about multiple redraws since they aren't doing
9 | * anything directly on the body.
10 | */
11 | beestat.layer.prototype.render = function() {
12 | rocket.EventTarget.removeAllEventListeners();
13 |
14 | beestat.current_layer = this;
15 |
16 | var body = $(document.body);
17 |
18 | var container = $.createElement('div');
19 | this.decorate_(container);
20 |
21 | this.run_loaders_();
22 |
23 | body.innerHTML('');
24 | body.appendChild(container);
25 |
26 | beestat.ecobee.notify_if_down();
27 | };
28 |
29 | beestat.layer.prototype.decorate_ = function(parent) {
30 | // Left for the sublcass to implement.
31 | };
32 |
33 | /**
34 | * Register a loader. Components do this. If the same function reference is
35 | * passed by multiple components, the duplicates will be removed. The loader
36 | * was added so that I could have multiple cards on the same layer that need
37 | * the same data. Each card adds a loader and when the layer loads it runs
38 | * these functions. This way a layer can get the data one time instead of each
39 | * component firing off a duplicate API call.
40 | *
41 | * @param {Function} loader A function to call when all of the components have
42 | * been added to the layer.
43 | */
44 | beestat.layer.prototype.register_loader = function(loader) {
45 | if (this.loaders_.indexOf(loader) === -1) {
46 | this.loaders_.push(loader);
47 | }
48 | };
49 |
50 | /**
51 | * Execute all of the loaders. This is run once the decorate function has
52 | * completed and thus all of the components in the layer have had a chance to
53 | * add their loaders.
54 | */
55 | beestat.layer.prototype.run_loaders_ = function() {
56 | this.loaders_.forEach(function(loader) {
57 | loader();
58 | });
59 | };
60 |
--------------------------------------------------------------------------------
/js/layer/air_quality.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Air Quality layer.
3 | */
4 | beestat.layer.air_quality = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.air_quality, beestat.layer);
8 |
9 | beestat.layer.air_quality.prototype.decorate_ = function(parent) {
10 | /*
11 | * Set the overflow on the body so the scrollbar is always present so
12 | * highcharts graphs render properly.
13 | */
14 | $('body').style({
15 | 'overflow-y': 'scroll',
16 | 'background': beestat.style.color.bluegray.light,
17 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
18 | });
19 |
20 | (new beestat.component.header('air_quality')).render(parent);
21 |
22 | // All the cards
23 | var cards = [];
24 |
25 | if (window.is_demo === true) {
26 | cards.push([
27 | {
28 | 'card': new beestat.component.card.demo(),
29 | 'size': 12
30 | }
31 | ]);
32 | }
33 |
34 | if (beestat.thermostat.supports_air_quality(beestat.setting('thermostat_id')) === false) {
35 | cards.push([
36 | {
37 | 'card': new beestat.component.card.air_quality_not_supported(
38 | beestat.setting('thermostat_id')
39 | ),
40 | 'size': 12
41 | }
42 | ]);
43 | }
44 |
45 | cards.push([
46 | {
47 | 'card': new beestat.component.card.air_quality_detail(
48 | beestat.setting('thermostat_id')
49 | ),
50 | 'size': 12
51 | }
52 | ]);
53 |
54 | cards.push([
55 | {
56 | 'card': new beestat.component.card.air_quality_summary(
57 | beestat.setting('thermostat_id')
58 | ),
59 | 'size': 12
60 | }
61 | ]);
62 |
63 | // Footer
64 | cards.push([
65 | {
66 | 'card': new beestat.component.card.footer(),
67 | 'size': 12
68 | }
69 | ]);
70 |
71 | (new beestat.component.layout(cards)).render(parent);
72 | };
73 |
--------------------------------------------------------------------------------
/js/layer/analyze.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Analyze layer.
3 | */
4 | beestat.layer.analyze = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.analyze, beestat.layer);
8 |
9 | beestat.layer.analyze.prototype.decorate_ = function(parent) {
10 | const thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
11 |
12 | /*
13 | * Set the overflow on the body so the scrollbar is always present so
14 | * highcharts graphs render properly.
15 | */
16 | $('body').style({
17 | 'overflow-y': 'scroll',
18 | 'background': beestat.style.color.bluegray.light,
19 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
20 | });
21 |
22 | (new beestat.component.header('analyze')).render(parent);
23 |
24 | // All the cards
25 | var cards = [];
26 |
27 | if (window.is_demo === true) {
28 | cards.push([
29 | {
30 | 'card': new beestat.component.card.demo(),
31 | 'size': 12
32 | }
33 | ]);
34 | }
35 |
36 | cards.push([
37 | {
38 | 'card': new beestat.component.card.runtime_thermostat_summary(
39 | thermostat.thermostat_id
40 | ),
41 | 'size': 12
42 | }
43 | ]);
44 |
45 | cards.push([
46 | {
47 | 'card': new beestat.component.card.temperature_profiles(
48 | thermostat.thermostat_id
49 | ),
50 | 'size': 12
51 | }
52 | ]);
53 |
54 | // Footer
55 | cards.push([
56 | {
57 | 'card': new beestat.component.card.footer(),
58 | 'size': 12
59 | }
60 | ]);
61 |
62 | (new beestat.component.layout(cards)).render(parent);
63 | };
64 |
--------------------------------------------------------------------------------
/js/layer/compare.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Compare layer.
3 | */
4 | beestat.layer.compare = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.compare, beestat.layer);
8 |
9 | beestat.layer.compare.prototype.decorate_ = function(parent) {
10 | /*
11 | * Set the overflow on the body so the scrollbar is always present so
12 | * highcharts graphs render properly.
13 | */
14 | $('body').style({
15 | 'overflow-y': 'scroll',
16 | 'background': beestat.style.color.bluegray.light,
17 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
18 | });
19 |
20 | (new beestat.component.header('compare')).render(parent);
21 |
22 | const thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
23 |
24 | // All the cards
25 | const cards = [];
26 |
27 | if (window.is_demo === true) {
28 | cards.push([
29 | {
30 | 'card': new beestat.component.card.demo(),
31 | 'size': 12
32 | }
33 | ]);
34 | }
35 |
36 | cards.push([
37 | {
38 | 'card': new beestat.component.card.comparison_settings(
39 | thermostat.thermostat_id
40 | ),
41 | 'size': 6
42 | },
43 | {
44 | 'card': new beestat.component.card.my_home(
45 | thermostat.thermostat_id
46 | ),
47 | 'size': 6
48 | }
49 | ]);
50 |
51 | cards.push([
52 | {
53 | 'card': new beestat.component.card.metrics(
54 | thermostat.thermostat_id
55 | ),
56 | 'size': 12
57 | }
58 | ]);
59 |
60 | // Footer
61 | cards.push([
62 | {
63 | 'card': new beestat.component.card.footer(),
64 | 'size': 12
65 | }
66 | ]);
67 |
68 | (new beestat.component.layout(cards)).render(parent);
69 | };
70 |
--------------------------------------------------------------------------------
/js/layer/contribute.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Contribute layer.
3 | */
4 | beestat.layer.contribute = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.contribute, beestat.layer);
8 |
9 | beestat.layer.contribute.prototype.decorate_ = function(parent) {
10 | /*
11 | * Set the overflow on the body so the scrollbar is always present so
12 | * highcharts graphs render properly.
13 | */
14 | $('body').style({
15 | 'overflow-y': 'scroll',
16 | 'background': beestat.style.color.bluegray.light,
17 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
18 | });
19 |
20 | (new beestat.component.header('contribute')).render(parent);
21 |
22 | // All the cards
23 | var cards = [];
24 |
25 | if (window.is_demo === true) {
26 | cards.push([
27 | {
28 | 'card': new beestat.component.card.demo(),
29 | 'size': 12
30 | }
31 | ]);
32 | }
33 |
34 | cards.push([
35 | {
36 | 'card': new beestat.component.card.contribute(),
37 | 'size': 8
38 | },
39 | {
40 | 'card': new beestat.component.card.contribute_benefits(),
41 | 'size': 4
42 | }
43 | ]);
44 |
45 | // History
46 | cards.push([
47 | {
48 | 'card': new beestat.component.card.contribute_status(),
49 | 'size': 12
50 | }
51 | ]);
52 |
53 | // Merchandise
54 | cards.push([
55 | {
56 | 'card': new beestat.component.card.merchandise(),
57 | 'size': 12
58 | }
59 | ]);
60 |
61 | // Footer
62 | cards.push([
63 | {
64 | 'card': new beestat.component.card.footer(),
65 | 'size': 12
66 | }
67 | ]);
68 |
69 | (new beestat.component.layout(cards)).render(parent);
70 | };
71 |
--------------------------------------------------------------------------------
/js/layer/detail.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Detail layer.
3 | */
4 | beestat.layer.detail = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.detail, beestat.layer);
8 |
9 | beestat.layer.detail.prototype.decorate_ = function(parent) {
10 | const thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
11 |
12 | /*
13 | * Set the overflow on the body so the scrollbar is always present so
14 | * highcharts graphs render properly.
15 | */
16 | $('body').style({
17 | 'overflow-y': 'scroll',
18 | 'background': beestat.style.color.bluegray.light,
19 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
20 | });
21 |
22 | (new beestat.component.header('detail')).render(parent);
23 |
24 | // All the cards
25 | var cards = [];
26 |
27 | if (window.is_demo === true) {
28 | cards.push([
29 | {
30 | 'card': new beestat.component.card.demo(),
31 | 'size': 12
32 | }
33 | ]);
34 | }
35 |
36 | cards.push([
37 | {
38 | 'card': new beestat.component.card.system(thermostat.thermostat_id),
39 | 'size': 4
40 | },
41 | {
42 | 'card': new beestat.component.card.sensors(),
43 | 'size': 4
44 | },
45 | {
46 | 'card': new beestat.component.card.alerts(),
47 | 'size': 4
48 | }
49 | ]);
50 |
51 | if (beestat.component.card.contribute_reminder.should_show() === true) {
52 | cards.push([
53 | {
54 | 'card': new beestat.component.card.contribute_reminder(),
55 | 'size': 12
56 | }
57 | ]);
58 | } else if (beestat.component.card.rate_app_reminder.should_show() === true) {
59 | cards.push([
60 | {
61 | 'card': new beestat.component.card.rate_app_reminder(),
62 | 'size': 12
63 | }
64 | ]);
65 | }
66 |
67 | cards.push([
68 | {
69 | 'card': new beestat.component.card.runtime_thermostat_detail(
70 | beestat.setting('thermostat_id')
71 | ),
72 | 'size': 12
73 | }
74 | ]);
75 |
76 | cards.push([
77 | {
78 | 'card': new beestat.component.card.runtime_sensor_detail(
79 | beestat.setting('thermostat_id')
80 | ),
81 | 'size': 12
82 | }
83 | ]);
84 |
85 | cards.push([
86 | {
87 | 'card': new beestat.component.card.footer(),
88 | 'size': 12
89 | }
90 | ]);
91 |
92 | (new beestat.component.layout(cards)).render(parent);
93 | };
94 |
--------------------------------------------------------------------------------
/js/layer/no_thermostats.js:
--------------------------------------------------------------------------------
1 | /**
2 | * No thermostats layer.
3 | */
4 | beestat.layer.no_thermostats = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.no_thermostats, beestat.layer);
8 |
9 | beestat.layer.no_thermostats.prototype.decorate_ = function(parent) {
10 | /*
11 | * Set the overflow on the body so the scrollbar is always present so
12 | * highcharts graphs render properly.
13 | */
14 | $('body').style({
15 | 'overflow-y': 'scroll',
16 | 'background': beestat.style.color.bluegray.light,
17 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
18 | });
19 |
20 | (new beestat.component.header('no_thermostats')).render(parent);
21 |
22 | // All the cards
23 | const cards = [];
24 |
25 | // Manage Thermostats
26 | cards.push([
27 | {
28 | 'card': new beestat.component.card.manage_thermostats(),
29 | 'size': 12
30 | }
31 | ]);
32 |
33 | // Footer
34 | cards.push([
35 | {
36 | 'card': new beestat.component.card.footer(),
37 | 'size': 12
38 | }
39 | ]);
40 |
41 | (new beestat.component.layout(cards)).render(parent);
42 | };
43 |
--------------------------------------------------------------------------------
/js/layer/settings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Setting layer.
3 | */
4 | beestat.layer.settings = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.settings, beestat.layer);
8 |
9 | beestat.layer.settings.prototype.decorate_ = function(parent) {
10 | /*
11 | * Set the overflow on the body so the scrollbar is always present so
12 | * highcharts graphs render properly.
13 | */
14 | $('body').style({
15 | 'overflow-y': 'scroll',
16 | 'background': beestat.style.color.bluegray.light,
17 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
18 | });
19 |
20 | (new beestat.component.header('setting')).render(parent);
21 |
22 | // All the cards
23 | const cards = [];
24 |
25 | if (window.is_demo === true) {
26 | cards.push([
27 | {
28 | 'card': new beestat.component.card.demo(),
29 | 'size': 12
30 | }
31 | ]);
32 | }
33 |
34 | // Settings
35 | cards.push([
36 | {
37 | 'card': new beestat.component.card.settings(),
38 | 'size': 12
39 | }
40 | ]);
41 |
42 | // Manage Thermostats
43 | cards.push([
44 | {
45 | 'card': new beestat.component.card.manage_thermostats(),
46 | 'size': 12
47 | }
48 | ]);
49 |
50 | // Footer
51 | cards.push([
52 | {
53 | 'card': new beestat.component.card.footer(),
54 | 'size': 12
55 | }
56 | ]);
57 |
58 | (new beestat.component.layout(cards)).render(parent);
59 | };
60 |
--------------------------------------------------------------------------------
/js/layer/visualize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Visualize layer.
3 | */
4 | beestat.layer.visualize = function() {
5 | beestat.layer.apply(this, arguments);
6 | };
7 | beestat.extend(beestat.layer.visualize, beestat.layer);
8 |
9 | beestat.layer.visualize.prototype.decorate_ = function(parent) {
10 | /*
11 | * Set the overflow on the body so the scrollbar is always present so
12 | * highcharts graphs render properly.
13 | */
14 | $('body').style({
15 | 'overflow-y': 'scroll',
16 | 'background': beestat.style.color.bluegray.light,
17 | 'padding': '0 ' + beestat.style.size.gutter + 'px'
18 | });
19 |
20 | beestat.dispatcher.addEventListener([
21 | 'setting.visualize.floor_plan_id',
22 | 'setting.visualize.hide_affiliate'
23 | ], function() {
24 | (new beestat.layer.visualize()).render();
25 | });
26 |
27 | (new beestat.component.header('visualize')).render(parent);
28 |
29 | // All the cards
30 | var cards = [];
31 |
32 | if (window.is_demo === true) {
33 | cards.push([
34 | {
35 | 'card': new beestat.component.card.demo(),
36 | 'size': 12
37 | }
38 | ]);
39 | }
40 |
41 | if (
42 | beestat.setting('visualize.floor_plan_id') !== null &&
43 | beestat.setting('visualize.floor_plan_id') !== undefined
44 | ) {
45 | cards.push([
46 | {
47 | 'card': new beestat.component.card.floor_plan_editor(
48 | beestat.setting('thermostat_id')
49 | ),
50 | 'size': 12
51 | }
52 | ]);
53 |
54 | cards.push([
55 | {
56 | 'card': new beestat.component.card.visualize_settings(),
57 | 'size': 12
58 | }
59 | ]);
60 |
61 | if (
62 | beestat.setting('visualize.hide_affiliate') === false &&
63 | window.is_demo === false
64 | ) {
65 | cards.push([
66 | {
67 | 'card': new beestat.component.card.visualize_affiliate(),
68 | 'size': 12
69 | }
70 | ]);
71 | }
72 |
73 | cards.push([
74 | {
75 | 'card': new beestat.component.card.three_d()
76 | .set_floor_plan_id(beestat.setting('visualize.floor_plan_id')),
77 | 'size': 12
78 | }
79 | ]);
80 | } else {
81 | cards.push([
82 | {
83 | 'card': new beestat.component.card.visualize_intro(
84 | beestat.setting('thermostat_id')
85 | ),
86 | 'size': 12
87 | }
88 | ]);
89 | cards.push([
90 | {
91 | 'card': new beestat.component.card.visualize_video(),
92 | 'size': 12
93 | }
94 | ]);
95 | }
96 |
97 | // Footer
98 | cards.push([
99 | {
100 | 'card': new beestat.component.card.footer(),
101 | 'size': 12
102 | }
103 | ]);
104 |
105 | (new beestat.component.layout(cards)).render(parent);
106 | };
107 |
--------------------------------------------------------------------------------
/js/lib/polylabel/polylabel.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | !function(a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define([],a):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).polylabel=a()}(function(){return(function d(e,f,b){function c(a,k){if(!f[a]){if(!e[a]){var i="function"==typeof require&&require;if(!k&&i)return i(a,!0);if(g)return g(a,!0);var j=new Error("Cannot find module '"+a+"'");throw j.code="MODULE_NOT_FOUND",j}var h=f[a]={exports:{}};e[a][0].call(h.exports,function(b){return c(e[a][1][b]||b)},h,h.exports,d,e,f,b)}return f[a].exports}for(var g="function"==typeof require&&require,a=0;ab!=c[1]>b&&j<(c[0]-a[0])*(b-a[1])/(c[1]-a[1])+a[0]&&(d=!d),e=Math.min(e,h(j,b,a,c))}return(d?1:-1)*Math.sqrt(e)}function h(g,h,i,e){var c=i[0],d=i[1],a=e[0]-c,b=e[1]-d;if(0!==a||0!==b){var f=((g-c)*a+(h-d)*b)/(a*a+b*b);f>1?(c=e[0],d=e[1]):f>0&&(c+=a*f,d+=b*f)}return(a=g-c)*a+(b=h-d)*b}b.exports=function(c,o,t){o=o||1;for(var k,l,m,n,i=0;im)&&(m=g[0]),(!i||g[1]>n)&&(n=g[1])}for(var p=Math.min(m-k,n-l),a=p/2,h=new d(null,e),q=k;qj.d&&(j=b,t&&console.log("found best %d after %d probes",Math.round(1e4*b.d)/1e4,s)),b.max-j.d<=o||(a=b.h/2,h.push(new f(b.x-a,b.y-a,a,c)),h.push(new f(b.x+a,b.y-a,a,c)),h.push(new f(b.x-a,b.y+a,a,c)),h.push(new f(b.x+a,b.y+a,a,c)),s+=4)}return t&&(console.log("num probes: "+s),console.log("best distance: "+j.d)),[j.x,j.y]}},{tinyqueue:2}],2:[function(c,b,d){"use strict";function a(b,d){if(!(this instanceof a))return new a(b,d);if(this.data=b||[],this.length=this.data.length,this.compare=d||e,b)for(var c=Math.floor(this.length/2);c>=0;c--)this._down(c)}function e(a,b){return ab?1:0}function f(a,b,c){var d=a[b];a[b]=a[c],a[c]=d}b.exports=a,a.prototype={push:function(a){this.data.push(a),this.length++,this._up(this.length-1)},pop:function(){var a=this.data[0];return this.data[0]=this.data[this.length-1],this.length--,this.data.pop(),this._down(0),a},peek:function(){return this.data[0]},_up:function(a){for(var b=this.data,d=this.compare;a>0;){var c=Math.floor((a-1)/2);if(0>d(b[a],b[c]))f(b,c,a),a=c;else break}},_down:function(b){for(var c=this.data,g=this.compare,h=this.length;;){var d=2*b+1,e=d+1,a=b;if(dg(c[d],c[a])&&(a=d),eg(c[e],c[a])&&(a=e),a===b)return;f(c,a,b),b=a}}}},{}]},{},[1])(1)})
4 |
--------------------------------------------------------------------------------
/js/lib/suncalc/suncalc.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | !function(){"use strict";var b=Math.PI,d=Math.sin,e=Math.cos,f=Math.tan,g=Math.asin,h=Math.atan2,i=Math.acos,c=b/180;function j(a){return new Date((a+.5-2440588)*864e5)}function k(a){var b;return a.valueOf()/864e5-.5+2440588-2451545}var l=23.4397*c;function m(a,b){return h(d(a)*e(l)-f(b)*d(l),e(a))}function n(b,a){return g(d(a)*e(l)+e(a)*d(l)*d(b))}function o(a,b,c){return h(d(a),e(a)*d(b)-f(c)*e(b))}function p(c,a,b){return g(d(a)*d(b)+e(a)*e(b)*e(c))}function q(a,b){return c*(280.16+360.9856235*a)-b}function r(a){return c*(357.5291+.98560028*a)}function s(a){var e=c*(1.9148*d(a)+.02*d(2*a)+3e-4*d(3*a));return a+e+102.9372*c+b}function t(b){var c=r(b),a=s(c);return{dec:n(a,0),ra:m(a,0)}}var a={};a.getPosition=function(f,g,h){var b=c*g,d=k(f),a=t(d),e=q(d,-(c*h))-a.ra;return{azimuth:o(e,b,a.dec),altitude:p(e,b,a.dec)}};var u=a.times=[[-0.833,"sunrise","sunset"],[-0.3,"sunriseEnd","sunsetStart"],[-6,"dawn","dusk"],[-12,"nauticalDawn","nauticalDusk"],[-18,"nightEnd","night"],[6,"goldenHourEnd","goldenHour"]];function v(a,c,d){return 9e-4+(a+c)/(2*b)+d}function w(a,b,c){return 2451545+a+.0053*d(b)-.0069*d(2*c)}function x(f,g,h,j,k,l,m){var c,a,b,n=(c=f,a=h,b=j,i((d(c)-d(a)*d(b))/(e(a)*e(b)))),o=v(n,g,k);return w(o,l,m)}function y(a){var b=c*(134.963+13.064993*a),f=c*(218.316+13.176396*a)+6.289*c*d(b),g=5.128*c*d(c*(93.272+13.22935*a)),h=385001-20905*e(b);return{ra:m(f,g),dec:n(f,g),dist:h}}function z(a,b){return new Date(a.valueOf()+864e5*b/24)}a.addTime=function(a,b,c){u.push([a,b,c])},a.getTimes=function(y,z,A,e){e=e||0;var F,B,G,a,o,f,g,p,h=-(c*A),C=c*z,D=-2.076*Math.sqrt(e)/60,q=Math.round((B=k(y))-9e-4-h/(2*b)),t=v(0,h,q),i=r(t),l=s(i),E=n(l,0),d=w(t,i,l),m={solarNoon:j(d),nadir:j(d-.5)};for(a=0,o=u.length;a=0&&(b=j-(v=Math.sqrt(t)/(2*Math.abs(h))),l=j+v,1>=Math.abs(b)&&k++,1>=Math.abs(l)&&k++,b< -1&&(b=l)),1===k?p<0?f=d+b:g=d+b:2===k&&(f=d+(o<0?l:b),g=d+(o<0?b:l)),!f||!g);d+=2)p=n;var q={};return f&&(q.rise=z(e,f)),g&&(q.set=z(e,g)),f||g||(q[o>0?"alwaysUp":"alwaysDown"]=!0),q},"object"==typeof exports&&"undefined"!=typeof module?module.exports=a:"function"==typeof define&&define.amd?define(a):window.SunCalc=a}()
4 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "beestat",
3 | "short_name": "beestat",
4 | "icons": [
5 | {
6 | "src": "favicon.png",
7 | "sizes": "36x36 48x48 72x72 96x96 128x128 144x144 192x192 256x256 512x512"
8 | }
9 | ],
10 | "start_url": "/",
11 | "display": "standalone",
12 | "background_color": "#263238",
13 | "theme_color": "#263238",
14 | "prefer_related_applications": true,
15 | "related_applications": [
16 | {
17 | "platform": "play",
18 | "id": "io.beestat"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/service_worker.js:
--------------------------------------------------------------------------------
1 | self.addEventListener('fetch', function(event) {});
2 |
--------------------------------------------------------------------------------