From 3e7fa66790b22f3085d693fc702a1415a0a4456d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 18 Dec 2023 13:53:52 +0100 Subject: [PATCH] Add valve entity (#19024) * Add valve entity * Update icon based on device class * Check assumed state first * Reset mode if entity id changes --- src/common/entity/domain_icon.ts | 12 ++ src/common/entity/state_active.ts | 2 + src/common/entity/state_color.ts | 1 + src/components/ha-valve-controls.ts | 102 ++++++++++ src/data/cover.ts | 8 +- src/data/entity_attributes.ts | 3 + src/data/valve.ts | 85 ++++++++ src/dialogs/more-info/const.ts | 2 + .../more-info/controls/more-info-cover.ts | 4 +- .../more-info/controls/more-info-valve.ts | 192 ++++++++++++++++++ .../more-info/state_more_info_control.ts | 1 + src/panels/lovelace/cards/hui-tile-card.ts | 4 + .../create-element/create-row-element.ts | 2 + .../entity-rows/hui-valve-entity-row.ts | 73 +++++++ src/resources/ha-style.ts | 1 + .../valve/ha-state-control-valve-buttons.ts | 145 +++++++++++++ .../valve/ha-state-control-valve-position.ts | 88 ++++++++ .../valve/ha-state-control-valve-toggle.ts | 167 +++++++++++++++ src/translations/en.json | 11 + 19 files changed, 898 insertions(+), 5 deletions(-) create mode 100644 src/components/ha-valve-controls.ts create mode 100644 src/data/valve.ts create mode 100644 src/dialogs/more-info/controls/more-info-valve.ts create mode 100644 src/panels/lovelace/entity-rows/hui-valve-entity-row.ts create mode 100644 src/state-control/valve/ha-state-control-valve-buttons.ts create mode 100644 src/state-control/valve/ha-state-control-valve-position.ts create mode 100644 src/state-control/valve/ha-state-control-valve-toggle.ts diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 565358e9e4..dfbbc69a5e 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -28,10 +28,12 @@ import { mdiLockAlert, mdiLockClock, mdiLockOpen, + mdiMeterGas, mdiMotionSensor, mdiPackage, mdiPackageDown, mdiPackageUp, + mdiPipeValve, mdiPowerPlug, mdiPowerPlugOff, mdiRestart, @@ -274,6 +276,16 @@ export const domainIconWithoutDefault = ( : mdiPackageUp : mdiPackage; + case "valve": + switch (stateObj?.attributes.device_class) { + case "water": + return mdiPipeValve; + case "gas": + return mdiMeterGas; + default: + return mdiPipeValve; + } + case "water_heater": return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler; diff --git a/src/common/entity/state_active.ts b/src/common/entity/state_active.ts index 7c304f0389..16ca41c9c1 100644 --- a/src/common/entity/state_active.ts +++ b/src/common/entity/state_active.ts @@ -42,6 +42,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean { return compareState !== "standby"; case "vacuum": return !["idle", "docked", "paused"].includes(compareState); + case "valve": + return compareState !== "closed"; case "plant": return compareState === "problem"; case "group": diff --git a/src/common/entity/state_color.ts b/src/common/entity/state_color.ts index 0abaca4c17..09145eed83 100644 --- a/src/common/entity/state_color.ts +++ b/src/common/entity/state_color.ts @@ -37,6 +37,7 @@ const STATE_COLORED_DOMAIN = new Set([ "timer", "update", "vacuum", + "valve", "water_heater", ]); diff --git a/src/components/ha-valve-controls.ts b/src/components/ha-valve-controls.ts new file mode 100644 index 0000000000..ae37795951 --- /dev/null +++ b/src/components/ha-valve-controls.ts @@ -0,0 +1,102 @@ +import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js"; +import { CSSResultGroup, LitElement, html, css, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { supportsFeature } from "../common/entity/supports-feature"; +import { + ValveEntity, + ValveEntityFeature, + canClose, + canOpen, + canStop, +} from "../data/valve"; +import type { HomeAssistant } from "../types"; +import "./ha-icon-button"; + +@customElement("ha-valve-controls") +class HaValveControls extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: ValveEntity; + + protected render() { + if (!this.stateObj) { + return nothing; + } + + return html` +
+ + + + + +
+ `; + } + + private _onOpenTap(ev): void { + ev.stopPropagation(); + this.hass.callService("valve", "open_valve", { + entity_id: this.stateObj.entity_id, + }); + } + + private _onCloseTap(ev): void { + ev.stopPropagation(); + this.hass.callService("valve", "close_valve", { + entity_id: this.stateObj.entity_id, + }); + } + + private _onStopTap(ev): void { + ev.stopPropagation(); + this.hass.callService("valve", "stop_valve", { + entity_id: this.stateObj.entity_id, + }); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: block; + } + .state { + white-space: nowrap; + } + .hidden { + visibility: hidden !important; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-valve-controls": HaValveControls; + } +} diff --git a/src/data/cover.ts b/src/data/cover.ts index 28ab3ebe1d..046da65aa3 100644 --- a/src/data/cover.ts +++ b/src/data/cover.ts @@ -65,7 +65,7 @@ export function canOpen(stateObj: CoverEntity) { return false; } const assumedState = stateObj.attributes.assumed_state === true; - return (!isFullyOpen(stateObj) && !isOpening(stateObj)) || assumedState; + return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj)); } export function canClose(stateObj: CoverEntity): boolean { @@ -73,7 +73,7 @@ export function canClose(stateObj: CoverEntity): boolean { return false; } const assumedState = stateObj.attributes.assumed_state === true; - return (!isFullyClosed(stateObj) && !isClosing(stateObj)) || assumedState; + return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj)); } export function canStop(stateObj: CoverEntity): boolean { @@ -85,7 +85,7 @@ export function canOpenTilt(stateObj: CoverEntity): boolean { return false; } const assumedState = stateObj.attributes.assumed_state === true; - return !isFullyOpenTilt(stateObj) || assumedState; + return assumedState || !isFullyOpenTilt(stateObj); } export function canCloseTilt(stateObj: CoverEntity): boolean { @@ -93,7 +93,7 @@ export function canCloseTilt(stateObj: CoverEntity): boolean { return false; } const assumedState = stateObj.attributes.assumed_state === true; - return !isFullyClosedTilt(stateObj) || assumedState; + return assumedState || !isFullyClosedTilt(stateObj); } export function canStopTilt(stateObj: CoverEntity): boolean { diff --git a/src/data/entity_attributes.ts b/src/data/entity_attributes.ts index 3a07ae022a..d7782e1dbf 100644 --- a/src/data/entity_attributes.ts +++ b/src/data/entity_attributes.ts @@ -75,6 +75,9 @@ export const DOMAIN_ATTRIBUTES_UNITS = { vacuum: { battery_level: "%", }, + valve: { + current_position: "%", + }, sensor: { battery_level: "%", }, diff --git a/src/data/valve.ts b/src/data/valve.ts new file mode 100644 index 0000000000..73f5d428ae --- /dev/null +++ b/src/data/valve.ts @@ -0,0 +1,85 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; +import { UNAVAILABLE } from "./entity"; +import { stateActive } from "../common/entity/state_active"; +import { HomeAssistant } from "../types"; + +export const enum ValveEntityFeature { + OPEN = 1, + CLOSE = 2, + SET_POSITION = 4, + STOP = 8, +} + +export function isFullyOpen(stateObj: ValveEntity) { + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 100; + } + return stateObj.state === "open"; +} + +export function isFullyClosed(stateObj: ValveEntity) { + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 0; + } + return stateObj.state === "closed"; +} + +export function isOpening(stateObj: ValveEntity) { + return stateObj.state === "opening"; +} + +export function isClosing(stateObj: ValveEntity) { + return stateObj.state === "closing"; +} + +export function canOpen(stateObj: ValveEntity) { + if (stateObj.state === UNAVAILABLE) { + return false; + } + const assumedState = stateObj.attributes.assumed_state === true; + return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj)); +} + +export function canClose(stateObj: ValveEntity): boolean { + if (stateObj.state === UNAVAILABLE) { + return false; + } + const assumedState = stateObj.attributes.assumed_state === true; + return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj)); +} + +export function canStop(stateObj: ValveEntity): boolean { + return stateObj.state !== UNAVAILABLE; +} + +interface ValveEntityAttributes extends HassEntityAttributeBase { + current_position?: number; + position?: number; +} + +export interface ValveEntity extends HassEntityBase { + attributes: ValveEntityAttributes; +} + +export function computeValvePositionStateDisplay( + stateObj: ValveEntity, + hass: HomeAssistant, + position?: number +) { + const statePosition = stateActive(stateObj) + ? stateObj.attributes.current_position + : undefined; + + const currentPosition = position ?? statePosition; + + return currentPosition && currentPosition !== 100 + ? hass.formatEntityAttributeValue( + stateObj, + "current_position", + Math.round(currentPosition) + ) + : ""; +} diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index 80c5d5994f..65375a6f6d 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -27,6 +27,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [ "lock", "siren", "switch", + "valve", "water_heater", ]; /** Domains with separate more info dialog. */ @@ -61,6 +62,7 @@ export const DOMAINS_WITH_MORE_INFO = [ "timer", "update", "vacuum", + "valve", "water_heater", "weather", ]; diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts index f1f5e06e38..a352002e4a 100644 --- a/src/dialogs/more-info/controls/more-info-cover.ts +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -42,7 +42,9 @@ class MoreInfoCover extends LitElement { protected willUpdate(changedProps: PropertyValues): void { super.willUpdate(changedProps); if (changedProps.has("stateObj") && this.stateObj) { - if (!this._mode) { + const entityId = this.stateObj.entity_id; + const oldEntityId = changedProps.get("stateObj")?.entity_id; + if (!this._mode || entityId !== oldEntityId) { this._mode = supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) || supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION) diff --git a/src/dialogs/more-info/controls/more-info-valve.ts b/src/dialogs/more-info/controls/more-info-valve.ts new file mode 100644 index 0000000000..f7bae2a9be --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-valve.ts @@ -0,0 +1,192 @@ +import { mdiMenu, mdiSwapVertical } from "@mdi/js"; +import { + CSSResultGroup, + LitElement, + PropertyValues, + css, + html, + nothing, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import "../../../components/ha-attributes"; +import "../../../components/ha-icon-button-group"; +import "../../../components/ha-icon-button-toggle"; +import { + ValveEntity, + ValveEntityFeature, + computeValvePositionStateDisplay, +} from "../../../data/valve"; +import "../../../state-control/valve/ha-state-control-valve-buttons"; +import "../../../state-control/valve/ha-state-control-valve-position"; +import "../../../state-control/valve/ha-state-control-valve-toggle"; +import type { HomeAssistant } from "../../../types"; +import "../components/ha-more-info-state-header"; +import { moreInfoControlStyle } from "../components/more-info-control-style"; + +type Mode = "position" | "button"; + +@customElement("more-info-valve") +class MoreInfoValve extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: ValveEntity; + + @state() private _mode?: Mode; + + private _setMode(ev) { + this._mode = ev.currentTarget.mode; + } + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("stateObj") && this.stateObj) { + const entityId = this.stateObj.entity_id; + const oldEntityId = changedProps.get("stateObj")?.entity_id; + if (!this._mode || entityId !== oldEntityId) { + this._mode = supportsFeature( + this.stateObj, + ValveEntityFeature.SET_POSITION + ) + ? "position" + : "button"; + } + } + } + + private get _stateOverride() { + const stateDisplay = this.hass.formatEntityState(this.stateObj!); + + const positionStateDisplay = computeValvePositionStateDisplay( + this.stateObj!, + this.hass + ); + + if (positionStateDisplay) { + return `${stateDisplay} βΈ± ${positionStateDisplay}`; + } + return stateDisplay; + } + + protected render() { + if (!this.hass || !this.stateObj) { + return nothing; + } + + const supportsPosition = supportsFeature( + this.stateObj, + ValveEntityFeature.SET_POSITION + ); + + const supportsOpenClose = + supportsFeature(this.stateObj, ValveEntityFeature.OPEN) || + supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) || + supportsFeature(this.stateObj, ValveEntityFeature.STOP); + + const supportsOpenCloseWithoutStop = + supportsFeature(this.stateObj, ValveEntityFeature.OPEN) && + supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) && + !supportsFeature(this.stateObj, ValveEntityFeature.STOP); + + return html` + +
+
+ ${ + this._mode === "position" + ? html` + ${supportsPosition + ? html` + + ` + : nothing} + ` + : nothing + } + ${ + this._mode === "button" + ? html` + ${supportsOpenCloseWithoutStop + ? html` + + ` + : supportsOpenClose + ? html` + + ` + : nothing} + ` + : nothing + } +
+ ${ + supportsPosition && supportsOpenClose + ? html` + + + + + ` + : nothing + } +
+ + + `; + } + + static get styles(): CSSResultGroup { + return [ + moreInfoControlStyle, + css` + .main-control { + display: flex; + flex-direction: row; + align-items: center; + } + .main-control > * { + margin: 0 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-valve": MoreInfoValve; + } +} diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts index 65a2e0a1a0..2be9b0877b 100644 --- a/src/dialogs/more-info/state_more_info_control.ts +++ b/src/dialogs/more-info/state_more_info_control.ts @@ -35,6 +35,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = { timer: () => import("./controls/more-info-timer"), update: () => import("./controls/more-info-update"), vacuum: () => import("./controls/more-info-vacuum"), + valve: () => import("./controls/more-info-valve"), water_heater: () => import("./controls/more-info-water_heater"), weather: () => import("./controls/more-info-weather"), }; diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index f20eb2ecde..1c06426d1a 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -251,6 +251,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard { return this._renderStateContent(stateObj, ["state", "current_position"]); } + if (domain === "valve" && active) { + return this._renderStateContent(stateObj, ["state", "current_position"]); + } + if (domain === "humidifier") { return this._renderStateContent(stateObj, ["state", "current_humidity"]); } diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index ed39e06c93..091f9b779c 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -49,6 +49,7 @@ const LAZY_LOAD_TYPES = { "time-entity": () => import("../entity-rows/hui-time-entity-row"), "timer-entity": () => import("../entity-rows/hui-timer-entity-row"), "update-entity": () => import("../entity-rows/hui-update-entity-row"), + "valve-entity": () => import("../entity-rows/hui-valve-entity-row"), conditional: () => import("../special-rows/hui-conditional-row"), "weather-entity": () => import("../entity-rows/hui-weather-entity-row"), divider: () => import("../special-rows/hui-divider-row"), @@ -94,6 +95,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { timer: "timer", update: "update", vacuum: "toggle", + valve: "valve", // Temporary. Once climate is rewritten, // water heater should get its own row. water_heater: "climate", diff --git a/src/panels/lovelace/entity-rows/hui-valve-entity-row.ts b/src/panels/lovelace/entity-rows/hui-valve-entity-row.ts new file mode 100644 index 0000000000..1d836e2ddb --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-valve-entity-row.ts @@ -0,0 +1,73 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + nothing, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../components/ha-valve-controls"; +import { ValveEntity } from "../../../data/valve"; +import { HomeAssistant } from "../../../types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import "../components/hui-generic-entity-row"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; +import { EntityConfig, LovelaceRow } from "./types"; + +@customElement("hui-valve-entity-row") +class HuiValveEntityRow extends LitElement implements LovelaceRow { + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EntityConfig; + + public setConfig(config: EntityConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected render() { + if (!this._config || !this.hass) { + return nothing; + } + + const stateObj = this.hass.states[this._config.entity] as ValveEntity; + + if (!stateObj) { + return html` + + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + + `; + } + + return html` + + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-valve-controls { + margin-right: -0.57em; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-valve-entity-row": HuiValveEntityRow; + } +} diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index fa4841c755..21d9b18214 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -174,6 +174,7 @@ const mainStyles = css` --state-switch-active-color: var(--amber-color); --state-update-active-color: var(--orange-color); --state-vacuum-active-color: var(--teal-color); + --state-valve-active-color: var(--blue-color); --state-sensor-battery-high-color: var(--green-color); --state-sensor-battery-low-color: var(--red-color); --state-sensor-battery-medium-color: var(--orange-color); diff --git a/src/state-control/valve/ha-state-control-valve-buttons.ts b/src/state-control/valve/ha-state-control-valve-buttons.ts new file mode 100644 index 0000000000..5421741087 --- /dev/null +++ b/src/state-control/valve/ha-state-control-valve-buttons.ts @@ -0,0 +1,145 @@ +import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js"; +import { + CSSResultGroup, + LitElement, + TemplateResult, + css, + html, + nothing, +} from "lit"; +import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; +import memoizeOne from "memoize-one"; +import { supportsFeature } from "../../common/entity/supports-feature"; +import "../../components/ha-control-button"; +import "../../components/ha-control-button-group"; +import "../../components/ha-control-slider"; +import "../../components/ha-svg-icon"; +import { + ValveEntity, + ValveEntityFeature, + canClose, + canOpen, + canStop, +} from "../../data/valve"; +import { HomeAssistant } from "../../types"; + +type ValveButton = "open" | "close" | "stop" | "none"; + +export const getValveButtons = memoizeOne( + (stateObj: ValveEntity): ValveButton[] => { + const supportsOpen = supportsFeature(stateObj, ValveEntityFeature.OPEN); + const supportsClose = supportsFeature(stateObj, ValveEntityFeature.CLOSE); + const supportsStop = supportsFeature(stateObj, ValveEntityFeature.STOP); + + const buttons: ValveButton[] = []; + if (supportsOpen) buttons.push("open"); + if (supportsStop) buttons.push("stop"); + if (supportsClose) buttons.push("close"); + return buttons; + } +); + +@customElement("ha-state-control-valve-buttons") +export class HaStateControlValveButtons extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: ValveEntity; + + private _onOpenTap(ev): void { + ev.stopPropagation(); + this.hass!.callService("valve", "open_valve", { + entity_id: this.stateObj!.entity_id, + }); + } + + private _onCloseTap(ev): void { + ev.stopPropagation(); + this.hass!.callService("valve", "close_valve", { + entity_id: this.stateObj!.entity_id, + }); + } + + private _onStopTap(ev): void { + ev.stopPropagation(); + this.hass!.callService("valve", "stop_valve", { + entity_id: this.stateObj!.entity_id, + }); + } + + protected renderButton(button: ValveButton | undefined) { + if (button === "open") { + return html` + + + + `; + } + if (button === "close") { + return html` + + + + `; + } + if (button === "stop") { + return html` + + + + `; + } + return nothing; + } + + protected render(): TemplateResult { + const buttons = getValveButtons(this.stateObj); + + return html` + + ${repeat( + buttons, + (button) => button, + (button) => this.renderButton(button) + )} + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-button-group { + height: 45vh; + max-height: 320px; + min-height: 200px; + --control-button-group-spacing: 6px; + --control-button-group-thickness: 100px; + } + ha-control-button { + --control-button-border-radius: 18px; + --mdc-icon-size: 24px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-state-control-valve-buttons": HaStateControlValveButtons; + } +} diff --git a/src/state-control/valve/ha-state-control-valve-position.ts b/src/state-control/valve/ha-state-control-valve-position.ts new file mode 100644 index 0000000000..992c960561 --- /dev/null +++ b/src/state-control/valve/ha-state-control-valve-position.ts @@ -0,0 +1,88 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display"; +import { stateColorCss } from "../../common/entity/state_color"; +import "../../components/ha-control-slider"; +import { CoverEntity } from "../../data/cover"; +import { UNAVAILABLE } from "../../data/entity"; +import { DOMAIN_ATTRIBUTES_UNITS } from "../../data/entity_attributes"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-state-control-valve-position") +export class HaStateControlValvePosition extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: CoverEntity; + + @state() value?: number; + + protected updated(changedProp: Map): void { + if (changedProp.has("stateObj")) { + const currentPosition = this.stateObj?.attributes.current_position; + this.value = + currentPosition != null ? Math.round(currentPosition) : undefined; + } + } + + private _valueChanged(ev: CustomEvent) { + const value = (ev.detail as any).value; + if (isNaN(value)) return; + + this.hass.callService("valve", "set_valve_position", { + entity_id: this.stateObj!.entity_id, + position: value, + }); + } + + protected render(): TemplateResult { + const color = stateColorCss(this.stateObj); + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-slider { + height: 45vh; + max-height: 320px; + min-height: 200px; + --control-slider-thickness: 100px; + --control-slider-border-radius: 24px; + --control-slider-color: var(--primary-color); + --control-slider-background: var(--disabled-color); + --control-slider-background-opacity: 0.2; + --control-slider-tooltip-font-size: 20px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-state-control-valve-position": HaStateControlValvePosition; + } +} diff --git a/src/state-control/valve/ha-state-control-valve-toggle.ts b/src/state-control/valve/ha-state-control-valve-toggle.ts new file mode 100644 index 0000000000..2eb0dbd934 --- /dev/null +++ b/src/state-control/valve/ha-state-control-valve-toggle.ts @@ -0,0 +1,167 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import { domainIcon } from "../../common/entity/domain_icon"; +import { stateColorCss } from "../../common/entity/state_color"; +import "../../components/ha-control-button"; +import "../../components/ha-control-switch"; +import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; +import { forwardHaptic } from "../../data/haptics"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-state-control-valve-toggle") +export class HaStateControlValveToggle extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: HassEntity; + + private _valueChanged(ev) { + const checked = ev.target.checked as boolean; + + if (checked) { + this._turnOn(); + } else { + this._turnOff(); + } + } + + private _turnOn() { + this._callService(true); + } + + private _turnOff() { + this._callService(false); + } + + private async _callService(turnOn): Promise { + if (!this.hass || !this.stateObj) { + return; + } + forwardHaptic("light"); + + await this.hass.callService( + "valve", + turnOn ? "open_valve" : "close_valve", + { + entity_id: this.stateObj.entity_id, + } + ); + } + + protected render(): TemplateResult { + const onColor = stateColorCss(this.stateObj, "open"); + const offColor = stateColorCss(this.stateObj, "closed"); + + const isOn = + this.stateObj.state === "open" || + this.stateObj.state === "closing" || + this.stateObj.state === "opening"; + const isOff = this.stateObj.state === "closed"; + + if ( + this.stateObj.attributes.assumed_state || + this.stateObj.state === UNKNOWN + ) { + return html` +
+ + + + + + +
+ `; + } + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-switch { + height: 45vh; + max-height: 320px; + min-height: 200px; + --control-switch-thickness: 100px; + --control-switch-border-radius: 24px; + --control-switch-padding: 6px; + --mdc-icon-size: 24px; + } + .buttons { + display: flex; + flex-direction: column; + width: 100px; + height: 45vh; + max-height: 320px; + min-height: 200px; + padding: 6px; + box-sizing: border-box; + } + ha-control-button { + flex: 1; + width: 100%; + --control-button-border-radius: 18px; + --mdc-icon-size: 24px; + } + ha-control-button.active { + --control-button-icon-color: white; + --control-button-background-color: var(--color); + --control-button-background-opacity: 1; + } + ha-control-button:not(:last-child) { + margin-bottom: 6px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-state-control-valve-toggle": HaStateControlValveToggle; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 793887c840..0fbc62fdb2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -255,6 +255,11 @@ "turn_off": "Turn off" } }, + "valve": { + "open_valve": "Open valve", + "close_valve": "Close valve", + "stop_valve": "Stop valve" + }, "water_heater": { "currently": "[%key:ui::card::climate::currently%]", "on_off": "On / off", @@ -1094,6 +1099,12 @@ "start_mowing": "Start mowing", "pause": "Pause", "dock": "Return to dock" + }, + "valve": { + "switch_mode": { + "button": "[%key:ui::dialogs::more_info_control::valve::switch_mode::button%]", + "position": "[%key:ui::dialogs::more_info_control::valve::switch_mode::position%]" + } } }, "entity_registry": {