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/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. diff --git a/deploy.sh b/deploy.sh index 2b5047b..71fa8b9 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1 +1 @@ -rsync -avi ./haiku/ pi@raspberrypi:/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/ 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/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 new file mode 100644 index 0000000..5480696 --- /dev/null +++ b/src/cards/haiku-global-config.js @@ -0,0 +1,89 @@ +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; + + if (!this.initialized) { + this.setAttribute('style', 'margin:0;'); + this.initStylesheet(); + this.initTheme(); + this.initConfigButton(); + 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 { + 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; + } + + 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..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() }
@@ -42,6 +44,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) { @@ -69,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` @@ -82,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 b2acaf3..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'); - } - - &.living-room { - background-image: url('http://irasuite.com/wp-content/uploads/2017/08/modern-living-room-chairs.jpeg'); - } + 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; - &.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,21 +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; - &:first-child { - margin-left: 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(); } - &:nth-child(3n+3) { - margin-right: 0; + @media only screen and (min-width: 1600px) { + @include tiles-thirds(); } } } \ No newline at end of file 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-light-control.js b/src/elements/haiku-light-control.js index 1a8f1d9..30e5622 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(this); } 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..ab18353 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(this); } 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 2f6cafd..d65e6aa 100644 --- a/src/elements/haiku-sensor-tile.js +++ b/src/elements/haiku-sensor-tile.js @@ -17,18 +17,88 @@ export class HaikuSensorTile extends HaikuTileBase { _render({ entity }) { 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` + + ${ entity } + `; + } + + renderDefaultSensorContent(entity) { + return html` + + + ${ this.getShortValue(entity) } + + ${ this.getUnit(entity) } + + + `; + } + getTitle(entity) { if (entity.attributes && entity.attributes.haiku_label) { return entity.attributes.haiku_label; @@ -82,6 +152,33 @@ 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'; + } } customElements.define('haiku-sensor-tile', HaikuSensorTile); diff --git a/src/elements/haiku-sensor-tile.scss b/src/elements/haiku-sensor-tile.scss index efd1303..7e5f6a6 100644 --- a/src/elements/haiku-sensor-tile.scss +++ b/src/elements/haiku-sensor-tile.scss @@ -1 +1,5 @@ @import '../styles/_tiles.scss'; + +.status-container { + padding: 12px 0; +} diff --git a/src/elements/haiku-settings-dialog.js b/src/elements/haiku-settings-dialog.js new file mode 100644 index 0000000..1aeb5d5 --- /dev/null +++ b/src/elements/haiku-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 HaikuSettingsDialog extends LitElement { + + constructor() { + super(); + } + + static get properties() { + return { + hass: Object, + entity: Object + }; + } + + _render({ entity }) { + return html` + {{ css }} + + +
      Haiku Customization
      +
      + +
      + + + + Light + Temperature + Humidity + Smoke Status (Binary) + Carbon Monoxide Status (Binary) + Air Quality + Motion Detected (Binary) + + + 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-settings-dialog', HaikuSettingsDialog); diff --git a/src/elements/haiku-settings-dialog.scss b/src/elements/haiku-settings-dialog.scss new file mode 100644 index 0000000..3745a92 --- /dev/null +++ b/src/elements/haiku-settings-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 new file mode 100644 index 0000000..b9ae01c --- /dev/null +++ b/src/elements/haiku-thermostat-tile.js @@ -0,0 +1,126 @@ +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 }) { + 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 1103946..67540d4 100644 --- a/src/elements/haiku-tile-base.js +++ b/src/elements/haiku-tile-base.js @@ -1,11 +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 { CustomizationService } from '../services/customization-service.js'; +import './haiku-settings-dialog.js'; export class HaikuTileBase extends LitElement { + constructor() { + super(); + this.customizationService = new CustomizationService(this); + } + handleClick(event) { event.stopPropagation(); if (event.altKey) { - console.log('TODO: show input dialog...'); + this.customizationService.openSettingsDialog(this.hass, this.entity); } else { const eventService = new EventService(); @@ -14,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 new file mode 100644 index 0000000..39d43af --- /dev/null +++ b/src/services/customization-service.js @@ -0,0 +1,76 @@ +import { EventService } from './event-service.js'; + +export class CustomizationService { + 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; + 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(); + } + + openGlobalConfigDialog(callback) { + this.settingsDialog = this._findMoreInfoDialog({ + 'entity_id': null + }); + this.settingsDialog.fire('more-info-page', { page: 'haiku_settings' }); + this.settingsDialogContent = document.createElement('haiku-global-config-dialog'); + this.settingsDialogContent.addEventListener('haiku-customization-complete', () => { + 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'); + + // 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) { + if (event && event.path[0].nodeName !== 'HA-MORE-INFO-DIALOG') { + return; + } + 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); + this.settingsDialog.removeEventListener('iron-overlay-closed', this.handleDialogCancel); + } + + _handleCustomizationComplete() { + this._handleDialogCancel(); + this.settingsDialog.close(); + } +} 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/_tiles.scss b/src/styles/_tiles.scss index 716eceb..c20c403 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,58 @@ } } } + +.status-value { + border: solid 4px #ccc; + width: 80px; + height: 80px; + border-radius: 50%; + padding: 3px; + margin: 0 auto; + text-align: center; + color: #fff; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.85); + + &.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 { + @include overlay-text(); + 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 { + @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 new file mode 100644 index 0000000..f628783 --- /dev/null +++ b/src/styles/global/haiku.scss @@ -0,0 +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); + } +} diff --git a/src/styles/global/themes/theme-dark.scss b/src/styles/global/themes/theme-dark.scss new file mode 100644 index 0000000..050e27f --- /dev/null +++ b/src/styles/global/themes/theme-dark.scss @@ -0,0 +1,99 @@ +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: #333; + // --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: #bada55; + /* --toggle-button-unchecked-color: var(--accent-color); */ + // --slider-color: #bada55; + // --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: #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 */ + // --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; +}