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.