From 2c0b2f4bc5166861c0ca4fd0ba681cee80662613 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 10:30:14 +0100 Subject: [PATCH] Convert cover UI to Lit + ensure proper tilt rendering (#10671) --- gallery/src/demos/demo-more-info-cover.ts | 164 ++++++++++++++++++ src/components/ha-cover-controls.ts | 60 +++---- src/components/ha-cover-tilt-controls.ts | 57 +++--- src/data/cover.ts | 95 ++++++++++ .../more-info/controls/more-info-cover.js | 124 ------------- .../more-info/controls/more-info-cover.ts | 140 +++++++++++++++ .../entity-rows/hui-cover-entity-row.ts | 4 +- src/state-summary/state-card-cover.js | 68 -------- src/state-summary/state-card-cover.ts | 56 ++++++ src/util/cover-model.js | 149 ---------------- 10 files changed, 511 insertions(+), 406 deletions(-) create mode 100644 gallery/src/demos/demo-more-info-cover.ts create mode 100644 src/data/cover.ts delete mode 100644 src/dialogs/more-info/controls/more-info-cover.js create mode 100644 src/dialogs/more-info/controls/more-info-cover.ts delete mode 100644 src/state-summary/state-card-cover.js create mode 100644 src/state-summary/state-card-cover.ts delete mode 100644 src/util/cover-model.js diff --git a/gallery/src/demos/demo-more-info-cover.ts b/gallery/src/demos/demo-more-info-cover.ts new file mode 100644 index 0000000000..f715d5fc9c --- /dev/null +++ b/gallery/src/demos/demo-more-info-cover.ts @@ -0,0 +1,164 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../src/components/ha-card"; +import { + SUPPORT_OPEN, + SUPPORT_STOP, + SUPPORT_CLOSE, + SUPPORT_SET_POSITION, + SUPPORT_OPEN_TILT, + SUPPORT_STOP_TILT, + SUPPORT_CLOSE_TILT, + SUPPORT_SET_TILT_POSITION, +} from "../../../src/data/cover"; +import "../../../src/dialogs/more-info/more-info-content"; +import { getEntity } from "../../../src/fake_data/entity"; +import { + MockHomeAssistant, + provideHass, +} from "../../../src/fake_data/provide_hass"; +import "../components/demo-more-infos"; + +const ENTITIES = [ + getEntity("cover", "position_buttons", "on", { + friendly_name: "Position Buttons", + supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE, + }), + getEntity("cover", "position_slider_half", "on", { + friendly_name: "Position Half-Open", + supported_features: + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + current_position: 50, + }), + getEntity("cover", "position_slider_open", "on", { + friendly_name: "Position Open", + supported_features: + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + current_position: 100, + }), + getEntity("cover", "position_slider_closed", "on", { + friendly_name: "Position Closed", + supported_features: + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + current_position: 0, + }), + getEntity("cover", "tilt_buttons", "on", { + friendly_name: "Tilt Buttons", + supported_features: + SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT, + }), + getEntity("cover", "tilt_slider_half", "on", { + friendly_name: "Tilt Half-Open", + supported_features: + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 50, + }), + getEntity("cover", "tilt_slider_open", "on", { + friendly_name: "Tilt Open", + supported_features: + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 100, + }), + getEntity("cover", "tilt_slider_closed", "on", { + friendly_name: "Tilt Closed", + supported_features: + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 0, + }), + getEntity("cover", "position_slider_tilt_slider", "on", { + friendly_name: "Both Sliders", + supported_features: + SUPPORT_OPEN + + SUPPORT_STOP + + SUPPORT_CLOSE + + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_position: 30, + current_tilt_position: 70, + }), + getEntity("cover", "position_tilt_slider", "on", { + friendly_name: "Position & Tilt Slider", + supported_features: + SUPPORT_OPEN + + SUPPORT_STOP + + SUPPORT_CLOSE + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 70, + }), + getEntity("cover", "position_slider_tilt", "on", { + friendly_name: "Position Slider & Tilt", + supported_features: + SUPPORT_OPEN + + SUPPORT_STOP + + SUPPORT_CLOSE + + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT, + current_position: 30, + }), + getEntity("cover", "position_slider_only_tilt_slider", "on", { + friendly_name: "Position Slider Only & Tilt Buttons", + supported_features: + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT, + current_position: 30, + }), + getEntity("cover", "position_slider_only_tilt", "on", { + friendly_name: "Position Slider Only & Tilt", + supported_features: + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_position: 30, + current_tilt_position: 70, + }), +]; + +@customElement("demo-more-info-cover") +class DemoMoreInfoCover extends LitElement { + @property() public hass!: MockHomeAssistant; + + @query("demo-more-infos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html` + ent.entityId)} + > + `; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.addEntities(ENTITIES); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-more-info-cover": DemoMoreInfoCover; + } +} diff --git a/src/components/ha-cover-controls.ts b/src/components/ha-cover-controls.ts index 85be8bf786..9f28b010d9 100644 --- a/src/components/ha-cover-controls.ts +++ b/src/components/ha-cover-controls.ts @@ -1,39 +1,30 @@ import { mdiStop } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon"; +import { + CoverEntity, + isClosing, + isFullyClosed, + isFullyOpen, + isOpening, + supportsClose, + supportsOpen, + supportsStop, +} from "../data/cover"; import { UNAVAILABLE } from "../data/entity"; import type { HomeAssistant } from "../types"; -import CoverEntity from "../util/cover-model"; import "./ha-icon-button"; @customElement("ha-cover-controls") class HaCoverControls extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj!: HassEntity; - - @state() private _entityObj?: CoverEntity; - - public willUpdate(changedProperties: PropertyValues): void { - super.willUpdate(changedProperties); - - if (changedProperties.has("stateObj")) { - this._entityObj = new CoverEntity(this.hass, this.stateObj); - } - } + @property({ attribute: false }) public stateObj!: CoverEntity; protected render(): TemplateResult { - if (!this._entityObj) { + if (!this.stateObj) { return html``; } @@ -41,7 +32,7 @@ class HaCoverControls extends LitElement {
+ supportsFeature(stateObj, SUPPORT_OPEN); + +export const supportsClose = (stateObj) => + supportsFeature(stateObj, SUPPORT_CLOSE); + +export const supportsSetPosition = (stateObj) => + supportsFeature(stateObj, SUPPORT_SET_POSITION); + +export const supportsStop = (stateObj) => + supportsFeature(stateObj, SUPPORT_STOP); + +export const supportsOpenTilt = (stateObj) => + supportsFeature(stateObj, SUPPORT_OPEN_TILT); + +export const supportsCloseTilt = (stateObj) => + supportsFeature(stateObj, SUPPORT_CLOSE_TILT); + +export const supportsStopTilt = (stateObj) => + supportsFeature(stateObj, SUPPORT_STOP_TILT); + +export const supportsSetTiltPosition = (stateObj) => + supportsFeature(stateObj, SUPPORT_SET_TILT_POSITION); + +export function isFullyOpen(stateObj: CoverEntity) { + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 100; + } + return stateObj.state === "open"; +} + +export function isFullyClosed(stateObj: CoverEntity) { + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 0; + } + return stateObj.state === "closed"; +} + +export function isFullyOpenTilt(stateObj: CoverEntity) { + return stateObj.attributes.current_tilt_position === 100; +} + +export function isFullyClosedTilt(stateObj: CoverEntity) { + return stateObj.attributes.current_tilt_position === 0; +} + +export function isOpening(stateObj: CoverEntity) { + return stateObj.state === "opening"; +} + +export function isClosing(stateObj: CoverEntity) { + return stateObj.state === "closing"; +} + +export function isTiltOnly(stateObj: CoverEntity) { + const supportsCover = + supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj); + const supportsTilt = + supportsOpenTilt(stateObj) || + supportsCloseTilt(stateObj) || + supportsStopTilt(stateObj); + return supportsTilt && !supportsCover; +} + +interface CoverEntityAttributes extends HassEntityAttributeBase { + current_position: number; + current_tilt_position: number; +} + +export interface CoverEntity extends HassEntityBase { + attributes: CoverEntityAttributes; +} diff --git a/src/dialogs/more-info/controls/more-info-cover.js b/src/dialogs/more-info/controls/more-info-cover.js deleted file mode 100644 index 6431959047..0000000000 --- a/src/dialogs/more-info/controls/more-info-cover.js +++ /dev/null @@ -1,124 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { attributeClassNames } from "../../../common/entity/attribute_class_names"; -import { featureClassNames } from "../../../common/entity/feature_class_names"; -import "../../../components/ha-cover-tilt-controls"; -import "../../../components/ha-labeled-slider"; -import LocalizeMixin from "../../../mixins/localize-mixin"; -import CoverEntity from "../../../util/cover-model"; - -const FEATURE_CLASS_NAMES = { - 4: "has-set_position", - 128: "has-set_tilt_position", -}; -class MoreInfoCover extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - -
-
- -
- -
- - - -
-
- - `; - } - - static get properties() { - return { - hass: Object, - stateObj: { - type: Object, - observer: "stateObjChanged", - }, - entityObj: { - type: Object, - computed: "computeEntityObj(hass, stateObj)", - }, - coverPositionSliderValue: Number, - coverTiltPositionSliderValue: Number, - }; - } - - computeEntityObj(hass, stateObj) { - return new CoverEntity(hass, stateObj); - } - - stateObjChanged(newVal) { - if (newVal) { - this.setProperties({ - coverPositionSliderValue: newVal.attributes.current_position, - coverTiltPositionSliderValue: newVal.attributes.current_tilt_position, - }); - } - } - - computeClassNames(stateObj) { - const classes = [ - attributeClassNames(stateObj, [ - "current_position", - "current_tilt_position", - ]), - featureClassNames(stateObj, FEATURE_CLASS_NAMES), - ]; - return classes.join(" "); - } - - coverPositionSliderChanged(ev) { - this.entityObj.setCoverPosition(ev.target.value); - } - - coverTiltPositionSliderChanged(ev) { - this.entityObj.setCoverTiltPosition(ev.target.value); - } -} - -customElements.define("more-info-cover", MoreInfoCover); diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts new file mode 100644 index 0000000000..61faf09af2 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -0,0 +1,140 @@ +import { css, CSSResult, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { attributeClassNames } from "../../../common/entity/attribute_class_names"; +import { featureClassNames } from "../../../common/entity/feature_class_names"; +import "../../../components/ha-attributes"; +import "../../../components/ha-cover-tilt-controls"; +import "../../../components/ha-labeled-slider"; +import { + CoverEntity, + FEATURE_CLASS_NAMES, + isTiltOnly, + supportsSetPosition, + supportsSetTiltPosition, +} from "../../../data/cover"; +import { HomeAssistant } from "../../../types"; + +@customElement("more-info-cover") +class MoreInfoCover extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: CoverEntity; + + protected render(): TemplateResult { + if (!this.stateObj) { + return html``; + } + + const _isTiltOnly = isTiltOnly(this.stateObj); + + return html` +
+
+ +
+ +
+ ${supportsSetTiltPosition(this.stateObj) + ? // Either render the labeled slider and put the tilt buttons into its slot + // or (if tilt position is not supported and therefore no slider is shown) + // render a title
(same style as for a labeled slider) and directly put + // the tilt controls on the more-info. + html` + ${!_isTiltOnly + ? html` ` + : html``} + ` + : !_isTiltOnly + ? html` +
+ ${this.hass.localize("ui.card.cover.tilt_position")} +
+ + ` + : html``} +
+
+ + `; + } + + private _computeClassNames(stateObj) { + const classes = [ + attributeClassNames(stateObj, [ + "current_position", + "current_tilt_position", + ]), + featureClassNames(stateObj, FEATURE_CLASS_NAMES), + ]; + return classes.join(" "); + } + + private _coverPositionSliderChanged(ev) { + this.hass.callService("cover", "set_cover_position", { + entity_id: this.stateObj.entity_id, + position: ev.target.value, + }); + } + + private _coverTiltPositionSliderChanged(ev) { + this.hass.callService("cover", "set_cover_tilt_position", { + entity_id: this.stateObj.entity_id, + tilt_position: ev.target.value, + }); + } + + static get styles(): CSSResult { + return css` + .current_position, + .tilt { + max-height: 0px; + overflow: hidden; + } + + .has-set_position .current_position, + .has-current_position .current_position, + .has-open_tilt .tilt, + .has-close_tilt .tilt, + .has-stop_tilt .tilt, + .has-set_tilt_position .tilt, + .has-current_tilt_position .tilt { + max-height: 208px; + } + + /* from ha-labeled-slider for consistent look */ + .title { + margin: 5px 0 8px; + color: var(--primary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-cover": MoreInfoCover; + } +} diff --git a/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts b/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts index 5503eac028..b42d0dc64d 100644 --- a/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts @@ -9,8 +9,8 @@ import { import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-cover-controls"; import "../../../components/ha-cover-tilt-controls"; +import { CoverEntity, isTiltOnly } from "../../../data/cover"; import { HomeAssistant } from "../../../types"; -import { isTiltOnly } from "../../../util/cover-model"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -38,7 +38,7 @@ class HuiCoverEntityRow extends LitElement implements LovelaceRow { return html``; } - const stateObj = this.hass.states[this._config.entity]; + const stateObj = this.hass.states[this._config.entity] as CoverEntity; if (!stateObj) { return html` diff --git a/src/state-summary/state-card-cover.js b/src/state-summary/state-card-cover.js deleted file mode 100644 index e2e6b54277..0000000000 --- a/src/state-summary/state-card-cover.js +++ /dev/null @@ -1,68 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../components/entity/state-info"; -import "../components/ha-cover-controls"; -import "../components/ha-cover-tilt-controls"; -import CoverEntity from "../util/cover-model"; - -class StateCardCover extends PolymerElement { - static get template() { - return html` - - - -
- ${this.stateInfoTemplate} -
- - -
-
- `; - } - - static get stateInfoTemplate() { - return html` - - `; - } - - static get properties() { - return { - hass: Object, - stateObj: Object, - inDialog: { - type: Boolean, - value: false, - }, - entityObj: { - type: Object, - computed: "computeEntityObj(hass, stateObj)", - }, - }; - } - - computeEntityObj(hass, stateObj) { - const entity = new CoverEntity(hass, stateObj); - return entity; - } -} -customElements.define("state-card-cover", StateCardCover); diff --git a/src/state-summary/state-card-cover.ts b/src/state-summary/state-card-cover.ts new file mode 100644 index 0000000000..9134ff9257 --- /dev/null +++ b/src/state-summary/state-card-cover.ts @@ -0,0 +1,56 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../components/entity/state-info"; +import "../components/ha-cover-controls"; +import "../components/ha-cover-tilt-controls"; +import { CoverEntity, isTiltOnly } from "../data/cover"; +import { haStyle } from "../resources/styles"; +import { HomeAssistant } from "../types"; + +@customElement("state-card-cover") +class StateCardCover extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: CoverEntity; + + @property({ type: Boolean }) public inDialog = false; + + protected render(): TemplateResult { + return html` +
+ + + +
+ `; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + :host { + line-height: 1.5; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-card-cover": StateCardCover; + } +} diff --git a/src/util/cover-model.js b/src/util/cover-model.js deleted file mode 100644 index eae1007cf8..0000000000 --- a/src/util/cover-model.js +++ /dev/null @@ -1,149 +0,0 @@ -import { supportsFeature } from "../common/entity/supports-feature"; - -/* eslint-enable no-bitwise */ -export default class CoverEntity { - constructor(hass, stateObj) { - this.hass = hass; - this.stateObj = stateObj; - this._attr = stateObj.attributes; - this._feat = this._attr.supported_features; - } - - get isFullyOpen() { - if (this._attr.current_position !== undefined) { - return this._attr.current_position === 100; - } - return this.stateObj.state === "open"; - } - - get isFullyClosed() { - if (this._attr.current_position !== undefined) { - return this._attr.current_position === 0; - } - return this.stateObj.state === "closed"; - } - - get isFullyOpenTilt() { - return this._attr.current_tilt_position === 100; - } - - get isFullyClosedTilt() { - return this._attr.current_tilt_position === 0; - } - - get isOpening() { - return this.stateObj.state === "opening"; - } - - get isClosing() { - return this.stateObj.state === "closing"; - } - - get supportsOpen() { - return supportsFeature(this.stateObj, 1); - } - - get supportsClose() { - return supportsFeature(this.stateObj, 2); - } - - get supportsSetPosition() { - return supportsFeature(this.stateObj, 4); - } - - get supportsStop() { - return supportsFeature(this.stateObj, 8); - } - - get supportsOpenTilt() { - return supportsFeature(this.stateObj, 16); - } - - get supportsCloseTilt() { - return supportsFeature(this.stateObj, 32); - } - - get supportsStopTilt() { - return supportsFeature(this.stateObj, 64); - } - - get supportsSetTiltPosition() { - return supportsFeature(this.stateObj, 128); - } - - get isTiltOnly() { - const supportsCover = - this.supportsOpen || this.supportsClose || this.supportsStop; - const supportsTilt = - this.supportsOpenTilt || this.supportsCloseTilt || this.supportsStopTilt; - return supportsTilt && !supportsCover; - } - - openCover() { - this.callService("open_cover"); - } - - closeCover() { - this.callService("close_cover"); - } - - stopCover() { - this.callService("stop_cover"); - } - - openCoverTilt() { - this.callService("open_cover_tilt"); - } - - closeCoverTilt() { - this.callService("close_cover_tilt"); - } - - stopCoverTilt() { - this.callService("stop_cover_tilt"); - } - - setCoverPosition(position) { - this.callService("set_cover_position", { position }); - } - - setCoverTiltPosition(tiltPosition) { - this.callService("set_cover_tilt_position", { - tilt_position: tiltPosition, - }); - } - - // helper method - - callService(service, data = {}) { - data.entity_id = this.stateObj.entity_id; - this.hass.callService("cover", service, data); - } -} - -export const supportsOpen = (stateObj) => supportsFeature(stateObj, 1); - -export const supportsClose = (stateObj) => supportsFeature(stateObj, 2); - -export const supportsSetPosition = (stateObj) => supportsFeature(stateObj, 4); - -export const supportsStop = (stateObj) => supportsFeature(stateObj, 8); - -export const supportsOpenTilt = (stateObj) => supportsFeature(stateObj, 16); - -export const supportsCloseTilt = (stateObj) => supportsFeature(stateObj, 32); - -export const supportsStopTilt = (stateObj) => supportsFeature(stateObj, 64); - -export const supportsSetTiltPosition = (stateObj) => - supportsFeature(stateObj, 128); - -export function isTiltOnly(stateObj) { - const supportsCover = - supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj); - const supportsTilt = - supportsOpenTilt(stateObj) || - supportsCloseTilt(stateObj) || - supportsStopTilt(stateObj); - return supportsTilt && !supportsCover; -}