diff --git a/css/dashboard.css b/css/dashboard.css
index 9a26383..0f1421e 100644
--- a/css/dashboard.css
+++ b/css/dashboard.css
@@ -497,6 +497,7 @@ input[type=range]::-moz-range-thumb {
.icon.menu_down:before { content: "\F035D"; }
.icon.menu_up:before { content: "\F0360"; }
.icon.message:before { content: "\F0361"; }
+.icon.microscope:before { content: "\F0654"; }
.icon.moon_waning_crescent:before { content: "\F0F65"; }
.icon.network_strength_4:before { content: "\F08FA"; }
.icon.network_strength_off:before { content: "\F08FC"; }
@@ -542,6 +543,7 @@ input[type=range]::-moz-range-thumb {
.icon.thermometer:before { content: "\F050F"; }
.icon.thermostat:before { content: "\F0393"; }
.icon.thumb_up:before { content: "\F0513"; }
+.icon.trash_can:before { content: "\F0A79"; }
.icon.tune:before { content: "\F062E"; }
.icon.twitter:before { content: "\F0544"; }
.icon.undo:before { content: "\F054C"; }
diff --git a/js/component/card/manage_thermostats.js b/js/component/card/manage_thermostats.js
new file mode 100644
index 0000000..de9b512
--- /dev/null
+++ b/js/component/card/manage_thermostats.js
@@ -0,0 +1,122 @@
+/**
+ * Setting
+ */
+beestat.component.card.manage_thermostats = function() {
+ beestat.component.card.apply(this, arguments);
+};
+beestat.extend(beestat.component.card.manage_thermostats, beestat.component.card);
+
+/**
+ * Decorate contents.
+ *
+ * @param {rocket.Elements} parent Parent
+ */
+beestat.component.card.manage_thermostats.prototype.decorate_contents_ = function(parent) {
+ var p = document.createElement('p');
+ p.innerText = 'Thermostats directly connected to your ecobee account are automatically added and synced. In some cases, shared thermostats cannot be automatically detected. Add them here.';
+ parent.appendChild(p);
+
+ // Existing
+ (new beestat.component.title('Existing Thermostats')).render(parent);
+
+ var sorted_thermostats = $.values(beestat.cache.thermostat)
+ .sort(function(a, b) {
+ return a.name > b.name;
+ });
+
+ const table = document.createElement('table');
+ sorted_thermostats.forEach(function(thermostat) {
+ const ecobee_thermostat = beestat.cache.ecobee_thermostat[thermostat.ecobee_thermostat_id];
+
+ const tr = document.createElement('tr');
+
+ const td_name = document.createElement('td');
+ const td_identifier = document.createElement('td');
+ const td_delete = document.createElement('td');
+
+ td_name.innerText = thermostat.name;
+ td_identifier.innerText = ecobee_thermostat.identifier;
+
+ const tile_delete = new beestat.component.tile()
+ .set_icon('trash_can')
+ .set_shadow(false)
+ .set_text_color(beestat.style.color.red.base)
+ .render($(td_delete));
+
+ tile_delete.addEventListener('click', function() {
+ console.info('delete');
+ })
+
+ tr.appendChild(td_name);
+ tr.appendChild(td_identifier);
+ tr.appendChild(td_delete);
+
+ table.appendChild(tr);
+ });
+
+ parent.appendChild(table);
+
+ // New
+ (new beestat.component.title('Add Thermostat')).render(parent);
+
+ const container = document.createElement('div');
+ parent.appendChild(container);
+
+ Object.assign(container.style, {
+ 'display': 'flex',
+ 'flex-direction': 'row',
+ 'grid-gap': `${beestat.style.size.gutter}px`,
+ });
+
+ const input_container = document.createElement('div');
+ container.appendChild(input_container);
+
+ new_identifier = new beestat.component.input.text()
+ .set_width(150)
+ .set_maxlength(12)
+ .set_placeholder('Serial #')
+ .render($(input_container));
+
+ const button_container = document.createElement('div');
+ container.appendChild(button_container);
+
+ const tile_add = new beestat.component.tile()
+ .set_background_color(beestat.style.color.green.base)
+ .set_background_hover_color(beestat.style.color.green.light)
+ .set_text_color('#fff')
+ .set_text('Add Thermostat')
+ .render($(button_container));
+
+ // TODO: Add thermostat button needs to make an API call. That call should
+ // look for an existing inactive thermostat on the account and add it back.
+ // If it doesn't exist, it should do an API call to ecobee to "sync" that
+ // thermostat which should immediately grab all of that data and create the
+ // rows for me using my existing code. Throw a loading spinner over the
+ // manage thermostats card while this happens, then call get thermostats when
+ // that's done to update beestat's cache.
+};
+
+/**
+ * Get the title of the card.
+ *
+ * @return {string} The title.
+ */
+beestat.component.card.manage_thermostats.prototype.get_title_ = function() {
+ return 'Manage Thermostats';
+};
+
+/**
+ * Decorate the menu
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.manage_thermostats.prototype.decorate_top_right_ = function(parent) {
+ var menu = (new beestat.component.menu()).render(parent);
+
+ menu.add_menu_item(new beestat.component.menu_item()
+ .set_text('Help')
+ .set_icon('help_circle')
+ .set_callback(function() {
+ window.open('TODO');
+ }));
+};
diff --git a/js/component/card/rookstack_survey_notification.js b/js/component/card/rookstack_survey_notification.js
new file mode 100644
index 0000000..2fa957c
--- /dev/null
+++ b/js/component/card/rookstack_survey_notification.js
@@ -0,0 +1,90 @@
+/**
+ * Blue banner asking people to check out a research opportunity.
+ */
+beestat.component.card.rookstack_survey_notification = function() {
+ var self = this;
+
+ beestat.dispatcher.addEventListener([
+ 'setting.display_2024_equipment_sizing_study_rookstack'
+ ], function() {
+ self.rerender();
+ });
+
+ beestat.component.card.apply(this, arguments);
+};
+beestat.extend(beestat.component.card.rookstack_survey_notification, beestat.component.card);
+
+beestat.component.card.rookstack_survey_notification.prototype.decorate_contents_ = function(parent) {
+ var self = this;
+
+ // Don't render anything if the user is an active Patron.
+ if (beestat.component.card.rookstack_survey_notification.should_show() === false) {
+ window.setTimeout(function() {
+ self.dispose();
+ }, 0);
+ return;
+ }
+
+ parent.style('background', beestat.style.color.blue.base);
+
+
+ new beestat.component.tile()
+ .set_icon('microscope')
+ .set_size('large')
+ .set_text([
+ 'I am interested in participating',
+ 'Learn more'
+ ])
+ .set_background_color(beestat.style.color.blue.dark)
+ .set_background_hover_color(beestat.style.color.blue.light)
+ .addEventListener('click', function() {
+ beestat.setting('clicked_2024_equipment_sizing_study_rookstack', moment().utc().format('YYYY-MM-DD HH:mm:ss'));
+ window.open('https://docs.google.com/presentation/d/1OY8RR6hMeL86ODH5LdxfrZ_es7nnRDq3cG-1qIfS7XI/present#slide=id.p', '_blank');
+ })
+ .render(parent);
+};
+
+/**
+ * Get the title of the card.
+ *
+ * @return {string} The title.
+ */
+beestat.component.card.rookstack_survey_notification.prototype.get_title_ = function() {
+ return 'Research Opportunity';
+};
+
+/**
+ * Get the subtitle of the card.
+ *
+ * @return {string} The subtitle.
+ */
+beestat.component.card.rookstack_survey_notification.prototype.get_subtitle_ = function() {
+ return 'Purdue University is looking for participants in a U.S. Department of Energy funded study that aims to develop and Artificial Intelligence solution to residential heating and cooling equipment sizing.';
+};
+
+/**
+ * Decorate the close button.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.rookstack_survey_notification.prototype.decorate_top_right_ = function(parent) {
+ new beestat.component.tile()
+ .set_type('pill')
+ .set_shadow(false)
+ .set_icon('close')
+ .set_text_color('#fff')
+ .set_background_hover_color(beestat.style.color.blue.light)
+ .addEventListener('click', function() {
+ beestat.setting('display_2024_equipment_sizing_study_rookstack', false);
+ })
+ .render(parent);
+};
+
+/**
+ * Determine whether or not this card should be shown.
+ *
+ * @return {boolean} Whether or not to show the card.
+ */
+beestat.component.card.rookstack_survey_notification.should_show = function() {
+ return beestat.setting('display_2024_equipment_sizing_study_rookstack') === true;
+};
diff --git a/js/js.php b/js/js.php
index 0312ee5..bfe39fc 100755
--- a/js/js.php
+++ b/js/js.php
@@ -88,6 +88,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
@@ -102,6 +103,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
diff --git a/js/layer/detail.js b/js/layer/detail.js
index 2340d14..74695b2 100644
--- a/js/layer/detail.js
+++ b/js/layer/detail.js
@@ -33,6 +33,15 @@ beestat.layer.detail.prototype.decorate_ = function(parent) {
]);
}
+ // Manage Thermostats
+ cards.push([
+ {
+ // 'card': new beestat.component.card.manage_thermostats(),
+ 'card': new beestat.component.card.rookstack_survey_notification(),
+ 'size': 12
+ }
+ ]);
+
cards.push([
{
'card': new beestat.component.card.system(thermostat.thermostat_id),
diff --git a/js/layer/settings.js b/js/layer/settings.js
index 37d5ade..85cee1e 100644
--- a/js/layer/settings.js
+++ b/js/layer/settings.js
@@ -39,6 +39,14 @@ beestat.layer.settings.prototype.decorate_ = function(parent) {
}
]);
+ // Manage Thermostats
+ // cards.push([
+ // {
+ // 'card': new beestat.component.card.manage_thermostats(),
+ // 'size': 12
+ // }
+ // ]);
+
// Footer
cards.push([
{