1
0
mirror of https://github.com/beestat/app.git synced 2025-05-24 02:14:03 -04:00
Loading now doesn't break if the sync fails, and as a bonus that made it trivial to display whether or not ecobee is down.
This commit is contained in:
Jon Ziebell 2020-01-29 22:39:05 -05:00
parent deaf81e214
commit a6564305b0
15 changed files with 191 additions and 113 deletions

View File

@ -857,7 +857,7 @@ final class database extends \mysqli {
'); ');
$row = $result->fetch_assoc(); $row = $result->fetch_assoc();
if($row['lock'] !== 1) { if($row['lock'] !== 1) {
throw new \Exception('Could not get lock.', 1209); throw new exception('Could not get lock.', 1209);
} }
} }

View File

@ -33,6 +33,8 @@ class runtime extends cora\api {
* *
* @param int $thermostat_id Optional thermostat_id to sync. If not set will * @param int $thermostat_id Optional thermostat_id to sync. If not set will
* sync all thermostats attached to this user. * sync all thermostats attached to this user.
*
* @return boolean true if the sync ran, false if not.
*/ */
public function sync($thermostat_id = null) { public function sync($thermostat_id = null) {
// Skip this for the demo // Skip this for the demo
@ -42,54 +44,58 @@ class runtime extends cora\api {
set_time_limit(0); set_time_limit(0);
if($thermostat_id === null) { try {
$thermostat_ids = array_keys( if($thermostat_id === null) {
$this->api( $thermostat_ids = array_keys(
'thermostat', $this->api(
'read_id', 'thermostat',
[ 'read_id',
'attributes' => [ [
'inactive' => 0 '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 { } 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 foreach($thermostat_ids as $thermostat_id) {
// thermostat. Not a huge deal, just FYI. // Get a lock to ensure that this is not invoked more than once at a time
$this->api( // per thermostat.
'user', $lock_name = 'runtime->sync(' . $thermostat_id . ')';
'update_sync_status', $this->database->get_lock($lock_name);
[
'key' => 'runtime'
]
);
$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;
} }
} }

View File

@ -46,9 +46,10 @@ class sensor extends cora\crud {
} }
/** /**
* Sync all sensors connected to this account. Once Nest support is * Sync all sensors for the current user. If we fail to get a lock, fail
* added this will need to check for all connected accounts and run the * silently (catch the exception) and just return false.
* appropriate ones. *
* @return boolean true if the sync ran, false if not.
*/ */
public function sync() { public function sync() {
// Skip this for the demo // Skip this for the demo
@ -56,20 +57,24 @@ class sensor extends cora\crud {
return; return;
} }
$lock_name = 'sensor->sync(' . $this->session->get_user_id() . ')'; try {
$this->database->get_lock($lock_name); $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( $this->api(
'user', 'user',
'update_sync_status', 'update_sync_status',
[ [
'key' => 'sensor' 'key' => 'sensor'
] ]
); );
$this->database->release_lock($lock_name); $this->database->release_lock($lock_name);
} catch(cora\exception $e) {
return false;
}
} }
} }

View File

@ -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() { public function sync() {
// Skip this for the demo // Skip this for the demo
if($this->setting->is_demo() === true) { if($this->setting->is_demo() === true) {
return; return true;
} }
$lock_name = 'thermostat->sync(' . $this->session->get_user_id() . ')'; try {
$this->database->get_lock($lock_name); $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( $this->api(
'user', 'user',
'update_sync_status', 'update_sync_status',
['key' => 'thermostat'] ['key' => 'thermostat']
); );
$this->database->release_lock($lock_name); $this->database->release_lock($lock_name);
return true;
} catch(cora\exception $e) {
return false;
}
} }
/** /**

View File

@ -20,7 +20,6 @@ beestat.ecobee_thermostat_models = {
'vulcanSmart': 'SmartThermostat' 'vulcanSmart': 'SmartThermostat'
}; };
/** /**
* Get a default value for an argument if it is not currently set. * 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; 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 // Register service worker
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', function() { window.addEventListener('load', function() {

View File

@ -178,8 +178,6 @@ beestat.api.prototype.load_ = function(response_text) {
) { ) {
window.location.href = '/'; window.location.href = '/';
return; 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) { } else if (response.success !== true) {
beestat.error( beestat.error(
'API call failed: ' + response.data.error_message, 'API call failed: ' + response.data.error_message,

25
js/beestat/ecobee.js Normal file
View File

@ -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();
}
}
};

View File

@ -95,6 +95,8 @@ beestat.poll = function() {
beestat.cache.set('ecobee_sensor', response.ecobee_sensor); beestat.cache.set('ecobee_sensor', response.ecobee_sensor);
beestat.enable_poll(); beestat.enable_poll();
beestat.dispatcher.dispatchEvent('poll'); beestat.dispatcher.dispatchEvent('poll');
beestat.ecobee.notify_if_down();
}); });
api.send(); api.send();

View File

@ -6,8 +6,6 @@ beestat.component = function() {
// Give every component a state object to use for storing data. // Give every component a state object to use for storing data.
this.state_ = {}; this.state_ = {};
// this.render_count_ = 0;
this.layer_ = beestat.current_layer; this.layer_ = beestat.current_layer;
if (this.rerender_on_breakpoint_ === true) { if (this.rerender_on_breakpoint_ === true) {
@ -28,26 +26,27 @@ beestat.extend(beestat.component, rocket.EventTarget);
* @return {beestat.component} This * @return {beestat.component} This
*/ */
beestat.component.prototype.render = function(parent) { beestat.component.prototype.render = function(parent) {
var self = this; if (this.rendered_ === false) {
var self = this;
if (parent !== undefined) { if (parent !== undefined) {
this.component_container_ = $.createElement('div') this.component_container_ = $.createElement('div')
.style('position', 'relative'); .style('position', 'relative');
this.decorate_(this.component_container_); this.decorate_(this.component_container_);
parent.appendChild(this.component_container_); parent.appendChild(this.component_container_);
} else { } else {
this.decorate_(); 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; return this;
}; };
@ -71,21 +70,20 @@ beestat.component.prototype.rerender = function() {
setTimeout(function() { setTimeout(function() {
self.dispatchEvent('render'); self.dispatchEvent('render');
}, 0); }, 0);
// this.render_count_++;
return this;
} }
return this;
}; };
/** /**
* Remove this component from the page. * Remove this component from the page.
*/ */
beestat.component.prototype.dispose = function() { beestat.component.prototype.dispose = function() {
var child = this.component_container_.parentNode(); if (this.rendered_ === true) {
var parent = child.parentNode(); var child = this.component_container_;
parent.removeChild(child); var parent = child.parentNode();
this.rendered_ = false; parent.removeChild(child);
this.rendered_ = false;
}
}; };
beestat.component.prototype.decorate_ = function() { beestat.component.prototype.decorate_ = function() {

View File

@ -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);
};

View File

@ -19,7 +19,6 @@ beestat.component.menu.prototype.decorate_ = function(parent) {
.set_bubble_text(this.bubble_text_) .set_bubble_text(this.bubble_text_)
.set_bubble_color(this.bubble_color_) .set_bubble_color(this.bubble_color_)
.set_text_color('#fff') .set_text_color('#fff')
// .set_background_hover_color(beestat.style.color.bluegray.light)
.set_background_hover_color('rgba(255, 255, 255, 0.1') .set_background_hover_color('rgba(255, 255, 255, 0.1')
.addEventListener('click', function() { .addEventListener('click', function() {
// Did I just try to open the same menu as last time? // 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. * Close this menu by hiding the container and removing the event listeners.
*/ */
beestat.component.menu.prototype.dispose = function() { 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) { if (beestat.component.menu.open_menu !== undefined) {
var container = beestat.component.menu.open_menu.container_; var container = beestat.component.menu.open_menu.container_;
container.style('transform', 'scale(0)'); container.style('transform', 'scale(0)');
delete beestat.component.menu.open_menu; delete beestat.component.menu.open_menu;
setTimeout(function() { setTimeout(function() {
self.menu_items_.forEach(function(menu_item) {
menu_item.dispose();
});
container.parentNode().removeChild(container); container.parentNode().removeChild(container);
}, 200); }, 200);
} }

View File

@ -34,7 +34,7 @@ beestat.component.menu_item.prototype.decorate_ = function(parent) {
.render(parent); .render(parent);
// Events // Events
parent.addEventListener('mouseenter', function() { parent.addEventListener('mouseenter', function() {
parent.style({ parent.style({
'background': beestat.style.color.blue.light, 'background': beestat.style.color.blue.light,
'color': '#fff' 'color': '#fff'
@ -48,7 +48,7 @@ beestat.component.menu_item.prototype.decorate_ = function(parent) {
}); });
parent.addEventListener('click', function() { parent.addEventListener('click', function() {
self.menu_.dispose(); self.menu_.dispose();
if(self.callback_ !== undefined) { if (self.callback_ !== undefined) {
self.callback_(); self.callback_();
} }
}); });

View File

@ -31,6 +31,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/beestat/highcharts.js"></script>' . PHP_EOL; echo '<script src="/js/beestat/highcharts.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/get_sync_progress.js"></script>' . PHP_EOL; echo '<script src="/js/beestat/get_sync_progress.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/user.js"></script>' . PHP_EOL; echo '<script src="/js/beestat/user.js"></script>' . PHP_EOL;
echo '<script src="/js/beestat/ecobee.js"></script>' . PHP_EOL;
// Layer // Layer
echo '<script src="/js/layer.js"></script>' . PHP_EOL; echo '<script src="/js/layer.js"></script>' . PHP_EOL;
@ -42,6 +43,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
// Component // Component
echo '<script src="/js/component.js"></script>' . PHP_EOL; echo '<script src="/js/component.js"></script>' . PHP_EOL;
echo '<script src="/js/component/alert.js"></script>' . PHP_EOL; echo '<script src="/js/component/alert.js"></script>' . PHP_EOL;
echo '<script src="/js/component/down_notification.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card.js"></script>' . PHP_EOL; echo '<script src="/js/component/card.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/runtime_thermostat_summary.js"></script>' . PHP_EOL; echo '<script src="/js/component/card/runtime_thermostat_summary.js"></script>' . PHP_EOL;
echo '<script src="/js/component/card/alerts.js"></script>' . PHP_EOL; echo '<script src="/js/component/card/alerts.js"></script>' . PHP_EOL;

View File

@ -22,6 +22,8 @@ beestat.layer.prototype.render = function() {
body.innerHTML(''); body.innerHTML('');
body.appendChild(container); body.appendChild(container);
beestat.ecobee.notify_if_down();
}; };
beestat.layer.prototype.decorate_ = function(parent) { beestat.layer.prototype.decorate_ = function(parent) {

View File

@ -128,7 +128,6 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
); );
api.set_callback(function(response) { api.set_callback(function(response) {
beestat.cache.set('user', response.user); beestat.cache.set('user', response.user);
// Rollbar isn't defined on dev. // Rollbar isn't defined on dev.
@ -217,6 +216,8 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
(new beestat.layer.dashboard()).render(); (new beestat.layer.dashboard()).render();
beestat.ecobee.notify_if_down();
/* /*
* If never seen an announcement, or if there is an unread important * If never seen an announcement, or if there is an unread important
* announcement, show the modal. * announcement, show the modal.