Add lock features for tile card (#20539)

* Added lock features for tile card

* Change success label
This commit is contained in:
Marc Geurts 2024-04-24 10:30:34 +02:00 committed by GitHub
parent 5fc950f09f
commit d9b71e754d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 369 additions and 10 deletions

View File

@ -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: `

View File

@ -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,

View File

@ -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`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls" style=${styleMap(style)}>
${this.stateObj.state === "jammed"
${isJammed(this.stateObj)
? html`
<div class="status">
<span></span>
@ -125,7 +124,7 @@ class MoreInfoLock extends LitElement {
`
: html`
<ha-control-button
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${!isAvailable(this.stateObj)}
class="open-button ${this._buttonState}"
@click=${this._open}
>
@ -139,7 +138,7 @@ class MoreInfoLock extends LitElement {
: nothing}
</div>
<div>
${isJammed
${isJammed(this.stateObj)
? html`
<ha-control-button-group class="jammed">
<ha-control-button @click=${this._unlock}>

View File

@ -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`
<ha-control-button-group>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.lock")}
.disabled=${!isAvailable(this.stateObj) || isLocked(this.stateObj)}
@click=${this._onTap}
data-service="lock"
class=${classMap({
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
})}
>
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.unlock")}
.disabled=${!isAvailable(this.stateObj) || !isLocked(this.stateObj)}
@click=${this._onTap}
data-service="unlock"
class=${classMap({
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
})}
>
<ha-svg-icon .path=${mdiLockOpen}></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
`;
}
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;
}
}

View File

@ -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`
<div class="buttons">
<p class="open-success">
<ha-svg-icon path=${mdiCheck}></ha-svg-icon>
${this.hass.localize("ui.card.lock.open_door_success")}
</p>
</div>
`
: html`
<ha-control-button-group>
<ha-control-button
.disabled=${!isAvailable(this.stateObj)}
class="open-button ${this._buttonState}"
@click=${this._open}
>
${this._buttonState === "confirm"
? this.hass.localize("ui.card.lock.open_door_confirm")
: this.hass.localize("ui.card.lock.open_door")}
</ha-control-button>
</ha-control-button-group>
`}
`;
}
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;
}
}

View File

@ -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

View File

@ -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<LovelaceCardFeatureConfig["type"]> = new Set([
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"lock-commands",
"lock-open-door",
"numeric-input",
"select-options",
"target-humidity",

View File

@ -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,

View File

@ -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",