├── .gitignore ├── LICENSE ├── README.md ├── configuration.example.php ├── cron.php ├── setup.php ├── sqlbee.php └── sqlbee.sql /.gitignore: -------------------------------------------------------------------------------- 1 | configuration.php 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jon Ziebell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _ _ 2 | | | | 3 | ___ ____| | |__ ___ ___ 4 | / __|/ _ | | _ \ / _ \/ _ \ 5 | \__ \ (_| | | |_) | __/ __/ 6 | |___/\__ |_|____/ \___|\___| 7 | | | 8 | |_| 9 | 10 | ## About 11 | sqlbee is a simple application that queries the ecobee API and extracts thermostat and runtime report data into a mySQL database. This is essentially the data from the System Monitor section of ecobee Home IQ. 12 | 13 | If you like this project, check out my other ecobee-related project: [beestat.io](https://beestat.io). It takes all the data from sqlbee and turns it into powerful graphs. 14 | 15 | _This project uses the MIT License, which means you can do whatever you want with the code and that I am not liable for anything. Have fun!_ 16 | 17 | ### What does it do? 18 | - Extracts current thermostat data like temperature, humidity, setpoints, etc (supports multiple thermostats) 19 | - Extracts runtime report data using the ecobee runtimeReport API endpoint\ 20 | 21 | ### What does it NOT do? 22 | - Does **NOT** offer a means of reading the extracted data 23 | - Does **NOT** analyze or display the data 24 | 25 | ## Requirements 26 | - An ecobee thermostat 27 | - An ecobee developer account (free) 28 | - A server with PHP (with the cURL extension) and mySQL; any recent verson of both should work fine 29 | 30 | ## Getting Started 31 | 1. Clone this project to a folder on your server. 32 | 2. Create the sqlbee database and tables on your mySQL database by running the SQL in `sqlbee.sql`. 33 | 3. Copy or rename configuration.example.php to configuration.php. 34 | 4. Create an ecobee developer account (https://www.ecobee.com/developers/). 35 | 5. Create your own ecobee app (Developer > Create New) and get the API key and set that as the `$client_id` variable in `configuration.php`. Use the PIN Authorization method when creating the app. 36 | 6. Set the `$database_*` variables in configuration.php to match your mySQL connection properties. 37 | 7. Execute setup.php by running `php -f setup.php` and follow the instructions. 38 | 8. Set up a cron job to run `cron.php` at your desired interval. Example crontab entry: `* * * * * php -f /var/www/sqlbee/cron.php`. 39 | 40 | ## Notes 41 | - After getting the project running, you might notice that roughly the past 15 minutes of rows in runtime_report have missing data. This is because the API reports these rows but the ecobee only transmits it's local data every 15 minutes. 42 | - Storage space is fairly minimal. Syncing thermostat history uses about 8,500 rows / 1.5MB per month per thermostat. Syncing sensor history uses about 8,500 rows / 1.5MB per month per sensor (the thermostat counts as a sensor). 43 | -------------------------------------------------------------------------------- /configuration.example.php: -------------------------------------------------------------------------------- 1 | get_thermostat_summary(); 15 | 16 | // Run this one regardless since it's not clear what revisions are used for all 17 | // of this data. 18 | $sqlbee->sync_thermostats(); 19 | 20 | // Sync the runtime report if any of the revision values has changed. 21 | foreach($response as $thermostat_id => $changed_revisions) { 22 | $sqlbee->sync_runtime_report($thermostat_id); 23 | } 24 | -------------------------------------------------------------------------------- /setup.php: -------------------------------------------------------------------------------- 1 | authorize(); 8 | 9 | echo PHP_EOL; 10 | echo ' ┌──────────────────────────────────────┐' . PHP_EOL; 11 | echo ' │ _ _ │' . PHP_EOL; 12 | echo ' │ | | | │' . PHP_EOL; 13 | echo ' │ ___ ____| | |__ ___ ___ │' . PHP_EOL; 14 | echo ' │ / __|/ _ | | _ \ / _ \/ _ \ │' . PHP_EOL; 15 | echo ' │ \__ \ (_| | | |_) | __/ __/ │' . PHP_EOL; 16 | echo ' │ |___/\__ |_|____/ \___|\___| │' . PHP_EOL; 17 | echo ' │ | | │' . PHP_EOL; 18 | echo ' │ |_| │' . PHP_EOL; 19 | echo ' │ │' . PHP_EOL; 20 | echo ' ├──────────────────────────────────────┤' . PHP_EOL; 21 | echo ' │ │' . PHP_EOL; 22 | echo ' │ 1. Open ecobee.com and go to │' . PHP_EOL; 23 | echo ' │ My Apps > Add Application │' . PHP_EOL; 24 | echo ' │ │' . PHP_EOL; 25 | echo ' │ 2. Enter your PIN │' . PHP_EOL; 26 | echo ' │ │' . PHP_EOL; 27 | echo ' │ ┌───────────────┐ │' . PHP_EOL; 28 | echo ' │ │ ' . strtoupper($response['ecobeePin']) . ' │ │' . PHP_EOL; 29 | echo ' │ └───────────────┘ │' . PHP_EOL; 30 | echo ' │ │' . PHP_EOL; 31 | echo ' │ Waiting for authorization │' . PHP_EOL; 32 | echo ' │ □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□ │'; 33 | for($i = 0; $i < 35; $i++) { 34 | echo chr(8); // Backspace 35 | } 36 | 37 | $authorized = false; 38 | 39 | $bar_width = 30; 40 | $bar_width_remain = $bar_width; 41 | 42 | // Ecobee enforces a silly 30 second minimum interval...so dumb. Adding a bit 43 | // just to be safe. 44 | $ecobee_requested_delay = $response['interval'] + 2; 45 | 46 | $last_ecobee = time(); 47 | while($bar_width_remain-- > 0 && $authorized === false) { 48 | echo '■'; 49 | 50 | if(time() - $last_ecobee > $ecobee_requested_delay) { 51 | try { 52 | $response = $sqlbee->grant_token($response['code']); 53 | $last_ecobee = time(); 54 | $authorized = true; 55 | } 56 | catch(\Exception $e) { 57 | $last_ecobee = time(); 58 | } 59 | 60 | } 61 | 62 | sleep(5); 63 | } 64 | 65 | // Fill the rest of the progress bar. 66 | while($bar_width_remain-- >= 0) { 67 | echo '■'; 68 | } 69 | 70 | echo PHP_EOL; 71 | 72 | if($authorized === true) { 73 | echo ' │ │' . PHP_EOL; 74 | echo ' │ SUCCESS! │' . PHP_EOL; 75 | echo ' │ │' . PHP_EOL; 76 | echo ' │ 3. Syncing... │' . PHP_EOL; 77 | 78 | // Sync over the thermostats 79 | $response = $sqlbee->get_thermostat_summary(); 80 | $sqlbee->sync_thermostats(); 81 | 82 | // Sneak in a setup variable just for this. 83 | $class = new \ReflectionClass('\sqlbee\configuration'); 84 | $class->setStaticPropertyValue('setup', true); 85 | 86 | foreach($response as $thermostat_id => $changed_revisions) { 87 | $sqlbee->sync_runtime_report($thermostat_id); 88 | } 89 | sleep(1); 90 | echo "\r"; 91 | echo ' │ Done │'; 92 | sleep (1); 93 | echo PHP_EOL; 94 | 95 | echo ' │ │' . PHP_EOL; 96 | echo ' │ 4. Dont forget to add a cron job │' . PHP_EOL; 97 | echo ' │ for cron.php to keep your data │' . PHP_EOL; 98 | echo ' │ up to date. │' . PHP_EOL; 99 | } 100 | else { 101 | echo ' │ │' . PHP_EOL; 102 | echo ' │ FAILURE! │' . PHP_EOL; 103 | } 104 | 105 | echo ' │ │' . PHP_EOL; 106 | echo ' └──────────────────────────────────────┘' . PHP_EOL; 107 | -------------------------------------------------------------------------------- /sqlbee.php: -------------------------------------------------------------------------------- 1 | mysqli = new \mysqli( 17 | configuration::$database_host, 18 | configuration::$database_username, 19 | configuration::$database_password, 20 | configuration::$database_name 21 | ); 22 | 23 | if ($this->mysqli->connect_error !== null) { 24 | throw new \Exception($this->mysqli->connect_error . '(' . $this->mysqli->connect_errno . ')'); 25 | } 26 | 27 | set_error_handler(array($this, 'error_handler')); 28 | set_exception_handler(array($this, 'exception_handler')); 29 | register_shutdown_function(array($this, 'shutdown_handler')); 30 | 31 | $this->mysqli->query('start transaction') or die($this->mysqli->error); 32 | 33 | // Everything in this script is done in UTC time. 34 | date_default_timezone_set('UTC'); 35 | } 36 | 37 | /** 38 | * Send an API call to ecobee and return the response. 39 | * 40 | * @param string $method GET or POST 41 | * @param string $endpoint The API endpoint 42 | * @param array $arguments POST or GET parameters 43 | * @param boolean $auto_refresh_token Whether or not to automatically get a 44 | * new token if the old one is expired. 45 | * 46 | * @return array The response of this API call. 47 | */ 48 | private function ecobee($method, $endpoint, $arguments, $auto_refresh_token = true) { 49 | $curl_handle = curl_init(); 50 | 51 | // Attach the client_id to all requests. 52 | $arguments['client_id'] = configuration::$client_id; 53 | 54 | // Authorize/token endpoints don't use the /1/ in the URL. Everything else 55 | // does. 56 | $full_endpoint = $endpoint; 57 | if($full_endpoint !== 'authorize' && $full_endpoint !== 'token') { 58 | $full_endpoint = '/1/' . $full_endpoint; 59 | 60 | // For non-authorization endpoints, add the access_token header. 61 | $query = 'select * from token order by token_id desc limit 1'; 62 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 63 | $token = $result->fetch_assoc(); 64 | curl_setopt($curl_handle, CURLOPT_HTTPHEADER , array( 65 | 'Authorization: Bearer ' . $token['access_token'] 66 | )); 67 | } 68 | else { 69 | $full_endpoint = '/' . $full_endpoint; 70 | } 71 | $url = 'https://api.ecobee.com' . $full_endpoint; 72 | 73 | if($method === 'GET') { 74 | $url .= '?' . http_build_query($arguments); 75 | } 76 | 77 | curl_setopt($curl_handle, CURLOPT_URL, $url); 78 | curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true); 79 | 80 | if($method === 'POST') { 81 | curl_setopt($curl_handle, CURLOPT_POST, true); 82 | curl_setopt($curl_handle, CURLOPT_POSTFIELDS, http_build_query($arguments)); 83 | } 84 | 85 | $curl_response = curl_exec($curl_handle); 86 | 87 | // Log this request and response 88 | if(configuration::$log_api_calls === true) { 89 | $query = ' 90 | insert into api_log( 91 | `method`, 92 | `endpoint`, 93 | `json_arguments`, 94 | `response` 95 | ) 96 | values( 97 | "' . $this->mysqli->real_escape_string($method) . '", 98 | "' . $this->mysqli->real_escape_string($full_endpoint) . '", 99 | "' . $this->mysqli->real_escape_string(json_encode($arguments)) . '", 100 | "' . $this->mysqli->real_escape_string($curl_response) . '" 101 | ) 102 | '; 103 | $this->mysqli->query($query) or die($this->mysqli->error); 104 | } 105 | 106 | if($curl_response === false || curl_errno($curl_handle) !== 0) { 107 | throw new \Exception('cURL error: ' . curl_error($curl_handle)); 108 | } 109 | 110 | $response = json_decode($curl_response, true); 111 | if($response === false) { 112 | throw new \Exception('Invalid JSON'); 113 | } 114 | 115 | curl_close($curl_handle); 116 | 117 | // If the token was expired, refresh it and try again. Trying again sets 118 | // auto_refresh_token to false to prevent accidental infinite refreshing if 119 | // something bad happens. 120 | if(isset($response['status']) === true && $response['status']['code'] === 14) { 121 | // Authentication token has expired. Refresh your tokens. 122 | if ($auto_refresh_token === true) { 123 | $this->refresh_token($token['refresh_token']); 124 | return $this->ecobee($method, $endpoint, $arguments, false); 125 | } 126 | else { 127 | throw new \Exception($response['status']['message']); 128 | } 129 | } 130 | else if(isset($response['status']) === true && $response['status']['code'] !== 0) { 131 | // Any other error 132 | throw new \Exception($response['status']['message']); 133 | } 134 | else { 135 | return $response; 136 | } 137 | } 138 | 139 | /** 140 | * Perform the first-time authorization for this app. 141 | * 142 | * @see https://www.ecobee.com/home/developer/api/documentation/v1/auth/pin-api-authorization.shtml 143 | * 144 | * @return array The response of this API call. Included will be the code 145 | * needed for the grant_token API call. 146 | */ 147 | public function authorize() { 148 | return $this->ecobee( 149 | 'GET', 150 | 'authorize', 151 | array( 152 | 'response_type' => 'ecobeePin', 153 | 'scope' => configuration::$scope 154 | ) 155 | ); 156 | } 157 | 158 | /** 159 | * Given a code returned by the authorize endpoint, obtain an access token 160 | * for use on all future API calls. 161 | * 162 | * @see https://www.ecobee.com/home/developer/api/documentation/v1/auth/auth-req-resp.shtml 163 | * @see https://www.ecobee.com/home/developer/api/documentation/v1/auth/authz-code-authorization.shtml 164 | * 165 | * @param string $code 166 | * 167 | * @return array The response of the API call. Included will be the 168 | * access_token needed for the API call and the refresh_token to use if the 169 | * access_token ends up being expired. 170 | */ 171 | public function grant_token($code) { 172 | $response = $this->ecobee( 173 | 'POST', 174 | 'token', 175 | array( 176 | 'grant_type' => 'ecobeePin', 177 | 'code' => $code 178 | ) 179 | ); 180 | 181 | if(isset($response['access_token']) === false || isset($response['refresh_token']) === false) { 182 | throw new \Exception('Could not grant token'); 183 | } 184 | 185 | $access_token_escaped = $this->mysqli->real_escape_string($response['access_token']); 186 | $refresh_token_escaped = $this->mysqli->real_escape_string($response['refresh_token']); 187 | $query = ' 188 | insert into token( 189 | `access_token`, 190 | `refresh_token` 191 | ) values( 192 | "' . $access_token_escaped . '", 193 | "' . $refresh_token_escaped . '" 194 | )'; 195 | $this->mysqli->query($query) or die($this->mysqli->error); 196 | 197 | return $response; 198 | } 199 | 200 | /** 201 | * Given the latest refresh token, obtain a fresh access token for use in 202 | * all future API calls. 203 | * 204 | * @param string $refresh_token 205 | * 206 | * @return array The response of the API call. 207 | */ 208 | public function refresh_token($refresh_token) { 209 | $response = $this->ecobee( 210 | 'POST', 211 | 'token', 212 | array( 213 | 'grant_type' => 'refresh_token', 214 | 'refresh_token' => $refresh_token 215 | ) 216 | ); 217 | 218 | if(isset($response['access_token']) === false || isset($response['refresh_token']) === false) { 219 | throw new \Exception('Could not grant token'); 220 | } 221 | 222 | $access_token_escaped = $this->mysqli->real_escape_string($response['access_token']); 223 | $refresh_token_escaped = $this->mysqli->real_escape_string($response['refresh_token']); 224 | $query = 'insert into token(`access_token`, `refresh_token`) values("' . $access_token_escaped . '", "' . $refresh_token_escaped . '")'; 225 | $this->mysqli->query($query) or die($this->mysqli->error); 226 | 227 | return $response; 228 | } 229 | 230 | /** 231 | * This is the main polling function and can be called fairly frequently. 232 | * This will get a list of all thermostats and their revisions, then return 233 | * any revision value that has changed so that other API calls can be made. 234 | * 235 | * @see https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostat-summary.shtml 236 | * 237 | * @return array An array of thermostat identifiers pointing at an array of 238 | * revision columns that changed. 239 | */ 240 | public function get_thermostat_summary() { 241 | $response = $this->ecobee( 242 | 'GET', 243 | 'thermostatSummary', 244 | array( 245 | 'body' => json_encode(array( 246 | 'selection' => array( 247 | 'selectionType' => 'registered', 248 | 'selectionMatch' => '' 249 | ) 250 | )) 251 | ) 252 | ); 253 | 254 | $return = array(); 255 | 256 | // Mark all thermostats as deleted 257 | $query = 'update thermostat set deleted = 1'; 258 | $this->mysqli->query($query) or die($this->mysqli->error); 259 | 260 | // Update revisions and a few other columns. Also create/delete new/old 261 | // thermostats on the fly. 262 | foreach($response['revisionList'] as $thermostat) { 263 | // Mutate the return data into what is essentially the essence of a 264 | // thermostat. 265 | $thermostat = explode(':', $thermostat); 266 | $thermostat = array( 267 | 'identifier' => $thermostat[0], 268 | 'name' => $thermostat[1], 269 | 'connected' => $thermostat[2] === 'true' ? '1' : '0', 270 | 'thermostat_revision' => $thermostat[3], 271 | 'alert_revision' => $thermostat[4], 272 | 'runtime_revision' => $thermostat[5], 273 | 'internal_revision' => $thermostat[6] 274 | ); 275 | 276 | // Check to see if this thermostat already exists. 277 | $query = 'select * from thermostat where identifier = "' . $this->mysqli->real_escape_string($thermostat['identifier']) . '"'; 278 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 279 | 280 | // If this thermostat does not already exist, create it. 281 | if($result->num_rows === 0) { 282 | $original_thermostat = array(); 283 | $query = ' 284 | insert into thermostat( 285 | identifier, 286 | name, 287 | connected, 288 | thermostat_revision, 289 | alert_revision, 290 | runtime_revision, 291 | internal_revision 292 | ) 293 | values( 294 | "' . $this->mysqli->real_escape_string($thermostat['identifier']) . '", 295 | "' . $this->mysqli->real_escape_string($thermostat['name']) . '", 296 | "' . $this->mysqli->real_escape_string($thermostat['connected']) . '", 297 | "' . $this->mysqli->real_escape_string($thermostat['thermostat_revision']) . '", 298 | "' . $this->mysqli->real_escape_string($thermostat['alert_revision']) . '", 299 | "' . $this->mysqli->real_escape_string($thermostat['runtime_revision']) . '", 300 | "' . $this->mysqli->real_escape_string($thermostat['internal_revision']) . '" 301 | )'; 302 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 303 | $thermostat_id = $this->mysqli->insert_id; 304 | } 305 | else { 306 | // If this thermostat already exists, update it. 307 | $original_thermostat = $result->fetch_assoc(); 308 | $query = ' 309 | update thermostat set 310 | name = "' . $this->mysqli->real_escape_string($thermostat['name']) . '", 311 | connected = "' . $this->mysqli->real_escape_string($thermostat['connected']) . '", 312 | thermostat_revision = "' . $this->mysqli->real_escape_string($thermostat['thermostat_revision']) . '", 313 | alert_revision = "' . $this->mysqli->real_escape_string($thermostat['alert_revision']) . '", 314 | runtime_revision = "' . $this->mysqli->real_escape_string($thermostat['runtime_revision']) . '", 315 | internal_revision = "' . $this->mysqli->real_escape_string($thermostat['internal_revision']) . '", 316 | deleted = 0 317 | where 318 | identifier = "' . $thermostat['identifier'] . '"'; 319 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 320 | $thermostat_id = $original_thermostat['thermostat_id']; 321 | } 322 | $diff = array_diff($thermostat, $original_thermostat); 323 | $return[$thermostat_id] = array_intersect_key($diff, array_flip(array('thermostat_revision', 'alert_revision', 'runtime_revision', 'internal_revision'))); 324 | } 325 | 326 | // Return the most recent values for any revision columns that have changed. 327 | // If it's a new thermostat, all of them will be returned. Keyed by 328 | // thermostat_id. 329 | return $return; 330 | } 331 | 332 | /** 333 | * Given a list of thermostats, get and update the runtime data in the 334 | * thermostat table. 335 | * 336 | * @see https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml 337 | * @see https://www.ecobee.com/home/developer/api/documentation/v1/objects/Selection.shtml 338 | */ 339 | public function sync_thermostats() { 340 | $response = $this->ecobee( 341 | 'GET', 342 | 'thermostat', 343 | array( 344 | 'body' => json_encode(array( 345 | 'selection' => array( 346 | 'selectionType' => 'registered', 347 | 'selectionMatch' => '', 348 | 'includeRuntime' => true, 349 | 'includeExtendedRuntime' => true, 350 | 'includeElectricity' => true, 351 | 'includeSettings' => true, 352 | 'includeLocation' => true, 353 | 'includeProgram' => true, 354 | 'includeEvents' => true, 355 | 'includeDevice' => true, 356 | 'includeTechnician' => true, 357 | 'includeUtility' => true, 358 | 'includeManagement' => true, 359 | 'includeAlerts' => true, 360 | 'includeWeather' => true, 361 | 'includeHouseDetails' => true, 362 | 'includeOemCfg' => true, 363 | 'includeEquipmentStatus' => true, 364 | 'includeNotificationSettings' => true, 365 | 'includeVersion' => true, 366 | 'includePrivacy' => true, 367 | 'includeAudio' => true, 368 | 'includeSensors' => true 369 | 370 | /** 371 | * 'includeReminders' => true 372 | * 373 | * While documented, this is not available for general API use 374 | * unless you are a technician user. 375 | * 376 | * The reminders and the includeReminders flag are something extra 377 | * for ecobee Technicians. It allows them to set and receive 378 | * reminders with more detail than the usual alert reminder type. 379 | * These reminders are only available to Technician users, which 380 | * is why you aren't seeing any new information when you set that 381 | * flag to true. Thanks for pointing out the lack of documentation 382 | * regarding this. We'll get this updated as soon as possible. 383 | * 384 | * 385 | * https://getsatisfaction.com/api/topics/what-does-includereminders-do-when-calling-get-thermostat?rfm=1 386 | */ 387 | 388 | /** 389 | * 'includeSecuritySettings' => true 390 | * 391 | * While documented, this is not made available for general API 392 | * use unless you are a utility. If you try to include this an 393 | * "Authentication failed" error will be returned. 394 | * 395 | * Special accounts such as Utilities are permitted an alternate 396 | * method of authorization using implicit authorization. This 397 | * method permits the Utility application to authorize against 398 | * their own specific account without the requirement of a PIN. 399 | * This method is limited to special contractual obligations and 400 | * is not available for 3rd party applications who are not 401 | * Utilities. 402 | * 403 | * https://www.ecobee.com/home/developer/api/documentation/v1/objects/SecuritySettings.shtml 404 | * https://www.ecobee.com/home/developer/api/documentation/v1/auth/auth-intro.shtml 405 | * 406 | */ 407 | 408 | ) 409 | )) 410 | ) 411 | ); 412 | 413 | // Update each thermostat with the actual and desired values. 414 | foreach($response['thermostatList'] as $thermostat) { 415 | $query = ' 416 | select 417 | thermostat_id 418 | from 419 | thermostat 420 | where 421 | identifier = "' . $this->mysqli->real_escape_string($thermostat['identifier']) . '" 422 | '; 423 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 424 | if($result->num_rows === 1) { 425 | $row = $result->fetch_assoc(); 426 | $thermostat_id = $row['thermostat_id']; 427 | } 428 | else { 429 | throw new \Exception('Invalid thermostat identifier'); 430 | } 431 | 432 | $query = ' 433 | update 434 | thermostat 435 | set 436 | json_runtime = "' . $this->mysqli->real_escape_string(json_encode($thermostat['runtime'])) . '", 437 | json_extended_runtime = "' . $this->mysqli->real_escape_string(json_encode($thermostat['extendedRuntime'])) . '", 438 | json_electricity = "' . $this->mysqli->real_escape_string(json_encode($thermostat['electricity'])) . '", 439 | json_settings = "' . $this->mysqli->real_escape_string(json_encode($thermostat['settings'])) . '", 440 | json_location = "' . $this->mysqli->real_escape_string(json_encode($thermostat['location'])) . '", 441 | json_program = "' . $this->mysqli->real_escape_string(json_encode($thermostat['program'])) . '", 442 | json_events = "' . $this->mysqli->real_escape_string(json_encode($thermostat['events'])) . '", 443 | json_device = "' . $this->mysqli->real_escape_string(json_encode($thermostat['devices'])) . '", 444 | json_technician = "' . $this->mysqli->real_escape_string(json_encode($thermostat['technician'])) . '", 445 | json_utility = "' . $this->mysqli->real_escape_string(json_encode($thermostat['utility'])) . '", 446 | json_management = "' . $this->mysqli->real_escape_string(json_encode($thermostat['management'])) . '", 447 | json_alerts = "' . $this->mysqli->real_escape_string(json_encode($thermostat['alerts'])) . '", 448 | json_weather = "' . $this->mysqli->real_escape_string(json_encode($thermostat['weather'])) . '", 449 | json_house_details = "' . $this->mysqli->real_escape_string(json_encode($thermostat['houseDetails'])) . '", 450 | json_oem_cfg = "' . $this->mysqli->real_escape_string(json_encode($thermostat['oemCfg'])) . '", 451 | json_equipment_status = "' . $this->mysqli->real_escape_string(trim($thermostat['equipmentStatus']) !== '' ? json_encode(explode(',', $thermostat['equipmentStatus'])) : json_encode(array())) . '", 452 | json_notification_settings = "' . $this->mysqli->real_escape_string(json_encode($thermostat['notificationSettings'])) . '", 453 | json_privacy = "' . $this->mysqli->real_escape_string(json_encode($thermostat['privacy'])) . '", 454 | json_version = "' . $this->mysqli->real_escape_string(json_encode($thermostat['version'])) . '", 455 | json_remote_sensors = "' . $this->mysqli->real_escape_string(json_encode($thermostat['remoteSensors'])) . '", 456 | json_audio = "' . $this->mysqli->real_escape_string(json_encode($thermostat['audio'])) . '" 457 | where 458 | thermostat_id = ' . $thermostat_id . ' 459 | '; 460 | $this->mysqli->query($query) or die($this->mysqli->error); 461 | 462 | // Mark all sensors as deleted 463 | $query = ' 464 | update 465 | sensor 466 | set 467 | deleted = 1 468 | where 469 | thermostat_id = "' . $this->mysqli->real_escape_string($thermostat_id) . '" 470 | '; 471 | $this->mysqli->query($query) or die($this->mysqli->error); 472 | 473 | // Create/update sensors. 474 | foreach($thermostat['remoteSensors'] as $sensor) { 475 | // Check to see if this thermostat already exists. 476 | $query = ' 477 | select 478 | * 479 | from 480 | sensor 481 | where 482 | thermostat_id = "' . $this->mysqli->real_escape_string($thermostat_id) . '" 483 | and identifier = "' . $this->mysqli->real_escape_string($sensor['id']) . '" 484 | '; 485 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 486 | 487 | // If this sensor does not already exist, create it. 488 | if($result->num_rows === 0) { 489 | $query = ' 490 | insert into sensor( 491 | thermostat_id, 492 | identifier, 493 | name, 494 | type, 495 | code, 496 | in_use, 497 | json_capability 498 | ) 499 | values( 500 | "' . $this->mysqli->real_escape_string($thermostat_id) . '", 501 | "' . $this->mysqli->real_escape_string($sensor['id']) . '", 502 | "' . $this->mysqli->real_escape_string($sensor['name']) . '", 503 | "' . $this->mysqli->real_escape_string($sensor['type']) . '", 504 | ' . ((isset($sensor['code']) === true) ? ('"' . $this->mysqli->real_escape_string($sensor['code']) . '"') : ('null')) . ', 505 | "' . ($sensor['inUse'] === true ? '1' : '0') . '", 506 | "' . $this->mysqli->real_escape_string(json_encode($sensor['capability'])) . '" 507 | )'; 508 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 509 | } 510 | else { 511 | $row = $result->fetch_assoc(); 512 | $query = ' 513 | update sensor set 514 | thermostat_id = "' . $this->mysqli->real_escape_string($thermostat_id) . '", 515 | name = "' . $this->mysqli->real_escape_string($sensor['name']) . '", 516 | type = "' . $this->mysqli->real_escape_string($sensor['type']) . '", 517 | code = ' . ((isset($sensor['code']) === true) ? ('"' . $this->mysqli->real_escape_string($sensor['code']) . '"') : ('null')) . ', 518 | in_use = "' . ($sensor['inUse'] === true ? '1' : '0') . '", 519 | json_capability = "' . $this->mysqli->real_escape_string(json_encode($sensor['capability'])) . '", 520 | deleted = 0 521 | where 522 | sensor_id = ' . $row['sensor_id'] . ' 523 | '; 524 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 525 | } 526 | } 527 | } 528 | } 529 | 530 | /** 531 | * Generates properly chunked time ranges and then syncs up to one week of 532 | * data at a time back to either the last entry in the table or the date the 533 | * thermostat was first connected. 534 | * 535 | * @param int $thermostat_id 536 | */ 537 | public function sync_runtime_report($thermostat_id) { 538 | $thermostat_id_escaped = $this->mysqli->real_escape_string($thermostat_id); 539 | $query = ' 540 | select 541 | * 542 | from 543 | runtime_report_thermostat 544 | where 545 | thermostat_id = "' . $thermostat_id_escaped . '" 546 | order by 547 | timestamp desc 548 | limit 1 549 | '; 550 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 551 | if($result->num_rows === 0) { 552 | $thermostat = $this->get_thermostat($thermostat_id); 553 | $desired_begin_gmt = strtotime($thermostat['json_runtime']['firstConnected']); 554 | } 555 | else { 556 | $row = $result->fetch_assoc(); 557 | $desired_begin_gmt = strtotime($row['timestamp']) - date('Z') - (3600 * 2); 558 | } 559 | 560 | // Set $end_gmt to the current time. 561 | $end_gmt = time() - date('Z'); 562 | 563 | $chunk_size = 86400 * 7; // 7 days (in seconds) 564 | 565 | // Start $begin_gmt at $end_gmt. In the loop, $begin_gmt is always 566 | // decremented by $chunk_size (no older than $desired_begin_gmt) prior to 567 | // calling sync_runtime_report_. Also, $end_gmt gets initially set to 568 | // $begin_gmt every loop to continually shift back both points. 569 | $begin_gmt = $end_gmt; 570 | do { 571 | $end_gmt = $begin_gmt; 572 | $begin_gmt = max($desired_begin_gmt, $begin_gmt - $chunk_size); 573 | $this->sync_runtime_report_($thermostat_id, $begin_gmt, $end_gmt); 574 | } while($begin_gmt > $desired_begin_gmt); 575 | } 576 | 577 | /** 578 | * Get the runtime report data for a specified thermostat. Updates the 579 | * runtime_report_thermostat and runtime_report_sensor tables. 580 | * 581 | * @param int $thermostat_id 582 | */ 583 | private function sync_runtime_report_($thermostat_id, $begin_gmt, $end_gmt) { 584 | $thermostat = $this->get_thermostat($thermostat_id); 585 | 586 | $begin_date = date('Y-m-d', $begin_gmt); 587 | $begin_interval = date('H', $begin_gmt) * 12 + round(date('i', $begin_gmt) / 5); 588 | 589 | $end_date = date('Y-m-d', $end_gmt); 590 | $end_interval = date('H', $end_gmt) * 12 + round(date('i', $end_gmt) / 5); 591 | 592 | if(configuration::$setup === true) { 593 | echo "\r"; 594 | $string = date('m/d/Y', $begin_gmt) . ' > ' . date('m/d/Y', $end_gmt); 595 | echo ' │ ' . $string; 596 | for($i = 0; $i < 33 - strlen($string); $i++) { 597 | echo ' '; 598 | } 599 | echo '│'; 600 | } 601 | 602 | $columns = array( 603 | 'auxHeat1' => 'auxiliary_heat_1', 604 | 'auxHeat2' => 'auxiliary_heat_2', 605 | 'auxHeat3' => 'auxiliary_heat_3', 606 | 'compCool1' => 'compressor_cool_1', 607 | 'compCool2' => 'compressor_cool_2', 608 | 'compHeat1' => 'compressor_heat_1', 609 | 'compHeat2' => 'compressor_heat_2', 610 | 'dehumidifier' => 'dehumidifier', 611 | 'dmOffset' => 'demand_management_offset', 612 | 'economizer' => 'economizer', 613 | 'fan' => 'fan', 614 | 'humidifier' => 'humidifier', 615 | 'hvacMode' => 'hvac_mode', 616 | 'outdoorHumidity' => 'outdoor_humidity', 617 | 'outdoorTemp' => 'outdoor_temperature', 618 | 'sky' => 'sky', 619 | 'ventilator' => 'ventilator', 620 | 'wind' => 'wind', 621 | 'zoneAveTemp' => 'zone_average_temperature', 622 | 'zoneCalendarEvent' => 'zone_calendar_event', 623 | 'zoneClimate' => 'zone_climate', 624 | 'zoneCoolTemp' => 'zone_cool_temperature', 625 | 'zoneHeatTemp' => 'zone_heat_temperature', 626 | 'zoneHumidity' => 'zone_humidity', 627 | 'zoneHumidityHigh' => 'zone_humidity_high', 628 | 'zoneHumidityLow' => 'zone_humidity_low', 629 | 'zoneHvacMode' => 'zone_hvac_mode', 630 | 'zoneOccupancy' => 'zone_occupancy' 631 | ); 632 | 633 | $response = $this->ecobee( 634 | 'GET', 635 | 'runtimeReport', 636 | array( 637 | 'body' => json_encode(array( 638 | 'selection' => array( 639 | 'selectionType' => 'thermostats', 640 | 'selectionMatch' => $thermostat['identifier'] // This is required by this API call 641 | ), 642 | 'startDate' => $begin_date, 643 | 'startInterval' => $begin_interval, 644 | 'endDate' => $end_date, 645 | 'endInterval' => $end_interval, 646 | 'columns' => implode(',', array_keys($columns)), 647 | 'includeSensors' => true 648 | )) 649 | ) 650 | ); 651 | 652 | $inserts = array(); 653 | $on_duplicate_keys = array(); 654 | foreach($response['reportList'][0]['rowList'] as $row) { 655 | $row = explode(',', $row); 656 | $row = array_map('trim', $row); 657 | $row = array_map(array($this->mysqli, 'real_escape_string'), $row); 658 | 659 | // Date and time are first two columns of the returned data. 660 | list($date, $time) = array_splice($row, 0, 2); 661 | 662 | // Put thermostat_id and date onto the front of the array to be inserted. 663 | array_unshift( 664 | $row, 665 | $thermostat_id, 666 | date('Y-m-d H:i:s', strtotime($date . ' ' . $time)) 667 | ); 668 | 669 | $insert = '("' . implode('","', $row) . '")'; 670 | $insert = str_replace('""', 'null', $insert); 671 | $inserts[] = $insert; 672 | } 673 | 674 | foreach(array_merge(array('thermostat_id' => 'thermostat_id', 'timestamp' => 'timestamp'), $columns) as $column) { 675 | $on_duplicate_keys[] = '`' . $column . '` = values(`' . $column . '`)'; 676 | } 677 | 678 | $query = 'insert into runtime_report_thermostat(`' . implode('`,`', array_merge(array('thermostat_id', 'timestamp'), array_values($columns))) . '`) values' . implode(',', $inserts) . ' on duplicate key update ' . implode(',', $on_duplicate_keys); 679 | $this->mysqli->query($query) or die($this->mysqli->error); 680 | 681 | 682 | // Check timestamp columns on both tables...I think the default value and the on update value can be removed as we use ecobee values here 683 | 684 | /** 685 | * runtime_report_sensor 686 | */ 687 | 688 | // Get a list of all sensors in the database for this thermostat. 689 | $query = 'select * from sensor where thermostat_id = ' . $thermostat_id; 690 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 691 | 692 | $sensors = array(); 693 | while($row = $result->fetch_assoc()) { 694 | $sensors[$row['identifier']] = $row; 695 | } 696 | 697 | // Create a sensor metric array keyed by the silly identifier. 698 | $sensor_metrics = array(); 699 | foreach($response['sensorList'][0]['sensors'] as $sensor_metric) { 700 | $sensor_metrics[$sensor_metric['sensorId']] = $sensor_metric; 701 | 702 | $sensor_identifier = substr( 703 | $sensor_metric['sensorId'], 704 | 0, 705 | strrpos($sensor_metric['sensorId'], ':') 706 | ); 707 | 708 | $sensor_metrics[$sensor_metric['sensorId']]['sensor'] = $sensors[$sensor_identifier]; 709 | } 710 | 711 | // Construct a more sensible data object that maps everything properly. 712 | $objects = array(); 713 | foreach($response['sensorList'][0]['data'] as $i => $row) { 714 | $row = explode(',', $row); 715 | $row = array_map('trim', $row); 716 | $row = array_map(array($this->mysqli, 'real_escape_string'), $row); 717 | 718 | // Date and time are first two columns of the returned data. 719 | $date = $row[0]; 720 | $time = $row[1]; 721 | $timestamp = date('Y-m-d H:i:s', strtotime($date . ' ' . $time)); 722 | 723 | for($j = 2; $j < count($row); $j++) { 724 | $column = $response['sensorList'][0]['columns'][$j]; 725 | $sensor_metric = $sensor_metrics[$column]; 726 | $sensor = $sensor_metric['sensor']; 727 | $sensor_id = $sensor['sensor_id']; 728 | $sensor_metric_type = $sensor_metric['sensorType']; 729 | 730 | // Need to generate a unique key per row per sensor as each row of 731 | // returned data represents data from multiple sensors. ಠ_ಠ 732 | $key = $i . '_' . $sensor_id; 733 | 734 | if(isset($objects[$key]) === false) { 735 | $objects[$key] = array( 736 | 'thermostat_id' => $thermostat_id, 737 | 'sensor_id' => $sensor_id, 738 | 'timestamp' => $timestamp, 739 | 'temperature' => null, 740 | 'humidity' => null, 741 | 'occupancy' => null 742 | ); 743 | } 744 | if($row[$j] !== '' && $row[$j] !== 'null') { 745 | $objects[$key][$sensor_metric_type] = $row[$j]; 746 | } 747 | } 748 | } 749 | 750 | // Get a nice integer-indexed array from the silly keyed array from earlier. 751 | $objects = array_values($objects); 752 | 753 | // And finally do the actual insert 754 | $inserts = array(); 755 | $on_duplicate_keys = array(); 756 | if(count($objects) > 0) { 757 | $columns = array_keys($objects[0]); 758 | 759 | foreach($objects as $object) { 760 | $insert = '("' . implode('","', array_values($object)) . '")'; 761 | $insert = str_replace('""', 'null', $insert); 762 | $inserts[] = $insert; 763 | } 764 | 765 | foreach($columns as $column) { 766 | $on_duplicate_keys[] = '`' . $column . '` = values(`' . $column . '`)'; 767 | } 768 | 769 | $query = ' 770 | insert into 771 | runtime_report_sensor(`' . implode('`,`', $columns) . '`) 772 | values' . implode(',', $inserts) . ' 773 | on duplicate key update ' . implode(',', $on_duplicate_keys); 774 | $this->mysqli->query($query) or die($this->mysqli->error); 775 | } 776 | } 777 | 778 | /** 779 | * Get a thermostat from a thermostat_id. 780 | * 781 | * @param int $thermostat_id 782 | * 783 | * @return array The thermostat. 784 | */ 785 | private function get_thermostat($thermostat_id) { 786 | $query = 'select * from thermostat where thermostat_id = "' . $this->mysqli->real_escape_string($thermostat_id) . '"'; 787 | $result = $this->mysqli->query($query) or die($this->mysqli->error); 788 | if($result->num_rows === 0) { 789 | throw new \Exception('Invalid thermostat_id'); 790 | } 791 | else { 792 | $thermostat = $result->fetch_assoc(); 793 | foreach($thermostat as $key => &$value) { 794 | if(substr($key, 0, 5) === 'json_') { 795 | $value = json_decode($value, true); 796 | } 797 | } 798 | return $thermostat; 799 | } 800 | } 801 | 802 | /** 803 | * Catches any non-exception errors (like requiring a file that does not 804 | * exist) so the script can finish nicely instead of dying and rolling back 805 | * the databaes transaction. 806 | * 807 | * @param number $code 808 | * @param string $message 809 | * @param string $file 810 | * @param number $line 811 | */ 812 | public function error_handler($code, $message, $file, $line) { 813 | $this->error = array( 814 | 'code' => $code, 815 | 'message' => $message, 816 | 'file' => $file, 817 | 'line' => $line, 818 | 'backtrace' => debug_backtrace(false) 819 | ); 820 | 821 | die(); // Do not continue execution; shutdown handler will now run. 822 | } 823 | 824 | /** 825 | * Catches uncaught exceptions so the script can finish nicely instead of 826 | * dying and rolling back the database transaction. 827 | * 828 | * @param Exceptoion $e 829 | */ 830 | public function exception_handler($e) { 831 | $this->error = array( 832 | 'code' => $e->getCode(), 833 | 'message' => $e->getMessage(), 834 | 'file' => $e->getFile(), 835 | 'line' => $e->getLine(), 836 | 'backtrace' => $e->getTrace() 837 | ); 838 | 839 | die(); // Do not continue execution; shutdown handler will now run. 840 | } 841 | 842 | /** 843 | * Runs last, checks one final time for errors. If any errors or exceptions 844 | * happened, log them to the error_log table, echo a few last words, then 845 | * die nicely. 846 | */ 847 | public function shutdown_handler() { 848 | try { 849 | // If I didn't catch an error/exception with my handlers, look here...this 850 | // will catch fatal errors that I can't. 851 | $error = error_get_last(); 852 | if($error !== null) { 853 | $this->error = array( 854 | 'code' => $error['type'], 855 | 'message' => $error['message'], 856 | 'file' => $error['file'], 857 | 'line' => $error['line'], 858 | 'backtrace' => debug_backtrace(false) 859 | ); 860 | } 861 | 862 | if(isset($this->error) === true) { 863 | $query = ' 864 | insert into error_log( 865 | `json_error` 866 | ) 867 | values( 868 | "' . $this->mysqli->real_escape_string(json_encode($this->error)) . '" 869 | ) 870 | '; 871 | $this->mysqli->query($query) or die($this->mysqli->error); 872 | $this->mysqli->query('commit') or die($this->mysqli->error); 873 | 874 | die('An error occured, check the error_log table for more details. Please report the issue at https://github.com/ziebelje/sqlbee/issues.' . PHP_EOL); 875 | } 876 | else { 877 | $this->mysqli->query('commit') or die($this->mysqli->error); 878 | } 879 | 880 | } 881 | catch(\Exception $e) { 882 | // If something breaks above, just spit out some stuff to be helpful. This 883 | // will only catch actual exceptions, not errors. 884 | var_dump($this->error); 885 | print_r(array( 886 | 'code' => $e->getCode(), 887 | 'message' => $e->getMessage(), 888 | 'file' => $e->getFile(), 889 | 'line' => $e->getLine(), 890 | 'backtrace' => $e->getTrace() 891 | )); 892 | $this->mysqli->query('commit') or die($this->mysqli->error); 893 | die('An error occured that could not be logged. See above for more details. Please report the issue at https://github.com/ziebelje/sqlbee/issues.' . PHP_EOL); 894 | } 895 | } 896 | 897 | } 898 | -------------------------------------------------------------------------------- /sqlbee.sql: -------------------------------------------------------------------------------- 1 | start transaction; 2 | 3 | create database `sqlbee` collate=utf8_unicode_ci; 4 | use `sqlbee`; 5 | 6 | create table `api_log` ( 7 | `api_log_id` int(10) unsigned not null auto_increment, 8 | `method` enum('get','post') not null, 9 | `endpoint` varchar(255) not null, 10 | `json_arguments` text not null, 11 | `response` mediumtext not null, 12 | `timestamp` timestamp not null default current_timestamp, 13 | `deleted` tinyint(1) not null default '0', 14 | primary key (`api_log_id`) 15 | ) engine=innodb default charset=utf8 collate=utf8_unicode_ci; 16 | 17 | create table `error_log` ( 18 | `error_log_id` int(10) unsigned not null auto_increment, 19 | `timestamp` timestamp not null default current_timestamp, 20 | `json_error` text not null, 21 | `deleted` tinyint(1) unsigned not null default '0', 22 | primary key (`error_log_id`) 23 | ) engine=innodb default charset=utf8 collate=utf8_unicode_ci; 24 | 25 | create table `thermostat` ( 26 | `thermostat_id` int(10) unsigned not null auto_increment, 27 | `identifier` varchar(255) not null, 28 | `name` varchar(255) not null, 29 | `connected` tinyint(1) not null, 30 | `thermostat_revision` varchar(255) not null, 31 | `alert_revision` varchar(255) not null, 32 | `runtime_revision` varchar(255) not null, 33 | `internal_revision` varchar(255) not null, 34 | `json_runtime` text, 35 | `json_extended_runtime` text, 36 | `json_electricity` text, 37 | `json_settings` text, 38 | `json_location` text, 39 | `json_program` text, 40 | `json_events` text, 41 | `json_device` text, 42 | `json_technician` text, 43 | `json_utility` text, 44 | `json_management` text, 45 | `json_alerts` text, 46 | `json_weather` text, 47 | `json_house_details` text, 48 | `json_oem_cfg` text, 49 | `json_equipment_status` text, 50 | `json_notification_settings` text, 51 | `json_privacy` text, 52 | `json_version` text, 53 | `json_remote_sensors` text, 54 | `json_audio` text, 55 | `deleted` tinyint(1) not null default '0', 56 | primary key (`thermostat_id`), 57 | unique key `identifier` (`identifier`) 58 | ) engine=innodb default charset=utf8 collate=utf8_unicode_ci; 59 | 60 | create table `sensor` ( 61 | `sensor_id` int(10) unsigned not null auto_increment, 62 | `thermostat_id` int(10) unsigned not null, 63 | `identifier` varchar(255) collate utf8_unicode_ci not null, 64 | `name` varchar(255) collate utf8_unicode_ci not null, 65 | `type` varchar(255) collate utf8_unicode_ci not null, 66 | `code` varchar(255) collate utf8_unicode_ci default null, 67 | `in_use` tinyint(1) not null, 68 | `json_capability` text collate utf8_unicode_ci not null, 69 | `deleted` tinyint(1) not null default '0', 70 | primary key (`sensor_id`), 71 | key `thermostat_id` (`thermostat_id`), 72 | constraint `sensor_ibfk_1` foreign key (`thermostat_id`) references `thermostat` (`thermostat_id`) 73 | ) engine=innodb default charset=utf8 collate=utf8_unicode_ci; 74 | 75 | create table `runtime_report_thermostat` ( 76 | `runtime_report_thermostat_id` int(10) unsigned not null auto_increment, 77 | `thermostat_id` int(10) unsigned not null, 78 | `timestamp` timestamp not null, 79 | `auxiliary_heat_1` int(10) unsigned default null, 80 | `auxiliary_heat_2` int(10) unsigned default null, 81 | `auxiliary_heat_3` int(10) unsigned default null, 82 | `compressor_cool_1` int(10) unsigned default null, 83 | `compressor_cool_2` int(10) unsigned default null, 84 | `compressor_heat_1` int(10) unsigned default null, 85 | `compressor_heat_2` int(10) unsigned default null, 86 | `dehumidifier` int(10) unsigned default null, 87 | `demand_management_offset` decimal(4,1) default null, 88 | `economizer` int(10) unsigned default null, 89 | `fan` int(10) unsigned default null, 90 | `humidifier` int(10) unsigned default null, 91 | `hvac_mode` varchar(255) default null, 92 | `outdoor_humidity` int(10) unsigned default null, 93 | `outdoor_temperature` decimal(4,1) default null, 94 | `sky` int(10) unsigned default null, 95 | `ventilator` int(10) unsigned default null, 96 | `wind` int(10) unsigned default null, 97 | `zone_average_temperature` decimal(4,1) default null, 98 | `zone_calendar_event` varchar(255) default null, 99 | `zone_climate` varchar(255) default null, 100 | `zone_cool_temperature` decimal(4,1) default null, 101 | `zone_heat_temperature` decimal(4,1) default null, 102 | `zone_humidity` int(10) unsigned default null, 103 | `zone_humidity_high` int(10) unsigned default null, 104 | `zone_humidity_low` int(10) unsigned default null, 105 | `zone_hvac_mode` varchar(255) default null, 106 | `zone_occupancy` int(10) unsigned default null, 107 | `deleted` tinyint(1) not null default '0', 108 | primary key (`runtime_report_thermostat_id`), 109 | unique key `thermostat_id_timestamp` (`thermostat_id`,`timestamp`), 110 | constraint `runtime_report_thermostat_ibfk_1` foreign key (`thermostat_id`) references `thermostat` (`thermostat_id`) 111 | ) engine=innodb default charset=utf8 collate=utf8_unicode_ci; 112 | 113 | create table `runtime_report_sensor` ( 114 | `runtime_report_sensor_id` int(10) unsigned not null auto_increment, 115 | `thermostat_id` int(10) unsigned not null, 116 | `sensor_id` int(10) unsigned not null, 117 | `timestamp` timestamp not null, 118 | `temperature` decimal(4,1) default null, 119 | `humidity` int(10) unsigned default null, 120 | `occupancy` tinyint(1) default null, 121 | `deleted` tinyint(1) not null default '0', 122 | primary key (`runtime_report_sensor_id`), 123 | unique key `sensor_id_timestamp` (`sensor_id`,`timestamp`), 124 | key `thermostat_id` (`thermostat_id`), 125 | constraint `runtime_report_sensor_ibfk_1` foreign key (`sensor_id`) references `sensor` (`sensor_id`), 126 | constraint `runtime_report_sensor_ibfk_2` foreign key (`thermostat_id`) references `thermostat` (`thermostat_id`) 127 | ) engine=innodb default charset=utf8 collate=utf8_unicode_ci; 128 | 129 | create table `token` ( 130 | `token_id` int(10) unsigned not null auto_increment, 131 | `access_token` text not null, 132 | `refresh_token` text not null, 133 | `timestamp` timestamp not null default current_timestamp, 134 | `deleted` tinyint(4) not null default '0', 135 | primary key (`token_id`) 136 | ) engine=innodb default charset=utf8 collate=utf8_unicode_ci; 137 | 138 | commit; 139 | --------------------------------------------------------------------------------