From 0bc12778f2713d49b2edba8eb19cd1704ff55964 Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Mon, 20 Aug 2018 00:23:19 -0500 Subject: [PATCH 01/10] Add theming framework --- .eslintrc.yml | 6 +- deploy.sh | 2 +- gulpfile.js | 13 ++- src/cards/haiku-global-config.js | 55 ++++++++++++ src/cards/haiku-global-config.scss | 0 src/cards/haiku-room-card.js | 4 + src/services/storage-service.js | 18 ++++ src/styles/global/haiku.scss | 1 + src/styles/global/themes/theme-dark.scss | 102 +++++++++++++++++++++++ 9 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 src/cards/haiku-global-config.js create mode 100644 src/cards/haiku-global-config.scss create mode 100644 src/services/storage-service.js create mode 100644 src/styles/global/haiku.scss create mode 100644 src/styles/global/themes/theme-dark.scss diff --git a/.eslintrc.yml b/.eslintrc.yml index bed81c2..39d8f51 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -6,10 +6,12 @@ env: node: true globals: + window: true document: true - _: true - customElements: true Event: true + HTMLElement: true + customElements: true + _: true rules: indent: diff --git a/deploy.sh b/deploy.sh index 2b5047b..899b3e1 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1 +1 @@ -rsync -avi ./haiku/ pi@raspberrypi:/home/homeassistant/.homeassistant/www/haiku/ +rsync -aviO ./haiku/ $HA_SSH_USER@$HA_SSH_HOST:/home/homeassistant/.homeassistant/www/haiku/ diff --git a/gulpfile.js b/gulpfile.js index ba58400..cf36623 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,7 +18,7 @@ gulp.task('lint', () => { }); gulp.task('build', ['js'], () => { - gulp.src(['.tmp/**/*.js']) + gulp.src(['.tmp/**/*.js', '.tmp/haiku.css']) .pipe(mergeCss()) .pipe(gulp.dest('haiku')); }); @@ -28,8 +28,15 @@ gulp.task('js', ['sass', 'lint'], () => { .pipe(gulp.dest('.tmp')); }); -gulp.task('sass', ['clean:dist'], () => { - return gulp.src(['src/**/*.scss']) +gulp.task('sass', ['sass:global'], () => { + return gulp.src(['src/**/*.scss', '!src/styles/global/**/*.scss']) + .pipe(sassLint()) + .pipe(sass().on('error', sass.logError)) + .pipe(gulp.dest('.tmp')); +}); + +gulp.task('sass:global', ['clean:dist'], () => { + return gulp.src(['src/styles/global/haiku.scss']) .pipe(sassLint()) .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('.tmp')); diff --git a/src/cards/haiku-global-config.js b/src/cards/haiku-global-config.js new file mode 100644 index 0000000..b2a1a73 --- /dev/null +++ b/src/cards/haiku-global-config.js @@ -0,0 +1,55 @@ +import 'https://unpkg.com/lodash@4.17.10/lodash.js?module'; +import { StorageService } from '../services/storage-service.js'; + +/** + * Haiku global config UI + */ +export class HaikuGlobalConfig extends HTMLElement { + + set hass(hass) { + this.ha = hass; + + if (!this.initialized) { + this.setAttribute('style', 'margin:0;'); + this.initStylesheet(); + this.initTheme(); + this.initialized = true; + } + } + + initStylesheet() { + if (!document.getElementById('haiku_global_css')) { + const globalCss = document.createElement('link'); + globalCss.setAttribute('id', 'haiku_global_css'); + globalCss.setAttribute('href', '/local/haiku/haiku.css'); + globalCss.setAttribute('rel', 'stylesheet'); + document.head.appendChild(globalCss); + } + } + + initTheme() { + const storageService = new StorageService(); + let theme = storageService.getItem('theme'); + if (!theme) { + theme = 'haiku-dark'; + storageService.setItem('theme', 'haiku-dark'); + } + const existingCssClasses = document.body.getAttribute('class'); + if (!existingCssClasses) { + document.body.setAttribute('class', theme); + } + else { + document.body.setAttribute('class', `${existingCssClasses} haiku-dark`); + } + } + + setConfig(config) { + this.config = config; + } + + getCardSize() { + return 0; + } +} + +customElements.define('haiku-global-config', HaikuGlobalConfig); diff --git a/src/cards/haiku-global-config.scss b/src/cards/haiku-global-config.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/cards/haiku-room-card.js b/src/cards/haiku-room-card.js index 691f710..72ee305 100644 --- a/src/cards/haiku-room-card.js +++ b/src/cards/haiku-room-card.js @@ -42,6 +42,10 @@ export class HaikuRoomCard extends LitElement { const states = []; _.each(this.config.entities, (key) => { const state = this.hass.states[key]; + if (!state) { + return; + } + const d = key.split('.')[0]; const t = state.attributes.haiku_type; if (d === domain || t === domain) { diff --git a/src/services/storage-service.js b/src/services/storage-service.js new file mode 100644 index 0000000..06afbe6 --- /dev/null +++ b/src/services/storage-service.js @@ -0,0 +1,18 @@ +export class StorageService { + getItem(key) { + const result = window.localStorage.getItem(`haiku:${key}`); + if (!result) { + return null; + } + return JSON.parse(result); + } + + setItem(key, value) { + const item = JSON.stringify(value); + window.localStorage.setItem(`haiku:${key}`, item); + } + + removeItem(key) { + window.localStorage.removeItem(`haiku:${key}`); + } +} diff --git a/src/styles/global/haiku.scss b/src/styles/global/haiku.scss new file mode 100644 index 0000000..93a2869 --- /dev/null +++ b/src/styles/global/haiku.scss @@ -0,0 +1 @@ +@import './themes/theme-dark.scss'; diff --git a/src/styles/global/themes/theme-dark.scss b/src/styles/global/themes/theme-dark.scss new file mode 100644 index 0000000..ef51954 --- /dev/null +++ b/src/styles/global/themes/theme-dark.scss @@ -0,0 +1,102 @@ +body.haiku-dark { + + + + + // Main Background Color + --primary-background-color: #212121; + background-color: #212121; + + // Sidebar + --sidebar-background-color: #1f1f1f; + --paper-listbox-background-color: #1f1f1f; + + /* text */ + --primary-text-color: #777; + --secondary-text-color: #444; + --text-primary-color: #999; + --disabled-text-color: #333; + /* main interface colors */ + --primary-color: #1c1c1c; + // --dark-primary-color: #0288d1; + // --light-primary-color: #b3e5fC; + // --accent-color: #ff9800; + // --divider-color: rgba(0, 0, 0, .12); + /* states and badges */ + // --state-icon-color: #44739e; + // --state-icon-active-color: #FDD835; + // --state-icon-unavailable-color: var(--disabled-text-color); + /* background and sidebar */ + // --card-background-color: #ffffff; + // --primary-background-color: #fafafa; + // --secondary-background-color: #e5e5e5; /* behind the cards on state */ + /* sidebar menu */ + --sidebar-text-color: #666; + // --sidebar-background-color: var(--paper-listbox-background-color); /* backward compatible with existing themes */ + --sidebar-icon-color: #999; + // --sidebar-selected-text-color: var(--primary-text-color); + /* --sidebar-selected-background-color: rgba(30,30,30,0.1); */ + // --sidebar-selected-icon-color: var(--primary-color); + /* controls */ + // --toggle-button-color: var(--primary-color); + /* --toggle-button-unchecked-color: var(--accent-color); */ + // --slider-color: var(--primary-color); + // --slider-secondary-color: var(--light-primary-color); + // --slider-bar-color: var(--disabled-text-color); + /* for label-badge */ + // --label-badge-background-color: white; + // --label-badge-text-color: rgb(76, 76, 76); + // --label-badge-red: #DF4C1E; + // --label-badge-blue: #039be5; + // --label-badge-green: #0DA035; + // --label-badge-yellow: #f4b400; + // --label-badge-grey: var(--paper-grey-500); + /* + Paper-styles color.html depency is stripped on build. + When a default paper-style color is used, it needs to be copied + from paper-styles/color.html to here. + */ + // --paper-grey-50: #fafafa; /* default for: --paper-toggle-button-unchecked-button-color */ + // --paper-grey-200: #eeeeee; /* for ha-date-picker-style */ + // --paper-grey-500: #9e9e9e; /* --label-badge-grey */ + /* for paper-spinner */ + // --google-red-500: #db4437; + // --google-blue-500: #4285f4; + // --google-green-500: #0f9d58; + // --google-yellow-500: #f4b400; + /* for paper-slider */ + // --paper-green-400: #66bb6a; + // --paper-blue-400: #42a5f5; + // --paper-orange-400: #ffa726; + /* opacity for dark text on a light background */ + // --dark-divider-opacity: 0.12; + // --dark-disabled-opacity: 0.38; /* or hint text or icon */ + // --dark-secondary-opacity: 0.54; + // --dark-primary-opacity: 0.87; + /* opacity for light text on a dark background */ + // --light-divider-opacity: 0.12; + // --light-disabled-opacity: 0.3; /* or hint text or icon */ + // --light-secondary-opacity: 0.7; + // --light-primary-opacity: 1.0; + /* derived colors, to keep existing themes mostly working */ + // --paper-card-background-color: var(--card-background-color); + // --paper-listbox-background-color: var(--card-background-color); + // --paper-item-icon-color: var(--state-icon-color); + // --paper-item-icon-active-color: var(--state-icon-active-color); + // --table-row-background-color: var(--primary-background-color); + // --table-row-alternative-background-color: var(--secondary-background-color); + /* set our toggle style */ + // --paper-toggle-button-checked-ink-color: var(--toggle-button-color); + // --paper-toggle-button-checked-button-color: var(--toggle-button-color); + // --paper-toggle-button-checked-bar-color: var(--toggle-button-color); + // --paper-toggle-button-unchecked-button-color: var(--toggle-button-unchecked-color, var(--paper-grey-50)); + // --paper-toggle-button-unchecked-bar-color: var(--toggle-button-unchecked-color, #000000); + /* set our slider style */ + // --paper-slider-knob-color: var(--slider-color); + // --paper-slider-knob-start-color: var(--slider-color); + // --paper-slider-pin-color: var(--slider-color); + // --paper-slider-active-color: var(--slider-color); + // --paper-slider-secondary-color: var(--slider-secondary-color); + // --paper-slider-container-color: var(--slider-bar-color); + // --ha-paper-slider-pin-font-size: 15px; +} From f27c5d2fe8e06b42907979a082fa87bec5ac4b07 Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Mon, 20 Aug 2018 08:41:47 -0500 Subject: [PATCH 02/10] Update dark theme --- src/styles/global/themes/theme-dark.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/styles/global/themes/theme-dark.scss b/src/styles/global/themes/theme-dark.scss index ef51954..d4bfc61 100644 --- a/src/styles/global/themes/theme-dark.scss +++ b/src/styles/global/themes/theme-dark.scss @@ -27,7 +27,7 @@ body.haiku-dark { // --state-icon-active-color: #FDD835; // --state-icon-unavailable-color: var(--disabled-text-color); /* background and sidebar */ - // --card-background-color: #ffffff; + --card-background-color: #333; // --primary-background-color: #fafafa; // --secondary-background-color: #e5e5e5; /* behind the cards on state */ /* sidebar menu */ @@ -38,9 +38,9 @@ body.haiku-dark { /* --sidebar-selected-background-color: rgba(30,30,30,0.1); */ // --sidebar-selected-icon-color: var(--primary-color); /* controls */ - // --toggle-button-color: var(--primary-color); + // --toggle-button-color: #bada55; /* --toggle-button-unchecked-color: var(--accent-color); */ - // --slider-color: var(--primary-color); + // --slider-color: #bada55; // --slider-secondary-color: var(--light-primary-color); // --slider-bar-color: var(--disabled-text-color); /* for label-badge */ @@ -86,9 +86,9 @@ body.haiku-dark { // --table-row-background-color: var(--primary-background-color); // --table-row-alternative-background-color: var(--secondary-background-color); /* set our toggle style */ - // --paper-toggle-button-checked-ink-color: var(--toggle-button-color); - // --paper-toggle-button-checked-button-color: var(--toggle-button-color); - // --paper-toggle-button-checked-bar-color: var(--toggle-button-color); + --paper-toggle-button-checked-ink-color: #bada55; + --paper-toggle-button-checked-button-color: #bada55; + --paper-toggle-button-checked-bar-color: #292e21; // --paper-toggle-button-unchecked-button-color: var(--toggle-button-unchecked-color, var(--paper-grey-50)); // --paper-toggle-button-unchecked-bar-color: var(--toggle-button-unchecked-color, #000000); /* set our slider style */ From e71db1205e73bc5aa3687f66edb9d9edc9a9d88f Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Thu, 23 Aug 2018 00:10:11 -0500 Subject: [PATCH 03/10] Add haiku settings dialog --- src/elements/haiku-sensor-settings-dialog.js | 33 ++++++++++++++++++++ src/elements/haiku-tile-base.js | 33 ++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/elements/haiku-sensor-settings-dialog.js diff --git a/src/elements/haiku-sensor-settings-dialog.js b/src/elements/haiku-sensor-settings-dialog.js new file mode 100644 index 0000000..92f0335 --- /dev/null +++ b/src/elements/haiku-sensor-settings-dialog.js @@ -0,0 +1,33 @@ +import { html, LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; + +export class HaikuSensorSettingsDialog extends LitElement { + + constructor() { + super(); + } + + static get properties() { + return { + hass: Object, + entity: Object + }; + } + + _render({ entity }) { + return html` + {{ css }} + + +
Sensor Settings
+ Save +
+ +
+ + +
+ `; + } +} + +customElements.define('haiku-sensor-settings-dialog', HaikuSensorSettingsDialog); diff --git a/src/elements/haiku-tile-base.js b/src/elements/haiku-tile-base.js index 1103946..1f285a3 100644 --- a/src/elements/haiku-tile-base.js +++ b/src/elements/haiku-tile-base.js @@ -1,17 +1,46 @@ import { LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; import { EventService } from '../services/event-service.js'; +import '../elements/haiku-sensor-settings-dialog.js'; export class HaikuTileBase extends LitElement { + constructor() { + super(); + this.dialog = this._findMoreInfoDialog(); + } + handleClick(event) { event.stopPropagation(); + const eventService = new EventService(); if (event.altKey) { - console.log('TODO: show input dialog...'); + this.dialog = this._findMoreInfoDialog(); + this.dialog.fire('more-info-page', { page: 'haiku_settings' }); + const sensorSettingsDialog = document.createElement('haiku-sensor-settings-dialog'); + sensorSettingsDialog.entity = this.entity; + this.dialog.shadowRoot.appendChild(sensorSettingsDialog); + this.dialog.open(); + // debugger; // eslint-disable-line no-debugger + this.dialog.addEventListener('iron-overlay-canceled', () => { + this._handleDialogCancel(); + this.dialog.removeEventListener(this._handleDialogCancel); + this.dialog.removeEventListener('iron-overlay-canceled'); + }); } else { - const eventService = new EventService(); eventService.fire(event.target, 'hass-more-info', { entityId: this.entity.entity_id }); } } + + _findMoreInfoDialog() { + const hassEl = document.getElementsByTagName('home-assistant')[0]; + const hassMainEl = hassEl.shadowRoot.querySelector('home-assistant-main'); + return hassMainEl.shadowRoot.querySelector('ha-more-info-dialog'); + } + + _handleDialogCancel() { + const el = this.dialog.shadowRoot.querySelector('haiku-sensor-settings-dialog'); + this.dialog.fire('more-info-page', { page: null }); + this.dialog.shadowRoot.removeChild(el); + } } From 4e69cb23277dd559da33b85fb79ab0f4822c8bdb Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Thu, 23 Aug 2018 02:06:43 -0500 Subject: [PATCH 04/10] Finish tile settings dialog --- src/cards/haiku-global-config.js | 1 - src/elements/haiku-sensor-settings-dialog.js | 33 --------- src/elements/haiku-tile-base.js | 45 ++++++++----- src/elements/haiku-tile-settings-dialog.js | 70 ++++++++++++++++++++ src/elements/haiku-tile-settings-dialog.scss | 12 ++++ 5 files changed, 109 insertions(+), 52 deletions(-) delete mode 100644 src/elements/haiku-sensor-settings-dialog.js create mode 100644 src/elements/haiku-tile-settings-dialog.js create mode 100644 src/elements/haiku-tile-settings-dialog.scss diff --git a/src/cards/haiku-global-config.js b/src/cards/haiku-global-config.js index b2a1a73..81e7dc3 100644 --- a/src/cards/haiku-global-config.js +++ b/src/cards/haiku-global-config.js @@ -1,4 +1,3 @@ -import 'https://unpkg.com/lodash@4.17.10/lodash.js?module'; import { StorageService } from '../services/storage-service.js'; /** diff --git a/src/elements/haiku-sensor-settings-dialog.js b/src/elements/haiku-sensor-settings-dialog.js deleted file mode 100644 index 92f0335..0000000 --- a/src/elements/haiku-sensor-settings-dialog.js +++ /dev/null @@ -1,33 +0,0 @@ -import { html, LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; - -export class HaikuSensorSettingsDialog extends LitElement { - - constructor() { - super(); - } - - static get properties() { - return { - hass: Object, - entity: Object - }; - } - - _render({ entity }) { - return html` - {{ css }} - - -
Sensor Settings
- Save -
- -
- - -
- `; - } -} - -customElements.define('haiku-sensor-settings-dialog', HaikuSensorSettingsDialog); diff --git a/src/elements/haiku-tile-base.js b/src/elements/haiku-tile-base.js index 1f285a3..a3fe077 100644 --- a/src/elements/haiku-tile-base.js +++ b/src/elements/haiku-tile-base.js @@ -1,31 +1,23 @@ import { LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; import { EventService } from '../services/event-service.js'; -import '../elements/haiku-sensor-settings-dialog.js'; +import './haiku-tile-settings-dialog.js'; export class HaikuTileBase extends LitElement { constructor() { super(); - this.dialog = this._findMoreInfoDialog(); + this.settingsDialog = this._findMoreInfoDialog(); + this.settingsDialogContent = null; + this.handleDialogCancel = (event) => this._handleDialogCancel(event); + this.handleCustomizationComplete = (event) => this._handleCustomizationComplete(event); } handleClick(event) { event.stopPropagation(); - const eventService = new EventService(); if (event.altKey) { - this.dialog = this._findMoreInfoDialog(); - this.dialog.fire('more-info-page', { page: 'haiku_settings' }); - const sensorSettingsDialog = document.createElement('haiku-sensor-settings-dialog'); - sensorSettingsDialog.entity = this.entity; - this.dialog.shadowRoot.appendChild(sensorSettingsDialog); - this.dialog.open(); - // debugger; // eslint-disable-line no-debugger - this.dialog.addEventListener('iron-overlay-canceled', () => { - this._handleDialogCancel(); - this.dialog.removeEventListener(this._handleDialogCancel); - this.dialog.removeEventListener('iron-overlay-canceled'); - }); + this._openSettingsDialog(); } else { + const eventService = new EventService(); eventService.fire(event.target, 'hass-more-info', { entityId: this.entity.entity_id }); @@ -38,9 +30,26 @@ export class HaikuTileBase extends LitElement { return hassMainEl.shadowRoot.querySelector('ha-more-info-dialog'); } + _openSettingsDialog() { + this.settingsDialog.fire('more-info-page', { page: 'haiku_settings' }); + this.settingsDialogContent = document.createElement('haiku-tile-settings-dialog'); + this.settingsDialogContent.entity = this.entity; + this.settingsDialogContent.hass = this.hass; + this.settingsDialogContent.addEventListener('haiku-customization-complete', this.handleCustomizationComplete); + this.settingsDialog.shadowRoot.appendChild(this.settingsDialogContent); + this.settingsDialog.addEventListener('iron-overlay-canceled', this.handleDialogCancel); + this.settingsDialog.open(); + } + _handleDialogCancel() { - const el = this.dialog.shadowRoot.querySelector('haiku-sensor-settings-dialog'); - this.dialog.fire('more-info-page', { page: null }); - this.dialog.shadowRoot.removeChild(el); + const el = this.settingsDialog.shadowRoot.querySelector('haiku-tile-settings-dialog'); + this.settingsDialog.shadowRoot.removeChild(el); + this.settingsDialog.fire('more-info-page', { page: null }); + this.settingsDialog.removeEventListener('iron-overlay-canceled', this.handleDialogCancel); + } + + _handleCustomizationComplete() { + this._handleDialogCancel(); + this.settingsDialog.close(); } } diff --git a/src/elements/haiku-tile-settings-dialog.js b/src/elements/haiku-tile-settings-dialog.js new file mode 100644 index 0000000..cf0cf36 --- /dev/null +++ b/src/elements/haiku-tile-settings-dialog.js @@ -0,0 +1,70 @@ +import { html, LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; +import { EventService } from '../services/event-service.js'; +import 'https://unpkg.com/lodash@4.17.10/lodash.js?module'; + +export class HaikuTileSettingsDialog extends LitElement { + + constructor() { + super(); + } + + static get properties() { + return { + hass: Object, + entity: Object + }; + } + + _render({ entity }) { + return html` + {{ css }} + + +
Tile Settings
+
+ +
+ + + + Temperature + Humidity + Smoke Status (Binary) + Carbon Monoxide Status (Binary) + Air Quality + Motion Detected (Binary) + Media Player + + + Save +
+ `; + } + + handleClick() { + const label = this.shadowRoot.querySelector('#label').value; + + const selectedItem = this.shadowRoot.querySelector('#type').selectedItem; + let type = null; + if (selectedItem) { + type = selectedItem.getAttribute('value'); + } + + const url = `config/customize/config/${ this.entity.entity_id }`; + + const $this = this; + this.hass.callApi('GET', url) + .then((response) => { + const data = _.merge(response.local, { + 'haiku_label': label, + 'haiku_type': type || undefined + }); + this.hass.callApi('POST', url, data).then(() => { + const eventService = new EventService(); + eventService.fire($this, 'haiku-customization-complete'); + }); + }); + } +} + +customElements.define('haiku-tile-settings-dialog', HaikuTileSettingsDialog); diff --git a/src/elements/haiku-tile-settings-dialog.scss b/src/elements/haiku-tile-settings-dialog.scss new file mode 100644 index 0000000..3745a92 --- /dev/null +++ b/src/elements/haiku-tile-settings-dialog.scss @@ -0,0 +1,12 @@ +.form { + padding: 0 2rem; + + & > paper-button { + float: right; + margin: 1rem 0; + } + + & > paper-dropdown-menu { + width: 100%; + } +} From e966dd67cc3e6be12cd637a5490d278993774fa1 Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Sat, 25 Aug 2018 23:27:11 -0500 Subject: [PATCH 05/10] Fix bugs, add discrete sensor renderers --- src/cards/haiku-global-config.js | 4 +- src/cards/haiku-room-card.scss | 5 +- src/elements/haiku-sensor-tile.js | 83 ++++++++++++++++++++-- src/elements/haiku-sensor-tile.scss | 44 ++++++++++++ src/elements/haiku-tile-base.js | 7 +- src/elements/haiku-tile-settings-dialog.js | 1 - 6 files changed, 129 insertions(+), 15 deletions(-) diff --git a/src/cards/haiku-global-config.js b/src/cards/haiku-global-config.js index 81e7dc3..4ab0452 100644 --- a/src/cards/haiku-global-config.js +++ b/src/cards/haiku-global-config.js @@ -37,8 +37,8 @@ export class HaikuGlobalConfig extends HTMLElement { if (!existingCssClasses) { document.body.setAttribute('class', theme); } - else { - document.body.setAttribute('class', `${existingCssClasses} haiku-dark`); + else if (existingCssClasses.indexOf(theme) === -1) { + document.body.setAttribute('class', `${existingCssClasses} ${theme}`); } } diff --git a/src/cards/haiku-room-card.scss b/src/cards/haiku-room-card.scss index b2acaf3..87d780c 100644 --- a/src/cards/haiku-room-card.scss +++ b/src/cards/haiku-room-card.scss @@ -57,6 +57,7 @@ .tiles { display: block; + margin: 0 -3px; & > * { display: block; @@ -64,10 +65,6 @@ float: left; margin: 6px 3px 0; - &:first-child { - margin-left: 0; - } - &:nth-child(3n+3) { margin-right: 0; } diff --git a/src/elements/haiku-sensor-tile.js b/src/elements/haiku-sensor-tile.js index 2f6cafd..6a4c01d 100644 --- a/src/elements/haiku-sensor-tile.js +++ b/src/elements/haiku-sensor-tile.js @@ -18,17 +18,86 @@ export class HaikuSensorTile extends HaikuTileBase { return html` {{ css }}
- - - ${ this.getShortValue(entity) } - - ${ this.getUnit(entity) } - - + ${ this.renderSensorContent(entity) } +
+ `; + } + + renderSensorContent(entity) { + let sensorType = 'default'; + + if (entity && entity.attributes && entity.attributes.haiku_type) { + sensorType = entity.attributes.haiku_type; + } + + switch (sensorType) { + case 'smoke_binary': + return this.renderSmokeSensorContent(entity); + case 'co_binary': + return this.renderCarbonMonoxideSensorContent(entity); + case 'air_quality': + return this.renderAirQualitySensorContent(entity); + case 'motion_binary': + return this.renderMotionSensorContent(entity); + case 'temperature': + case 'humidity': + case 'default': + default: + return this.renderDefaultSensorContent(entity); + } + } + + renderSmokeSensorContent(entity) { + return html` +
+
+ Smoke + +
`; } + renderCarbonMonoxideSensorContent(entity) { + return html` +
+
+ Carbon
Monoxide
+ +
+
+ `; + } + + renderAirQualitySensorContent(entity) { + return html` +
+
+ Air
Quality
+ +
+
+ `; + } + + renderMotionSensorContent(entity) { + return html` + + `; + } + + renderDefaultSensorContent(entity) { + return html` + + + ${ this.getShortValue(entity) } + + ${ this.getUnit(entity) } + + + `; + } + getTitle(entity) { if (entity.attributes && entity.attributes.haiku_label) { return entity.attributes.haiku_label; diff --git a/src/elements/haiku-sensor-tile.scss b/src/elements/haiku-sensor-tile.scss index efd1303..c5256da 100644 --- a/src/elements/haiku-sensor-tile.scss +++ b/src/elements/haiku-sensor-tile.scss @@ -1 +1,45 @@ @import '../styles/_tiles.scss'; + +.status-container { + padding: 12px 0; +} + +.status-value { + border: solid 4px #bada55; + width: 80px; + height: 80px; + border-radius: 50%; + padding: 3px; + margin: 0 auto; + text-align: center; + color: #fff; + + & > ha-icon { + font-size: 24px; + display: block; + margin: 0 auto; + margin-bottom: 8px; + margin-top: 6px; + } + + & > span { + font-size: 11px; + display: block; + margin: 0 auto; + line-height: 14px; + text-transform: uppercase; + margin-bottom: 9px; + margin-top: 16px; + + &.multiline { + margin-bottom: 7px; + margin-top: 7px; + } + } + + & > label { + font-size: 32px; + text-transform: uppercase; + letter-spacing: -1px; + } +} diff --git a/src/elements/haiku-tile-base.js b/src/elements/haiku-tile-base.js index a3fe077..5419a6e 100644 --- a/src/elements/haiku-tile-base.js +++ b/src/elements/haiku-tile-base.js @@ -38,14 +38,19 @@ export class HaikuTileBase extends LitElement { this.settingsDialogContent.addEventListener('haiku-customization-complete', this.handleCustomizationComplete); this.settingsDialog.shadowRoot.appendChild(this.settingsDialogContent); this.settingsDialog.addEventListener('iron-overlay-canceled', this.handleDialogCancel); + this.settingsDialog.addEventListener('iron-overlay-closed', this.handleDialogCancel); this.settingsDialog.open(); } - _handleDialogCancel() { + _handleDialogCancel(event) { + if (event && event.path[0].nodeName !== 'HA-MORE-INFO-DIALOG') { + return; + } const el = this.settingsDialog.shadowRoot.querySelector('haiku-tile-settings-dialog'); this.settingsDialog.shadowRoot.removeChild(el); this.settingsDialog.fire('more-info-page', { page: null }); this.settingsDialog.removeEventListener('iron-overlay-canceled', this.handleDialogCancel); + this.settingsDialog.removeEventListener('iron-overlay-closed', this.handleDialogCancel); } _handleCustomizationComplete() { diff --git a/src/elements/haiku-tile-settings-dialog.js b/src/elements/haiku-tile-settings-dialog.js index cf0cf36..b3874e3 100644 --- a/src/elements/haiku-tile-settings-dialog.js +++ b/src/elements/haiku-tile-settings-dialog.js @@ -33,7 +33,6 @@ export class HaikuTileSettingsDialog extends LitElement { Carbon Monoxide Status (Binary) Air Quality Motion Detected (Binary) - Media Player Save From 1524f67024fa9f9b95c01a2968e10d13bd142761 Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Sun, 26 Aug 2018 22:13:54 -0500 Subject: [PATCH 06/10] Refactor customization dialog, add customization to lights --- src/elements/haiku-light-control.js | 19 +++++++- src/elements/haiku-light-group.js | 21 +++++++-- src/elements/haiku-light-menu.js | 6 +-- src/elements/haiku-sensor-tile.js | 42 ++++++++++++++++-- src/elements/haiku-sensor-tile.scss | 14 +++++- ...ngs-dialog.js => haiku-settings-dialog.js} | 9 ++-- ...dialog.scss => haiku-settings-dialog.scss} | 0 src/elements/haiku-tile-base.js | 44 ++----------------- src/services/customization-service.js | 42 ++++++++++++++++++ 9 files changed, 141 insertions(+), 56 deletions(-) rename src/elements/{haiku-tile-settings-dialog.js => haiku-settings-dialog.js} (88%) rename src/elements/{haiku-tile-settings-dialog.scss => haiku-settings-dialog.scss} (100%) create mode 100644 src/services/customization-service.js diff --git a/src/elements/haiku-light-control.js b/src/elements/haiku-light-control.js index 1a8f1d9..da326fd 100644 --- a/src/elements/haiku-light-control.js +++ b/src/elements/haiku-light-control.js @@ -1,10 +1,14 @@ import { LitElement, html } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; import { LightService } from '../services/light-service.js'; +import { EventService } from '../services/event-service.js'; +import { CustomizationService } from '../services/customization-service.js'; +import './haiku-settings-dialog.js'; export class HaikuLightControl extends LitElement { constructor() { super(); this.collapsed = true; + this.customizationService = new CustomizationService(); } static get properties() { @@ -19,7 +23,7 @@ export class HaikuLightControl extends LitElement { {{ css }}
  • - + ${entity.attributes.haiku_label || entity.attributes.friendly_name} @@ -35,6 +39,19 @@ export class HaikuLightControl extends LitElement { const service = new LightService(this.hass); service.toggle(this.entity.entity_id); } + + handleClick(event) { + event.stopPropagation(); + if (event.altKey) { + this.customizationService.openSettingsDialog(this.hass, this.entity); + } + else { + const eventService = new EventService(); + eventService.fire(event.target, 'hass-more-info', { + entityId: this.entity.entity_id + }); + } + } } customElements.define('haiku-light-control', HaikuLightControl); diff --git a/src/elements/haiku-light-group.js b/src/elements/haiku-light-group.js index 4645f4f..b4180ba 100644 --- a/src/elements/haiku-light-group.js +++ b/src/elements/haiku-light-group.js @@ -1,11 +1,14 @@ import { LitElement, html } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; -import './haiku-light-control.js'; import { LightService } from '../services/light-service.js'; +import { CustomizationService } from '../services/customization-service.js'; +import './haiku-light-control.js'; +import './haiku-settings-dialog.js'; export class HaikuLightGroup extends LitElement { constructor() { super(); this.collapsed = true; + this.customizationService = new CustomizationService(); } static get properties() { @@ -21,10 +24,10 @@ export class HaikuLightGroup extends LitElement { {{ css }}
  • - + - + ${ entity.attributes.haiku_label || entity.attributes.friendly_name } @@ -40,7 +43,17 @@ export class HaikuLightGroup extends LitElement { `; } - toggleMenuState$() { + handleClick(event) { + event.stopPropagation(); + if (event.altKey) { + this.customizationService.openSettingsDialog(this.hass, this.entity); + } + else { + this.toggleMenuState(); + } + } + + toggleMenuState() { this.collapsed = !this.collapsed; } diff --git a/src/elements/haiku-light-menu.js b/src/elements/haiku-light-menu.js index c40b3ec..910b20b 100644 --- a/src/elements/haiku-light-menu.js +++ b/src/elements/haiku-light-menu.js @@ -27,10 +27,10 @@ export class HaikuLightMenu extends LitElement { {{ css }}
    • - + - + Lighting @@ -48,7 +48,7 @@ export class HaikuLightMenu extends LitElement { }) ? 'on' : 'off'; } - toggleMenuState$() { + toggleMenuState() { this.collapsed = !this.collapsed; } diff --git a/src/elements/haiku-sensor-tile.js b/src/elements/haiku-sensor-tile.js index 6a4c01d..2d46374 100644 --- a/src/elements/haiku-sensor-tile.js +++ b/src/elements/haiku-sensor-tile.js @@ -17,7 +17,7 @@ export class HaikuSensorTile extends HaikuTileBase { _render({ entity }) { return html` {{ css }} -
      +
      ${ this.renderSensorContent(entity) }
      `; @@ -50,7 +50,7 @@ export class HaikuSensorTile extends HaikuTileBase { renderSmokeSensorContent(entity) { return html`
      -
      +
      Smoke
      @@ -61,7 +61,7 @@ export class HaikuSensorTile extends HaikuTileBase { renderCarbonMonoxideSensorContent(entity) { return html`
      -
      +
      Carbon
      Monoxide
      @@ -83,6 +83,7 @@ export class HaikuSensorTile extends HaikuTileBase { renderMotionSensorContent(entity) { return html` + ${ entity } `; } @@ -151,6 +152,41 @@ export class HaikuSensorTile extends HaikuTileBase { _hasUnit(entity) { return entity.attributes && entity.attributes.unit_of_measurement; } + + getStatusClass(state) { + const NORMAL_STATES = [ + 'ok' + ]; + + const WARNING_STATES = [ + 'warning' + ]; + + const CRITICAL_STATES = [ + 'emergency' + ]; + if (NORMAL_STATES.includes(state.toLowerCase())) { + return 'status-normal'; + } + + if (WARNING_STATES.includes(state.toLowerCase())) { + return 'status-warning'; + } + + if (CRITICAL_STATES.includes(state.toLowerCase())) { + return 'status-critical'; + } + + return 'status-unknown'; + } + + getName(entity) { + if (entity.attributes && entity.attributes.friendly_name) { + return entity.attributes.friendly_name; + } + + return entity.entity_id; + } } customElements.define('haiku-sensor-tile', HaikuSensorTile); diff --git a/src/elements/haiku-sensor-tile.scss b/src/elements/haiku-sensor-tile.scss index c5256da..a38f998 100644 --- a/src/elements/haiku-sensor-tile.scss +++ b/src/elements/haiku-sensor-tile.scss @@ -5,7 +5,7 @@ } .status-value { - border: solid 4px #bada55; + border: solid 4px #ccc; width: 80px; height: 80px; border-radius: 50%; @@ -14,6 +14,18 @@ text-align: center; color: #fff; + &.status-normal { + border-color: #bada55; + } + + &.status-warning { + border-color: #ffc800; + } + + &.status-critical { + border-color: #d20c0c; + } + & > ha-icon { font-size: 24px; display: block; diff --git a/src/elements/haiku-tile-settings-dialog.js b/src/elements/haiku-settings-dialog.js similarity index 88% rename from src/elements/haiku-tile-settings-dialog.js rename to src/elements/haiku-settings-dialog.js index b3874e3..1aeb5d5 100644 --- a/src/elements/haiku-tile-settings-dialog.js +++ b/src/elements/haiku-settings-dialog.js @@ -2,7 +2,7 @@ import { html, LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/ import { EventService } from '../services/event-service.js'; import 'https://unpkg.com/lodash@4.17.10/lodash.js?module'; -export class HaikuTileSettingsDialog extends LitElement { +export class HaikuSettingsDialog extends LitElement { constructor() { super(); @@ -20,13 +20,14 @@ export class HaikuTileSettingsDialog extends LitElement { {{ css }} -
      Tile Settings
      +
      Haiku Customization
      - + + Light Temperature Humidity Smoke Status (Binary) @@ -66,4 +67,4 @@ export class HaikuTileSettingsDialog extends LitElement { } } -customElements.define('haiku-tile-settings-dialog', HaikuTileSettingsDialog); +customElements.define('haiku-settings-dialog', HaikuSettingsDialog); diff --git a/src/elements/haiku-tile-settings-dialog.scss b/src/elements/haiku-settings-dialog.scss similarity index 100% rename from src/elements/haiku-tile-settings-dialog.scss rename to src/elements/haiku-settings-dialog.scss diff --git a/src/elements/haiku-tile-base.js b/src/elements/haiku-tile-base.js index 5419a6e..2cd4efe 100644 --- a/src/elements/haiku-tile-base.js +++ b/src/elements/haiku-tile-base.js @@ -1,20 +1,18 @@ import { LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; import { EventService } from '../services/event-service.js'; -import './haiku-tile-settings-dialog.js'; +import { CustomizationService } from '../services/customization-service.js'; +import './haiku-settings-dialog.js'; export class HaikuTileBase extends LitElement { constructor() { super(); - this.settingsDialog = this._findMoreInfoDialog(); - this.settingsDialogContent = null; - this.handleDialogCancel = (event) => this._handleDialogCancel(event); - this.handleCustomizationComplete = (event) => this._handleCustomizationComplete(event); + this.customizationService = new CustomizationService(); } handleClick(event) { event.stopPropagation(); if (event.altKey) { - this._openSettingsDialog(); + this.customizationService.openSettingsDialog(this.hass, this.entity); } else { const eventService = new EventService(); @@ -23,38 +21,4 @@ export class HaikuTileBase extends LitElement { }); } } - - _findMoreInfoDialog() { - const hassEl = document.getElementsByTagName('home-assistant')[0]; - const hassMainEl = hassEl.shadowRoot.querySelector('home-assistant-main'); - return hassMainEl.shadowRoot.querySelector('ha-more-info-dialog'); - } - - _openSettingsDialog() { - this.settingsDialog.fire('more-info-page', { page: 'haiku_settings' }); - this.settingsDialogContent = document.createElement('haiku-tile-settings-dialog'); - this.settingsDialogContent.entity = this.entity; - this.settingsDialogContent.hass = this.hass; - this.settingsDialogContent.addEventListener('haiku-customization-complete', this.handleCustomizationComplete); - this.settingsDialog.shadowRoot.appendChild(this.settingsDialogContent); - this.settingsDialog.addEventListener('iron-overlay-canceled', this.handleDialogCancel); - this.settingsDialog.addEventListener('iron-overlay-closed', this.handleDialogCancel); - this.settingsDialog.open(); - } - - _handleDialogCancel(event) { - if (event && event.path[0].nodeName !== 'HA-MORE-INFO-DIALOG') { - return; - } - const el = this.settingsDialog.shadowRoot.querySelector('haiku-tile-settings-dialog'); - this.settingsDialog.shadowRoot.removeChild(el); - this.settingsDialog.fire('more-info-page', { page: null }); - this.settingsDialog.removeEventListener('iron-overlay-canceled', this.handleDialogCancel); - this.settingsDialog.removeEventListener('iron-overlay-closed', this.handleDialogCancel); - } - - _handleCustomizationComplete() { - this._handleDialogCancel(); - this.settingsDialog.close(); - } } diff --git a/src/services/customization-service.js b/src/services/customization-service.js new file mode 100644 index 0000000..a6e364b --- /dev/null +++ b/src/services/customization-service.js @@ -0,0 +1,42 @@ +export class CustomizationService { + constructor() { + this.settingsDialog = this._findMoreInfoDialog(); + this.settingsDialogContent = null; + this.handleDialogCancel = (event) => this._handleDialogCancel(event); + this.handleCustomizationComplete = (event) => this._handleCustomizationComplete(event); + } + + openSettingsDialog(hass, entity) { + this.settingsDialog.fire('more-info-page', { page: 'haiku_settings' }); + this.settingsDialogContent = document.createElement('haiku-settings-dialog'); + this.settingsDialogContent.entity = entity; + this.settingsDialogContent.hass = hass; + this.settingsDialogContent.addEventListener('haiku-customization-complete', this.handleCustomizationComplete); + this.settingsDialog.shadowRoot.appendChild(this.settingsDialogContent); + this.settingsDialog.addEventListener('iron-overlay-canceled', this.handleDialogCancel); + this.settingsDialog.addEventListener('iron-overlay-closed', this.handleDialogCancel); + this.settingsDialog.open(); + } + + _findMoreInfoDialog() { + const hassEl = document.getElementsByTagName('home-assistant')[0]; + const hassMainEl = hassEl.shadowRoot.querySelector('home-assistant-main'); + return hassMainEl.shadowRoot.querySelector('ha-more-info-dialog'); + } + + _handleDialogCancel(event) { + if (event && event.path[0].nodeName !== 'HA-MORE-INFO-DIALOG') { + return; + } + const el = this.settingsDialog.shadowRoot.querySelector('haiku-settings-dialog'); + this.settingsDialog.shadowRoot.removeChild(el); + this.settingsDialog.fire('more-info-page', { page: null }); + this.settingsDialog.removeEventListener('iron-overlay-canceled', this.handleDialogCancel); + this.settingsDialog.removeEventListener('iron-overlay-closed', this.handleDialogCancel); + } + + _handleCustomizationComplete() { + this._handleDialogCancel(); + this.settingsDialog.close(); + } +} From 2a17167bcc9123122cbe03e2937cfbd21140391e Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Sat, 1 Sep 2018 23:42:56 -0400 Subject: [PATCH 07/10] Add support for SSH port number --- deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 899b3e1..71fa8b9 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1 +1 @@ -rsync -aviO ./haiku/ $HA_SSH_USER@$HA_SSH_HOST:/home/homeassistant/.homeassistant/www/haiku/ +rsync -aviO -e "ssh -p $HA_SSH_PORT -o StrictHostKeyChecking=no" ./haiku/ $HA_SSH_USER@$HA_SSH_HOST:/home/homeassistant/.homeassistant/www/haiku/ From 22d839db114f1a8a4b5fd76736f05f52e9147ed2 Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Sat, 1 Sep 2018 23:43:50 -0400 Subject: [PATCH 08/10] Fixes for homeassistant@0.77.2, add thermostat tile --- src/cards/haiku-room-card.js | 15 ++- src/cards/haiku-room-card.scss | 65 +++++++----- src/elements/haiku-light-control.js | 2 +- src/elements/haiku-light-group.js | 2 +- src/elements/haiku-sensor-tile.js | 8 -- src/elements/haiku-sensor-tile.scss | 52 --------- src/elements/haiku-thermostat-tile.js | 128 +++++++++++++++++++++++ src/elements/haiku-thermostat-tile.scss | 29 +++++ src/elements/haiku-tile-base.js | 10 +- src/services/customization-service.js | 25 ++++- src/styles/_tiles.scss | 54 +++++++++- src/styles/global/themes/theme-dark.scss | 3 - 12 files changed, 292 insertions(+), 101 deletions(-) create mode 100644 src/elements/haiku-thermostat-tile.js create mode 100644 src/elements/haiku-thermostat-tile.scss diff --git a/src/cards/haiku-room-card.js b/src/cards/haiku-room-card.js index 72ee305..c1459de 100644 --- a/src/cards/haiku-room-card.js +++ b/src/cards/haiku-room-card.js @@ -3,6 +3,7 @@ import 'https://unpkg.com/lodash@4.17.10/lodash.js?module'; import '../elements/haiku-light-menu.js'; import '../elements/haiku-sensor-tile.js'; import '../elements/haiku-fan-tile.js'; +import '../elements/haiku-thermostat-tile.js'; /** * A card that summarizes a rooms entities. @@ -29,6 +30,7 @@ export class HaikuRoomCard extends LitElement {
      + ${ this.renderThermostats() } ${ this.renderSensors() } ${ this.renderFans() }
      @@ -73,6 +75,17 @@ export class HaikuRoomCard extends LitElement { `; } + renderThermostats() { + const thermostats = this.getEntitiesByDomain('climate'); + return html` + ${_.map(thermostats, (thermostat) => this.renderThermostat(thermostat))} + `; + } + + renderThermostat(thermostat) { + return html``; + } + renderSensors() { const sensors = this.getEntitiesByDomain('sensor'); return html` @@ -86,7 +99,7 @@ export class HaikuRoomCard extends LitElement { getCustomBackgroundStyle() { if (this.config.background_image) { - return `background-image: url("${this.config.background_image}");`; + return `background-image: ${this.config.background_image};`; } else { return ''; diff --git a/src/cards/haiku-room-card.scss b/src/cards/haiku-room-card.scss index 87d780c..84b23d2 100644 --- a/src/cards/haiku-room-card.scss +++ b/src/cards/haiku-room-card.scss @@ -8,38 +8,18 @@ } .haiku-room-card { - background-size: auto 100%; - background-repeat: no-repeat; - background-position: center center; height: 30rem; overflow-x: hidden; overflow-y: scroll; -webkit-overflow-scrolling: touch; padding: 1rem 1.5rem; - &.bedroom { - background-image: url('https://mir-s3-cdn-cf.behance.net/project_modules/max_3840/a06fac31446953.5650db6e9de74.jpg'); - } - - &.bedroom-alternate { - background-image: url('https://st.hzcdn.com/simgs/9e1145d707fcf5b3_4-1447/modern-bedroom.jpg'); - } - - &.recreation { - background-image: url('https://designingidea.com/wp-content/uploads/2016/11/large-modern-game-room-with-gray-color-theme.jpg'); - } + background: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(0,0,0,0.45) 100%), radial-gradient(at top center, rgba(255,255,255,0.40) 0%, rgba(0,0,0,0.40) 120%) #989898; + background-blend-mode: multiply,multiply; - &.living-room { - background-image: url('http://irasuite.com/wp-content/uploads/2017/08/modern-living-room-chairs.jpeg'); - } - - &.kitchen { - background-image: url('https://germankitchencenter.com/images/Nobilia_JGF%20(20).jpg'); - } - - &.dining-room { - background-image: url('https://www.tinydt.net/wp-content/uploads/2018/04/glamorous-at-x-in-modern-dining-room-lighting-fixtures-formal-living-light-lowes-ceiling-lights-home-depot-low-canada-decorating.jpg'); - } + background-size: auto 100%; + background-repeat: no-repeat; + background-position: center center; } .haiku-room-card-title { @@ -55,18 +35,47 @@ margin-bottom: 0; } +@mixin tiles-thirds() { + width: 32%; + &:nth-child(3n+3) { + margin-right: -1px; + } +} + +@mixin tiles-halves() { + width: 48%; + &:nth-child(odd) { + margin-right: -1px; + } +} + .tiles { display: block; margin: 0 -3px; & > * { display: block; - width: 32%; float: left; margin: 6px 3px 0; - &:nth-child(3n+3) { - margin-right: 0; + @media only screen and (max-width: 599px) { + @include tiles-thirds(); + } + + @media only screen and (min-width: 600px) and (max-width: 849px) { + @include tiles-halves(); + } + + @media only screen and (min-width: 850px) and (max-width: 899px) { + @include tiles-thirds(); + } + + @media only screen and (min-width: 900px) and (max-width: 1599px) { + @include tiles-halves(); + } + + @media only screen and (min-width: 1600px) { + @include tiles-thirds(); } } } \ No newline at end of file diff --git a/src/elements/haiku-light-control.js b/src/elements/haiku-light-control.js index da326fd..30e5622 100644 --- a/src/elements/haiku-light-control.js +++ b/src/elements/haiku-light-control.js @@ -8,7 +8,7 @@ export class HaikuLightControl extends LitElement { constructor() { super(); this.collapsed = true; - this.customizationService = new CustomizationService(); + this.customizationService = new CustomizationService(this); } static get properties() { diff --git a/src/elements/haiku-light-group.js b/src/elements/haiku-light-group.js index b4180ba..ab18353 100644 --- a/src/elements/haiku-light-group.js +++ b/src/elements/haiku-light-group.js @@ -8,7 +8,7 @@ export class HaikuLightGroup extends LitElement { constructor() { super(); this.collapsed = true; - this.customizationService = new CustomizationService(); + this.customizationService = new CustomizationService(this); } static get properties() { diff --git a/src/elements/haiku-sensor-tile.js b/src/elements/haiku-sensor-tile.js index 2d46374..d65e6aa 100644 --- a/src/elements/haiku-sensor-tile.js +++ b/src/elements/haiku-sensor-tile.js @@ -179,14 +179,6 @@ export class HaikuSensorTile extends HaikuTileBase { return 'status-unknown'; } - - getName(entity) { - if (entity.attributes && entity.attributes.friendly_name) { - return entity.attributes.friendly_name; - } - - return entity.entity_id; - } } customElements.define('haiku-sensor-tile', HaikuSensorTile); diff --git a/src/elements/haiku-sensor-tile.scss b/src/elements/haiku-sensor-tile.scss index a38f998..7e5f6a6 100644 --- a/src/elements/haiku-sensor-tile.scss +++ b/src/elements/haiku-sensor-tile.scss @@ -3,55 +3,3 @@ .status-container { padding: 12px 0; } - -.status-value { - border: solid 4px #ccc; - width: 80px; - height: 80px; - border-radius: 50%; - padding: 3px; - margin: 0 auto; - text-align: center; - color: #fff; - - &.status-normal { - border-color: #bada55; - } - - &.status-warning { - border-color: #ffc800; - } - - &.status-critical { - border-color: #d20c0c; - } - - & > ha-icon { - font-size: 24px; - display: block; - margin: 0 auto; - margin-bottom: 8px; - margin-top: 6px; - } - - & > span { - font-size: 11px; - display: block; - margin: 0 auto; - line-height: 14px; - text-transform: uppercase; - margin-bottom: 9px; - margin-top: 16px; - - &.multiline { - margin-bottom: 7px; - margin-top: 7px; - } - } - - & > label { - font-size: 32px; - text-transform: uppercase; - letter-spacing: -1px; - } -} diff --git a/src/elements/haiku-thermostat-tile.js b/src/elements/haiku-thermostat-tile.js new file mode 100644 index 0000000..570bda8 --- /dev/null +++ b/src/elements/haiku-thermostat-tile.js @@ -0,0 +1,128 @@ +import { html } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; +import { HaikuTileBase } from './haiku-tile-base.js'; + +export class HaikuThermostatTile extends HaikuTileBase { + + constructor() { + super(); + } + + static get properties() { + return { + hass: Object, + entity: Object + }; + } + + _render({ entity }) { + console.log(entity); + // console.log(entity.state); + return html` + {{ css }} +
      +
      +
      + ${ this.getModeLabel(entity) } + +
      +
      +
      + `; + } + + getModeLabel(entity) { + if (entity.attributes && entity.attributes.operation_mode) { + if (entity.attributes.fan_mode === 'on') { + return entity.attributes.operation_mode + 'ing'; + } + return 'Set to'; + } + return 'Unknown'; + } + + getStatusClasses(entity) { + const classList = []; + if (entity.attributes) { + if (entity.attributes.operation_mode === 'cool') { + classList.push('cool'); + } + else if (entity.attributes.operation_mode === 'heat') { + classList.push('heat'); + } + + if (entity.attributes.fan_mode === 'on') { + classList.push('on'); + } + } + return classList.join(' '); + } + + getTargetTemperature(entity) { + if (entity.attributes) { + if (entity.attributes.operation_mode === 'cool') { + return entity.attributes.temperature || entity.attributes.target_temp_high; + } + + if (entity.attributes.operation_mode === 'heat') { + return entity.attributes.temperature || entity.attributes.target_temp_low; + } + } + return 'Off'; + } + + getUnit(entity) { + // TODO: Climate domain doesn't appear to return unit_of_measurement. + // Need to find out how to reliably get this information. + if (entity.attributes) { + let value = this.getTargetTemperature(entity); + if (value === 'Off') { + value = entity.attributes.current_temperature; + } + + if (value > 45) { + return 'F'; + } + return 'C'; + } + return ''; + } + + // getShortValue(entity) { + // if (this._hasUnit(entity)) { + // if (entity.attributes.unit_of_measurement.match(/°/)) { + // return `${ Math.round(entity.state) }°`; + // } + // } + + // if (isNaN(entity.state)) { + // return entity.state; + // } + + // return Math.round(entity.state).toString(); + // } + + // getLongValue(entity) { + // if (this._hasUnit(entity)) { + // return entity.state + entity.attributes.unit_of_measurement; + // } + // return entity.state; + // } + + // getUnit(entity) { + // if (this._hasUnit(entity)) { + // return entity.attributes.unit_of_measurement.replace(/°/, ''); + // } + // return ''; + // } + + // _hasUnit(entity) { + // return entity.attributes && entity.attributes.unit_of_measurement; + // } +} + +customElements.define('haiku-thermostat-tile', HaikuThermostatTile); diff --git a/src/elements/haiku-thermostat-tile.scss b/src/elements/haiku-thermostat-tile.scss new file mode 100644 index 0000000..848004c --- /dev/null +++ b/src/elements/haiku-thermostat-tile.scss @@ -0,0 +1,29 @@ +@import '../styles/_tiles.scss'; + +.status-container { + padding: 12px 0; +} + +.status-value { + cursor: pointer; + + & > * { + cursor: pointer; + } + + &.cool.on { + border-color: #00c6fb; + // background-image: linear-gradient(to top, #00c6fb 0%, #005bea 100%); + } + + &.heat.on { + border-color: #ff5858; + // background-image: linear-gradient(-60deg, #ff5858 0%, #f09819 100%); + } + + & .unit { + font-size: 15px; + margin-left: -15px; + color: #999; + } +} \ No newline at end of file diff --git a/src/elements/haiku-tile-base.js b/src/elements/haiku-tile-base.js index 2cd4efe..67540d4 100644 --- a/src/elements/haiku-tile-base.js +++ b/src/elements/haiku-tile-base.js @@ -6,7 +6,7 @@ import './haiku-settings-dialog.js'; export class HaikuTileBase extends LitElement { constructor() { super(); - this.customizationService = new CustomizationService(); + this.customizationService = new CustomizationService(this); } handleClick(event) { @@ -21,4 +21,12 @@ export class HaikuTileBase extends LitElement { }); } } + + getName(entity) { + if (entity.attributes && entity.attributes.friendly_name) { + return entity.attributes.friendly_name; + } + + return entity.entity_id; + } } diff --git a/src/services/customization-service.js b/src/services/customization-service.js index a6e364b..54f298f 100644 --- a/src/services/customization-service.js +++ b/src/services/customization-service.js @@ -1,12 +1,16 @@ +import { EventService } from './event-service.js'; + export class CustomizationService { - constructor() { - this.settingsDialog = this._findMoreInfoDialog(); + constructor(node) { + this.node = node; + this.eventService = new EventService(); this.settingsDialogContent = null; this.handleDialogCancel = (event) => this._handleDialogCancel(event); this.handleCustomizationComplete = (event) => this._handleCustomizationComplete(event); } openSettingsDialog(hass, entity) { + this.settingsDialog = this._findMoreInfoDialog(entity); this.settingsDialog.fire('more-info-page', { page: 'haiku_settings' }); this.settingsDialogContent = document.createElement('haiku-settings-dialog'); this.settingsDialogContent.entity = entity; @@ -18,10 +22,21 @@ export class CustomizationService { this.settingsDialog.open(); } - _findMoreInfoDialog() { + _findMoreInfoDialog(entity) { const hassEl = document.getElementsByTagName('home-assistant')[0]; - const hassMainEl = hassEl.shadowRoot.querySelector('home-assistant-main'); - return hassMainEl.shadowRoot.querySelector('ha-more-info-dialog'); + let dialog = hassEl.shadowRoot.querySelector('ha-more-info-dialog'); + + // TODO: ha-more-info-dialog is now created on demand. As a quick fix, we'll call the standard + // hass-more-info to bootstrap the dialog, then close it. Should figure out a better way. + if (!dialog) { + this.eventService.fire(this.node, 'hass-more-info', { + entityId: entity.entity_id + }); + dialog = hassEl.shadowRoot.querySelector('ha-more-info-dialog'); + dialog.close(); + } + + return dialog; } _handleDialogCancel(event) { diff --git a/src/styles/_tiles.scss b/src/styles/_tiles.scss index 716eceb..da7ff7d 100644 --- a/src/styles/_tiles.scss +++ b/src/styles/_tiles.scss @@ -18,7 +18,7 @@ & > .stat-value { @include overlay-text(); - font-size: 54px; + font-size: 48px; font-weight: 400; display: block; margin-top: 1.5rem; @@ -33,3 +33,55 @@ } } } + +.status-value { + border: solid 4px #ccc; + width: 80px; + height: 80px; + border-radius: 50%; + padding: 3px; + margin: 0 auto; + text-align: center; + color: #fff; + + &.status-normal { + border-color: #bada55; + } + + &.status-warning { + border-color: #ffc800; + } + + &.status-critical { + border-color: #d20c0c; + } + + & > ha-icon { + font-size: 24px; + display: block; + margin: 0 auto; + margin-bottom: 8px; + margin-top: 6px; + } + + & > span { + font-size: 11px; + display: block; + margin: 0 auto; + line-height: 14px; + text-transform: uppercase; + margin-bottom: 9px; + margin-top: 16px; + + &.multiline { + margin-bottom: 7px; + margin-top: 7px; + } + } + + & > label { + font-size: 32px; + text-transform: uppercase; + letter-spacing: -1px; + } +} diff --git a/src/styles/global/themes/theme-dark.scss b/src/styles/global/themes/theme-dark.scss index d4bfc61..050e27f 100644 --- a/src/styles/global/themes/theme-dark.scss +++ b/src/styles/global/themes/theme-dark.scss @@ -1,8 +1,5 @@ body.haiku-dark { - - - // Main Background Color --primary-background-color: #212121; background-color: #212121; From 9659a9b94f3fd89e0f864a1140a507a815859839 Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Sun, 2 Sep 2018 00:08:35 -0400 Subject: [PATCH 09/10] Update README --- README.md | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fabebc1..91368b6 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,12 @@ resources: - url: /local/node_modules/haiku/cards/haiku-room-card.js type: module views: - - tab_icon: mdi:home + - title: Overview + tab_icon: mdi:home # ... cards: - type: "custom:haiku-room-card" name: Master Bedroom - class: bedroom entities: - group.lighting_master_bedroom - sensor.lumi_lumiweather_022cc5ba_1_1026 @@ -66,39 +66,64 @@ you deploy the `haiku` directory. Each room card can be configured with these options: - `name` is the room name displayed at the bottom of the card -- `class` is the type of room that will determine the background image based on theme (any of -`bedroom`, `bedroom-alternate`, `recreation`, `living-room`, `kitchen`, or `dining-room`) - `entities` is an array of entities or groups (defined in `groups.yaml`) +- `background_image` any valid CSS value for `background-image` + - You can specify the image from a camera feed by specifying `background-image: "url('http://hassio.local:8123/your_camera_image_feed')"` + - You can also specify a `url(...)` for a static image (you can host these externally or place them in your www folder and reference + them from there). + - You can specify other valid CSS values like gradients `background_image: "linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%)"` -As mentioned above, a fully-descriptive name is more useful in a global context. If you want to customize the name of a -group or entity in Haiku, simply go to the Customization section of your config and add a custom `haiku_label` attribute -to the group or entity or edit your `customize.yaml` directly: +As mentioned above, a fully-descriptive name is more useful in a global context. If you want to customize the options for a +group or entity in Haiku, you can hold alt/option and click the tile for the entity you want to customize. This allows you to edit the +`haiku_type` and `haiku_label` custom properties. + +You can also edit these properties in your `customize.yaml` directly: ```yaml fan.ge_12730_fan_control_switch_level: haiku_label: Ceiling Fan + +switch.example_light_switch: + haiku_label: Kitchen Light Switch + haiku_type: light ``` +- `haiku_label` can be any string value +- `haiku_type` should be one of: + - `light` + - `temperature` + - `humidity` + - `smoke_binary` + - `co_binary` + - `air_quality` + - `motion_binary` + ## Developing and Contributing ### Development Setup + You can clone this repository and start developing with a few commands: ```bash npm install -g gulp npm install + +export HA_SSH_PORT=22 +export HA_SSH_USER=pi +export HA_SSH_HOST=example.local + gulp watch ``` The `watch` command will watch the `src/**/*` glob pattern, rebuild the package on changes, and call the `deploy.sh` script. -The deployment script makes some assumptions that you have key-based SSH authentication and you're `pi@raspberrypi` is a valid -SSH target. It also assumes the destination directory to be `/home/homeassistant/.homeassistant/www/haiku` You can customize -this script as necessary. +The deployment script makes some assumptions that you have key-based SSH authentication. It also assumes the destination +directory to be `/home/homeassistant/.homeassistant/www/haiku` You can customize this script as necessary. ### Contributing + This is a fun project for exploring Polymer and Lovelace -- one that satisfies my own personal needs for Home Assistant. PRs are welcome, but I can't make any guarantees as to my availability for PR reviews or bug fixes. Forking and customizing for your needs might be the quickest path. From 2163eb3bd598a744c5f645cbdc9941e7cc814faa Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Sun, 30 Sep 2018 12:31:49 -0500 Subject: [PATCH 10/10] Add global config dialog --- package.json | 2 +- src/cards/haiku-global-config.js | 39 +++++++++++- src/elements/haiku-global-config-dialog.js | 62 ++++++++++++++++++++ src/elements/haiku-global-config-dialog.scss | 12 ++++ src/elements/haiku-thermostat-tile.js | 2 - src/services/customization-service.js | 21 ++++++- src/styles/_tiles.scss | 3 + src/styles/global/haiku.scss | 26 ++++++++ 8 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 src/elements/haiku-global-config-dialog.js create mode 100644 src/elements/haiku-global-config-dialog.scss diff --git a/package.json b/package.json index eb478fb..fa550f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@haiku-ui/haiku", - "version": "0.0.1", + "version": "0.1.0", "description": "A collection of cards and other web components for the Home Assistant Lovelace UI.", "private": false, "repository": { diff --git a/src/cards/haiku-global-config.js b/src/cards/haiku-global-config.js index 4ab0452..5480696 100644 --- a/src/cards/haiku-global-config.js +++ b/src/cards/haiku-global-config.js @@ -1,10 +1,18 @@ import { StorageService } from '../services/storage-service.js'; +import { CustomizationService } from '../services/customization-service.js'; +import '../elements/haiku-global-config-dialog.js'; +import 'https://unpkg.com/lodash@4.17.10/lodash.js?module'; /** * Haiku global config UI */ export class HaikuGlobalConfig extends HTMLElement { + constructor() { + super(); + this.customizationService = new CustomizationService(this); + } + set hass(hass) { this.ha = hass; @@ -12,6 +20,7 @@ export class HaikuGlobalConfig extends HTMLElement { this.setAttribute('style', 'margin:0;'); this.initStylesheet(); this.initTheme(); + this.initConfigButton(); this.initialized = true; } } @@ -37,11 +46,37 @@ export class HaikuGlobalConfig extends HTMLElement { if (!existingCssClasses) { document.body.setAttribute('class', theme); } - else if (existingCssClasses.indexOf(theme) === -1) { - document.body.setAttribute('class', `${existingCssClasses} ${theme}`); + else { + let cssClasses = existingCssClasses.split(' '); + cssClasses = _.filter(cssClasses, (cssClass) => { + return cssClass.indexOf('haiku-') === -1; + }); + document.body.setAttribute('class', `${cssClasses.join(' ')} ${theme}`); } } + initConfigButton() { + if (!document.getElementById('haiku_config_button')) { + const configButton = document.createElement('button'); + configButton.setAttribute('class', 'haiku-config-button'); + const icon = document.createElement('ha-icon'); + icon.setAttribute('icon', 'mdi:settings'); + configButton.appendChild(icon); + configButton.onclick = () => { + this._openGlobalConfigDialog(); + }; + document.body.appendChild(configButton); + } + } + + _openGlobalConfigDialog() { + const $this = this; + this.customizationService.openGlobalConfigDialog(() => { + console.log('saved...'); + $this.initTheme(); + }); + } + setConfig(config) { this.config = config; } diff --git a/src/elements/haiku-global-config-dialog.js b/src/elements/haiku-global-config-dialog.js new file mode 100644 index 0000000..0af48ce --- /dev/null +++ b/src/elements/haiku-global-config-dialog.js @@ -0,0 +1,62 @@ +import { html, LitElement } from 'https://unpkg.com/@polymer/lit-element@^0.5.2/lit-element.js?module'; +import { StorageService } from '../services/storage-service.js'; +import { EventService } from '../services/event-service.js'; +import 'https://unpkg.com/lodash@4.17.10/lodash.js?module'; + +export class HaikuGlobalConfigDialog extends LitElement { + + constructor() { + super(); + } + + static get properties() { + return { + hass: Object + }; + } + + _render() { + return html` + {{ css }} + + +
      Haiku Global Settings
      +
      + +
      + + + None + Haiku Light + Haiku Dark + + + Save +
      + `; + } + + _getCurrentTheme() { + const storageService = new StorageService(); + let theme = storageService.getItem('theme'); + if (!theme) { + theme = 'haiku-none'; + storageService.setItem('theme', 'haiku-none'); + } + return theme; + } + + handleClick() { + const storageService = new StorageService(); + const eventService = new EventService(); + const selectedItem = this.shadowRoot.querySelector('#theme').selectedItem; + let theme = null; + if (selectedItem) { + theme = selectedItem.getAttribute('value'); + } + storageService.setItem('theme', theme); + eventService.fire(this, 'haiku-customization-complete'); + } +} + +customElements.define('haiku-global-config-dialog', HaikuGlobalConfigDialog); diff --git a/src/elements/haiku-global-config-dialog.scss b/src/elements/haiku-global-config-dialog.scss new file mode 100644 index 0000000..3745a92 --- /dev/null +++ b/src/elements/haiku-global-config-dialog.scss @@ -0,0 +1,12 @@ +.form { + padding: 0 2rem; + + & > paper-button { + float: right; + margin: 1rem 0; + } + + & > paper-dropdown-menu { + width: 100%; + } +} diff --git a/src/elements/haiku-thermostat-tile.js b/src/elements/haiku-thermostat-tile.js index 570bda8..b9ae01c 100644 --- a/src/elements/haiku-thermostat-tile.js +++ b/src/elements/haiku-thermostat-tile.js @@ -15,8 +15,6 @@ export class HaikuThermostatTile extends HaikuTileBase { } _render({ entity }) { - console.log(entity); - // console.log(entity.state); return html` {{ css }}
      { + callback(); + this._handleCustomizationComplete(); + }); + this.settingsDialog.shadowRoot.appendChild(this.settingsDialogContent); + this.settingsDialog.addEventListener('iron-overlay-canceled', this.handleDialogCancel); + this.settingsDialog.addEventListener('iron-overlay-closed', this.handleDialogCancel); + this.settingsDialog.open(); + } + _findMoreInfoDialog(entity) { const hassEl = document.getElementsByTagName('home-assistant')[0]; let dialog = hassEl.shadowRoot.querySelector('ha-more-info-dialog'); @@ -43,7 +59,10 @@ export class CustomizationService { if (event && event.path[0].nodeName !== 'HA-MORE-INFO-DIALOG') { return; } - const el = this.settingsDialog.shadowRoot.querySelector('haiku-settings-dialog'); + let el = this.settingsDialog.shadowRoot.querySelector('haiku-settings-dialog'); + if (!el) { + el = this.settingsDialog.shadowRoot.querySelector('haiku-global-config-dialog'); + } this.settingsDialog.shadowRoot.removeChild(el); this.settingsDialog.fire('more-info-page', { page: null }); this.settingsDialog.removeEventListener('iron-overlay-canceled', this.handleDialogCancel); diff --git a/src/styles/_tiles.scss b/src/styles/_tiles.scss index da7ff7d..c20c403 100644 --- a/src/styles/_tiles.scss +++ b/src/styles/_tiles.scss @@ -43,6 +43,7 @@ margin: 0 auto; text-align: center; color: #fff; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.85); &.status-normal { border-color: #bada55; @@ -65,6 +66,7 @@ } & > span { + @include overlay-text(); font-size: 11px; display: block; margin: 0 auto; @@ -80,6 +82,7 @@ } & > label { + @include overlay-text(); font-size: 32px; text-transform: uppercase; letter-spacing: -1px; diff --git a/src/styles/global/haiku.scss b/src/styles/global/haiku.scss index 93a2869..f628783 100644 --- a/src/styles/global/haiku.scss +++ b/src/styles/global/haiku.scss @@ -1 +1,27 @@ @import './themes/theme-dark.scss'; + +.haiku-config-button { + position: fixed; + width: 50px; + height: 50px; + background-image: linear-gradient(to bottom, #555 0%, #444 100%); + border-radius: 50%; + border: none; + bottom: 20px; + right: 20px; + z-index: 99999; + outline: none; + color: white; + text-shadow: 0px 0px 9px rgba(0, 0, 0, 0.9); + cursor: pointer; + box-shadow: 0px 0px 9px rgba(0, 0, 0, 0.15); + + ha-icon { + transition: all 0.2s; + transform: none; + } + + &:hover ha-icon { + transform: rotate(-100deg); + } +}