diff --git a/api/cora/database.php b/api/cora/database.php index ba1fb29..1894421 100644 --- a/api/cora/database.php +++ b/api/cora/database.php @@ -857,7 +857,7 @@ final class database extends \mysqli { '); $row = $result->fetch_assoc(); if($row['lock'] !== 1) { - throw new \Exception('Could not get lock.', 1209); + throw new exception('Could not get lock.', 1209); } } diff --git a/api/runtime.php b/api/runtime.php index cce8ff3..a958f84 100644 --- a/api/runtime.php +++ b/api/runtime.php @@ -33,6 +33,8 @@ class runtime extends cora\api { * * @param int $thermostat_id Optional thermostat_id to sync. If not set will * sync all thermostats attached to this user. + * + * @return boolean true if the sync ran, false if not. */ public function sync($thermostat_id = null) { // Skip this for the demo @@ -42,54 +44,58 @@ class runtime extends cora\api { set_time_limit(0); - if($thermostat_id === null) { - $thermostat_ids = array_keys( - $this->api( - 'thermostat', - 'read_id', - [ - 'attributes' => [ - 'inactive' => 0 + try { + if($thermostat_id === null) { + $thermostat_ids = array_keys( + $this->api( + 'thermostat', + 'read_id', + [ + 'attributes' => [ + 'inactive' => 0 + ] ] - ] - ) - ); - } else { - $this->user_lock($thermostat_id); - $thermostat_ids = [$thermostat_id]; - } - - foreach($thermostat_ids as $thermostat_id) { - // Get a lock to ensure that this is not invoked more than once at a time - // per thermostat. - $lock_name = 'runtime->sync(' . $thermostat_id . ')'; - $this->database->get_lock($lock_name); - - $thermostat = $this->api('thermostat', 'get', $thermostat_id); - - if( - $thermostat['sync_begin'] === $thermostat['first_connected'] || - ( - $thermostat['sync_begin'] !== null && - strtotime($thermostat['sync_begin']) <= strtotime('-1 year') - ) - ) { - $this->sync_forwards($thermostat_id); + ) + ); } else { - $this->sync_backwards($thermostat_id); + $this->user_lock($thermostat_id); + $thermostat_ids = [$thermostat_id]; } - // If only syncing one thermostat this will delay the sync of the other - // thermostat. Not a huge deal, just FYI. - $this->api( - 'user', - 'update_sync_status', - [ - 'key' => 'runtime' - ] - ); + foreach($thermostat_ids as $thermostat_id) { + // Get a lock to ensure that this is not invoked more than once at a time + // per thermostat. + $lock_name = 'runtime->sync(' . $thermostat_id . ')'; + $this->database->get_lock($lock_name); - $this->database->release_lock($lock_name); + $thermostat = $this->api('thermostat', 'get', $thermostat_id); + + if( + $thermostat['sync_begin'] === $thermostat['first_connected'] || + ( + $thermostat['sync_begin'] !== null && + strtotime($thermostat['sync_begin']) <= strtotime('-1 year') + ) + ) { + $this->sync_forwards($thermostat_id); + } else { + $this->sync_backwards($thermostat_id); + } + + // If only syncing one thermostat this will delay the sync of the other + // thermostat. Not a huge deal, just FYI. + $this->api( + 'user', + 'update_sync_status', + [ + 'key' => 'runtime' + ] + ); + + $this->database->release_lock($lock_name); + } + } catch(cora\exception $e) { + return false; } } diff --git a/api/sensor.php b/api/sensor.php index 3f1e903..93757ff 100644 --- a/api/sensor.php +++ b/api/sensor.php @@ -46,9 +46,10 @@ class sensor extends cora\crud { } /** - * Sync all sensors connected to this account. Once Nest support is - * added this will need to check for all connected accounts and run the - * appropriate ones. + * Sync all sensors for the current user. If we fail to get a lock, fail + * silently (catch the exception) and just return false. + * + * @return boolean true if the sync ran, false if not. */ public function sync() { // Skip this for the demo @@ -56,20 +57,24 @@ class sensor extends cora\crud { return; } - $lock_name = 'sensor->sync(' . $this->session->get_user_id() . ')'; - $this->database->get_lock($lock_name); + try { + $lock_name = 'sensor->sync(' . $this->session->get_user_id() . ')'; + $this->database->get_lock($lock_name); - $this->api('ecobee_sensor', 'sync'); + $this->api('ecobee_sensor', 'sync'); - $this->api( - 'user', - 'update_sync_status', - [ - 'key' => 'sensor' - ] - ); + $this->api( + 'user', + 'update_sync_status', + [ + 'key' => 'sensor' + ] + ); - $this->database->release_lock($lock_name); + $this->database->release_lock($lock_name); + } catch(cora\exception $e) { + return false; + } } } diff --git a/api/thermostat.php b/api/thermostat.php index 1063807..c4e4b4d 100644 --- a/api/thermostat.php +++ b/api/thermostat.php @@ -22,26 +22,35 @@ class thermostat extends cora\crud { ]; /** - * Sync all thermostats for the current user with their associated service. + * Sync all thermostats for the current user. If we fail to get a lock, fail + * silently (catch the exception) and just return false. + * + * @return boolean true if the sync ran, false if not. */ public function sync() { // Skip this for the demo if($this->setting->is_demo() === true) { - return; + return true; } - $lock_name = 'thermostat->sync(' . $this->session->get_user_id() . ')'; - $this->database->get_lock($lock_name); + try { + $lock_name = 'thermostat->sync(' . $this->session->get_user_id() . ')'; + $this->database->get_lock($lock_name); - $this->api('ecobee_thermostat', 'sync'); + $this->api('ecobee_thermostat', 'sync'); - $this->api( - 'user', - 'update_sync_status', - ['key' => 'thermostat'] - ); + $this->api( + 'user', + 'update_sync_status', + ['key' => 'thermostat'] + ); - $this->database->release_lock($lock_name); + $this->database->release_lock($lock_name); + + return true; + } catch(cora\exception $e) { + return false; + } } /** diff --git a/js/beestat.js b/js/beestat.js index fff8aad..29bf2b2 100644 --- a/js/beestat.js +++ b/js/beestat.js @@ -20,7 +20,6 @@ beestat.ecobee_thermostat_models = { 'vulcanSmart': 'SmartThermostat' }; - /** * Get a default value for an argument if it is not currently set. * @@ -106,16 +105,6 @@ beestat.get_thermostat_color = function(thermostat_id) { return beestat.style.color.bluegray.dark; }; -/** - * Get the current user. - * - * @return {object} - */ -beestat.get_user = function() { - var user_id = Object.keys(beestat.cache.user)[0]; - return beestat.cache.user[user_id]; -}; - // Register service worker if ('serviceWorker' in navigator) { window.addEventListener('load', function() { diff --git a/js/beestat/api.js b/js/beestat/api.js index c53fa0e..606c31b 100644 --- a/js/beestat/api.js +++ b/js/beestat/api.js @@ -178,8 +178,6 @@ beestat.api.prototype.load_ = function(response_text) { ) { window.location.href = '/'; return; - } else if (response.data && response.data.error_code === 1209) { - // Could not get lock; safe to ignore as that means sync is running. } else if (response.success !== true) { beestat.error( 'API call failed: ' + response.data.error_message, diff --git a/js/beestat/ecobee.js b/js/beestat/ecobee.js new file mode 100644 index 0000000..69578d5 --- /dev/null +++ b/js/beestat/ecobee.js @@ -0,0 +1,25 @@ +beestat.ecobee = {}; + +/** + * Check to see if ecobee is down. If so, render the footer component. + */ +beestat.ecobee.notify_if_down = function() { + if ( + beestat.cache !== undefined && + beestat.cache.thermostat !== undefined && + beestat.user.get() !== undefined + ) { + var last_update = moment.utc(beestat.user.get().sync_status.thermostat); + var down = last_update.isBefore(moment().subtract(15, 'minutes')); + + if (beestat.ecobee.down_notification_ === undefined) { + beestat.ecobee.down_notification_ = new beestat.component.down_notification(); + } + + if (down === true) { + beestat.ecobee.down_notification_.render($('body')); + } else { + beestat.ecobee.down_notification_.dispose(); + } + } +}; diff --git a/js/beestat/poll.js b/js/beestat/poll.js index 2910465..3fe537d 100644 --- a/js/beestat/poll.js +++ b/js/beestat/poll.js @@ -95,6 +95,8 @@ beestat.poll = function() { beestat.cache.set('ecobee_sensor', response.ecobee_sensor); beestat.enable_poll(); beestat.dispatcher.dispatchEvent('poll'); + + beestat.ecobee.notify_if_down(); }); api.send(); diff --git a/js/component.js b/js/component.js index b54a47f..050f97f 100644 --- a/js/component.js +++ b/js/component.js @@ -6,8 +6,6 @@ beestat.component = function() { // Give every component a state object to use for storing data. this.state_ = {}; - // this.render_count_ = 0; - this.layer_ = beestat.current_layer; if (this.rerender_on_breakpoint_ === true) { @@ -28,26 +26,27 @@ beestat.extend(beestat.component, rocket.EventTarget); * @return {beestat.component} This */ beestat.component.prototype.render = function(parent) { - var self = this; + if (this.rendered_ === false) { + var self = this; - if (parent !== undefined) { - this.component_container_ = $.createElement('div') - .style('position', 'relative'); - this.decorate_(this.component_container_); - parent.appendChild(this.component_container_); - } else { - this.decorate_(); + if (parent !== undefined) { + this.component_container_ = $.createElement('div') + .style('position', 'relative'); + this.decorate_(this.component_container_); + parent.appendChild(this.component_container_); + } else { + this.decorate_(); + } + + // The element should now exist on the DOM. + setTimeout(function() { + self.dispatchEvent('render'); + }, 0); + + // The render function was called. + this.rendered_ = true; } - // The element should now exist on the DOM. - setTimeout(function() { - self.dispatchEvent('render'); - }, 0); - - // The render function was called. - this.rendered_ = true; - // this.render_count_++; - return this; }; @@ -71,21 +70,20 @@ beestat.component.prototype.rerender = function() { setTimeout(function() { self.dispatchEvent('render'); }, 0); - - // this.render_count_++; - - return this; } + return this; }; /** * Remove this component from the page. */ beestat.component.prototype.dispose = function() { - var child = this.component_container_.parentNode(); - var parent = child.parentNode(); - parent.removeChild(child); - this.rendered_ = false; + if (this.rendered_ === true) { + var child = this.component_container_; + var parent = child.parentNode(); + parent.removeChild(child); + this.rendered_ = false; + } }; beestat.component.prototype.decorate_ = function() { diff --git a/js/component/down_notification.js b/js/component/down_notification.js new file mode 100644 index 0000000..6ec54bd --- /dev/null +++ b/js/component/down_notification.js @@ -0,0 +1,34 @@ +/** + * Ecobee is down! + */ +beestat.component.down_notification = function() { + beestat.component.apply(this, arguments); +}; +beestat.extend(beestat.component.down_notification, beestat.component); + +beestat.component.down_notification.prototype.rerender_on_breakpoint_ = false; + +/** + * Decorate a floating banner at the bottom of the page. + * + * @param {rocket.Elements} parent + */ +beestat.component.down_notification.prototype.decorate_ = function(parent) { + var div = $.createElement('div'); + div.style({ + 'position': 'fixed', + 'bottom': '0px', + 'left': '0px', + 'width': '100%', + 'text-align': 'center', + 'padding-left': beestat.style.size.gutter, + 'padding-right': beestat.style.size.gutter, + 'background': beestat.style.color.red.dark + }); + + var last_update = moment.utc(beestat.user.get().sync_status.thermostat).local() + .format('h:m a'); + div.appendChild($.createElement('p').innerText('Ecobee seems to be down. Your data will update as soon as possible. Last update was at ' + last_update + '.')); + + parent.appendChild(div); +}; diff --git a/js/component/menu.js b/js/component/menu.js index 0321021..7f5a467 100644 --- a/js/component/menu.js +++ b/js/component/menu.js @@ -19,7 +19,6 @@ beestat.component.menu.prototype.decorate_ = function(parent) { .set_bubble_text(this.bubble_text_) .set_bubble_color(this.bubble_color_) .set_text_color('#fff') - // .set_background_hover_color(beestat.style.color.bluegray.light) .set_background_hover_color('rgba(255, 255, 255, 0.1') .addEventListener('click', function() { // Did I just try to open the same menu as last time? @@ -41,12 +40,20 @@ beestat.component.menu.prototype.decorate_ = function(parent) { * Close this menu by hiding the container and removing the event listeners. */ beestat.component.menu.prototype.dispose = function() { + var self = this; + beestat.component.menu.open_menu.rendered_ = false; + this.rendered_ = false; + if (beestat.component.menu.open_menu !== undefined) { var container = beestat.component.menu.open_menu.container_; container.style('transform', 'scale(0)'); delete beestat.component.menu.open_menu; setTimeout(function() { + self.menu_items_.forEach(function(menu_item) { + menu_item.dispose(); + }); + container.parentNode().removeChild(container); }, 200); } diff --git a/js/component/menu_item.js b/js/component/menu_item.js index 32ecb93..1d82d2a 100644 --- a/js/component/menu_item.js +++ b/js/component/menu_item.js @@ -34,7 +34,7 @@ beestat.component.menu_item.prototype.decorate_ = function(parent) { .render(parent); // Events - parent.addEventListener('mouseenter', function() { + parent.addEventListener('mouseenter', function() { parent.style({ 'background': beestat.style.color.blue.light, 'color': '#fff' @@ -48,7 +48,7 @@ beestat.component.menu_item.prototype.decorate_ = function(parent) { }); parent.addEventListener('click', function() { self.menu_.dispose(); - if(self.callback_ !== undefined) { + if (self.callback_ !== undefined) { self.callback_(); } }); diff --git a/js/js.php b/js/js.php index 8ea0dd5..11a964a 100755 --- a/js/js.php +++ b/js/js.php @@ -31,6 +31,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; // Layer echo '' . PHP_EOL; @@ -42,6 +43,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd // Component echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; diff --git a/js/layer.js b/js/layer.js index f58c2e4..70f9f8d 100644 --- a/js/layer.js +++ b/js/layer.js @@ -22,6 +22,8 @@ beestat.layer.prototype.render = function() { body.innerHTML(''); body.appendChild(container); + + beestat.ecobee.notify_if_down(); }; beestat.layer.prototype.decorate_ = function(parent) { diff --git a/js/layer/load.js b/js/layer/load.js index 6bef212..0211a80 100644 --- a/js/layer/load.js +++ b/js/layer/load.js @@ -128,7 +128,6 @@ beestat.layer.load.prototype.decorate_ = function(parent) { ); api.set_callback(function(response) { - beestat.cache.set('user', response.user); // Rollbar isn't defined on dev. @@ -217,6 +216,8 @@ beestat.layer.load.prototype.decorate_ = function(parent) { (new beestat.layer.dashboard()).render(); + beestat.ecobee.notify_if_down(); + /* * If never seen an announcement, or if there is an unread important * announcement, show the modal.