From 7bc2ca3b6551422bb2ff9237077bfd35bad10d41 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 20 Jun 2023 15:25:26 +0200 Subject: [PATCH] Add more info lock (#15995) * Add more info lock * Use same height for pending state * Fix attributes * Add unlocking/locking to switch * Improve code support --- src/data/lock.ts | 20 ++ .../dialog-enter-code.ts | 21 +- .../show-enter-code-dialog.ts | 3 +- .../ha-more-info-alarm_control_panel-modes.ts | 2 +- .../lock/ha-more-info-lock-toggle.ts | 199 ++++++++++++++ src/dialogs/more-info/const.ts | 1 + .../controls/more-info-alarm_control_panel.ts | 2 +- .../more-info/controls/more-info-lock.ts | 248 ++++++++++++++---- .../hui-alarm-modes-tile-feature.ts | 2 +- src/translations/en.json | 5 + 10 files changed, 438 insertions(+), 65 deletions(-) create mode 100644 src/data/lock.ts rename src/dialogs/{more-info/components/alarm_control_panel => enter-code}/dialog-enter-code.ts (91%) rename src/dialogs/{more-info/components/alarm_control_panel => enter-code}/show-enter-code-dialog.ts (91%) create mode 100644 src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts diff --git a/src/data/lock.ts b/src/data/lock.ts new file mode 100644 index 0000000000..a6c9914559 --- /dev/null +++ b/src/data/lock.ts @@ -0,0 +1,20 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; + +export const FORMAT_TEXT = "text"; +export const FORMAT_NUMBER = "number"; + +export const enum LockEntityFeature { + OPEN = 1, +} + +interface LockEntityAttributes extends HassEntityAttributeBase { + code_format?: string; + changed_by?: string | null; +} + +export interface LockEntity extends HassEntityBase { + attributes: LockEntityAttributes; +} diff --git a/src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts b/src/dialogs/enter-code/dialog-enter-code.ts similarity index 91% rename from src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts rename to src/dialogs/enter-code/dialog-enter-code.ts index acacb74666..b4cfc63016 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts +++ b/src/dialogs/enter-code/dialog-enter-code.ts @@ -1,14 +1,15 @@ import { mdiCheck, mdiClose } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-button"; -import "../../../../components/ha-control-button"; -import { createCloseHeading } from "../../../../components/ha-dialog"; -import "../../../../components/ha-textfield"; -import type { HaTextField } from "../../../../components/ha-textfield"; -import { HomeAssistant } from "../../../../types"; -import { HassDialog } from "../../../make-dialog-manager"; +import { ifDefined } from "lit/directives/if-defined"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-button"; +import "../../components/ha-control-button"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-textfield"; +import type { HaTextField } from "../../components/ha-textfield"; +import { HomeAssistant } from "../../types"; +import { HassDialog } from "../make-dialog-manager"; import { EnterCodeDialogParams } from "./show-enter-code-dialog"; const BUTTONS = [ @@ -72,7 +73,8 @@ export class DialogEnterCode } private _inputValueChange(e) { - const val = (e.currentTarget! as any).value; + const field = e.currentTarget as HaTextField; + const val = field.value; this._showClearButton = !!val; } @@ -97,6 +99,7 @@ export class DialogEnterCode id="code" .label=${this.hass.localize("ui.dialogs.enter_code.input_label")} type="password" + pattern=${ifDefined(this._dialogParams.codePattern)} input-mode="text" > diff --git a/src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts b/src/dialogs/enter-code/show-enter-code-dialog.ts similarity index 91% rename from src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts rename to src/dialogs/enter-code/show-enter-code-dialog.ts index 802ae23fb1..6356c20364 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts +++ b/src/dialogs/enter-code/show-enter-code-dialog.ts @@ -1,7 +1,8 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; +import { fireEvent } from "../../common/dom/fire_event"; export interface EnterCodeDialogParams { codeFormat: "text" | "number"; + codePattern?: string; submitText?: string; cancelText?: string; title?: string; diff --git a/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts b/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts index ea8881c5b5..22f028621b 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts +++ b/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts @@ -14,7 +14,7 @@ import { } from "../../../../data/alarm_control_panel"; import { UNAVAILABLE } from "../../../../data/entity"; import { HomeAssistant } from "../../../../types"; -import { showEnterCodeDialogDialog } from "./show-enter-code-dialog"; +import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog"; @customElement("ha-more-info-alarm_control_panel-modes") export class HaMoreInfoAlarmControlPanelModes extends LitElement { diff --git a/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts b/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts new file mode 100644 index 0000000000..f8a005002c --- /dev/null +++ b/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts @@ -0,0 +1,199 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +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 { LockEntity } from "../../../../data/lock"; +import { HomeAssistant } from "../../../../types"; +import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog"; + +@customElement("ha-more-info-lock-toggle") +export class HaMoreInfoLockToggle extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LockEntity; + + @state() private _isOn = false; + + public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("stateObj")) { + this._isOn = + this.stateObj.state === "locked" || this.stateObj.state === "locking"; + } + } + + private _valueChanged(ev) { + const checked = ev.target.checked as boolean; + + if (checked) { + this._turnOn(); + } else { + this._turnOff(); + } + } + + private async _turnOn() { + this._isOn = true; + try { + await this._callService(true); + } catch (err) { + this._isOn = false; + } + } + + private async _turnOff() { + this._isOn = false; + try { + await this._callService(false); + } catch (err) { + this._isOn = true; + } + } + + private async _callService(turnOn: boolean): Promise { + if (!this.hass || !this.stateObj) { + return; + } + forwardHaptic("light"); + + let code: string | undefined; + + if (this.stateObj.attributes.code_format) { + const response = await showEnterCodeDialogDialog(this, { + codeFormat: "text", + codePattern: this.stateObj.attributes.code_format, + title: this.hass.localize( + `ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}` + ), + submitText: this.hass.localize( + `ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}` + ), + }); + if (response == null) { + throw new Error("cancel"); + } + code = response; + } + + await this.hass.callService("lock", turnOn ? "lock" : "unlock", { + entity_id: this.stateObj.entity_id, + code, + }); + } + + protected render(): TemplateResult { + const locking = this.stateObj.state === "locking"; + const unlocking = this.stateObj.state === "unlocking"; + + const color = stateColorCss(this.stateObj); + + const onIcon = domainIcon( + "lock", + this.stateObj, + locking ? "locking" : "locked" + ); + + const offIcon = domainIcon( + "lock", + this.stateObj, + unlocking ? "unlocking" : "unlocked" + ); + + if (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-more-info-lock-toggle": HaMoreInfoLockToggle; + } +} diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index 8675c499e1..79af1e13d3 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -22,6 +22,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [ "fan", "input_boolean", "light", + "lock", "siren", "switch", ]; diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts index 7fadaa5776..7f73407ae1 100644 --- a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts +++ b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts @@ -7,8 +7,8 @@ import { stateColorCss } from "../../../common/entity/state_color"; import "../../../components/ha-outlined-button"; import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel"; import type { HomeAssistant } from "../../../types"; +import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog"; import "../components/alarm_control_panel/ha-more-info-alarm_control_panel-modes"; -import { showEnterCodeDialogDialog } from "../components/alarm_control_panel/show-enter-code-dialog"; import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; import "../components/ha-more-info-state-header"; diff --git a/src/dialogs/more-info/controls/more-info-lock.ts b/src/dialogs/more-info/controls/more-info-lock.ts index 33afb0e93f..5fa6ec5df3 100644 --- a/src/dialogs/more-info/controls/more-info-lock.ts +++ b/src/dialogs/more-info/controls/more-info-lock.ts @@ -1,43 +1,156 @@ -import "@material/mwc-button"; -import type { HassEntity } from "home-assistant-js-websocket"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import "@material/web/iconbutton/outlined-icon-button"; +import { mdiDoorOpen, mdiLock, mdiLockOff } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { domainIcon } from "../../../common/entity/domain_icon"; +import { stateColorCss } from "../../../common/entity/state_color"; +import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; -import "../../../components/ha-textfield"; -import type { HaTextField } from "../../../components/ha-textfield"; +import { UNAVAILABLE } from "../../../data/entity"; +import { LockEntity, LockEntityFeature } from "../../../data/lock"; import type { HomeAssistant } from "../../../types"; +import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog"; +import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; +import "../components/ha-more-info-state-header"; +import "../components/lock/ha-more-info-lock-toggle"; @customElement("more-info-lock") class MoreInfoLock extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public stateObj?: LockEntity; - @query("ha-textfield") private _textfield?: HaTextField; + private async _open() { + this._callService("open"); + } + + private async _lock() { + this._callService("lock"); + } + + private async _unlock() { + this._callService("unlock"); + } + + private async _callService(service: "open" | "lock" | "unlock") { + let code: string | undefined; + + if (this.stateObj!.attributes.code_format) { + const response = await showEnterCodeDialogDialog(this, { + codeFormat: "text", + codePattern: this.stateObj!.attributes.code_format, + title: this.hass.localize( + `ui.dialogs.more_info_control.lock.${service}` + ), + submitText: this.hass.localize( + `ui.dialogs.more_info_control.lock.${service}` + ), + }); + if (!response) { + return; + } + code = response; + } + + this.hass.callService("lock", service, { + entity_id: this.stateObj!.entity_id, + code, + }); + } protected render() { if (!this.hass || !this.stateObj) { return nothing; } + + const supportsOpen = supportsFeature(this.stateObj, LockEntityFeature.OPEN); + + const color = stateColorCss(this.stateObj); + const style = { + "--icon-color": color, + }; + + const isJammed = this.stateObj.state === "jammed"; + return html` - ${this.stateObj.attributes.code_format - ? html`
- - ${this.stateObj.state === "locked" - ? html`${this.hass.localize("ui.card.lock.unlock")}` - : html`${this.hass.localize("ui.card.lock.lock")}`} -
` - : ""} + +
+ ${ + this.stateObj.state === "jammed" + ? html` +
+ +
+ +
+
+ ` + : html` + + + ` + } + ${ + supportsOpen || isJammed + ? html` +
+ ${supportsOpen + ? html` + + + + ` + : nothing} + ${isJammed + ? html` + + + + + + + ` + : nothing} +
+ ` + : nothing + } +
+ { const domain = computeDomain(stateObj.entity_id); diff --git a/src/translations/en.json b/src/translations/en.json index c803418f1a..706882414a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -978,6 +978,11 @@ "disarm_action": "Disarm", "arm_title": "Arm", "arm_action": "Arm" + }, + "lock": { + "open": "Open", + "lock": "Lock", + "unlock": "Unlock" } }, "entity_registry": {