├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── .gitignore ├── .well-known └── assetlinks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── api ├── address.php ├── announcement.php ├── beestat.sql ├── cora │ ├── api.php │ ├── api_cache.php │ ├── api_call.php │ ├── api_log.php │ ├── api_user.php │ ├── crud.php │ ├── database.php │ ├── exception.php │ ├── request.php │ ├── session.php │ └── setting.example.php ├── ecobee.php ├── ecobee_api_cache.php ├── ecobee_api_log.php ├── ecobee_initialize.php ├── ecobee_sensor.php ├── ecobee_thermostat.php ├── ecobee_token.php ├── external_api.php ├── external_api_cache.php ├── external_api_log.php ├── floor_plan.php ├── index.php ├── mailgun.php ├── mailgun_api_cache.php ├── mailgun_api_log.php ├── patreon.php ├── patreon_api_cache.php ├── patreon_api_log.php ├── patreon_initialize.php ├── patreon_token.php ├── profile.php ├── runtime.php ├── runtime_sensor.php ├── runtime_thermostat.php ├── runtime_thermostat_summary.php ├── runtime_thermostat_text.php ├── sensor.php ├── smarty_streets.php ├── smarty_streets_api_cache.php ├── smarty_streets_api_log.php ├── stripe.php ├── stripe_api_cache.php ├── stripe_api_log.php ├── stripe_event.php ├── stripe_payment_link.php ├── thermostat.php └── user.php ├── css └── dashboard.css ├── favicon.ico ├── favicon.png ├── favicon_apple.png ├── font ├── material_icon │ ├── material_icon.eot │ ├── material_icon.ttf │ ├── material_icon.woff │ └── material_icon.woff2 └── montserrat │ ├── montserrat_100.eot │ ├── montserrat_100.otf │ ├── montserrat_100.ttf │ ├── montserrat_100.woff │ ├── montserrat_200.eot │ ├── montserrat_200.otf │ ├── montserrat_200.ttf │ ├── montserrat_200.woff │ ├── montserrat_300.eot │ ├── montserrat_300.otf │ ├── montserrat_300.ttf │ ├── montserrat_300.woff │ ├── montserrat_400.eot │ ├── montserrat_400.otf │ ├── montserrat_400.ttf │ ├── montserrat_400.woff │ ├── montserrat_500.eot │ ├── montserrat_500.otf │ ├── montserrat_500.ttf │ ├── montserrat_500.woff │ ├── montserrat_600.eot │ ├── montserrat_600.otf │ ├── montserrat_600.ttf │ ├── montserrat_600.woff │ ├── montserrat_700.eot │ ├── montserrat_700.otf │ ├── montserrat_700.ttf │ ├── montserrat_700.woff │ ├── montserrat_800.eot │ ├── montserrat_800.otf │ ├── montserrat_800.ttf │ ├── montserrat_800.woff │ ├── montserrat_900.eot │ ├── montserrat_900.otf │ ├── montserrat_900.ttf │ └── montserrat_900.woff ├── img ├── logo.png ├── merchandise │ ├── sticker_logo.png │ └── sticker_logo_text.png └── visualize │ ├── skybox │ ├── back.png │ ├── down.png │ ├── front.png │ ├── left.png │ ├── right.png │ └── up.png │ └── stripe.png ├── index.php ├── js ├── .eslintrc.json ├── beestat.js ├── beestat │ ├── address.js │ ├── affiliate.js │ ├── api.js │ ├── area.js │ ├── cache.js │ ├── clone.js │ ├── comparisons.js │ ├── crypto.js │ ├── date.js │ ├── debounce.js │ ├── dispatcher.js │ ├── distance.js │ ├── ecobee.js │ ├── error.js │ ├── extend.js │ ├── floor_plan.js │ ├── highcharts.js │ ├── math.js │ ├── platform.js │ ├── poll.js │ ├── requestor.js │ ├── runtime_sensor.js │ ├── runtime_thermostat.js │ ├── setting.js │ ├── style.js │ ├── temperature.js │ ├── text_dimensions.js │ ├── thermostat.js │ ├── time.js │ └── user.js ├── component.js ├── component │ ├── alert.js │ ├── card.js │ ├── card │ │ ├── air_quality_detail.js │ │ ├── air_quality_not_supported.js │ │ ├── air_quality_summary.js │ │ ├── alerts.js │ │ ├── comparison_settings.js │ │ ├── contribute.js │ │ ├── contribute_benefits.js │ │ ├── contribute_reminder.js │ │ ├── contribute_status.js │ │ ├── demo.js │ │ ├── early_access.js │ │ ├── floor_plan_editor.js │ │ ├── footer.js │ │ ├── manage_thermostats.js │ │ ├── merchandise.js │ │ ├── metrics.js │ │ ├── my_home.js │ │ ├── rate_app_reminder.js │ │ ├── rookstack_survey_notification.js │ │ ├── runtime_sensor_detail.js │ │ ├── runtime_thermostat_detail.js │ │ ├── runtime_thermostat_summary.js │ │ ├── sensors.js │ │ ├── settings.js │ │ ├── system.js │ │ ├── temperature_profiles.js │ │ ├── three_d.js │ │ ├── visualize_affiliate.js │ │ ├── visualize_intro.js │ │ ├── visualize_settings.js │ │ └── visualize_video.js │ ├── chart.js │ ├── chart │ │ ├── air_quality.js │ │ ├── co2_concentration.js │ │ ├── runtime_sensor_detail_occupancy.js │ │ ├── runtime_sensor_detail_temperature.js │ │ ├── runtime_thermostat_detail_equipment.js │ │ ├── runtime_thermostat_detail_temperature.js │ │ ├── runtime_thermostat_summary.js │ │ ├── temperature_profiles.js │ │ └── voc_concentration.js │ ├── down_notification.js │ ├── floor_plan.js │ ├── floor_plan_entity.js │ ├── floor_plan_entity │ │ ├── point.js │ │ ├── room.js │ │ └── wall.js │ ├── header.js │ ├── icon.js │ ├── input.js │ ├── input │ │ ├── checkbox.js │ │ ├── radio.js │ │ ├── range.js │ │ ├── select.js │ │ └── text.js │ ├── layout.js │ ├── loading.js │ ├── logo.js │ ├── menu.js │ ├── menu_item.js │ ├── metric.js │ ├── metric │ │ ├── balance_point.js │ │ ├── balance_point │ │ │ ├── heat_1.js │ │ │ ├── heat_2.js │ │ │ └── resist.js │ │ ├── property.js │ │ ├── property │ │ │ ├── age.js │ │ │ └── square_feet.js │ │ ├── runtime_per_degree_day.js │ │ ├── runtime_per_degree_day │ │ │ ├── auxiliary_heat_1.js │ │ │ ├── auxiliary_heat_2.js │ │ │ ├── cool_1.js │ │ │ ├── cool_2.js │ │ │ ├── heat_1.js │ │ │ └── heat_2.js │ │ ├── setback.js │ │ ├── setback │ │ │ ├── cool.js │ │ │ └── heat.js │ │ ├── setpoint.js │ │ └── setpoint │ │ │ ├── cool.js │ │ │ └── heat.js │ ├── modal.js │ ├── modal │ │ ├── air_quality_detail_custom.js │ │ ├── announcements.js │ │ ├── change_floor_plan.js │ │ ├── change_system_type.js │ │ ├── change_thermostat.js │ │ ├── create_floor_plan.js │ │ ├── delete_floor_plan.js │ │ ├── download_data.js │ │ ├── enjoy_beestat.js │ │ ├── error.js │ │ ├── filter_info.js │ │ ├── floor_plan_elevation_help.js │ │ ├── newsletter.js │ │ ├── runtime_sensor_detail_custom.js │ │ ├── runtime_thermostat_detail_custom.js │ │ ├── runtime_thermostat_summary_custom.js │ │ ├── temperature_profiles_info.js │ │ ├── thermostat_info.js │ │ ├── update_floor_plan.js │ │ ├── visualize_custom.js │ │ └── weather.js │ ├── radio_group.js │ ├── scene.js │ ├── tile.js │ ├── tile │ │ ├── floor_plan.js │ │ ├── floor_plan_group.js │ │ ├── thermostat.js │ │ └── thermostat │ │ │ └── switcher.js │ ├── tile_group.js │ └── title.js ├── js.php ├── layer.js ├── layer │ ├── air_quality.js │ ├── analyze.js │ ├── compare.js │ ├── contribute.js │ ├── detail.js │ ├── load.js │ ├── no_thermostats.js │ ├── settings.js │ └── visualize.js └── lib │ ├── clipper │ └── clipper.js │ ├── highcharts │ ├── boost.js │ ├── exporting.js │ ├── highcharts-more.js │ ├── highcharts.js │ └── offline-exporting.js │ ├── moment │ └── moment.js │ ├── polylabel │ └── polylabel.js │ ├── rocket │ └── rocket.js │ ├── sentry │ └── sentry.js │ ├── suncalc │ └── suncalc.js │ └── threejs │ └── threejs.js ├── manifest.json ├── service_worker.js └── welcome └── index.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: beestat 2 | github: beestat 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Something broken? Let me know! 4 | title: '' 5 | labels: Bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Please submit all feature requests to community.beestat.io 4 | title: '' 5 | labels: Idea 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please submit all feature requests to community.beestat.io. This facilitates better discussion and allows the community to vote on things they view as important. 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | api/cora/setting.php 2 | .internal/ 3 | -------------------------------------------------------------------------------- /.well-known/assetlinks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "relation": ["delegate_permission/common.handle_all_urls"], 4 | "target": { 5 | "namespace": "android_app", 6 | "package_name": "io.beestat", 7 | "sha256_cert_fingerprints": ["CD:96:DE:AD:E9:74:E6:B0:37:C4:D8:5A:D7:66:72:94:99:5E:14:22:53:29:0C:10:84:9E:0A:FD:F0:D6:FB:2F"] 8 | } 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributing Guidelines
4 | Self-Hosting Instructions 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to beestat! 👋

2 |

3 | 4 | 5 | 6 | 7 |

8 | 9 |

10 | 11 | 12 |

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 | --------------------------------------------------------------------------------