From d9b71e754d98e1a321813d8563c947b17dae76b2 Mon Sep 17 00:00:00 2001 From: Marc Geurts Date: Wed, 24 Apr 2024 10:30:34 +0200 Subject: [PATCH] Add lock features for tile card (#20539) * Added lock features for tile card * Change success label --- gallery/src/pages/lovelace/tile-card.ts | 24 +++ src/data/lock.ts | 31 +++- .../more-info/controls/more-info-lock.ts | 11 +- .../hui-lock-commands-card-feature.ts | 127 ++++++++++++++ .../hui-lock-open-door-card-feature.ts | 158 ++++++++++++++++++ src/panels/lovelace/card-features/types.ts | 10 ++ .../create-card-feature-element.ts | 4 + .../hui-card-features-editor.ts | 8 +- src/translations/en.json | 6 + 9 files changed, 369 insertions(+), 10 deletions(-) create mode 100644 src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts create mode 100644 src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts diff --git a/gallery/src/pages/lovelace/tile-card.ts b/gallery/src/pages/lovelace/tile-card.ts index 62ab5abd23..58c5ad77bd 100644 --- a/gallery/src/pages/lovelace/tile-card.ts +++ b/gallery/src/pages/lovelace/tile-card.ts @@ -2,6 +2,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, query } from "lit/decorators"; import { CoverEntityFeature } from "../../../../src/data/cover"; import { LightColorMode } from "../../../../src/data/light"; +import { LockEntityFeature } from "../../../../src/data/lock"; import { VacuumEntityFeature } from "../../../../src/data/vacuum"; import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; @@ -20,6 +21,11 @@ const ENTITIES = [ getEntity("light", "unavailable", "unavailable", { friendly_name: "Unavailable entity", }), + getEntity("lock", "front_door", "locked", { + friendly_name: "Front Door Lock", + device_class: "lock", + supported_features: LockEntityFeature.OPEN, + }), getEntity("climate", "thermostat", "heat", { current_temperature: 73, min_temp: 45, @@ -138,6 +144,24 @@ const CONFIGS = [ - type: "color-temp" `, }, + { + heading: "Lock commands feature", + config: ` +- type: tile + entity: lock.front_door + features: + - type: "lock-commands" + `, + }, + { + heading: "Lock open door feature", + config: ` +- type: tile + entity: lock.front_door + features: + - type: "lock-open-door" + `, + }, { heading: "Vacuum commands feature", config: ` diff --git a/src/data/lock.ts b/src/data/lock.ts index 7d155369f4..26d4217802 100644 --- a/src/data/lock.ts +++ b/src/data/lock.ts @@ -5,9 +5,7 @@ import { import { getExtendedEntityRegistryEntry } from "./entity_registry"; import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog"; import { HomeAssistant } from "../types"; - -export const FORMAT_TEXT = "text"; -export const FORMAT_NUMBER = "number"; +import { UNAVAILABLE } from "./entity"; export const enum LockEntityFeature { OPEN = 1, @@ -24,6 +22,33 @@ export interface LockEntity extends HassEntityBase { type ProtectedLockService = "lock" | "unlock" | "open"; +export function isLocked(stateObj: LockEntity) { + return stateObj.state === "locked"; +} + +export function isUnlocking(stateObj: LockEntity) { + return stateObj.state === "unlocking"; +} + +export function isLocking(stateObj: LockEntity) { + return stateObj.state === "locking"; +} + +export function isJammed(stateObj: LockEntity) { + return stateObj.state === "jammed"; +} + +export function isAvailable(stateObj: LockEntity) { + if (stateObj.state === UNAVAILABLE) { + return false; + } + const assumedState = stateObj.attributes.assumed_state === true; + return ( + assumedState || + (!isLocking(stateObj) && !isUnlocking(stateObj) && !isJammed(stateObj)) + ); +} + export const callProtectedLockService = async ( element: HTMLElement, hass: HomeAssistant, diff --git a/src/dialogs/more-info/controls/more-info-lock.ts b/src/dialogs/more-info/controls/more-info-lock.ts index ac0f45e43a..2236a52351 100644 --- a/src/dialogs/more-info/controls/more-info-lock.ts +++ b/src/dialogs/more-info/controls/more-info-lock.ts @@ -9,11 +9,12 @@ import "../../../components/ha-control-button"; import "../../../components/ha-control-button-group"; import "../../../components/ha-outlined-icon-button"; import "../../../components/ha-state-icon"; -import { UNAVAILABLE } from "../../../data/entity"; import { LockEntity, LockEntityFeature, callProtectedLockService, + isAvailable, + isJammed, } from "../../../data/lock"; import "../../../state-control/lock/ha-state-control-lock-toggle"; import type { HomeAssistant } from "../../../types"; @@ -85,15 +86,13 @@ class MoreInfoLock extends LitElement { "--state-color": color, }; - const isJammed = this.stateObj.state === "jammed"; - return html`
- ${this.stateObj.state === "jammed" + ${isJammed(this.stateObj) ? html`
@@ -125,7 +124,7 @@ class MoreInfoLock extends LitElement { ` : html` @@ -139,7 +138,7 @@ class MoreInfoLock extends LitElement { : nothing}
- ${isJammed + ${isJammed(this.stateObj) ? html` diff --git a/src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts b/src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts new file mode 100644 index 0000000000..b0763a2074 --- /dev/null +++ b/src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts @@ -0,0 +1,127 @@ +import { mdiLock, mdiLockOpen } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { computeDomain } from "../../../common/entity/compute_domain"; + +import "../../../components/ha-control-button"; +import "../../../components/ha-control-button-group"; +import { + callProtectedLockService, + isAvailable, + isLocking, + isUnlocking, + isLocked, +} from "../../../data/lock"; +import { HomeAssistant } from "../../../types"; +import { LovelaceCardFeature } from "../types"; +import { LockCommandsCardFeatureConfig } from "./types"; +import { forwardHaptic } from "../../../data/haptics"; + +export const supportsLockCommandsCardFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return domain === "lock"; +}; + +@customElement("hui-lock-commands-card-feature") +class HuiLockCommandsCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HassEntity; + + @state() private _config?: LockCommandsCardFeatureConfig; + + static getStubConfig(): LockCommandsCardFeatureConfig { + return { + type: "lock-commands", + }; + } + + public setConfig(config: LockCommandsCardFeatureConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + private _onTap(ev): void { + ev.stopPropagation(); + const service = ev.target.dataset.service; + if (!this.hass || !this.stateObj || !service) { + return; + } + forwardHaptic("light"); + callProtectedLockService(this, this.hass, this.stateObj, service); + } + + protected render() { + if ( + !this._config || + !this.hass || + !this.stateObj || + !supportsLockCommandsCardFeature(this.stateObj) + ) { + return nothing; + } + + return html` + + + + + + + + + `; + } + + static get styles(): CSSResultGroup { + return css` + @keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + .pulse { + animation: pulse 1s infinite; + } + ha-control-button-group { + margin: 0 12px 12px 12px; + --control-button-group-spacing: 12px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-lock-commands-card-feature": HuiLockCommandsCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts new file mode 100644 index 0000000000..4b13e44323 --- /dev/null +++ b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts @@ -0,0 +1,158 @@ +import { mdiCheck } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { computeDomain } from "../../../common/entity/compute_domain"; + +import { supportsFeature } from "../../../common/entity/supports-feature"; +import "../../../components/ha-control-button"; +import "../../../components/ha-control-button-group"; +import { + LockEntityFeature, + callProtectedLockService, + isAvailable, +} from "../../../data/lock"; +import { HomeAssistant } from "../../../types"; +import { LovelaceCardFeature } from "../types"; +import { LockOpenDoorCardFeatureConfig } from "./types"; + +export const supportsLockOpenDoorCardFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN); +}; + +const CONFIRM_TIMEOUT_SECOND = 5; +const OPENED_TIMEOUT_SECOND = 3; + +type ButtonState = "normal" | "confirm" | "success"; + +@customElement("hui-lock-open-door-card-feature") +class HuiLockOpenDoorCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HassEntity; + + @state() public _buttonState: ButtonState = "normal"; + + @state() private _config?: LockOpenDoorCardFeatureConfig; + + private _buttonTimeout?: number; + + static getStubConfig(): LockOpenDoorCardFeatureConfig { + return { + type: "lock-open-door", + }; + } + + public setConfig(config: LockOpenDoorCardFeatureConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + private _setButtonState(buttonState: ButtonState, timeoutSecond?: number) { + clearTimeout(this._buttonTimeout); + this._buttonState = buttonState; + if (timeoutSecond) { + this._buttonTimeout = window.setTimeout(() => { + this._buttonState = "normal"; + }, timeoutSecond * 1000); + } + } + + private async _open() { + if (this._buttonState !== "confirm") { + this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND); + return; + } + if (!this.hass || !this.stateObj) { + return; + } + callProtectedLockService(this, this.hass, this.stateObj!, "open"); + + this._setButtonState("success", OPENED_TIMEOUT_SECOND); + } + + protected render() { + if ( + !this._config || + !this.hass || + !this.stateObj || + !supportsLockOpenDoorCardFeature(this.stateObj) + ) { + return nothing; + } + + return html` + ${this._buttonState === "success" + ? html` +
+

+ + ${this.hass.localize("ui.card.lock.open_door_success")} +

+
+ ` + : html` + + + ${this._buttonState === "confirm" + ? this.hass.localize("ui.card.lock.open_door_confirm") + : this.hass.localize("ui.card.lock.open_door")} + + + `} + `; + } + + static get styles(): CSSResultGroup { + return css` + .buttons { + display: flex; + align-items: center; + justify-content: center; + margin-top: 0; + } + ha-control-button { + font-size: 14px; + } + ha-control-button-group { + margin: 0 12px 12px 12px; + --control-button-group-spacing: 12px; + } + .open-button { + width: 130px; + } + .open-button.confirm { + --control-button-background-color: var(--warning-color); + } + .open-success { + font-size: 14px; + line-height: 14px; + display: flex; + align-items: center; + flex-direction: row; + gap: 8px; + font-weight: 500; + color: var(--success-color); + } + ha-control-button-group + ha-attributes:not([empty]) { + margin-top: 16px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-lock-open-door-card-feature": HuiLockOpenDoorCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index 7b3771a2d8..82fa311876 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -26,6 +26,14 @@ export interface LightColorTempCardFeatureConfig { type: "light-color-temp"; } +export interface LockCommandsCardFeatureConfig { + type: "lock-commands"; +} + +export interface LockOpenDoorCardFeatureConfig { + type: "lock-open-door"; +} + export interface FanPresetModesCardFeatureConfig { type: "fan-preset-modes"; style?: "dropdown" | "icons"; @@ -143,6 +151,8 @@ export type LovelaceCardFeatureConfig = | LawnMowerCommandsCardFeatureConfig | LightBrightnessCardFeatureConfig | LightColorTempCardFeatureConfig + | LockCommandsCardFeatureConfig + | LockOpenDoorCardFeatureConfig | NumericInputCardFeatureConfig | SelectOptionsCardFeatureConfig | TargetHumidityCardFeatureConfig diff --git a/src/panels/lovelace/create-element/create-card-feature-element.ts b/src/panels/lovelace/create-element/create-card-feature-element.ts index 4054f4610e..2710c69ebf 100644 --- a/src/panels/lovelace/create-element/create-card-feature-element.ts +++ b/src/panels/lovelace/create-element/create-card-feature-element.ts @@ -14,6 +14,8 @@ import "../card-features/hui-humidifier-toggle-card-feature"; import "../card-features/hui-lawn-mower-commands-card-feature"; import "../card-features/hui-light-brightness-card-feature"; import "../card-features/hui-light-color-temp-card-feature"; +import "../card-features/hui-lock-commands-card-feature"; +import "../card-features/hui-lock-open-door-card-feature"; import "../card-features/hui-numeric-input-card-feature"; import "../card-features/hui-select-options-card-feature"; import "../card-features/hui-target-temperature-card-feature"; @@ -45,6 +47,8 @@ const TYPES: Set = new Set([ "lawn-mower-commands", "light-brightness", "light-color-temp", + "lock-commands", + "lock-open-door", "numeric-input", "select-options", "target-humidity", diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts index 444144925f..c4b938230a 100644 --- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts @@ -35,6 +35,8 @@ import { supportsHumidifierToggleCardFeature } from "../../card-features/hui-hum import { supportsLawnMowerCommandCardFeature } from "../../card-features/hui-lawn-mower-commands-card-feature"; import { supportsLightBrightnessCardFeature } from "../../card-features/hui-light-brightness-card-feature"; import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature"; +import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature"; +import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature"; import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature"; import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature"; import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature"; @@ -56,8 +58,8 @@ const UI_FEATURE_TYPES = [ "climate-preset-modes", "cover-open-close", "cover-position", - "cover-tilt-position", "cover-tilt", + "cover-tilt-position", "fan-preset-modes", "fan-speed", "humidifier-modes", @@ -65,6 +67,8 @@ const UI_FEATURE_TYPES = [ "lawn-mower-commands", "light-brightness", "light-color-temp", + "lock-commands", + "lock-open-door", "numeric-input", "select-options", "target-humidity", @@ -111,6 +115,8 @@ const SUPPORTS_FEATURE_TYPES: Record< "lawn-mower-commands": supportsLawnMowerCommandCardFeature, "light-brightness": supportsLightBrightnessCardFeature, "light-color-temp": supportsLightColorTempCardFeature, + "lock-commands": supportsLockCommandsCardFeature, + "lock-open-door": supportsLockOpenDoorCardFeature, "numeric-input": supportsNumericInputCardFeature, "select-options": supportsSelectOptionsCardFeature, "target-humidity": supportsTargetHumidityCardFeature, diff --git a/src/translations/en.json b/src/translations/en.json index c360ded244..18b28941f5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5948,6 +5948,12 @@ "light-color-temp": { "label": "Light color temperature" }, + "lock-commands": { + "label": "Lock commands" + }, + "lock-open-door": { + "label": "Lock open door" + }, "vacuum-commands": { "label": "Vacuum commands", "commands": "Commands",