From 8b46c1ee79ccb08506f4b50dfcf6a1028fa5a8b5 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Thu, 21 May 2020 00:46:40 +0300 Subject: [PATCH 1/9] hot fix HA 0.110.0 --- src/style.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/style.js b/src/style.js index 4f05880..c433ddf 100644 --- a/src/style.js +++ b/src/style.js @@ -157,14 +157,13 @@ const style = css` justify-content: space-between; } .mh-humidifier__toggle { - margin-top: -7px; + margin-top: -5px; margin-right: 4px } .toggle-button { width: calc(var(--mh-unit) * .75); height: calc(var(--mh-unit) * .75); --mdc-icon-button-size: calc(var(--mh-unit) * .75); - padding: 3px; color: var(--mh-icon-color); } .toggle-button.open { From 5fe3337e3d4450da761ca3a83128dd081f19cffe Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Wed, 3 Jun 2020 01:59:46 +0300 Subject: [PATCH 2/9] version 2.0 --- .babelrc | 3 +- package.json | 2 +- src/components/buttons.js | 89 +++ src/components/controls.js | 165 ----- src/components/dropdown.js | 12 +- src/components/fanSpeedMenu.js | 53 -- src/components/indicators.js | 93 +++ src/components/info.js | 117 ---- src/components/ledButtonMenu.js | 52 -- src/components/power.js | 49 ++ src/components/powerstrip.js | 78 --- ...getHumiditySlider.js => targetHumidity.js} | 54 +- src/components/togglePanel.js | 59 -- src/main.js | 596 +++++++++++------- src/model.js | 243 ------- src/models/button.js | 134 ++++ src/models/humidifier.js | 62 ++ src/models/indicator.js | 54 ++ src/models/targetHumidity.js | 59 ++ src/style.js | 75 ++- src/utils/trimTo.js | 13 - src/utils/utils.js | 43 ++ 22 files changed, 1029 insertions(+), 1076 deletions(-) create mode 100644 src/components/buttons.js delete mode 100644 src/components/controls.js delete mode 100644 src/components/fanSpeedMenu.js create mode 100644 src/components/indicators.js delete mode 100644 src/components/info.js delete mode 100644 src/components/ledButtonMenu.js create mode 100644 src/components/power.js delete mode 100644 src/components/powerstrip.js rename src/components/{targetHumiditySlider.js => targetHumidity.js} (56%) delete mode 100644 src/components/togglePanel.js delete mode 100644 src/model.js create mode 100644 src/models/button.js create mode 100644 src/models/humidifier.js create mode 100644 src/models/indicator.js create mode 100644 src/models/targetHumidity.js delete mode 100644 src/utils/trimTo.js create mode 100644 src/utils/utils.js diff --git a/.babelrc b/.babelrc index 404710f..00a39d3 100644 --- a/.babelrc +++ b/.babelrc @@ -8,8 +8,7 @@ "esmodules": true } } - ], - ["minify"] + ] ], "comments": false, "plugins": [ diff --git a/package.json b/package.json index 23d6ca5..854a301 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mini-humidifier", - "version": "v1.0.8", + "version": "v2.0.1", "description": "humidifier card for Home Assistant Lovelace UI", "keywords": [ "home-assistant", diff --git a/src/components/buttons.js b/src/components/buttons.js new file mode 100644 index 0000000..974d782 --- /dev/null +++ b/src/components/buttons.js @@ -0,0 +1,89 @@ +import { LitElement, html, css } from 'lit-element'; +import { styleMap } from 'lit-html/directives/style-map'; +import sharedStyle from '../sharedStyle'; +import './dropdown'; + +class HumidifierButtons extends LitElement { + static get properties() { + return { + buttons: {}, + }; + } + + renderButton(button) { + if (button.isUnavailable) + return ''; + + return html` + button.handleToggle(e)} + ?disabled="${button.disabled}" + ?color=${button.isOn}> + + `; + } + + renderDropdown(dropdown) { + let selected = ''; + if (dropdown.state !== null && dropdown.state !== undefined) + selected = dropdown.state.toString(); + + return html` + dropdown.handleChange(e)} + .items=${dropdown.source} + .icon=${dropdown.icon} + .disabled="${dropdown.disabled}" + .active=${dropdown.isActive} + .selected=${selected}> + + `; + } + + renderInternal(button) { + if (button.type === 'dropdown') + return this.renderDropdown(button); + + return this.renderButton(button); + } + + render() { + const context = this; + return html`${Object.entries(this.buttons) + .map(b => b[1]) + .filter(b => !b.hide) + .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))) + .map(button => context.renderInternal(button))}`; + } + + static get styles() { + return [ + sharedStyle, + css` + :host { + position: relative; + box-sizing: border-box; + margin: 0; + overflow: hidden; + transition: background .5s; + --paper-item-min-height: var(--mh-unit); + --mh-dropdown-unit: var(--mh-unit); + } + :host([color]) { + background: var(--mh-active-color); + transition: background .25s; + opacity: 1; + } + :host([disabled]) { + opacity: .25; + pointer-events: none; + } + `]; + } +} + +customElements.define('mh-buttons', HumidifierButtons); diff --git a/src/components/controls.js b/src/components/controls.js deleted file mode 100644 index 4002458..0000000 --- a/src/components/controls.js +++ /dev/null @@ -1,165 +0,0 @@ -import { LitElement, html, css } from 'lit-element'; - -import './fanSpeedMenu'; -import './ledButtonMenu'; - -import sharedStyle from '../sharedStyle'; - -class MiniHumidifierControls extends LitElement { - static get properties() { - return { - humidifier: {}, - hass: {}, - config: {}, - }; - } - - toggleDry(e) { - return this.humidifier.toggleDry(e); - } - - toggleBuzzer(e) { - return this.humidifier.toggleBuzzer(e); - } - - toggleLedBrightness(e) { - return this.humidifier.toggleLedBrightness(e); - } - - toggleChildLock(e) { - return this.humidifier.toggleChildLock(e); - } - - renderDryButton(context) { - return html` - context.toggleDry(e)} - ?color=${context.humidifier.isDryOn}> - - `; - } - - renderSpeedMenu(context) { - return html` - - - `; - } - - renderLedButton(context) { - if (context.config.led_button.type === 'dropdown') - return html` - - - `; - - return html` - context.toggleLedBrightness(e)} - ?color=${context.humidifier.isLedBrightnessOn}> - - `; - } - - renderBuzzerButton(context) { - return html` - context.toggleBuzzer(e)} - ?color=${context.humidifier.isBuzzerOn}> - - `; - } - - renderChildLockButton(context) { - return html` - context.toggleChildLock(e)} - ?color=${context.humidifier.isChildLockOn}> - - `; - } - - render() { - const context = this; - const dryButtonConf = this.config.dry_button; - const fanModeButtonConf = this.config.fan_mode_button; - const ledButtonConf = this.config.led_button; - const buzzerButtonConf = this.config.buzzer_button; - const childlockButtonConf = this.config.child_lock_button; - - const source = [ - { - hide: dryButtonConf.hide, - order: dryButtonConf.order, - render: this.renderDryButton, - }, - { - hide: fanModeButtonConf.hide, - order: fanModeButtonConf.order, - render: this.renderSpeedMenu, - }, - { - hide: ledButtonConf.hide, - order: ledButtonConf.order, - render: this.renderLedButton, - }, - { - hide: buzzerButtonConf.hide, - order: buzzerButtonConf.order, - render: this.renderBuzzerButton, - }, - { - hide: childlockButtonConf.hide, - order: childlockButtonConf.order, - render: this.renderChildLockButton, - }] - .filter(b => !b.hide) - .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))); - - return html` -
- ${source.map(item => item.render(context))} -
- `; - } - - static get styles() { - return [ - sharedStyle, - css` - :host { - position: relative; - box-sizing: border-box; - margin: 0; - overflow: hidden; - transition: background .5s; - } - :host([color]) { - background: var(--mh-active-color); - transition: background .25s; - opacity: 1; - } - :host([disabled]) { - opacity: .25; - pointer-events: none; - } - .mh-humidifier-info__controls { - display: flex; - width: 100%; - justify-content: space-evenly; - } - `]; - } -} - -customElements.define('mp-humidifier-controls', MiniHumidifierControls); diff --git a/src/components/dropdown.js b/src/components/dropdown.js index 1293d17..1ce8f58 100644 --- a/src/components/dropdown.js +++ b/src/components/dropdown.js @@ -2,10 +2,9 @@ import { LitElement, html, css } from 'lit-element'; import sharedStyle from '../sharedStyle'; -class MiniHumidifierDropdown extends LitElement { +class MiniClimateDropdown extends LitElement { static get properties() { return { - humidifier: {}, items: [], label: String, selected: String, @@ -62,6 +61,7 @@ class MiniHumidifierDropdown extends LitElement { :host { position: relative; overflow: hidden; + --paper-item-min-height: 40px; } paper-menu-button :host([disabled]) { @@ -80,9 +80,13 @@ class MiniHumidifierDropdown extends LitElement { pointer-events: none; } .mh-dropdown__button.icon { - height: var(--mh-unit); margin: 0; } + ha-icon-button { + width: calc(var(--mh-dropdown-unit)); + height: calc(var(--mh-dropdown-unit)); + --mdc-icon-button-size: calc(var(--mh-dropdown-unit)); + } paper-item > *:nth-child(2) { margin-left: 4px; } @@ -98,4 +102,4 @@ class MiniHumidifierDropdown extends LitElement { } } -customElements.define('mh-dropdown', MiniHumidifierDropdown); +customElements.define('mh-dropdown', MiniClimateDropdown); diff --git a/src/components/fanSpeedMenu.js b/src/components/fanSpeedMenu.js deleted file mode 100644 index db0dd6e..0000000 --- a/src/components/fanSpeedMenu.js +++ /dev/null @@ -1,53 +0,0 @@ -import { LitElement, html, css } from 'lit-element'; - -import './dropdown'; - -class MiniHumidifierFanSpeedMenu extends LitElement { - static get properties() { - return { - humidifier: {}, - config: {}, - icon: String, - }; - } - - get source() { - return this.humidifier.fanSpeed || {}; - } - - get sources() { - return this.humidifier.fanSpeedSource - .filter(s => !s.hide) - .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))) - .map(s => ({ name: s.name, id: s.id, type: 'source' })); - } - - render() { - return html` - - - `; - } - - handleSource(ev) { - const { id } = ev.detail; - this.humidifier.setFanSpeed(ev, id); - } - - static get styles() { - return css` - :host { - min-width: var(--mh-unit); - } - `; - } -} - -customElements.define('mh-fan-speed-menu', MiniHumidifierFanSpeedMenu); diff --git a/src/components/indicators.js b/src/components/indicators.js new file mode 100644 index 0000000..6ba95db --- /dev/null +++ b/src/components/indicators.js @@ -0,0 +1,93 @@ +import { LitElement, html, css } from 'lit-element'; + +import { styleMap } from 'lit-html/directives/style-map'; + +class HumidifierIndicators extends LitElement { + static get properties() { + return { + indicators: {}, + }; + } + + renderIcon(indicator) { + const { icon } = indicator; + + if (!icon) + return ''; + + return html``; + } + + renderUnit(unit) { + if (!unit) + return ''; + + return html`${unit}`; + } + + renderIndicator(indicator) { + return html` +
+ ${this.renderIcon(indicator)} + ${indicator.value} + ${this.renderUnit(indicator.unit)} +
+ `; + } + + render() { + const context = this; + + return html` +
+ ${Object.entries(this.indicators) + .map(i => i[1]) + .filter(i => !i.hide) + .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))) + .map(i => context.renderIndicator(i))} +
+ `; + } + + static get styles() { + return css` + :host { + position: relative; + box-sizing: border-box; + font-size: calc(var(--mh-unit) * .35); + line-height: calc(var(--mh-unit) * .35); + } + .mh-indicators__container { + display: flex; + flex-wrap: wrap; + margin-right: calc(var(--mh-unit) * .075); + } + .state { + position: relative; + display: flex; + flex-wrap: nowrap; + margin-right: calc(var(--mh-unit) * .1); + } + .state__value_icon { + height: calc(var(--mh-unit) * .475); + width: calc(var(--mh-unit) * .5); + color: var(--mh-icon-color); + --mdc-icon-size: calc(var(--mh-unit) * 0.5); + } + .state__value { + margin: 0 1px; + font-weight: var(--mh-info-font-weight); + line-height: calc(var(--mh-unit) * .475); + } + .state__uom { + font-size: calc(var(--mh-unit) * .275); + line-height: calc(var(--mh-unit) * .525); + margin-left: 1px; + height: calc(var(--mh-unit) * .475); + opacity: 0.8; + } + `; + } +} + +customElements.define('mh-indicators', HumidifierIndicators); diff --git a/src/components/info.js b/src/components/info.js deleted file mode 100644 index 1665adf..0000000 --- a/src/components/info.js +++ /dev/null @@ -1,117 +0,0 @@ -import { LitElement, html, css } from 'lit-element'; -import { unsafeHTML } from 'lit-html/directives/unsafe-html'; - -class MiniHumidifierInfo extends LitElement { - static get properties() { - return { - humidifier: {}, - hass: {}, - config: {}, - }; - } - - renderDepth(context) { - const icon = context.config.depth.icon_template - ? unsafeHTML(context.humidifier.depthIcon) - : html``; - - return html` -
- ${icon} - ${context.humidifier.depth} - ${context.config.depth.unit} -
- `; - } - - renderTemperature(context) { - const icon = context.config.temperature.icon_template - ? unsafeHTML(context.humidifier.temperatureIcon) - : html``; - - return html` -
- ${icon} - ${context.humidifier.temperature} - ${context.config.temperature.unit} -
- `; - } - - renderHumidity(context) { - const icon = context.config.humidity.icon_template - ? unsafeHTML(context.humidifier.humidityIcon) - : html``; - return html` -
- ${icon} - ${context.humidifier.humidity} - ${context.config.humidity.unit} -
- `; - } - - render() { - const context = this; - const temperatureConf = this.config.temperature; - const humidityConf = this.config.humidity; - const depthConf = this.config.depth; - - const source = [ - { hide: humidityConf.hide, order: humidityConf.order, render: this.renderHumidity }, - { hide: depthConf.hide, order: depthConf.order, render: this.renderDepth }, - { hide: temperatureConf.hide, order: temperatureConf.order, render: this.renderTemperature }] - .filter(i => !i.hide) - .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))); - - return html` -
- ${source.map(item => item.render(context))} -
- `; - } - - static get styles() { - return css` - :host { - position: relative; - box-sizing: border-box; - min-width: 0; - overflow: hidden; - font-size: calc(var(--mh-unit) * .35); - line-height: calc(var(--mh-unit) * .35); - } - .mh-humidifier-state__container { - display: flex; - } - .state { - position: relative; - display: flex; - flex-wrap: nowrap; - margin-right: calc(var(--mh-unit) * .1); - } - .state__value_icon { - height: calc(var(--mh-unit) * .475); - width: calc(var(--mh-unit) * .5); - color: var(--mh-icon-color); - --mdc-icon-size: calc(var(--mh-unit) * 0.5); - } - .state__value { - margin: 0 1px; - font-weight: var(--mh-info-font-weight); - line-height: calc(var(--mh-unit) * .475); - } - .state__uom { - font-size: calc(var(--mh-unit) * .275); - line-height: calc(var(--mh-unit) * .55); - height: calc(var(--mh-unit) * .475); - opacity: 0.8; - } - .humidity .state__value { - margin: 0; - } - `; - } -} - -customElements.define('mp-humidifier-state', MiniHumidifierInfo); diff --git a/src/components/ledButtonMenu.js b/src/components/ledButtonMenu.js deleted file mode 100644 index 031d23f..0000000 --- a/src/components/ledButtonMenu.js +++ /dev/null @@ -1,52 +0,0 @@ -import { LitElement, html, css } from 'lit-element'; - -import './dropdown'; - -class MiniHumidifierLedButtonMenu extends LitElement { - static get properties() { - return { - humidifier: {}, - config: {}, - icon: String, - }; - } - - get source() { - return this.humidifier.ledButtonValue || {}; - } - - get sources() { - return this.humidifier.ledButtonSource - .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))) - .map(s => ({ name: s.name, id: s.id, type: 'source' })); - } - - render() { - return html` - - - `; - } - - handleSource(ev) { - const { id } = ev.detail; - - this.humidifier.setLedButtonBrightness(ev, id); - } - - static get styles() { - return css` - :host { - min-width: var(--mh-unit); - } - `; - } -} - -customElements.define('mh-led-button-menu', MiniHumidifierLedButtonMenu); diff --git a/src/components/power.js b/src/components/power.js new file mode 100644 index 0000000..25bd679 --- /dev/null +++ b/src/components/power.js @@ -0,0 +1,49 @@ +import { css, html, LitElement } from 'lit-element'; +import sharedStyle from '../sharedStyle'; + +class PowerButton extends LitElement { + static get properties() { + return { + humidifier: { type: Object }, + config: { type: Object }, + hass: { type: Object }, + }; + } + + render() { + if (this.config.power.hide) + return ''; + + if (this.config.power.type === 'toggle') { + return html` + + + `; + } + + return html` + this.humidifier.togglePower(e)} + ?color=${this.humidifier.isOn}> + + `; + } + + static get styles() { + return [ + sharedStyle, + css` + :host { + position: relative; + box-sizing: border-box; + min-width: 0; + font-weight: var(--mh-info-font-weight); + } + `]; + } +} + +customElements.define('mh-power', PowerButton); diff --git a/src/components/powerstrip.js b/src/components/powerstrip.js deleted file mode 100644 index c5a1041..0000000 --- a/src/components/powerstrip.js +++ /dev/null @@ -1,78 +0,0 @@ -import { LitElement, html, css } from 'lit-element'; - -import sharedStyle from '../sharedStyle'; - -import getLabel from '../utils/getLabel'; - -import './targetHumiditySlider'; - -class MiniHumidifierPowerstrip extends LitElement { - static get properties() { - return { - hass: {}, - humidifier: {}, - config: {}, - }; - } - - renderPowerButton() { - if (this.config.power_button.hide) - return ''; - - if (this.config.power_button.type === 'toggle') { - return html` - - - `; - } - - return html` - this.humidifier.togglePower(e)} - ?color=${this.humidifier.isOn}> - - `; - } - - render() { - if (this.humidifier.isUnavailable) - return html` - - ${getLabel(this.hass, 'state.default.unavailable', 'Unavailable')} - `; - - return html` - - - ${this.renderPowerButton()} - `; - } - - static get styles() { - return [ - sharedStyle, - css` - :host { - display: flex; - margin: 0; - max-height: var(--mh-unit); - } - mp-target-humidity-slider { - flex: 1; - } - .label { - display: flex; - align-items: center; - } - `, - ]; - } -} - -customElements.define('mh-powerstrip', MiniHumidifierPowerstrip); diff --git a/src/components/targetHumiditySlider.js b/src/components/targetHumidity.js similarity index 56% rename from src/components/targetHumiditySlider.js rename to src/components/targetHumidity.js index cf7f00e..0d66931 100644 --- a/src/components/targetHumiditySlider.js +++ b/src/components/targetHumidity.js @@ -1,53 +1,53 @@ -import { LitElement, html, css } from 'lit-element'; -import { unsafeHTML } from 'lit-html/directives/unsafe-html'; +import { css, html, LitElement } from 'lit-element'; -class MiniHumidifierTargetHumiditySlider extends LitElement { +class TargetHumidity extends LitElement { static get properties() { return { - humidifier: {}, - hass: {}, - config: {}, + targetHumidity: { type: Object }, + sliderValue: { type: Number }, }; } - handleTargetHumidityChange(ev) { - const val = parseFloat(ev.target.value); - this.sliderValue = val; - this.humidifier.setTargetHumidity(ev, val); + constructor() { + super(); + this.targetHumidity = {}; } - renderTargetHumidifierState(sliderValue) { - if (this.config.target_humidity.hide) - return html`
`; + handleChange(e) { + e.stopPropagation(); + this.sliderValue = e.target.value; + this.targetHumidity.handleChange(this.sliderValue); + return this.requestUpdate('sliderValue'); + } - const icon = this.config.humidity.icon_template - ? unsafeHTML(this.humidifier.targetHumidityIcon) - : html``; + renderState() { + if (this.targetHumidity.hide) + return html`
`; return html`
- ${icon} - ${sliderValue} - ${this.config.target_humidity.unit} + + ${this.sliderValue} + ${this.targetHumidity.unit}
`; } render() { - const sliderValue = this.sliderValue || this.humidifier.targetHumidity.value; + this.sliderValue = this.sliderValue || this.targetHumidity.value; return html`
this.handleChange(e)} @click=${e => e.stopPropagation()} - min=${this.humidifier.targetHumidity.min} - max=${this.humidifier.targetHumidity.max} - step=${this.humidifier.targetHumidity.step} - value=${this.humidifier.targetHumidity.value} + min=${this.targetHumidity.min} + max=${this.targetHumidity.max} + step=${this.targetHumidity.step} + value=${this.targetHumidity.value} dir=${'ltr'} ignore-bar-touch pin> - ${this.renderTargetHumidifierState(sliderValue)} + ${this.renderState(this.sliderValue)}
`; } @@ -99,4 +99,4 @@ class MiniHumidifierTargetHumiditySlider extends LitElement { } } -customElements.define('mp-target-humidity-slider', MiniHumidifierTargetHumiditySlider); +customElements.define('mh-target-humidity', TargetHumidity); diff --git a/src/components/togglePanel.js b/src/components/togglePanel.js deleted file mode 100644 index 0d4e3fc..0000000 --- a/src/components/togglePanel.js +++ /dev/null @@ -1,59 +0,0 @@ -import { LitElement, html, css } from 'lit-element'; - -import sharedStyle from '../sharedStyle'; - -import './controls'; - -class MiniHumidifierTogglePanel extends LitElement { - static get properties() { - return { - humidifier: {}, - hass: {}, - config: {}, - visible: Boolean, - }; - } - - render() { - return html` -
- ${this.renderPanelContent()} -
- `; - } - - renderPanelContent() { - if (!this.visible) - return ''; - - return html` -
- - -
- `; - } - - static get styles() { - return [ - sharedStyle, - css` - :host { - position: relative; - box-sizing: border-box; - margin: 0; - overflow: hidden; - } - mp-humidifier-controls { - display: flex; - flex: 1; - justify-content: flex-end; - } - `]; - } -} - -customElements.define('mp-toggle-panel', MiniHumidifierTogglePanel); diff --git a/src/main.js b/src/main.js index f7cd2c1..7497cb3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,19 +1,23 @@ import { html, LitElement } from 'lit-element'; import { classMap } from 'lit-html/directives/class-map'; import { styleMap } from 'lit-html/directives/style-map'; -import HumidifierObject from './model'; + import style from './style'; import sharedStyle from './sharedStyle'; import handleClick from './utils/handleClick'; -import trimTo from './utils/trimTo'; +import { compileTemplate, toggleState } from './utils/utils'; +import ICON from './const'; import './components/dropdown'; -import './components/powerstrip'; -import './components/controls'; -import './components/info'; -import './components/togglePanel'; +import './components/indicators'; +import './components/buttons'; +import './components/targetHumidity'; +import './components/power'; -import ICON from './const'; +import IndicatorObject from './models/indicator'; +import ButtonObject from './models/button'; +import TargetHumidityObject from './models/targetHumidity'; +import HumidifierObject from './models/humidifier'; if (!customElements.get('ha-slider')) { customElements.define( @@ -34,6 +38,9 @@ class MiniHumidifier extends LitElement { super(); this.initial = true; this.toggle = false; + this.indicators = {}; + this.buttons = {}; + this.targetHumidity = {}; } static get properties() { @@ -63,6 +70,10 @@ class MiniHumidifier extends LitElement { this.entity = entity; this.humidifier = new HumidifierObject(hass, this.config, entity); } + + this.updateIndicators(hass); + this.updateButtons(hass); + this.updateTargetHumidity(hass); } get hass() { @@ -73,147 +84,326 @@ class MiniHumidifier extends LitElement { return this.config.name || this.humidifier.name; } - getFanModeButtonConfig(config) { - const fanModeConf = { - icon: ICON.FAN, - hide: false, - order: 1, - ...config.fan_mode_button || {}, - }; + updateIndicators(hass) { + const indicators = { }; + let changed = false; + + for (let i = 0; i < this.config.indicators.length; i += 1) { + const config = this.config.indicators[i]; + const { id } = config; + + const entityId = config.source.entity || this.humidifier.id; + const entity = hass.states[entityId]; + + if (entity) { + indicators[id] = new IndicatorObject(entity, config, this.humidifier); + } + + if (entity !== (this.indicators[id] && this.indicators[id].entity)) + changed = true; + } + + if (changed) + this.indicators = indicators; + } + + updateButtons(hass) { + const buttons = { }; + let changed = false; + + for (let i = 0; i < this.config.buttons.length; i += 1) { + const config = this.config.buttons[i]; + const { id } = config; + + const entityId = (config.state && config.state.entity) || this.humidifier.id; + const entity = hass.states[entityId]; + + if (entity) { + buttons[id] = new ButtonObject(entity, config, this.humidifier); + } + + if (entity !== (this.buttons[id] && this.buttons[id].entity)) + changed = true; + } + + if (changed) + this.buttons = buttons; + } + + updateTargetHumidity(hass) { + const entityId = (this.config.target_humidity.source + && this.config.target_humidity.source.entity) || this.config.entity; + + const entity = hass.states[entityId]; + const targetHumidity = new TargetHumidityObject(hass, entity, this.config, this.humidifier); - fanModeConf.source = { - auto: 'Auto', - silent: 'Silent', - medium: 'Medium', - high: 'High', - ...(config.fan_mode_button || {}).source, + if (this.targetHumidity.value !== targetHumidity.value) { + this.targetHumidity = targetHumidity; + } + } + + getIndicatorConfig(key, value, config) { + const item = { + id: key, + source: { enitity: undefined, attribute: undefined, mapper: undefined }, + icon: '', + ...value, }; - const source = [ - { - id: 'auto', - value: 'Auto', - name: 'Auto', - hide: false, + item.functions = item.functions || {}; + const context = { ...value }; + context.entity_config = config; + context.toggle_state = toggleState; + + if (item.source.mapper) { + item.functions.mapper = compileTemplate(item.source.mapper, context); + } + + if (typeof item.icon === 'object') { + item.functions.icon = {}; + + if (item.icon.template) + item.functions.icon.template = compileTemplate(item.icon.template, context); + + if (item.icon.style) + item.functions.icon.style = compileTemplate(item.icon.style, context); + } + + return item; + } + + getIndicatorsConfig(config) { + const defaultIndicators = { + depth: { + icon: ICON.DEPTH, + unit: '%', + round: 0, order: 0, - }, - { - id: 'silent', - value: 'Silent', - name: 'Silent', + max_value: 125, + volume: 4, + unit_type: 'liters', hide: false, - order: 1, + source: { + attribute: 'depth', + mapper: (val) => { + const value = (100 * (val || 0)) / this.max_value; + return this.unit_type === 'liters' ? (value * this.volume) / 100 : value; + }, + }, }, - { - id: 'medium', - value: 'Medium', - name: 'Medium', + temperature: { + icon: ICON.TEMPERATURE, + unit: '°C', + round: 1, + order: 1, hide: false, - order: 2, + source: { attribute: 'temperature' }, }, - { - id: 'high', - value: 'High', - name: 'High', + humidity: { + icon: ICON.HUMIDITY, + unit: '%', + round: 1, + order: 2, hide: false, - order: 3, + source: { attribute: 'temperature' }, }, - { - id: 'strong', - value: 'Strong', - name: 'Strong', - hide: true, - order: 4, - }]; + }; - const data = Object.entries(fanModeConf.source); + const data = Object.entries(config.indicators || {}); for (let i = 0; i < data.length; i += 1) { const key = data[i][0]; - const value = data[i][1]; + const value = data[i][1] || {}; - const item = source.find(s => s.id.toUpperCase() === key.toUpperCase()); - - if (item) { - if (typeof (value) === 'object') { - if ('value' in value) - item.value = value.value; - if ('name' in value) - item.name = value.name; - if ('hide' in value) - item.hide = value.hide; - if ('order' in value) - item.order = value.order; - } else { - item.name = value; - } - } + defaultIndicators[key] = { ...defaultIndicators[key] || {}, ...value }; } - fanModeConf.source = source; - return fanModeConf; + return Object.entries(defaultIndicators).map(i => this.getIndicatorConfig(i[0], i[1], config)); } - getLedButtonConfig(config) { - const ledButtonSource = (config.led_button || {}).source || {}; - - const ledButtonConfig = { - icon: ICON.LEDBUTTON, - hide: false, + getButtonConfig(key, value, config) { + const item = { + id: key, + icon: 'mdi:radiobox-marked', type: 'button', - order: 2, - ...config.led_button || {}, + toggle_action: undefined, + ...value, }; - const source = { - bright: { - value: 0, + item.functions = {}; + + const context = { ...value }; + context.call_service = (domain, service, options) => this.hass.callService( + domain, service, options, + ); + context.entity_config = config; + context.toggle_state = toggleState; + + if (item.disabled) { + item.functions.disabled = compileTemplate(item.disabled, context); + } + + if (item.state && item.state.mapper) { + item.functions.state = { mapper: compileTemplate(item.state.mapper, context) }; + } + + if (item.active) { + item.functions.active = compileTemplate(item.active, context); + } + + if (item.source && item.source.__filter) { + item.functions.source = { filter: compileTemplate(item.source.__filter, context) }; + } + + if (item.toggle_action) { + item.functions.toggle_action = compileTemplate(item.toggle_action, context); + } + + if (item.change_action) { + item.functions.change_action = compileTemplate(item.change_action, context); + } + + if (item.style) + item.functions.style = compileTemplate(item.style, context); + + return item; + } + + getButtonsConfig(config) { + const defaultButtonsConfig = { + dry: { + icon: ICON.DRY, + hide: false, order: 0, - name: 'Bright', + state: { attribute: 'dry', mapper: state => (state ? 'on' : 'off') }, + toggle_action: (state, entity) => { + const service = state === 'on' ? 'fan_set_dry_off' : 'fan_set_dry_on'; + const options = { entity_id: entity.entity_id }; + return this.call_service('xiaomi_miio', service, options); + }, }, - dim: { - value: 1, + mode: { + icon: ICON.FAN, + type: 'dropdown', + hide: false, order: 1, - name: 'Dim', + source: { + auto: 'auto', + silent: 'silent', + medium: 'medium', + high: 'high', + }, + active: (state, entity) => (entity.state !== 'off'), + disabled: (state, entity) => (entity.attributes.depth === 0), + state: { attribute: 'mode' }, + change_action: (selected, entity) => { + const options = { entity_id: entity.entity_id, speed: selected }; + return this.call_service('fan', 'set_speed', options); + }, }, - off: { - value: 2, + led: { + icon: ICON.LEDBUTTON, + type: 'dropdown', + hide: false, order: 2, - name: 'Off', + active: state => (state !== 2), + source: { 0: 'Bright', 1: 'Dim', 2: 'Off' }, + state: { attribute: 'led_brightness' }, + change_action: (selected, entity) => { + const options = { entity_id: entity.entity_id, brightness: selected }; + return this.call_service('xiaomi_miio', 'fan_set_led_brightness', options); + }, + }, + buzzer: { + icon: ICON.BUZZER, + hide: false, + order: 3, + state: { attribute: 'buzzer', mapper: state => (state ? 'on' : 'off') }, + toggle_action: (state, entity) => { + const service = state === 'on' ? 'fan_set_buzzer_off' : 'fan_set_buzzer_on'; + const options = { entity_id: entity.entity_id }; + return this.call_service('xiaomi_miio', service, options); + }, + }, + child_lock: { + icon: ICON.CHILDLOCK, + hide: false, + order: 4, + state: { attribute: 'child_lock', mapper: state => (state ? 'on' : 'off') }, + toggle_action: (state, entity) => { + const service = state === 'on' ? 'fan_set_child_lock_off' : 'fan_set_child_lock_on'; + const options = { entity_id: entity.entity_id }; + return this.call_service('xiaomi_miio', service, options); + }, }, }; - if (ledButtonSource.bright) { - if (typeof (ledButtonSource.bright) === 'object') { - source.bright = { ...source.bright, ...ledButtonSource.bright }; - } else { - source.bright.name = ledButtonSource.bright; - } + const entries = Object.entries(config.buttons || {}); + + for (let i = 0; i < entries.length; i += 1) { + const key = entries[i][0]; + const value = entries[i][1] || {}; + + defaultButtonsConfig[key] = { ...defaultButtonsConfig[key] || {}, ...value }; } - if (ledButtonSource.dim) { - if (typeof (ledButtonSource.dim) === 'object') { - source.dim = { ...source.dim, ...ledButtonSource.dim }; - } else { - source.dim.name = ledButtonSource.dim; - } + const data = Object.entries(defaultButtonsConfig); + + const buttons = []; + + for (let i = 0; i < data.length; i += 1) { + const key = data[i][0]; + const value = data[i][1]; + const button = this.getButtonConfig(key, value, config); + + if (!('order' in button)) + button.order = i + 1; + + buttons.push(button); } - if (ledButtonSource.off) { - if (typeof (ledButtonSource.off) === 'object') { - source.off = { ...source.off, ...ledButtonSource.off }; - } else { - source.off.name = ledButtonSource.off; - } + return buttons; + } + + getTargetTemperatureConfig(config) { + const item = { + icon: ICON.HUMIDITY, + unit: '%', + min: 30, + max: 80, + step: 10, + hide: false, + source: { entity: undefined, attribute: 'target_humidity' }, + change_action: (selected, entity) => { + const options = { entity_id: entity.entity_id, humidity: selected }; + return this.call_service('xiaomi_miio', 'fan_set_target_humidity', options); + }, + ...config.target_humidity || {}, + }; + + item.functions = {}; + const context = { ...config.target_humidity || {} }; + context.call_service = (domain, service, options) => this.hass.callService( + domain, service, options, + ); + context.entity_config = config; + context.toggle_state = toggleState; + + if (typeof item.icon === 'object') { + item.functions.icon = {}; + + if (item.icon.template) + item.functions.icon.template = compileTemplate(item.icon.template, context); + + if (item.icon.style) + item.functions.icon.style = compileTemplate(item.icon.style, context); } - ledButtonConfig.source = Object.keys(source).map(key => ({ - id: key, - name: source[key].name, - order: source[key].order, - value: source[key].value, - })); - return ledButtonConfig; + if (item.change_action) { + item.functions.change_action = compileTemplate(item.change_action, context); + } + + return item; } setConfig(config) { @@ -221,8 +411,6 @@ class MiniHumidifier extends LitElement { throw new Error('Specify an entity from within the fan domain.'); this.config = { - toggle_power: true, - fan_modes: [], tap_action: { action: 'more-info', navigation_path: '', @@ -233,108 +421,62 @@ class MiniHumidifier extends LitElement { }, ...config, }; - - this.config.depth = { - icon: ICON.DEPTH, - icon_template: '', - max_value: 125, - unit_type: 'percent', - fixed: 0, - order: 0, - unit: '%', - volume: 4, - hide: false, - ...config.depth || {}, - }; - this.config.fan_mode_button = this.getFanModeButtonConfig(config); - this.config.child_lock_button = { - icon: ICON.CHILDLOCK, - hide: false, - order: 4, - ...config.child_lock_button || {}, - }; - this.config.buzzer_button = { - icon: ICON.BUZZER, - hide: false, - order: 3, - ...config.buzzer_button || {}, - }; - this.config.led_button = this.getLedButtonConfig(config); - this.config.temperature = { - icon: ICON.TEMPERATURE, - unit: '°C', - source: { enitity: undefined, attribute: undefined }, - order: 1, - fixed: 1, - hide: false, - ...config.temperature || {}, - }; - this.config.humidity = { - icon: ICON.HUMIDITY, - unit: '%', - source: { enitity: undefined, attribute: undefined }, - order: 2, - fixed: 1, - hide: false, - ...config.humidity || {}, - }; - this.config.target_humidity = { - icon: ICON.HUMIDITY, - hide: false, - unit: '%', - min: 30, - max: 80, - step: 10, - ...config.target_humidity || {}, - }; - this.config.dry_button = { - icon: ICON.DRY, - hide: false, - order: 0, - ...config.dry_button || {}, - }; - this.config.toggle_button = { - icon: ICON.TOGGLE, - hide: false, - default: false, - ...config.toggle_button || {}, - }; - this.config.power_button = { + this.config.power = { icon: ICON.POWER, - type: 'toggle', + type: 'button', hide: false, - ...config.power_button || {}, + ...config.power || {}, }; - - this.toggle = this.config.toggle_button.default; + this.config.target_humidity = this.getTargetTemperatureConfig(config); + this.config.indicators = this.getIndicatorsConfig(config); + this.config.buttons = this.getButtonsConfig(config); } - render({ config } = this) { + render() { return html` this.handlePopup(e)}> + style=${this.computeStyles()}>
${this.renderIcon()}
- ${this.renderEntityName()} +
+
this.handlePopup(e)}> + ${this.renderEntityName()} +
+
+ ${this.renderTargetHumidifier()} + + +
+
+ ${this.renderBottomPanel()}
- -
- ${this.renderBottomPanel(config)} + ${this.renderTogglePanel()}
`; } + renderTargetHumidifier() { + if (this.humidifier.isUnavailable) + return ''; + + return html` + + + `; + } + handlePopup(e) { e.stopPropagation(); handleClick(this, this._hass, this.config, this.config.tap_action, this.humidifier.id); @@ -345,10 +487,6 @@ class MiniHumidifier extends LitElement { this.toggle = !this.toggle; } - toggleButtonCls() { - return this.toggle ? 'open' : ''; - } - renderIcon() { const state = this.humidifier.isActive; return html` @@ -357,39 +495,41 @@ class MiniHumidifier extends LitElement { `; } - renderToggle() { - if (this.config.toggle_button.hide) + renderTogglePanel() { + if (!this.toggle) return ''; return html` -
- this.handleToggle(e)}> - +
+ +
`; } - renderBottomPanel(config) { + renderBottomPanel() { if (this.humidifier.isUnavailable) return ''; return html` -
- - - ${this.renderToggle()} +
+ + ${this.renderToggleButton()}
- - + `; + } + + renderToggleButton() { + if (this.config.buttons.filter(b => !b.hide).length === 0) + return ''; + + const cls = this.toggle ? 'open' : ''; + return html` + this.handleToggle(e)}> + `; } @@ -406,20 +546,18 @@ class MiniHumidifier extends LitElement { if (this.humidifier.isUnavailable) return ''; + const { mode } = this.buttons; + const { selected } = mode; + const label = selected ? selected.name : mode.state; + return html` -
- - ${this.secondaryInfoLabel} +
+ + ${label}
`; } - get secondaryInfoLabel() { - const item = this.humidifier.fanSpeed; - - return trimTo(item ? item.name : '', 9); - } - computeIcon() { return this.config.icon ? this.config.icon : this.humidifier.icon || ICON.DEFAULT; } diff --git a/src/model.js b/src/model.js deleted file mode 100644 index 0f97080..0000000 --- a/src/model.js +++ /dev/null @@ -1,243 +0,0 @@ -import jinja from 'jinja-js'; - -export default class HumidifierObject { - constructor(hass, config, entity) { - this.hass = hass || {}; - this.config = config || {}; - this.entity = entity || {}; - this.state = entity.state; - this.attr = { - friendly_name: '', - depth: 0, - target_humidity: 0, - mode: '', - dry: false, - buzzer: false, - child_lock: false, - led_brightness: 0, - ...entity.attributes || {}, - }; - } - - get id() { - return this.entity.entity_id; - } - - round(value, decimals) { - return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`); - } - - get depth() { - const depth = this.attr.depth || 0; - - let value = (100 * depth) / this.config.depth.max_value; - - if (this.config.depth.unit_type === 'liters') { - value = (value * this.config.depth.volume) / 100; - } - - return this.round(value, this.config.depth.fixed); - } - - get depthIcon() { - if (!this.config.depth.icon_template) - return ''; - - const context = { depth: this.depth, raw: this.attr.depth || 0 }; - return jinja.render(this.config.depth.icon_template, context); - } - - get targetHumidity() { - const humidity = this.attr.target_humidity || 0; - return { - min: this.config.target_humidity.min, - max: this.config.target_humidity.max, - step: this.config.target_humidity.step, - value: humidity, - }; - } - - get targetHumidityIcon() { - if (!this.config.target_humidity.icon_template) - return ''; - - const context = { targetHumidity: this.targetHumidity.value }; - return jinja.render(this.config.target_humidity.icon_template, context); - } - - get fanSpeed() { - if (this.attr.mode) - return this.fanSpeedSource.find(s => s.value.toUpperCase() === this.attr.mode.toUpperCase()); - - return undefined; - } - - get fanSpeedSource() { - return this.config.fan_mode_button.source; - } - - get ledButtonValue() { - return this.ledButtonSource.find(s => s.value === this.attr.led_brightness); - } - - get ledButtonSource() { - return this.config.led_button.source; - } - - get icon() { - return this.attr.icon; - } - - get name() { - return this.attr.friendly_name || ''; - } - - get isOff() { - return this.state === 'off'; - } - - get isActive() { - return (this.isOff === false && this.isUnavailable === false) || false; - } - - get isUnavailable() { - return this.state === 'unavailable'; - } - - get isOn() { - return this.state === 'on'; - } - - get isDryOn() { - return this.attr.dry === true; - } - - get isBuzzerOn() { - return this.attr.buzzer === true; - } - - get isChildLockOn() { - return this.attr.child_lock === true; - } - - get isLedBrightnessOn() { - return this.attr.led_brightness !== 2; - } - - get isFanDisabled() { - return this.attr.depth === 0; - } - - get temperature() { - const value = this.getValue(this.config.temperature.source, this.attr.temperature); - return this.round(value, this.config.temperature.fixed); - } - - get temperatureIcon() { - if (!this.config.temperature.icon_template) - return ''; - - const context = { temperature: this.temperature }; - return jinja.render(this.config.temperature.icon_template, context); - } - - get humidity() { - const value = this.getValue(this.config.humidity.source, this.attr.humidity); - return this.round(value, this.config.humidity.fixed); - } - - get humidityIcon() { - if (!this.config.humidity.icon_template) - return ''; - - const context = { humidity: this.humidity }; - return jinja.render(this.config.humidity.icon_template, context); - } - - getValue(config, defaultValue) { - if (!config) - return defaultValue; - - if (config.entity && this.hass.states) { - const entity = this.hass.states[config.entity]; - - if (entity && config.attribute) - return entity.attributes[config.attribute]; - - if (entity) - return entity.state; - } - - if (config.attribute) - return this.attr[config.attribute]; - - return defaultValue; - } - - toggleLedBrightness(e) { - if (this.isLedBrightnessOn) - return this.setLedButtonBrightness(e, 'off'); - - return this.setLedButtonBrightness(e, 'bright'); - } - - setLedButtonBrightness(e, id) { - const item = this.ledButtonSource.find(s => s.id === id); - if (item) - return this.callService(e, 'fan_set_led_brightness', { brightness: item.value }, 'xiaomi_miio'); - - throw new Error(`could not find value for key ${id}`); - } - - togglePower(e) { - if (this.isOn) - return this.callService(e, 'turn_off', undefined, 'fan'); - - return this.callService(e, 'turn_on', undefined, 'fan'); - } - - toggleChildLock(e) { - if (this.isChildLockOn) - return this.callService(e, 'fan_set_child_lock_off', undefined, 'xiaomi_miio'); - - return this.callService(e, 'fan_set_child_lock_on', undefined, 'xiaomi_miio'); - } - - toggleBuzzer(e) { - if (this.isBuzzerOn) - return this.callService(e, 'fan_set_buzzer_off', undefined, 'xiaomi_miio'); - - return this.callService(e, 'fan_set_buzzer_on', undefined, 'xiaomi_miio'); - } - - toggleDry(e) { - if (this.isDryOn) - return this.callService(e, 'fan_set_dry_off', undefined, 'xiaomi_miio'); - - return this.callService(e, 'fan_set_dry_on', undefined, 'xiaomi_miio'); - } - - getAttribute(attribute) { - return this.attr[attribute] || ''; - } - - setTargetHumidity(e, value) { - return this.callService(e, 'fan_set_target_humidity', { humidity: value }, 'xiaomi_miio'); - } - - setFanSpeed(e, id) { - const item = this.fanSpeedSource.find(s => s.id === id); - if (item) - return this.callService(e, 'set_speed', { speed: item.value }, 'fan'); - - throw new Error(`could not find value for key ${id}`); - } - - callService(e, service, inOptions, domain) { - e.stopPropagation(); - return this.hass.callService(domain, service, { - entity_id: this.config.entity, - ...inOptions, - }); - } -} diff --git a/src/models/button.js b/src/models/button.js new file mode 100644 index 0000000..a62f453 --- /dev/null +++ b/src/models/button.js @@ -0,0 +1,134 @@ +import { getEntityValue } from '../utils/utils'; + +export default class ButtonObject { + constructor(entity, config, humidifier) { + this.config = config || {}; + this.entity = entity || {}; + this.humidifier = humidifier || {}; + } + + get id() { + return this.config.id; + } + + get type() { + return this.config.type; + } + + get order() { + return this.config.order; + } + + get hide() { + return this.config.hide; + } + + get icon() { + return this.config.icon; + } + + get originalState() { + return getEntityValue(this.entity, this.config.state); + } + + get state() { + let state = this.originalState; + + if (this.config.functions.state && this.config.functions.state.mapper) { + state = this.config.functions.state.mapper(state, this.entity, + this.humidifier.entity); + } + + return state; + } + + get isActive() { + if (this.config.functions.active) { + return this.config.functions.active(this.state, this.entity, + this.humidifier.entity); + } + + return false; + } + + get isUnavailable() { + return !this.state || this.state.toString().trim().toUpperCase() === 'UNAVAILABLE'; + } + + get isOff() { + return this.state && this.state.toString().trim().toUpperCase() === 'OFF'; + } + + get isOn() { + return (this.isOff === false && this.isUnavailable === false) || false; + } + + get disabled() { + if (this.config.functions.disabled) { + return this.config.functions.disabled(this.state, this.entity, + this.humidifier.entity); + } + + return false; + } + + get style() { + if (this.config.functions.style) { + return this.config.functions.style(this.state, this.entity, + this.humidifier.entity) || {}; + } + + return {}; + } + + get source() { + const { functions } = this.config; + let source; + if (functions && functions.source && functions.source.__init) { + source = functions.source.__init(this.entity, this.config); + } else { + source = Object.entries(this.config.source || {}) + .filter(s => s[0] !== '__filter') + .map(s => ({ id: s[0], name: s[1] })); + } + + if (this.config.functions.source && this.config.functions.source.filter) { + return this.config.functions.source.filter(source, this.state, this.entity, + this.humidifier.entity); + } + + return source; + } + + get selected() { + const { state } = this; + if (state === undefined || state === null) + return undefined; + + return this.source.find(s => s.id === state.toString()); + } + + handleToggle(e) { + e.stopPropagation(); + + if (this.config.functions.toggle_action) { + return this.config.functions.toggle_action(this.state, this.entity, + this.humidifier.entity); + } + + return this.humidifier.callService('switch', 'toggle', { entity_id: this.entity.entity_id }); + } + + handleChange(e) { + e.stopPropagation(); + + const selected = e.detail.id; + + if (this.config.functions.change_action) { + return this.config.functions.change_action(selected, this.state, this.entity, + this.humidifier.entity); + } + + return undefined; + } +} diff --git a/src/models/humidifier.js b/src/models/humidifier.js new file mode 100644 index 0000000..df10cc1 --- /dev/null +++ b/src/models/humidifier.js @@ -0,0 +1,62 @@ +export default class HumidifierObject { + constructor(hass, config, entity) { + this.hass = hass || {}; + this.config = config || {}; + this.entity = entity || {}; + this.state = entity.state; + this.attr = { + friendly_name: '', + depth: 0, + target_humidity: 0, + mode: '', + dry: false, + buzzer: false, + child_lock: false, + led_brightness: 0, + ...entity.attributes || {}, + }; + } + + get id() { + return this.entity.entity_id; + } + + get icon() { + return this.attr.icon; + } + + get name() { + return this.attr.friendly_name || ''; + } + + get isOff() { + return this.state === 'off'; + } + + get isActive() { + return (this.isOff === false && this.isUnavailable === false) || false; + } + + get isUnavailable() { + return this.state === 'unavailable'; + } + + get isOn() { + return this.state === 'on'; + } + + togglePower(e) { + if (this.isOn) + return this.callService(e, 'turn_off', undefined, 'fan'); + + return this.callService(e, 'turn_on', undefined, 'fan'); + } + + callService(e, service, inOptions, domain) { + e.stopPropagation(); + return this.hass.callService(domain, service, { + entity_id: this.config.entity, + ...inOptions, + }); + } +} diff --git a/src/models/indicator.js b/src/models/indicator.js new file mode 100644 index 0000000..2438e18 --- /dev/null +++ b/src/models/indicator.js @@ -0,0 +1,54 @@ +import { getEntityValue, round } from '../utils/utils'; + +export default class IndicatorObject { + constructor(entity, config, humidifier) { + this.config = config || {}; + this.entity = entity || {}; + this.humidifier = humidifier || {}; + } + + get id() { + return this.config.id; + } + + get originalValue() { + return getEntityValue(this.entity, this.config.source); + } + + get value() { + let value = this.originalValue; + + if (this.config.functions.mapper) { + value = this.config.functions.mapper(value, this.entity, + this.humidifier.entity); + } + + if ('round' in this.config && Number.isNaN(value) === false) + value = round(value, this.config.round); + + return value; + } + + get unit() { + return this.config.unit; + } + + get icon() { + if (this.config.functions.icon && this.config.functions.icon.template) { + return this.config.functions.icon.template(this.value, this.entity, + this.humidifier.entity); + } else if (this.config.icon && typeof this.config.icon === 'string') { + return this.config.icon; + } + + return ''; + } + + get iconStyle() { + if (this.config.functions.icon && this.config.functions.icon.style) + return this.config.functions.icon.style(this.value, this.entity, + this.humidifier.entity) || {}; + + return {}; + } +} diff --git a/src/models/targetHumidity.js b/src/models/targetHumidity.js new file mode 100644 index 0000000..79bb5a4 --- /dev/null +++ b/src/models/targetHumidity.js @@ -0,0 +1,59 @@ +import { getEntityValue } from '../utils/utils'; + +export default class TargetHumidityObject { + constructor(hass, entity, config, humidifier) { + this.entity = entity || {}; + this.config = config; + this.hass = hass; + this.humidifier = humidifier; + } + + get min() { + return this.config.target_humidity.min; + } + + get max() { + return this.config.target_humidity.max; + } + + get step() { + return this.config.target_humidity.step; + } + + get value() { + return getEntityValue(this.entity, this.config.target_humidity.source); + } + + get icon() { + return this.config.target_humidity.icon; + } + + get hide() { + return this.config.target_humidity.hide; + } + + get unit() { + return this.config.target_humidity.unit; + } + + get state() { + let state = this.value; + + if (this.config.target_humidity.functions.state + && this.config.target_humidity.functions.state.mapper) { + state = this.config.target_humidity.functions.state.mapper(state, this.entity, + this.humidifier.entity); + } + + return state; + } + + handleChange(value) { + if (this.config.target_humidity.functions.change_action) { + return this.config.target_humidity.functions.change_action(value, this.state, + this.entity, this.humidifier.entity); + } + + return undefined; + } +} diff --git a/src/style.js b/src/style.js index c433ddf..b8f8a84 100644 --- a/src/style.js +++ b/src/style.js @@ -74,19 +74,18 @@ const style = css` } .mh-humidifier__core { position: relative; + padding-right: 5px; } .entity__info { - justify-content: center; - display: flex; - flex-direction: column; - margin-left: var(--mh-entity-info-left-offset); - position: relative; - overflow: hidden; user-select: none; - max-width: 130px; + margin-left: var(--mh-entity-info-left-offset); + flex: 1; + min-width: 0; + white-space: nowrap; } .entity__icon { color: var(--mh-icon-color); + white-space: nowrap; } .entity__icon[color] { color: var(--mh-icon-active-color); @@ -117,7 +116,7 @@ const style = css` } .entity__secondary_info_icon { color: var(--mh-icon-color); - height: calc(var(--mh-unit) * .475); + height: calc(var(--mh-unit) * .5); width: calc(var(--mh-unit) * .5); min-width: calc(var(--mh-unit) * .5); --mdc-icon-size: calc(var(--mh-unit) * 0.5); @@ -129,14 +128,8 @@ const style = css` font-size: calc(var(--mh-unit) * .35); font-weight: var(--mh-info-font-weight); line-height: calc(var(--mh-unit) * .5); - } - mh-powerstrip { - flex: 1; - justify-content: flex-end; - margin-right: 0; - margin-left: auto; - width: auto; - min-width: 0; + vertical-align: middle; + display: inline-block; } ha-card.--initial .mh-humidifier { padding: 16px 16px 5px 16px; @@ -147,34 +140,50 @@ const style = css` ha-card.--group .mh-humidifier { padding: 2px 0 0 0; } - mp-humidifier-state { - margin: 0; - } - .mh-humidifier__bottom { - margin: 0; - margin-top: calc(var(--mh-unit) * .075); - margin-left: calc(calc(calc(var(--mh-unit) / 5) + var(--mh-unit)) + var(--mh-entity-info-left-offset)); - justify-content: space-between; - } - .mh-humidifier__toggle { - margin-top: -5px; - margin-right: 4px - } .toggle-button { width: calc(var(--mh-unit) * .75); height: calc(var(--mh-unit) * .75); --mdc-icon-button-size: calc(var(--mh-unit) * .75); color: var(--mh-icon-color); + margin-left: auto; + margin-top: calc(var(--mh-unit) * -.125); } .toggle-button.open { transform: rotate(180deg); color: var(--mh-active-color) } - mp-target-humidity-slider { - flex: 1; + .wrap { + display: flex; + flex-direction: row; } - mp-humidifier-state { - flex: 1; + .bottom { + margin-top: calc(var(--mh-unit) * .075); + } + .entity__info__name_wrap { + margin-right: calc(var(--mh-unit) * .5); + max-width: calc(var(--mh-unit) * 3); + cursor: pointer; + } + + + .mh-toggle_content { + margin-top: calc(var(--mh-unit) * .05); + } + mh-buttons { + width: 100%; + justify-content: space-evenly; + display: flex; + } + .ctl-wrap { + width: 100%; + display: flex; + flex-direction: row; + } + mh-power { + margin-left: auto; + } + mh-target-humidity { + width: 100%; } `; diff --git a/src/utils/trimTo.js b/src/utils/trimTo.js deleted file mode 100644 index c90281d..0000000 --- a/src/utils/trimTo.js +++ /dev/null @@ -1,13 +0,0 @@ -const trimTo = (value, size) => { - if (!value) - return value; - - const str = value.toString(); - - if (str && str.length > size) - return str.substr(0, str.length - 3).concat('...'); - - return str; -}; - -export default trimTo; diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 0000000..da329b1 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,43 @@ +const toggleState = (state) => { + if (!state) + return state; + + if (state.toString().trim().toUpperCase() === 'ON') + return 'OFF'; + + if (state.toString().trim().toUpperCase() === 'OFF') + return 'ON'; + + return state; +}; + +const getEntityValue = (entity, config) => { + if (!entity) + return undefined; + + if (!config) + return entity.state; + + if (config.attribute) + return entity.attributes[config.attribute]; + + return entity.state; +}; + +const round = (value, decimals) => Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`); + +const compileTemplate = (template, context) => { + try { + // eslint-disable-next-line no-new-func + return (new Function('', `return ${template.toString()}`)).call(context || {}); + } catch (e) { + throw new Error(`\n[COMPILE ERROR]: [${e.toString()}]\n[SOURCE]: ${template}\n`); + } +}; + +export { + round, + compileTemplate, + getEntityValue, + toggleState, +}; From 92113acb33be712a947d30cf25014b1eb811d4d8 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Wed, 3 Jun 2020 14:45:14 +0300 Subject: [PATCH 3/9] update --- package-lock.json | 93 +++++++++++++++------------------------- package.json | 3 +- rollup.config.js | 4 +- src/components/power.js | 22 ++++++---- src/const.js | 4 ++ src/initialize.js | 8 ++++ src/main.js | 86 ++++++++++++++++++++++++++++++------- src/models/button.js | 11 +++-- src/models/humidifier.js | 27 ++++-------- src/style.js | 15 ++++++- 10 files changed, 164 insertions(+), 109 deletions(-) create mode 100644 src/initialize.js diff --git a/package-lock.json b/package-lock.json index 344fd4a..c8c4fcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mini-humidifier", - "version": "v1.0.8", + "version": "v2.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1002,6 +1002,34 @@ "to-fast-properties": "^2.0.0" } }, + "@rollup/plugin-json": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.0.3.tgz", + "integrity": "sha512-QMUT0HZNf4CX17LMdwaslzlYHUKTYGuuk34yYIgZrNdu+pMEfqMS55gck7HEeHBKXHM4cz5Dg1OVwythDdbbuQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.8" + } + }, + "@rollup/pluginutils": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.10.tgz", + "integrity": "sha512-d44M7t+PjmMrASHbhgpSbVgtL6EFyX7J4mYxwQ/c5eoaE6N2VgCgEcWVzNnwycIloti+/MpwFr8qfw+nRw00sw==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -2816,12 +2844,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, - "estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3514,15 +3536,6 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, - "is-reference": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz", - "integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==", - "dev": true, - "requires": { - "@types/estree": "0.0.39" - } - }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", @@ -3567,11 +3580,6 @@ "dev": true, "optional": true }, - "jinja-js": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/jinja-js/-/jinja-js-0.1.8.tgz", - "integrity": "sha1-CxPuuW6QmT8EBRTXzmT1N4b1hRE=" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3705,15 +3713,6 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -4123,6 +4122,12 @@ } } }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -4402,19 +4407,6 @@ "fsevents": "~2.1.2" } }, - "rollup-plugin-commonjs": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", - "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1", - "is-reference": "^1.1.2", - "magic-string": "^0.25.2", - "resolve": "^1.11.0", - "rollup-pluginutils": "^2.8.1" - } - }, "rollup-plugin-node-resolve": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz", @@ -4426,15 +4418,6 @@ "resolve": "^1.1.6" } }, - "rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1" - } - }, "run-async": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", @@ -4688,12 +4671,6 @@ "dev": true, "optional": true }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", diff --git a/package.json b/package.json index 854a301..e783f22 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "author": "Artem Sedykh ", "license": "MIT", "dependencies": { - "jinja-js": "^0.1.8", "lit-element": "^2.2.1", "lit-html": "^1.1.2", "resize-observer-polyfill": "^1.5.1" @@ -29,13 +28,13 @@ "@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-transform-template-literals": "^7.2.0", "@babel/preset-env": "^7.3.1", + "@rollup/plugin-json": "^4.0.3", "babel-plugin-iife-wrap": "^1.1.0", "babel-preset-minify": "^0.5.1", "eslint": "^5.16.0", "eslint-config-airbnb-base": "^13.1.0", "eslint-plugin-import": "2.16.0", "rollup": "^2.10.5", - "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^3.4.0" }, "scripts": { diff --git a/rollup.config.js b/rollup.config.js index 2bbcfd8..b428477 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,5 @@ import resolve from 'rollup-plugin-node-resolve'; -import commonjs from 'rollup-plugin-commonjs'; +import json from '@rollup/plugin-json'; export default { input: 'src/main.js', @@ -10,6 +10,6 @@ export default { }, plugins: [ resolve(), - commonjs(), + json(), ], }; diff --git a/src/components/power.js b/src/components/power.js index 25bd679..3b0389d 100644 --- a/src/components/power.js +++ b/src/components/power.js @@ -1,33 +1,37 @@ import { css, html, LitElement } from 'lit-element'; +import { styleMap } from 'lit-html/directives/style-map'; import sharedStyle from '../sharedStyle'; class PowerButton extends LitElement { static get properties() { return { - humidifier: { type: Object }, - config: { type: Object }, + power: { type: Object }, hass: { type: Object }, }; } render() { - if (this.config.power.hide) + if (this.power.hide) return ''; - if (this.config.power.type === 'toggle') { + if (this.power.type === 'toggle') { return html` `; } return html` - this.humidifier.togglePower(e)} - ?color=${this.humidifier.isOn}> + this.power.handleToggle(e)} + ?disabled="${this.power.disabled}" + ?color=${this.power.isOn}> `; } diff --git a/src/const.js b/src/const.js index d844c06..b95b2e7 100644 --- a/src/const.js +++ b/src/const.js @@ -14,3 +14,7 @@ const ICON = { }; export default ICON; +export const STATES_OFF = ['closed', 'locked', 'off']; +export const UNAVAILABLE = 'unavailable'; +export const UNKNOWN = 'unknown'; +export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN]; diff --git a/src/initialize.js b/src/initialize.js new file mode 100644 index 0000000..90c111d --- /dev/null +++ b/src/initialize.js @@ -0,0 +1,8 @@ +import { version } from '../package'; + +// eslint-disable-next-line no-console +console.info( + `%c MINI-HUMIDIFIER-CARD %c ${version} `, + 'color: white; background: coral; font-weight: 700;', + 'color: coral; background: white; font-weight: 700;', +); diff --git a/src/main.js b/src/main.js index 7497cb3..6ffdc4e 100644 --- a/src/main.js +++ b/src/main.js @@ -18,6 +18,8 @@ import IndicatorObject from './models/indicator'; import ButtonObject from './models/button'; import TargetHumidityObject from './models/targetHumidity'; import HumidifierObject from './models/humidifier'; +import getLabel from './utils/getLabel'; +import './initialize'; if (!customElements.get('ha-slider')) { customElements.define( @@ -41,6 +43,7 @@ class MiniHumidifier extends LitElement { this.indicators = {}; this.buttons = {}; this.targetHumidity = {}; + this.power = {}; } static get properties() { @@ -74,6 +77,7 @@ class MiniHumidifier extends LitElement { this.updateIndicators(hass); this.updateButtons(hass); this.updateTargetHumidity(hass); + this.updatePower(hass); } get hass() { @@ -130,6 +134,17 @@ class MiniHumidifier extends LitElement { this.buttons = buttons; } + updatePower(hass) { + const config = this.config.power; + + const entityId = (config.state && config.state.entity) || this.humidifier.id; + const entity = hass.states[entityId]; + const power = entity ? new ButtonObject(entity, config, this.humidifier) : {}; + + if (entity !== (this.power && this.power.entity)) + this.power = power; + } + updateTargetHumidity(hass) { const entityId = (this.config.target_humidity.source && this.config.target_humidity.source.entity) || this.config.entity; @@ -221,9 +236,8 @@ class MiniHumidifier extends LitElement { return Object.entries(defaultIndicators).map(i => this.getIndicatorConfig(i[0], i[1], config)); } - getButtonConfig(key, value, config) { + getButtonConfig(value, config) { const item = { - id: key, icon: 'mdi:radiobox-marked', type: 'button', toggle_action: undefined, @@ -354,7 +368,8 @@ class MiniHumidifier extends LitElement { for (let i = 0; i < data.length; i += 1) { const key = data[i][0]; const value = data[i][1]; - const button = this.getButtonConfig(key, value, config); + const button = this.getButtonConfig(value, config); + button.id = key; if (!('order' in button)) button.order = i + 1; @@ -365,7 +380,7 @@ class MiniHumidifier extends LitElement { return buttons; } - getTargetTemperatureConfig(config) { + getTargetHumidityConfig(config) { const item = { icon: ICON.HUMIDITY, unit: '%', @@ -406,6 +421,21 @@ class MiniHumidifier extends LitElement { return item; } + getPowerConfig(config) { + const item = { + icon: ICON.POWER, + type: 'button', + hide: false, + toggle_action: (state, entity) => { + const service = state === 'on' ? 'turn_off' : 'turn_on'; + return this.call_service('fan', service, { entity_id: entity.entity_id }); + }, + ...config.power || {}, + }; + + return this.getButtonConfig(item, config); + } + setConfig(config) { if (!config.entity || config.entity.split('.')[0] !== 'fan') throw new Error('Specify an entity from within the fan domain.'); @@ -421,15 +451,18 @@ class MiniHumidifier extends LitElement { }, ...config, }; - this.config.power = { - icon: ICON.POWER, - type: 'button', + this.config.toggle = { + icon: ICON.TOGGLE, hide: false, - ...config.power || {}, + default: false, + ...config.toggle || {}, }; - this.config.target_humidity = this.getTargetTemperatureConfig(config); + this.config.power = this.getPowerConfig(config); + this.config.target_humidity = this.getTargetHumidityConfig(config); this.config.indicators = this.getIndicatorsConfig(config); this.config.buttons = this.getButtonsConfig(config); + + this.toggle = this.config.toggle.default; } render() { @@ -449,12 +482,9 @@ class MiniHumidifier extends LitElement { ${this.renderEntityName()}
+ ${this.renderUnavailable()} ${this.renderTargetHumidifier()} - - + ${this.renderPower()}
${this.renderBottomPanel()} @@ -477,6 +507,29 @@ class MiniHumidifier extends LitElement { `; } + renderPower() { + if (this.humidifier.isUnavailable) + return ''; + + return html` + + + `; + } + + renderUnavailable() { + if (!this.humidifier.isUnavailable) + return ''; + + return html` + + ${getLabel(this.hass, 'state.default.unavailable', 'Unavailable')} + + `; + } + handlePopup(e) { e.stopPropagation(); handleClick(this, this._hass, this.config, this.config.tap_action, this.humidifier.id); @@ -524,10 +577,13 @@ class MiniHumidifier extends LitElement { if (this.config.buttons.filter(b => !b.hide).length === 0) return ''; + if (this.config.toggle.hide) + return ''; + const cls = this.toggle ? 'open' : ''; return html` this.handleToggle(e)}> `; diff --git a/src/models/button.js b/src/models/button.js index a62f453..3afbfce 100644 --- a/src/models/button.js +++ b/src/models/button.js @@ -1,4 +1,5 @@ import { getEntityValue } from '../utils/utils'; +import { STATES_OFF, UNAVAILABLE_STATES } from '../const'; export default class ButtonObject { constructor(entity, config, humidifier) { @@ -52,15 +53,19 @@ export default class ButtonObject { } get isUnavailable() { - return !this.state || this.state.toString().trim().toUpperCase() === 'UNAVAILABLE'; + return this.entity === undefined || UNAVAILABLE_STATES.includes(this.state); } get isOff() { - return this.state && this.state.toString().trim().toUpperCase() === 'OFF'; + return this.entity !== undefined + && STATES_OFF.includes(this.state) + && !UNAVAILABLE_STATES.includes(this.state); } get isOn() { - return (this.isOff === false && this.isUnavailable === false) || false; + return this.entity !== undefined + && !STATES_OFF.includes(this.state) + && !UNAVAILABLE_STATES.includes(this.state); } get disabled() { diff --git a/src/models/humidifier.js b/src/models/humidifier.js index df10cc1..6b02294 100644 --- a/src/models/humidifier.js +++ b/src/models/humidifier.js @@ -1,3 +1,5 @@ +import { STATES_OFF, UNAVAILABLE_STATES } from '../const'; + export default class HumidifierObject { constructor(hass, config, entity) { this.hass = hass || {}; @@ -30,7 +32,9 @@ export default class HumidifierObject { } get isOff() { - return this.state === 'off'; + return this.entity !== undefined + && STATES_OFF.includes(this.state) + && !UNAVAILABLE_STATES.includes(this.state); } get isActive() { @@ -38,25 +42,12 @@ export default class HumidifierObject { } get isUnavailable() { - return this.state === 'unavailable'; + return this.entity === undefined || UNAVAILABLE_STATES.includes(this.state); } get isOn() { - return this.state === 'on'; - } - - togglePower(e) { - if (this.isOn) - return this.callService(e, 'turn_off', undefined, 'fan'); - - return this.callService(e, 'turn_on', undefined, 'fan'); - } - - callService(e, service, inOptions, domain) { - e.stopPropagation(); - return this.hass.callService(domain, service, { - entity_id: this.config.entity, - ...inOptions, - }); + return this.entity !== undefined + && !STATES_OFF.includes(this.state) + && !UNAVAILABLE_STATES.includes(this.state); } } diff --git a/src/style.js b/src/style.js index b8f8a84..32869e2 100644 --- a/src/style.js +++ b/src/style.js @@ -164,8 +164,19 @@ const style = css` max-width: calc(var(--mh-unit) * 3); cursor: pointer; } - - + .--unavailable .ctl-wrap { + margin-left: auto; + margin-top: auto; + margin-bottom: auto; + } + .--unavailable .ctl-wrap .unavailable { + margin-left: auto; + margin-right: 0; + } + .--unavailable .entity__info { + margin-top: auto; + margin-bottom: auto; + } .mh-toggle_content { margin-top: calc(var(--mh-unit) * .05); } From 20338caf2ef29f3a8477510d7df1888715c78f43 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Wed, 3 Jun 2020 16:11:37 +0300 Subject: [PATCH 4/9] refactoring --- src/components/button.js | 90 ++++++++++++++++++++++++++++++++ src/components/buttons.js | 12 ++--- src/components/power.js | 24 ++++++--- src/components/targetHumidity.js | 23 +++++++- src/const.js | 1 + 5 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 src/components/button.js diff --git a/src/components/button.js b/src/components/button.js new file mode 100644 index 0000000..842dbac --- /dev/null +++ b/src/components/button.js @@ -0,0 +1,90 @@ +import { LitElement, html, css } from 'lit-element'; +import { styleMap } from 'lit-html/directives/style-map'; +import sharedStyle from '../sharedStyle'; +import { ACTION_TIMEOUT } from '../const'; + +class HumidifierButton extends LitElement { + constructor() { + super(); + this._isOn = false; + this.timer = undefined; + this.cls = undefined; + } + + static get properties() { + return { + button: { type: Object }, + cls: { type: String }, + }; + } + + handleToggle(e) { + const { entity } = this.button; + + this._isOn = !this._isOn; + this.button.handleToggle(e); + + if (this.timer) + clearTimeout(this.timer); + + this.timer = setTimeout(async () => { + if (this.button.entity === entity) { + this._isOn = this.button.isOn; + return this.requestUpdate('_isOn'); + } + }, ACTION_TIMEOUT); + + return this.requestUpdate('_isOn'); + } + + render() { + return html` + this.handleToggle(e)} + ?disabled="${this.button.disabled || this.button.isUnavailable}" + ?color=${this._isOn}> + + `; + } + + updated(changedProps) { + if (changedProps.has('button')) { + this._isOn = this.button.isOn; + + if (this.timer) + clearTimeout(this.timer); + + return this.requestUpdate('_isOn'); + } + } + + static get styles() { + return [ + sharedStyle, + css` + :host { + position: relative; + box-sizing: border-box; + margin: 0; + overflow: hidden; + transition: background .5s; + --paper-item-min-height: var(--mh-unit); + --mh-dropdown-unit: var(--mh-unit); + } + :host([color]) { + background: var(--mh-active-color); + transition: background .25s; + opacity: 1; + } + :host([disabled]) { + opacity: .25; + pointer-events: none; + } + `]; + } +} + +customElements.define('mh-button', HumidifierButton); diff --git a/src/components/buttons.js b/src/components/buttons.js index 974d782..bdd0f48 100644 --- a/src/components/buttons.js +++ b/src/components/buttons.js @@ -2,6 +2,7 @@ import { LitElement, html, css } from 'lit-element'; import { styleMap } from 'lit-html/directives/style-map'; import sharedStyle from '../sharedStyle'; import './dropdown'; +import './button'; class HumidifierButtons extends LitElement { static get properties() { @@ -15,14 +16,9 @@ class HumidifierButtons extends LitElement { return ''; return html` - button.handleToggle(e)} - ?disabled="${button.disabled}" - ?color=${button.isOn}> - + + `; } diff --git a/src/components/power.js b/src/components/power.js index 3b0389d..d6db78d 100644 --- a/src/components/power.js +++ b/src/components/power.js @@ -1,8 +1,15 @@ import { css, html, LitElement } from 'lit-element'; import { styleMap } from 'lit-html/directives/style-map'; import sharedStyle from '../sharedStyle'; +import './button'; class PowerButton extends LitElement { + constructor() { + super(); + this._isOn = false; + this.timer = undefined; + } + static get properties() { return { power: { type: Object }, @@ -25,17 +32,18 @@ class PowerButton extends LitElement { } return html` - this.power.handleToggle(e)} - ?disabled="${this.power.disabled}" - ?color=${this.power.isOn}> - + + `; } + updated(changedProps) { + if (changedProps.has('power')) { + this._isOn = this.power.isOn; + } + } + static get styles() { return [ sharedStyle, diff --git a/src/components/targetHumidity.js b/src/components/targetHumidity.js index 0d66931..ce0de19 100644 --- a/src/components/targetHumidity.js +++ b/src/components/targetHumidity.js @@ -1,4 +1,5 @@ import { css, html, LitElement } from 'lit-element'; +import { ACTION_TIMEOUT } from '../const'; class TargetHumidity extends LitElement { static get properties() { @@ -11,12 +12,25 @@ class TargetHumidity extends LitElement { constructor() { super(); this.targetHumidity = {}; + this.timer = undefined; } handleChange(e) { e.stopPropagation(); this.sliderValue = e.target.value; + const { entity } = this.targetHumidity; this.targetHumidity.handleChange(this.sliderValue); + + if (this.timer) + clearTimeout(this.timer); + + this.timer = setTimeout(async () => { + if (this.targetHumidity.entity === entity) { + this.sliderValue = this.targetHumidity.value; + return this.requestUpdate('sliderValue'); + } + }, ACTION_TIMEOUT); + return this.requestUpdate('sliderValue'); } @@ -34,7 +48,6 @@ class TargetHumidity extends LitElement { } render() { - this.sliderValue = this.sliderValue || this.targetHumidity.value; return html`
@@ -51,6 +64,12 @@ class TargetHumidity extends LitElement {
`; } + updated(changedProps) { + if (changedProps.has('targetHumidity')) { + this.sliderValue = this.targetHumidity.value; + } + } + static get styles() { return css` :host { diff --git a/src/const.js b/src/const.js index b95b2e7..d3f215e 100644 --- a/src/const.js +++ b/src/const.js @@ -18,3 +18,4 @@ export const STATES_OFF = ['closed', 'locked', 'off']; export const UNAVAILABLE = 'unavailable'; export const UNKNOWN = 'unknown'; export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN]; +export const ACTION_TIMEOUT = 3500; From 350119d763e1765d597b945e9b6a94eb327d9d76 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Wed, 3 Jun 2020 17:23:12 +0300 Subject: [PATCH 5/9] refactoring --- .babelrc | 3 +- src/components/button.js | 5 +- src/components/buttons.js | 14 +-- src/components/dropdown-base.js | 105 +++++++++++++++++++++ src/components/dropdown.js | 157 +++++++++++++++----------------- src/components/power.js | 1 + src/main.js | 6 +- src/models/button.js | 14 +-- src/models/humidifier.js | 4 + src/style.js | 5 +- 10 files changed, 201 insertions(+), 113 deletions(-) create mode 100644 src/components/dropdown-base.js diff --git a/.babelrc b/.babelrc index 00a39d3..404710f 100644 --- a/.babelrc +++ b/.babelrc @@ -8,7 +8,8 @@ "esmodules": true } } - ] + ], + ["minify"] ], "comments": false, "plugins": [ diff --git a/src/components/button.js b/src/components/button.js index 842dbac..74cfb78 100644 --- a/src/components/button.js +++ b/src/components/button.js @@ -8,7 +8,6 @@ class HumidifierButton extends LitElement { super(); this._isOn = false; this.timer = undefined; - this.cls = undefined; } static get properties() { @@ -19,10 +18,11 @@ class HumidifierButton extends LitElement { } handleToggle(e) { + e.stopPropagation(); const { entity } = this.button; this._isOn = !this._isOn; - this.button.handleToggle(e); + this.button.handleToggle(); if (this.timer) clearTimeout(this.timer); @@ -41,7 +41,6 @@ class HumidifierButton extends LitElement { return html` this.handleToggle(e)} ?disabled="${this.button.disabled || this.button.isUnavailable}" diff --git a/src/components/buttons.js b/src/components/buttons.js index bdd0f48..1c8ba32 100644 --- a/src/components/buttons.js +++ b/src/components/buttons.js @@ -1,5 +1,4 @@ import { LitElement, html, css } from 'lit-element'; -import { styleMap } from 'lit-html/directives/style-map'; import sharedStyle from '../sharedStyle'; import './dropdown'; import './button'; @@ -17,25 +16,16 @@ class HumidifierButtons extends LitElement { return html` `; } renderDropdown(dropdown) { - let selected = ''; - if (dropdown.state !== null && dropdown.state !== undefined) - selected = dropdown.state.toString(); - return html` dropdown.handleChange(e)} - .items=${dropdown.source} - .icon=${dropdown.icon} - .disabled="${dropdown.disabled}" - .active=${dropdown.isActive} - .selected=${selected}> + .dropdown=${dropdown}> `; } diff --git a/src/components/dropdown-base.js b/src/components/dropdown-base.js new file mode 100644 index 0000000..408875b --- /dev/null +++ b/src/components/dropdown-base.js @@ -0,0 +1,105 @@ +import { LitElement, html, css } from 'lit-element'; + +import sharedStyle from '../sharedStyle'; + +class HumidifierDropdownBase extends LitElement { + static get properties() { + return { + items: [], + label: String, + selected: String, + icon: String, + active: Boolean, + disabled: Boolean, + }; + } + + get selectedId() { + return this.items.map(item => item.id).indexOf(this.selected); + } + + onChange(e) { + const id = e.target.selected; + if (id !== this.selectedId && this.items[id]) { + this.dispatchEvent(new CustomEvent('change', { + detail: this.items[id], + })); + e.target.selected = -1; + } + } + + render() { + return html` + e.stopPropagation()}> + + + + ${this.items.map(item => html` + + ${item.name} + `)} + + + `; + } + + static get styles() { + return [ + sharedStyle, + css` + :host { + position: relative; + overflow: hidden; + --paper-item-min-height: 40px; + } + paper-menu-button + :host([disabled]) { + opacity: .25; + pointer-events: none; + } + :host([faded]) { + opacity: .75; + } + .mh-dropdown { + padding: 0; + display: block; + } + ha-icon-button[disabled] { + opacity: .25; + pointer-events: none; + } + .mh-dropdown__button.icon { + margin: 0; + } + ha-icon-button { + width: calc(var(--mh-dropdown-unit)); + height: calc(var(--mh-dropdown-unit)); + --mdc-icon-button-size: calc(var(--mh-dropdown-unit)); + } + paper-item > *:nth-child(2) { + margin-left: 4px; + } + paper-menu-button[focused] ha-icon-button { + color: var(--mh-accent-color); + } + paper-menu-button[focused] ha-icon-button[focused] { + color: var(--mh-text-color); + transform: rotate(0deg); + } + `, + ]; + } +} + +customElements.define('mh-dropdown-base', HumidifierDropdownBase); diff --git a/src/components/dropdown.js b/src/components/dropdown.js index 1ce8f58..c86bb5d 100644 --- a/src/components/dropdown.js +++ b/src/components/dropdown.js @@ -1,105 +1,98 @@ import { LitElement, html, css } from 'lit-element'; - +import { styleMap } from 'lit-html/directives/style-map'; import sharedStyle from '../sharedStyle'; +import './dropdown-base'; +import './button'; +import { ACTION_TIMEOUT } from '../const'; + +class HumidifierDropDown extends LitElement { + constructor() { + super(); + this.dropdown = {}; + this.timer = undefined; + this._state = undefined; + } -class MiniClimateDropdown extends LitElement { static get properties() { return { - items: [], - label: String, - selected: String, - icon: String, - active: Boolean, - disabled: Boolean, + dropdown: { type: Object }, }; } - get selectedId() { - return this.items.map(item => item.id).indexOf(this.selected); - } + handleChange(e) { + e.stopPropagation(); - onChange(e) { - const id = e.target.selected; - if (id !== this.selectedId && this.items[id]) { - this.dispatchEvent(new CustomEvent('change', { - detail: this.items[id], - })); - e.target.selected = -1; - } + const selected = e.detail.id; + const { entity } = this.dropdown; + this._state = selected; + + this.dropdown.handleChange(selected); + + if (this.timer) + clearTimeout(this.timer); + + this.timer = setTimeout(async () => { + if (this.dropdown.entity === entity) { + this._state = (this.dropdown.state !== undefined && this.dropdown.state !== null) + ? this.dropdown.state.toString() : ''; + + return this.requestUpdate('_state'); + } + }, ACTION_TIMEOUT); + + return this.requestUpdate('_state'); } render() { return html` - e.stopPropagation()}> - - - - ${this.items.map(item => html` - - ${item.name} - `)} - - + this.handleChange(e)} + .items=${this.dropdown.source} + .icon=${this.dropdown.icon} + .disabled="${this.dropdown.disabled}" + .active=${this.dropdown.isActive(this._state)} + .selected=${this._state}> + `; } + updated(changedProps) { + if (changedProps.has('dropdown')) { + this._state = (this.dropdown.state !== undefined && this.dropdown.state !== null) + ? this.dropdown.state.toString() : ''; + + if (this.timer) + clearTimeout(this.timer); + + return this.requestUpdate('_state'); + } + } + static get styles() { return [ sharedStyle, css` - :host { - position: relative; - overflow: hidden; - --paper-item-min-height: 40px; - } - paper-menu-button - :host([disabled]) { - opacity: .25; - pointer-events: none; - } - :host([faded]) { - opacity: .75; - } - .mh-dropdown { - padding: 0; - display: block; - } - ha-icon-button[disabled] { - opacity: .25; - pointer-events: none; - } - .mh-dropdown__button.icon { - margin: 0; - } - ha-icon-button { - width: calc(var(--mh-dropdown-unit)); - height: calc(var(--mh-dropdown-unit)); - --mdc-icon-button-size: calc(var(--mh-dropdown-unit)); - } - paper-item > *:nth-child(2) { - margin-left: 4px; - } - paper-menu-button[focused] ha-icon-button { - color: var(--mh-accent-color); - } - paper-menu-button[focused] ha-icon-button[focused] { - color: var(--mh-text-color); - transform: rotate(0deg); - } - `, - ]; + :host { + position: relative; + box-sizing: border-box; + margin: 0; + overflow: hidden; + transition: background .5s; + --paper-item-min-height: var(--mh-unit); + --mh-dropdown-unit: var(--mh-unit); + } + :host([color]) { + background: var(--mh-active-color); + transition: background .25s; + opacity: 1; + } + :host([disabled]) { + opacity: .25; + pointer-events: none; + } + `]; } } -customElements.define('mh-dropdown', MiniClimateDropdown); +customElements.define('mh-dropdown', HumidifierDropDown); diff --git a/src/components/power.js b/src/components/power.js index d6db78d..cc89261 100644 --- a/src/components/power.js +++ b/src/components/power.js @@ -33,6 +33,7 @@ class PowerButton extends LitElement { return html` `; diff --git a/src/main.js b/src/main.js index 6ffdc4e..2e9121b 100644 --- a/src/main.js +++ b/src/main.js @@ -196,13 +196,13 @@ class MiniHumidifier extends LitElement { order: 0, max_value: 125, volume: 4, - unit_type: 'liters', + type: 'liters', hide: false, source: { attribute: 'depth', mapper: (val) => { const value = (100 * (val || 0)) / this.max_value; - return this.unit_type === 'liters' ? (value * this.volume) / 100 : value; + return this.type === 'liters' ? (value * this.volume) / 100 : value; }, }, }, @@ -320,7 +320,7 @@ class MiniHumidifier extends LitElement { type: 'dropdown', hide: false, order: 2, - active: state => (state !== 2), + active: state => (state !== 2 && state !== '2'), source: { 0: 'Bright', 1: 'Dim', 2: 'Off' }, state: { attribute: 'led_brightness' }, change_action: (selected, entity) => { diff --git a/src/models/button.js b/src/models/button.js index 3afbfce..6058fbe 100644 --- a/src/models/button.js +++ b/src/models/button.js @@ -43,9 +43,9 @@ export default class ButtonObject { return state; } - get isActive() { + isActive(state) { if (this.config.functions.active) { - return this.config.functions.active(this.state, this.entity, + return this.config.functions.active(state, this.entity, this.humidifier.entity); } @@ -113,9 +113,7 @@ export default class ButtonObject { return this.source.find(s => s.id === state.toString()); } - handleToggle(e) { - e.stopPropagation(); - + handleToggle() { if (this.config.functions.toggle_action) { return this.config.functions.toggle_action(this.state, this.entity, this.humidifier.entity); @@ -124,11 +122,7 @@ export default class ButtonObject { return this.humidifier.callService('switch', 'toggle', { entity_id: this.entity.entity_id }); } - handleChange(e) { - e.stopPropagation(); - - const selected = e.detail.id; - + handleChange(selected) { if (this.config.functions.change_action) { return this.config.functions.change_action(selected, this.state, this.entity, this.humidifier.entity); diff --git a/src/models/humidifier.js b/src/models/humidifier.js index 6b02294..3d02727 100644 --- a/src/models/humidifier.js +++ b/src/models/humidifier.js @@ -50,4 +50,8 @@ export default class HumidifierObject { && !STATES_OFF.includes(this.state) && !UNAVAILABLE_STATES.includes(this.state); } + + callService(domain, service, options) { + return this.hass.callService(domain, service, options); + } } diff --git a/src/style.js b/src/style.js index 32869e2..27d009e 100644 --- a/src/style.js +++ b/src/style.js @@ -160,8 +160,8 @@ const style = css` margin-top: calc(var(--mh-unit) * .075); } .entity__info__name_wrap { - margin-right: calc(var(--mh-unit) * .5); - max-width: calc(var(--mh-unit) * 3); + margin-right: 0; + max-width: calc(var(--mh-unit) * 2.25); cursor: pointer; } .--unavailable .ctl-wrap { @@ -192,6 +192,7 @@ const style = css` } mh-power { margin-left: auto; + min-width: calc(var(--mh-unit) * .875); } mh-target-humidity { width: 100%; From 638f689518637ce6020b23c85984a64bd5980806 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Wed, 3 Jun 2020 18:40:32 +0300 Subject: [PATCH 6/9] update targetHumidity icon functions --- src/components/targetHumidity.js | 7 ++++++- src/main.js | 6 ++---- src/models/targetHumidity.js | 21 ++++++++++++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/targetHumidity.js b/src/components/targetHumidity.js index ce0de19..df71403 100644 --- a/src/components/targetHumidity.js +++ b/src/components/targetHumidity.js @@ -1,4 +1,5 @@ import { css, html, LitElement } from 'lit-element'; +import { styleMap } from 'lit-html/directives/style-map'; import { ACTION_TIMEOUT } from '../const'; class TargetHumidity extends LitElement { @@ -40,7 +41,11 @@ class TargetHumidity extends LitElement { return html`
- + + ${this.sliderValue} ${this.targetHumidity.unit}
diff --git a/src/main.js b/src/main.js index 2e9121b..949acce 100644 --- a/src/main.js +++ b/src/main.js @@ -396,7 +396,7 @@ class MiniHumidifier extends LitElement { ...config.target_humidity || {}, }; - item.functions = {}; + item.functions = { icon: {} }; const context = { ...config.target_humidity || {} }; context.call_service = (domain, service, options) => this.hass.callService( domain, service, options, @@ -405,8 +405,6 @@ class MiniHumidifier extends LitElement { context.toggle_state = toggleState; if (typeof item.icon === 'object') { - item.functions.icon = {}; - if (item.icon.template) item.functions.icon.template = compileTemplate(item.icon.template, context); @@ -508,7 +506,7 @@ class MiniHumidifier extends LitElement { } renderPower() { - if (this.humidifier.isUnavailable) + if (this.humidifier.isUnavailable || this.power.hide) return ''; return html` diff --git a/src/models/targetHumidity.js b/src/models/targetHumidity.js index 79bb5a4..e060613 100644 --- a/src/models/targetHumidity.js +++ b/src/models/targetHumidity.js @@ -25,7 +25,26 @@ export default class TargetHumidityObject { } get icon() { - return this.config.target_humidity.icon; + const config = this.config.target_humidity; + + if (config.functions.icon.template) { + return config.functions.icon.template(this.value, this.entity, + this.humidifier.entity); + } else if (config.icon && typeof config.icon === 'string') { + return config.icon; + } + + return ''; + } + + get iconStyle() { + const config = this.config.target_humidity; + + if (config.functions.icon && config.functions.icon.style) + return config.functions.icon.style(this.value, this.entity, + this.humidifier.entity) || {}; + + return {}; } get hide() { From ec24d2ef1687a52d95e28d8e16155892c69222c7 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Wed, 3 Jun 2020 19:53:05 +0300 Subject: [PATCH 7/9] update README.md and targetHumidity --- README.md | 436 ++++++++--------------------------- src/main.js | 6 +- src/models/targetHumidity.js | 28 +-- 3 files changed, 118 insertions(+), 352 deletions(-) diff --git a/README.md b/README.md index 9709e64..1220cd3 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -# Mini Humidifier Card for Xiaomi Smartmi Zhimi Air Humidifier +# Mini Humidifier Card [![Last Version](https://img.shields.io/github/package-json/v/artem-sedykh/mini-humidifier?label.svg=release)](https://github.com/artem-sedykh/mini-humidifier/releases/latest) [![Build Status](https://travis-ci.com/artem-sedykh/mini-humidifier.svg?branch=master)](https://travis-ci.com/artem-sedykh/mini-humidifier) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/artem-sedykh/mini-humidifier) [![buymeacoffee_badge](https://img.shields.io/badge/Donate-buymeacoffe-ff813f?style=flat)](https://www.buymeacoffee.com/anavrin72) +> Attention! The config version *v1.0.8* **very differs** from version >= *v2.0.1* + Tested on [zhimi.humidifier.cb1](https://www.home-assistant.io/integrations/fan.xiaomi_miio/) A minimalistic yet customizable humidifier card for [Home Assistant](https://github.com/home-assistant/home-assistant) Lovelace UI. Inspired by [mini media player](https://github.com/kalkih/mini-media-player). -![Preview Image](https://user-images.githubusercontent.com/861063/79672681-0f241580-81dd-11ea-913c-234c287a6264.png) +![Preview Image](https://user-images.githubusercontent.com/861063/83651482-3171c700-a5c2-11ea-8053-f66472a8d539.png) ## Install @@ -25,7 +27,7 @@ Inspired by [mini media player](https://github.com/kalkih/mini-media-player). ```yaml resources: - - url: /local/mini-humidifier-bundle.js?v=1.0.9 + - url: /local/mini-humidifier-bundle.js?v=2.0.1 type: module ``` @@ -36,14 +38,14 @@ Inspired by [mini media player](https://github.com/kalkih/mini-media-player). 2. Grab `mini-humidifier-bundle.js` ```console - $ wget https://github.com/artem-sedykh/mini-humidifier/releases/download/v1.0.1/mini-humidifier-bundle.js + $ wget https://github.com/artem-sedykh/mini-humidifier/releases/download/v2.0.1/mini-humidifier-bundle.js ``` 3. Add a reference to `mini-humidifier-bundle.js` inside your `ui-lovelace.yaml`. ```yaml resources: - - url: /local/mini-humidifier-bundle.js?v=1.0.9 + - url: /local/mini-humidifier-bundle.js?v=2.0.1 type: module ``` @@ -56,7 +58,7 @@ Inspired by [mini media player](https://github.com/kalkih/mini-media-player). ```yaml resources: - - url: /local/mini-humidifier-bundle.js?v=1.0.9 + - url: /local/mini-humidifier-bundle.js?v=2.0.1 type: module ``` @@ -73,113 +75,65 @@ Inspired by [mini media player](https://github.com/kalkih/mini-media-player). | entity | string | **required** | v1.0.1 | An entity_id from an entity within the `fan` domain. | name | string | optional | v1.0.1 | Override the entities friendly name. | icon | string | optional | v1.0.1 | Specify a custom icon from any of the available mdi icons. -| group | boolean | optional | v1.0.1 | Removes paddings, background color and box-shadow. -| **power_button** | object | optional | v1.0.3 | Power button, [example](#power-button-configuration). -| power_button: `type` | string | optional | v1.0.3 | `toggle` or `button`, default `mdi:power` -| power_button: `icon` | string | optional | v1.0.3 | Custom icon for type `buttom`, default value `mdi:fan` -| power_button: `hide` | boolean | optional | v1.0.3 | Hide button, default value `False` -| **dry_button** | object | optional | v1.0.1 | Dry mode on/off button -| dry_button: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:weather-sunny` -| dry_button: `hide` | boolean | optional | v1.0.1 | Hide button, default value `False` -| dry_button: `order` | number | optional | v1.0.1 | Sort order, default value `0` -| **fan_mode_button** | object | optional | v1.0.1 | Dry mode on/off button -| fan_mode_button: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:fan` -| fan_mode_button: `hide` | boolean | optional | v1.0.1 | Hide button, default value `False` -| fan_mode_button: `order` | number | optional | v1.0.1 | Sort order, default value `1` -| fan_mode_button: `source` | object | optional | v1.0.1 | Source for fan mode drop down list, [example](#fan-mode-source). -| fan_mode_button: `source: auto` | object | optional | v1.0.6 | auto mode configuration -| fan_mode_button: `source: auto: value` | string | optional | v1.0.6 | value, default `Auto` -| fan_mode_button: `source: auto: name` | string | optional | v1.0.6 | Display name, default `Auto` -| fan_mode_button: `source: auto: order` | number | optional | v1.0.6 | Sort order, default `0` -| fan_mode_button: `source: auto: hide` | boolean | optional | v1.0.6 | Hide from dropdown list, default `False` -| fan_mode_button: `source: silent` | object | optional | v1.0.6 | silent mode configuration -| fan_mode_button: `source: silent: value` | string | optional | v1.0.6 | value, default `Silent` -| fan_mode_button: `source: silent: name` | string | optional | v1.0.6 | Display name, default `Silent` -| fan_mode_button: `source: silent: order` | number | optional | v1.0.6 | Sort order, default `1` -| fan_mode_button: `source: silent: hide` | boolean | optional | v1.0.6 | Hide from dropdown list, default `False` -| fan_mode_button: `source: medium` | object | optional | v1.0.6 | medium mode configuration -| fan_mode_button: `source: medium: value` | string | optional | v1.0.6 | value, default `Medium` -| fan_mode_button: `source: medium: name` | string | optional | v1.0.6 | Display name, default `Medium` -| fan_mode_button: `source: medium: order` | number | optional | v1.0.6 | Sort order, default `2` -| fan_mode_button: `source: medium: hide` | boolean | optional | v1.0.6 | Hide from dropdown list, default `False` -| fan_mode_button: `source: high` | object | optional | v1.0.6 | high mode configuration -| fan_mode_button: `source: high: value` | string | optional | v1.0.6 | value, default `High` -| fan_mode_button: `source: high: name` | string | optional | v1.0.6 | Display name, default `High` -| fan_mode_button: `source: high: order` | number | optional | v1.0.6 | Sort order, default `3` -| fan_mode_button: `source: high: hide` | boolean | optional | v1.0.6 | Hide from dropdown list, default `False` -| fan_mode_button: `source: strong` | object | optional | v1.0.7 | strong mode configuration, for `ZHIMI.HUMIDIFIER.V1` -| fan_mode_button: `source: strong: value` | string | optional | v1.0.7 | value, default `High` -| fan_mode_button: `source: strong: name` | string | optional | v1.0.7 | Display name, default `Strong` -| fan_mode_button: `source: strong: order` | number | optional | v1.0.7 | Sort order, default `4` -| fan_mode_button: `source: strong: hide` | boolean | optional | v1.0.7 | Hide from dropdown list, default `True` -| **led_button** | object | optional | v1.0.1 | Button Illumination on/off -| led_button: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:lightbulb-on-outline` -| led_button: `hide` | boolean | optional | v1.0.1 | Hide button, default value `False` -| led_button: `order` | number | optional | v1.0.1 | Sort order, default value `2` -| led_button: `type` | string | optional | v1.0.2 | Render type, available values `button or dropdown` default value `button` -| led_button: `source` | object | optional | v1.0.2 | Source for dropdown button type, supported values are 0 (Bright), 1 (Dim), 2 (Off), [example](#led-button-dropdown-list-configuration). -| led_button: `source:bright` | object | optional | v1.0.2 | 0 (Bright) -| led_button: `source:bright:value` | number | optional | v1.0.2 | Bright value, default `0` -| led_button: `source:bright:name` | string | optional | v1.0.2 | name, default `Bright` -| led_button: `source:bright:order` | number | optional | v1.0.2 | Sort order, default `0` -| led_button: `source:dim` | object | optional | v1.0.2 | 1 (Dim) -| led_button: `source:dim:value` | number | optional | v1.0.2 | Dim value, default `1` -| led_button: `source:dim:name` | string | optional | v1.0.2 | name, default `Dim` -| led_button: `source:dim:order` | number | optional | v1.0.2 | Sort order, default `1` -| led_button: `source:'off'` | object | optional | v1.0.2 | 2 (Off), the key must be written in quotation marks, without them the parameter will be false -| led_button: `source:'off':value` | string | optional | v1.0.2 | Off value, default `2` -| led_button: `source:'off':name` | string | optional | v1.0.2 | name, default `Off` -| led_button: `source:'off':order` | number | optional | v1.0.2 | Sort order, default `2` -| **buzzer_button** | object | optional | v1.0.1 | Buzzer on/off -| buzzer_button: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:bell-outline` -| buzzer_button: `hide` | boolean | optional | v1.0.1 | Hide button, default value `False` -| buzzer_button: `order` | number | optional | v1.0.1 | Sort order, default value `3` -| **child_lock_button** | object | optional | v1.0.1 | Child lock on/off -| child_lock_button: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:lock` -| child_lock_button: `hide` | boolean | optional | v1.0.1 | Hide button, default value `False` -| child_lock_button: `order` | number | optional | v1.0.1 | Sort order, default value `4` -| **toggle_button** | object | optional | v1.0.1 | Toggle button. -| toggle_button: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:dots-horizontal` -| toggle_button: `hide` | boolean | optional | v1.0.1 | Hide button, default value `False` -| toggle_button: `default` | boolean | optional | v1.0.5 | Default toggle_button state, default value `off`, [example](#always-show-control-buttons). -| **depth** | object | optional | v1.0.1 | Information indicator, showing how much water is left in the humidifier -| depth: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:beaker-outline` -| depth: `icon_template` | string | optional | v1.0.8 | Custom icon template, context values: `depth`(calculated value) and `raw`(raw depth value) [example](#icon-template-example) -| depth: `hide` | boolean | optional | v1.0.1 | Hide indicator, default value `False` -| depth: `order` | number | optional | v1.0.1 | Indicator sort order, default value `0` -| depth: `unit_type` | string | optional | v1.0.1 | Indicator type available Values: `liters` or `percent`, default `percent` -| depth: `unit` | string | optional | v1.0.1 | display unit, default `%` -| depth: `max_value` | number | optional | v1.0.1 | Depth attribute value with a full tank of humidifier, default `125` -| depth: `volume` | number | optional | v1.0.1 | Humidifier tank volume, needed to calculate values in liters, default `4` liters -| depth: `fixed` | number | optional | v1.0.1 | Rounding the calculated values, default value `0` -| **temperature** | object | optional | v1.0.1 | Information indicator, showing temperature -| temperature: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:thermometer-low` -| temperature: `icon_template` | string | optional | v1.0.8 | Custom icon template, context value: `temperature` -| temperature: `hide` | boolean | optional | v1.0.1 | Hide indicator, default value `False` -| temperature: `order` | number | optional | v1.0.1 | Indicator sort order, default value `1` -| temperature: `unit` | string | optional | v1.0.1 | display unit, default `°C` -| temperature: `fixed` | number | optional | v1.0.7 | Rounding the calculated values, default value `1` -| temperature: `source` | object | optional | v1.0.6 | data source, by default, data taken from the attribute `temperature` [examples](#temperature-source-examples). -| temperature: `source: entity` | string | optional | v1.0.6 | custom entity, if the attribute is not set, the state value -| temperature: `source: attribute` | string | optional | v1.0.6 | if the entity parameter is not set, then the data will be obtained from the specified entity attribute, otherwise from the current entity attribute -| **humidity** | object | optional | v1.0.1 | Information indicator, showing humidity -| humidity: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:water-outline` -| humidity: `icon_template` | string | optional | v1.0.8 | Custom icon template, context value: `humidity` -| humidity: `hide` | boolean | optional | v1.0.1 | Hide indicator, default value `False` -| humidity: `order` | number | optional | v1.0.1 | Indicator sort order, default value `2` -| humidity: `unit` | string | optional | v1.0.1 | display unit, default `%` -| humidity: `fixed` | number | optional | v1.0.7 | Rounding the calculated values, default value `1` -| humidity: `source` | object | optional | v1.0.6 | data source, by default, data taken from the attribute `humidity` -| humidity: `source: entity` | string | optional | v1.0.6 | custom entity, if the attribute is not set, the state value -| humidity: `source: attribute` | string | optional | v1.0.6 | if the entity parameter is not set, then the data will be obtained from the specified entity attribute, otherwise from the current entity attribute -| **target_humidity** | object | optional | v1.0.1 | Target humidity -| target_humidity: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:water-outline` -| target_humidity: `icon_template` | string | optional | v1.0.8 | Custom icon template, context value: `targetHumidity` +| group | boolean | optional | v1.0.1 | Removes paddings, background color and box-shadow. [example](#group) +| **toggle** | object | optional | v2.0.1 | Toggle button. +| toggle: `icon` | string | optional | v2.0.1 | Custom icon, default value `mdi:dots-horizontal` +| toggle: `hide` | boolean | optional | v2.0.1 | Hide button, default value `False` +| toggle: `default` | boolean | optional | v2.0.1 | Default toggle button state, default value `off`, [example](#toggle-button). +| **power** | object | optional | v2.0.1 | Power button, [example](#power-button). +| power: `type` | string | optional | v2.0.1 | `toggle` or `button`, default `button` +| power: `icon` | string | optional | v2.0.1 | Specify a custom icon from any of the available mdi icons, default `mdi:power` +| power: `hide` | boolean | optional | v2.0.1 | Hide power button, default value `False` +| power: `disabled` | function | optional | v2.0.1 | button disabled calculation function, default unset +| power: `style` | function | optional | v2.0.1 | function for getting custom styles, default unset +| power: `state` | object | optional | v2.0.1 | config to get power button state. +| power: `state:entity` | string | optional | v2.0.1 | power button entity_id, default current entity +| power: `state:attribute` | string | optional | v2.0.1 | state value attribute default 'unset' +| power: `state:mapper` | function | optional | v2.0.1 | state value processing function, default `unset` +| power: `toggle_action` | function | optional | v2.0.1 | button click processing function +| **target_humidity** | object | optional | v2.0.1 | target humidity config, [example](#target-humidity). +| target_humidity: `icon` | string | optional | v1.0.1 | Custom icon, default value `mdi:water` +| target_humidity: `icon` | object | optional | v2.0.1 | icon config +| target_humidity: `icon:template` | function | optional | v2.0.1 | icon retrieval function +| target_humidity: `icon:style` | function | optional | v2.0.1 | function to get icon styles | target_humidity: `hide` | boolean | optional | v1.0.1 | Hide indicator, default value `False` | target_humidity: `unit` | string | optional | v1.0.1 | display unit, default `%` -| target_humidity: `min` | number | optional | v1.0.1 | minimum target humidity, default value `30` [see](https://www.home-assistant.io/integrations/fan.xiaomi_miio/) -| target_humidity: `max` | number | optional | v1.0.1 | maximum target humidity, default value `80` [see](https://www.home-assistant.io/integrations/fan.xiaomi_miio/) +| target_humidity: `min` | number | optional | v1.0.1 | minimum target humidity, default value `30` +| target_humidity: `max` | number | optional | v1.0.1 | maximum target humidity, default value `80` | target_humidity: `step` | number | optional | v1.0.1 | slider step, default value `10` +| target_humidity: `state` | object | optional | v2.0.1 | configuration to ge target_humidity value +| target_humidity: `state:entity` | object | optional | v2.0.1 | target_humidity entity_id, default current entity +| target_humidity: `state:attribute` | object | optional | v2.0.1 | default value `target_humidity` +| target_humidity: `change_action` | function | optional | v2.0.1 | target_humidity change function +| **indicators** | object | optional | v2.0.1 | any indicators, [examples](#indicators). +| indicators: `name` | object | optional | v2.0.1 | the name of your indicator see [examples](#indicators). +| indicators: `name:icon` | string | optional | v2.0.1 | Specify a custom icon from any of the available mdi icons. +| indicators: `name:icon` | object | optional | v2.0.1 | icon object +| indicators: `name:icon:template` | function | optional | v2.0.1 | icon template function +| indicators: `name:icon:style` | function | optional | v2.0.1 | styles +| indicators: `name:unit` | string | optional | v2.0.1 | display unit. +| indicators: `name:round` | number | optional | v2.0.1 | rounding number value. +| indicators: `name:source` | number | optional | v2.0.1 | data source. +| indicators: `name:source:entity` | string | optional | v2.0.1 | indicator entity_id +| indicators: `name:source:attribute` | string | optional | v2.0.1 | entity attribute +| indicators: `name:source:mapper` | function | optional | v2.0.1 | value processing function +| **buttons** | object | optional | v2.0.1 | any buttons, [example](#buttons). +| buttons: `name` | object | optional | v2.0.1 | the name of your button see examples +| buttons: `name:icon` | string | optional | v2.0.1 | Specify a custom icon from any of the available mdi icons. +| buttons: `name:type` | string | optional | v2.0.1 | `dropdown` or `button` default `bitton` +| buttons: `name:order` | number | optional | v2.0.1 | sort order +| buttons: `name:state` | object | optional | v2.0.1 | config to get button state. +| buttons: `name:state:entity` | string | optional | v2.0.1 | button entity_id. +| buttons: `name:state:attribute` | string | optional | v2.0.1 | entity attribute +| buttons: `name:state:mapper` | function | optional | v2.0.1 | state processing function +| buttons: `name:disabled` | function | optional | v2.0.1 | calc disabled button +| buttons: `name:active` | function | optional | v2.0.1 | for type `dropdown` +| buttons: `name:source` | object | optional | v2.0.1 | for type `dropdown` +| buttons: `name:source:item` | string | optional | v2.0.1 | source item, format horizontal: horizontal +| buttons: `name:source:__filter` | function | optional | v2.0.1 | filter function +| buttons: `name:change_action` | function | optional | v2.0.1 | for type `dropdown` +| buttons: `name:toggle_action` | function | optional | v2.0.1 | for type `button` +| buttons: `name:style` | function | optional | v2.0.1 | styles | scale | number | optional | v1.0.3 | UI scale modifier, default is `1`. | tap_action | [action object](#action-object-options) | true | v1.0.4 | Action on click/tap, [examples](#action-object-options-examples). @@ -209,243 +163,51 @@ Can be specified by color name, hexadecimal, rgb, rgba, hsl, hsla, basically any | mini-humidifier-scale | 1 | Scale of the card -### Example usage - -#### Basic card -Basic card example - -```yaml -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device -``` - -#### Entity card -For use Entities card you need to add `group: on` - -Entities card example - - -```yaml -- type: entities - title: Entities - state_color: true - entities: - - type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - group: on - - - entity: switch.living_room_wall_switch_right -``` - -#### Fan mode source - -```yaml -# Abbreviated record, override name only -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - fan_mode_button: - source: - auto: Авто - silent: Тихий - medium: Средний - high: Высокоий - -# Full record to override other parameters -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - fan_mode_button: - source: - auto: - name: Авто - order: 4 -``` - - -#### Led button dropdown list configuration - +#### target humidity -led button dropdown list +> Functions available for the target_humidity: -```yaml -# Abbreviated record, override name only -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - led_button: - type: dropdown - source: - bright: 'Bright' - dim: 'Dim' - 'off': 'Off' - -# Full record to override other parameters -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - led_button: - type: dropdown - source: - bright: - name: Bright - order: 2 - dim: - name: Dim - order: 1 - 'off': - name: 'Off' - order: 0 -``` - - - -#### Power button configuration - - -power button configuration - -```yaml -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - power_button: - type: button - icon: mdi:power -``` +| Name | Type | execution context | arguments | return type | +|------|------|-------------------|-----------|-------------| +|`state:mapper` | function | target_humidity config | current_value, entity, humidifier_entity | any +|`change_action` | function | target_humidity config | value, current_value, entity, humidifier_entity | promise +|`icon:template` | function | target_humidity config | current_value, entity, humidifier_entity | string +|`icon:style` | function | target_humidity config | current_value, entity, humidifier_entity | object +`current_value` - selected value +`value` - target_humidity value +`entity` - target_humidity entity +`humidifier_entity` - humidifier entity +**execution context methods:** -#### Action object options examples +| Name | arguments | description | return type | +|------|-----------|-------------|-------------| +|`toggle_state` | sate | toggle state, example: `this.toggle_state('on') => off` | string +|`call_service` | domain, service, options, | call Home Assistant service | promise +> Configuration example for the target_humidity: ```yaml -# toggle example -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - tap_action: - action: toggle - -# call-service example -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - tap_action: - action: call-service - service: xiaomi_miio.fan_set_led_brightness - service_data: - brightness: 1 - -# navigate example - type: custom:mini-humidifier entity: fan.xiaomi_miio_device - tap_action: - action: navigate - navigation_path: '/lovelace/4' - -# navigate example -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - tap_action: - action: url - url: 'https://www.google.com/' - -# none example -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - tap_action: none - -# more-info for custom entity example -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - tap_action: - action: more-info - entity: sensor.humidity_158d000444c824 -``` - - -#### Always show control buttons - - -Always show control buttons - -```yaml -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - toggle_button: - hide: on - default: on - power_button: - type: button - icon: mdi:power -``` - - -#### Temperature source examples - - -```yaml -# Display temperature using sensor -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - temperature: - source: - entity: sensor.temperature - -# Display temperature from custom attribute -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - temperature: - source: - attribute: use_time - -# Using entity and attribute, display the sensor battery level for example -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - temperature: + target_humidity: + state: + attribute: target_humidity + mapper: (current_value, entity, humidifier_entity) => current_value + icon: + template: (current_value, entity, humidifier_entity) => 'mdi:tray-full' + style: "(current_value, entity, humidifier_entity) => ({ color: 'red' })" unit: '%' - icon: 'mdi:battery' - source: - entity: sensor.temperature - attribute: battery_level -``` - - -#### ZHIMI.HUMIDIFIER.V1 configuration - - -```yaml -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device -# hide the indicator showing the amount of water - depth: - hide: on -# hide dry button - dry_button: - hide: on - fan_mode_button: - source: -# hide auto mode option - auto: - hide: on -# show strong option - strong: - hide: off -``` - - -#### icon template example - -when changing `depth` value we change the icon -```yaml -- type: custom:mini-humidifier - entity: fan.xiaomi_miio_device - depth: - icon_template: > - {% if depth < 10 %} - - {% elseif depth < 45 %} - - {% else %} - - {% endif %} -``` -used plugin [jinja-js](https://github.com/sstur/jinja-js) - -expample + hide: off + min: 30 + max: 80 + step: 10 + change_action: > + (value, current_value, entity, humidifier_entity) => { + const options = { entity_id: entity.entity_id, humidity: value }; + return this.call_service('xiaomi_miio', 'fan_set_target_humidity', options); + } +``` ## Development diff --git a/src/main.js b/src/main.js index 949acce..5775c4d 100644 --- a/src/main.js +++ b/src/main.js @@ -388,7 +388,7 @@ class MiniHumidifier extends LitElement { max: 80, step: 10, hide: false, - source: { entity: undefined, attribute: 'target_humidity' }, + state: { entity: undefined, attribute: 'target_humidity' }, change_action: (selected, entity) => { const options = { entity_id: entity.entity_id, humidity: selected }; return this.call_service('xiaomi_miio', 'fan_set_target_humidity', options); @@ -416,6 +416,10 @@ class MiniHumidifier extends LitElement { item.functions.change_action = compileTemplate(item.change_action, context); } + if (item.state && item.state.mapper) { + item.functions.state = { mapper: compileTemplate(item.state.mapper, context) }; + } + return item; } diff --git a/src/models/targetHumidity.js b/src/models/targetHumidity.js index e060613..797aa9d 100644 --- a/src/models/targetHumidity.js +++ b/src/models/targetHumidity.js @@ -20,8 +20,20 @@ export default class TargetHumidityObject { return this.config.target_humidity.step; } + get originalValue() { + return getEntityValue(this.entity, this.config.target_humidity.state); + } + get value() { - return getEntityValue(this.entity, this.config.target_humidity.source); + const value = this.originalValue; + + if (this.config.target_humidity.functions.state + && this.config.target_humidity.functions.state.mapper) { + return this.config.target_humidity.functions.state.mapper(value, this.entity, + this.humidifier.entity); + } + + return value; } get icon() { @@ -55,21 +67,9 @@ export default class TargetHumidityObject { return this.config.target_humidity.unit; } - get state() { - let state = this.value; - - if (this.config.target_humidity.functions.state - && this.config.target_humidity.functions.state.mapper) { - state = this.config.target_humidity.functions.state.mapper(state, this.entity, - this.humidifier.entity); - } - - return state; - } - handleChange(value) { if (this.config.target_humidity.functions.change_action) { - return this.config.target_humidity.functions.change_action(value, this.state, + return this.config.target_humidity.functions.change_action(value, this.value, this.entity, this.humidifier.entity); } From 7731424544384c7b5ac4bf6d2c82cff07e5db490 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Thu, 4 Jun 2020 00:33:31 +0300 Subject: [PATCH 8/9] update README.md and fixes --- README.md | 531 ++++++++++++++++++++++++++++++++++++++++ src/components/power.js | 2 - src/main.js | 6 +- src/models/indicator.js | 4 + 4 files changed, 538 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1220cd3..ba467b1 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Inspired by [mini media player](https://github.com/kalkih/mini-media-player). | indicators: `name:icon:style` | function | optional | v2.0.1 | styles | indicators: `name:unit` | string | optional | v2.0.1 | display unit. | indicators: `name:round` | number | optional | v2.0.1 | rounding number value. +| indicators: `name:hide` | boolean | optional | v2.0.1 | hide indicator, default `false` | indicators: `name:source` | number | optional | v2.0.1 | data source. | indicators: `name:source:entity` | string | optional | v2.0.1 | indicator entity_id | indicators: `name:source:attribute` | string | optional | v2.0.1 | entity attribute @@ -123,6 +124,7 @@ Inspired by [mini media player](https://github.com/kalkih/mini-media-player). | buttons: `name:type` | string | optional | v2.0.1 | `dropdown` or `button` default `bitton` | buttons: `name:order` | number | optional | v2.0.1 | sort order | buttons: `name:state` | object | optional | v2.0.1 | config to get button state. +| buttons: `name:hide` | object | optional | v2.0.1 | hide button, default `false` | buttons: `name:state:entity` | string | optional | v2.0.1 | button entity_id. | buttons: `name:state:attribute` | string | optional | v2.0.1 | entity attribute | buttons: `name:state:mapper` | function | optional | v2.0.1 | state processing function @@ -209,6 +211,535 @@ Can be specified by color name, hexadecimal, rgb, rgba, hsl, hsla, basically any } ``` +> The default configuration is configured for `zhimi.humidifier.cb1`, +> to set target humidity, use the service `xiaomi_miio.fan_set_target_humidity` +> Example: + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + target_humidity: + icon: 'mdi:water' + state: + attribute: target_humidity + unit: '%' + min: 30 + max: 80 + step: 10 + change_action: > + (selected, _, entity) => { + const options = { entity_id: entity.entity_id, humidity: selected }; + return this.call_service('xiaomi_miio', 'fan_set_target_humidity', options); + } +``` +#### power button + +> Functions available for the power: + +| Name | Type | execution context | arguments | return type | +|------|------|-------------------|-----------|-------------| +|`state:mapper` | function | power config | state, entity, humidifier_entity | string +|`disabled` | function | power config | state, entity, humidifier_entity | boolean +|`style` | function | power config | state, entity, humidifier_entity | object +|`toggle_action` | function | power config | state, entity, humidifier_entity | promise + +`state` - current power state +`entity` - current power entity +`humidifier_entity` - humidifier entity + +**execution context methods:** + +| Name | arguments | description | return type | +|------|-----------|-------------|-------------| +|`toggle_state` | sate | toggle state, example: `this.toggle_state('on') => off` | string +|`call_service` | domain, service, options, | call Home Assistant service | promise + +> The power button can be of two types: `button` or `toggle`, default type: `button` +> Attention, the following configuration attributes (icon, disabled, state:attribute, style, toggle_action) are not available for the toggle type, +> since a standard ha-entity-toggle is used, the state of which I do not control + +> Configuration example for the power button type `toggle`: + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + power: + hide: off + state: + mapper: (state, entity, humidifier_entity) => state +``` + +> Configuration example for the power button type `button`: + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + power: + icon: 'mdi:power' + type: button + state: + mapper: (state, entity, humidifier_entity) => state + hide: off + disabled: (state, entity, humidifier_entity) => false + style: "(state, entity, humidifier_entity) => ({ color: 'red' })" + toggle_action: > + (state, entity) => { + const service = state === 'on' ? 'turn_off' : 'turn_on'; + return this.call_service('fan', service, { entity_id: entity.entity_id }); + } +``` +> The default configuration is configured for `zhimi.humidifier.cb1`, +> to on / off, use the service `fan.turn_on`, `fan.turn_off` +> Example: + + ```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + power: + icon: 'mdi:power' + type: button + hide: off + toggle_action: > + (current_state, entity) => { + const service = current_state === 'on' ? 'turn_off' : 'turn_on'; + return this.call_service('fan', service, { entity_id: entity.entity_id }); + } + ``` + +#### Indicators + +> The indicators display additional information on the card, for example, you can display humidity, depth, temperature, etc. +> The default configuration for `zhimi.humidifier.cb1` uses three indicators depth, temperature, humidity. +> [zhimi.humidifier.cb1 indicators](#default-indicators) + +> Adding a simple indicator: +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + indicators: + test: + icon: mdi:water + unit: '%' + round: 1 + source: + entity: sensor.humidity +``` +##### indicator functions + +> Consider configuring an indicator using javascript +> Functions available for the indicator: + +| Name | Type | execution context | arguments | return type | +|------|------|-------------------|-----------|-------------| +|`source:mapper` | function | indicator config | value, entity, humidifier_entity | any +|`icon:template` | function | indicator config | value, entity, humidifier_entity | string +|`icon:style` | function | indicator config | value, entity, humidifier_entity | object + +`value` - current indicator value +`entity` - indicator entity +`humidifier_entity` - humidifier entity + +##### source mapper + +> Using the mapper function, you can change the indicator value: +> For zhimi.humidifier.cb1, a maximum depth value of 125 is used, which is 4 liters of tank, +> let's get how much water is left in liters or in percent +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + indicators: + depth: + icon: 'mdi:tray-full' + unit: '%' + round: 0 + # variable used in mapper + max_value: 125 + # variable used in mapper + volume: 4 + # variable used in mapper + type: 'percent' + source: + attribute: depth + mapper: > + (val) => { + const value = (100 * (val || 0)) / this.max_value; + return this.type === 'liters' ? (value * this.volume) / 100 : value; + } +``` + +##### icon template, style + +> The indicator icon can be calculated dynamically + for example: +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + indicators: + depth: + icon: + template: > + (value) => { + if (value === 0) + return 'mdi:tray'; + + if (value <= 20) + return 'mdi:tray-minus'; + + return 'mdi:tray-full'; + } + style: > + (value) => { + if (value === 0) + return { color: 'red' }; + + if (value <= 20) + return { color: '#FD451D' }; + + return {}; + } + unit: '%' + round: 0 + # variable used in mapper + max_value: 125 + # variable used in mapper + volume: 4 + # variable used in mapper + type: 'liters' + source: + attribute: depth + mapper: > + (val) => { + const value = (100 * (val || 0)) / this.max_value; + return this.type === 'liters' ? (value * this.volume) / 100 : value; + } +``` + +##### default-indicators + +> The plugin is configured by default for zhimi.humidifier.cb1 and 3 default indicators are available in it temperature, humidity, depth +> Their configuration looks like this: + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + indicators: + depth: + icon: 'mdi:tray-full' + unit: '%' + round: 0 + order: 0 + max_value: 125 + volume: 4 + type: 'percent' + source: + attribute: depth + mapper: > + (val) => { + const value = (100 * (val || 0)) / this.max_value; + return this.type === 'liters' ? (value * this.volume) / 100 : value; + } + temperature: + icon: 'mdi:thermometer-low' + unit: '°C' + round: 1 + order: 1 + source: + attribute: temperature + humidity: + icon: 'mdi:water' + unit: '%' + round: 1 + order: 2 + source: + attribute: humidity +``` + +> You can override the default indicators or even hide them and add your own +> We will display the depth value in liters and change the humidity icon as well as hide the temperature: + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + indicators: + depth: + unit: 'L' + type: 'liters' + humidity: + icon: 'mdi:water-outline' + temperature: + hide: on +``` + +#### Buttons + +> You can add various buttons, supported types: button and dropdown + +##### buttons functions + +| Name | Type | execution context | arguments | return type | +|------|------|-------------------|-----------|-------------| +|`state:mapper` | function | button config | state, entity, humidifier_entity | any +|`source:__filter` | function | button config | state, entity, humidifier_entity | object({ id..., name... }) array +|`active` | function | button config | state, entity, humidifier_entity | boolean +|`disabled` | function | button config | state, entity, humidifier_entity | boolean +|`style` | function | button config | state, entity, humidifier_entity | object +|`toggle_action` | function | button config | state, entity, humidifier_entity | promise +|`change_action` | function | button config | selected, state, entity, humidifier_entity | promise + +`state` - current button state value +`entity` - button entity +`humidifier_entity` - humidifier entity +`source` - dropdown source object array: [ { id: 'id', name: 'name' }, ... ] +`selected` - selected dropdown value + +**execution context methods:** + +| Name | arguments | description | return type | +|------|-----------|-------------|-------------| +|`toggle_state` | sate | toggle state, example: `this.toggle_state('on') => off` | string +|`call_service` | domain, service, options, | call Home Assistant service | promise + + +##### default buttons + +> The following buttons are added to the default configuration: dry, mode, led, buzzer, child_lock +> These buttons are configured for zhimi.humidifier.cb1 +> It looks like this: + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + buttons: + dry: + icon: 'mdi:weather-sunny' + order: 0 + state: + attribute: dry + # the dry attribute is of type boolean, for the button the state should be on/off/closed/locked/unavailable/unknown + mapper: "(state) => (state ? 'on' : 'off')" + # service is used xiaomi_miio.fan_set_dry_on or xiaomi_miio.fan_set_dry_off + toggle_action: > + (state, entity) => { + const service = state === 'on' ? 'fan_set_dry_off' : 'fan_set_dry_on'; + const options = { entity_id: entity.entity_id }; + return this.call_service('xiaomi_miio', service, options); + } + # dropdown example + mode: + icon: 'mdi:fan' + order: 1 + type: dropdown + state: + attribute: mode + source: + auto: auto + silent: silent + medium: medium + high: high + # The button will light up when the humidifier is on. + active: "(state, entity) => (entity.state !== 'off')" + # the button will be locked when depth is 0 + # zhimi.humidifier.cb1 does not allow changing the mode when there is no water + disabled: "(state, entity) => (entity.attributes.depth === 0)" + # using service: fan.set_speed + change_action: > + (selected, entity) => { + const options = { entity_id: entity.entity_id, speed: selected }; + return this.call_service('fan', 'set_speed', options); + } + led: + icon: mdi:lightbulb-on-outline + order: 2 + type: dropdown + state: + attribute: led_brightness + source: + 0: Bright + 1: Dim + 2: Off + # button is active while any state except 2 is selected + active: "state => (state !== 2 && state !== '2')" + # using service: xiaomi_miio.fan_set_led_brightness + change_action: > + (selected, entity) => { + const options = { entity_id: entity.entity_id, brightness: selected }; + return this.call_service('xiaomi_miio', 'fan_set_led_brightness', options); + } + buzzer: + icon: 'mdi:bell-outline' + order: 3 + state: + attribute: buzzer + mapper: "(state) => (state ? 'on' : 'off')" + # using service: xiaomi_miio.fan_set_buzzer_on and xiaomi_miio.fan_set_buzzer_off + toggle_action: > + (state, entity) => { + const service = state === 'on' ? 'fan_set_buzzer_off' : 'fan_set_buzzer_on'; + const options = { entity_id: entity.entity_id }; + return this.call_service('xiaomi_miio', service, options); + } + child_lock: + icon: 'mdi:lock' + order: 4 + state: + attribute: child_lock + mapper: "(state) => (state ? 'on' : 'off')" + # using service: xiaomi_miio.fan_set_child_lock_on and xiaomi_miio.fan_set_child_lock_off + toggle_action: > + (state, entity) => { + const service = state === 'on' ? 'fan_set_child_lock_off' : 'fan_set_child_lock_on'; + const options = { entity_id: entity.entity_id }; + return this.call_service('xiaomi_miio', service, options); + } +``` + +> You can override the default indicators or even hide them and add your own +> Let's add translations for the mode and led buttons and hide the child_lock button + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + buttons: + mode: + source: + auto: Авто + silent: Тихий + medium: Средний + high: Высокий + led: + source: + 0: Ярко + 1: Тускло + 2: Выкл + child_lock: + hide: on +``` + +> For some models of humidifiers, there are only two button backlight modes, +> let's change our drop-down list to a button for an example. +> it can be done in different ways, consider a few: + +> 1. override current led button + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + buttons: + led: + type: 'button' + on_states: [0, 1] + off_value: 2 + on_value: 1 + state: + attribute: led_brightness + mapper: "(value) => (this.on_states.includes(value) ? 'on' : 'off')" + toggle_action: > + (state, entity) => { + const value = state === 'on' ? this.off_value : this.on_value; + const options = { entity_id: entity.entity_id, brightness: value }; + return this.call_service('xiaomi_miio', 'fan_set_led_brightness', options); + } +``` + +> 2. Hide led button and add new + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + buttons: + led: + hide: on + new_led: + icon: 'mdi:lightbulb-on-outline' + type: 'button' + order: 2 + on_states: [0, 1] + off_value: 2 + on_value: 1 + state: + attribute: led_brightness + mapper: "(value) => (this.on_states.includes(value) ? 'on' : 'off')" + toggle_action: > + (state, entity) => { + const value = state === 'on' ? this.off_value : this.on_value; + const options = { entity_id: entity.entity_id, brightness: value }; + return this.call_service('xiaomi_miio', 'fan_set_led_brightness', options); + } +``` + +#### toggle button + +> toggle button configuration + +> For example, we want to always show control buttons, and toggle button hide: + +```yaml +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + toggle: + default: on + hide: on +``` + +#### group + +> To display entities inside the container, set the group configuration parameter to `on` + +```yaml +- type: entities + title: Climate + show_header_toggle: true + state_color: true + entities: + - entity: fan.xiaomi_miio_device + type: custom:mini-humidifier + group: on +``` + +#### Action object options examples + +```yaml +# toggle example +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + tap_action: + action: toggle + +# call-service example +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + tap_action: + action: call-service + service: xiaomi_miio.fan_set_led_brightness + service_data: + brightness: 1 + +# navigate example +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + tap_action: + action: navigate + navigation_path: '/lovelace/4' + +# navigate example +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + tap_action: + action: url + url: 'https://www.google.com/' + +# none example +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + tap_action: none + +# more-info for custom entity example +- type: custom:mini-humidifier + entity: fan.xiaomi_miio_device + tap_action: + action: more-info + entity: sensor.humidity +``` ## Development *If you plan to contribute back to this repo, please fork & create the PR against the [dev](https://github.com/artem-sedykh/mini-humidifier/tree/dev) branch.* diff --git a/src/components/power.js b/src/components/power.js index cc89261..a0d072c 100644 --- a/src/components/power.js +++ b/src/components/power.js @@ -1,5 +1,4 @@ import { css, html, LitElement } from 'lit-element'; -import { styleMap } from 'lit-html/directives/style-map'; import sharedStyle from '../sharedStyle'; import './button'; @@ -24,7 +23,6 @@ class PowerButton extends LitElement { if (this.power.type === 'toggle') { return html` diff --git a/src/main.js b/src/main.js index 5775c4d..3c1ceeb 100644 --- a/src/main.js +++ b/src/main.js @@ -196,7 +196,7 @@ class MiniHumidifier extends LitElement { order: 0, max_value: 125, volume: 4, - type: 'liters', + type: 'percent', hide: false, source: { attribute: 'depth', @@ -220,7 +220,7 @@ class MiniHumidifier extends LitElement { round: 1, order: 2, hide: false, - source: { attribute: 'temperature' }, + source: { attribute: 'humidity' }, }, }; @@ -389,7 +389,7 @@ class MiniHumidifier extends LitElement { step: 10, hide: false, state: { entity: undefined, attribute: 'target_humidity' }, - change_action: (selected, entity) => { + change_action: (selected, _, entity) => { const options = { entity_id: entity.entity_id, humidity: selected }; return this.call_service('xiaomi_miio', 'fan_set_target_humidity', options); }, diff --git a/src/models/indicator.js b/src/models/indicator.js index 2438e18..366b84f 100644 --- a/src/models/indicator.js +++ b/src/models/indicator.js @@ -33,6 +33,10 @@ export default class IndicatorObject { return this.config.unit; } + get hide() { + return this.config.hide; + } + get icon() { if (this.config.functions.icon && this.config.functions.icon.template) { return this.config.functions.icon.template(this.value, this.entity, From ab2e13725918b33f3262cbdc4fe15bb29a3b67d4 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Thu, 4 Jun 2020 01:00:14 +0300 Subject: [PATCH 9/9] update info.md, refactoring --- info.md | 15 +++++---------- release_notes/v2.0.1.md | 10 ++++++++++ src/components/power.js | 1 - src/utils/utils.js | 10 ++++++---- 4 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 release_notes/v2.0.1.md diff --git a/info.md b/info.md index f677fdf..f21683c 100644 --- a/info.md +++ b/info.md @@ -2,19 +2,14 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/artem-sedykh/mini-humidifier) [![buymeacoffee_badge](https://img.shields.io/badge/Donate-buymeacoffe-ff813f?style=flat)](https://www.buymeacoffee.com/anavrin72) -A minimalistic yet customizable humidifier(Xiaomi Smartmi Zhimi Air Humidifier) card for [Home Assistant](https://github.com/home-assistant/home-assistant) Lovelace UI. +A minimalistic yet customizable humidifier card for [Home Assistant](https://github.com/home-assistant/home-assistant) Lovelace UI. Tested on [zhimi.humidifier.cb1](https://www.home-assistant.io/integrations/fan.xiaomi_miio/) -![Preview Image](https://user-images.githubusercontent.com/861063/79672681-0f241580-81dd-11ea-913c-234c287a6264.png) +> Attention! The config version *v1.0.8* **very differs** from version >= *v2.0.1* +> Read the [README](https://github.com/artem-sedykh/mini-humidifier) before upgrading -## Examples +![Preview Image](https://user-images.githubusercontent.com/861063/83651482-3171c700-a5c2-11ea-8053-f66472a8d539.png) -#### Basic card -Basic card example -#### Entity card -Entities card example - - -**Check the repository for card options & example configurations** +**Check the [repository](https://github.com/artem-sedykh/mini-humidifier) for card options & example configurations** diff --git a/release_notes/v2.0.1.md b/release_notes/v2.0.1.md new file mode 100644 index 0000000..39306e1 --- /dev/null +++ b/release_notes/v2.0.1.md @@ -0,0 +1,10 @@ +## v2.0.1 +[![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-humidifier/v2.0.1/total.svg)](https://github.com/artem-sedykh/mini-humidifier/releases/tag/v2.0.1) +### Attention +> The config version *v1.0.8* **very differs** from version >= *v2.0.1* +> No backward compatibility, Sorry +### CHANGES + +- Added ability to redefine all control functions for compatibility with different humidifier integrations +- The default configuration is saved for the `zhimi.humidifier.cb1` model +- Read [README](https://github.com/artem-sedykh/mini-humidifier/blob/master/README.md) and decide to upgrade or not diff --git a/src/components/power.js b/src/components/power.js index a0d072c..65e798a 100644 --- a/src/components/power.js +++ b/src/components/power.js @@ -6,7 +6,6 @@ class PowerButton extends LitElement { constructor() { super(); this._isOn = false; - this.timer = undefined; } static get properties() { diff --git a/src/utils/utils.js b/src/utils/utils.js index da329b1..19b4c40 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,12 +1,14 @@ +import { STATES_OFF, UNAVAILABLE_STATES } from '../const'; + const toggleState = (state) => { if (!state) return state; - if (state.toString().trim().toUpperCase() === 'ON') - return 'OFF'; + if (!STATES_OFF.includes(state) && !UNAVAILABLE_STATES.includes(state)) + return 'off'; - if (state.toString().trim().toUpperCase() === 'OFF') - return 'ON'; + if (STATES_OFF.includes(state) && !UNAVAILABLE_STATES.includes(state)) + return 'on'; return state; };