From 422e9a0ac9b756e8d6247bfd92a1640e5720329a Mon Sep 17 00:00:00 2001 From: Pierre-Emmanuel Mercier Date: Fri, 15 Apr 2022 12:42:02 +0200 Subject: [PATCH] feat(lock): add lock support (#316) * feat(card): lock * feat(lock): lock card * fix: missing hide state * fix: review * fix: review --- .hass_dev/lovelace-mushroom-showcase.yaml | 1 + .hass_dev/views/lock-view.yaml | 47 +++++ docs/cards/lock.md | 24 +++ docs/images/lock-dark.png | Bin 0 -> 2870 bytes docs/images/lock-light.png | Bin 0 -> 3049 bytes src/cards/lock-card/const.ts | 4 + .../controls/lock-buttons-control.ts | 69 +++++++ src/cards/lock-card/lock-card-config.ts | 32 ++++ src/cards/lock-card/lock-card-editor.ts | 93 ++++++++++ src/cards/lock-card/lock-card.ts | 171 ++++++++++++++++++ src/cards/lock-card/utils.ts | 25 +++ .../controls/vacuum-commands-control.ts | 2 +- src/ha/data/lock.ts | 41 +++++ src/mushroom.ts | 1 + src/utils/colors.ts | 2 + 15 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 .hass_dev/views/lock-view.yaml create mode 100644 docs/cards/lock.md create mode 100644 docs/images/lock-dark.png create mode 100644 docs/images/lock-light.png create mode 100644 src/cards/lock-card/const.ts create mode 100644 src/cards/lock-card/controls/lock-buttons-control.ts create mode 100644 src/cards/lock-card/lock-card-config.ts create mode 100644 src/cards/lock-card/lock-card-editor.ts create mode 100644 src/cards/lock-card/lock-card.ts create mode 100644 src/cards/lock-card/utils.ts create mode 100644 src/ha/data/lock.ts diff --git a/.hass_dev/lovelace-mushroom-showcase.yaml b/.hass_dev/lovelace-mushroom-showcase.yaml index d180ffbb7..28028a297 100644 --- a/.hass_dev/lovelace-mushroom-showcase.yaml +++ b/.hass_dev/lovelace-mushroom-showcase.yaml @@ -12,3 +12,4 @@ views: - !include views/update-view.yaml - !include views/media-player-view.yaml - !include views/vacuum-view.yaml + - !include views/lock-view.yaml diff --git a/.hass_dev/views/lock-view.yaml b/.hass_dev/views/lock-view.yaml new file mode 100644 index 000000000..470503378 --- /dev/null +++ b/.hass_dev/views/lock-view.yaml @@ -0,0 +1,47 @@ +title: Lock +icon: mdi:lock +cards: + - type: grid + title: Simple + cards: + - type: custom:mushroom-lock-card + entity: lock.front_door + - type: custom:mushroom-lock-card + entity: lock.front_door + name: Custom name and icon + icon: mdi:robot-outline + columns: 2 + square: false + - type: grid + title: Controls + cards: + - type: custom:mushroom-lock-card + entity: lock.front_door + name: Buttons control + - type: custom:mushroom-lock-card + entity: lock.front_door + name: Position control + columns: 2 + square: false + - type: custom:mushroom-lock-card + entity: lock.front_door + name: Multiple controls + - type: vertical-stack + title: Layout + cards: + - type: grid + columns: 2 + square: false + cards: + - type: custom:mushroom-lock-card + entity: lock.front_door + - type: grid + columns: 2 + square: false + cards: + - type: custom:mushroom-lock-card + entity: lock.front_door + layout: "vertical" + - type: custom:mushroom-lock-card + entity: lock.front_door + layout: "horizontal" \ No newline at end of file diff --git a/docs/cards/lock.md b/docs/cards/lock.md new file mode 100644 index 000000000..a4af5faf3 --- /dev/null +++ b/docs/cards/lock.md @@ -0,0 +1,24 @@ +# Lock card + +![Lock light](../images/lock-light.png) +![Lock dark](../images/lock-dark.png) + +## Description + +A lock card allow you to control a lock entity. + +## Configuration variables + +All the options are available in the lovelace editor but you can use `yaml` if you want. + +| Name | Type | Default | Description | +| :------------------ | :------ | :---------- | :------------------------------------------------------------------------ | +| `entity` | string | Required | Lock entity | +| `icon` | string | Optional | Custom icon | +| `name` | string | Optional | Custom name | +| `icon_color` | string | Optional | Custom icon color | +| `layout` | string | Optional | Layout of the card. Vertical, horizontal and default layout are supported | +| `hide_state` | boolean | `false` | Hide the entity state | +| `tap_action` | action | `more-info` | Home assistant action to perform on tap | +| `hold_action` | action | `more-info` | Home assistant action to perform on hold | +| `double_tap_action` | action | `more-info` | Home assistant action to perform on double_tap | diff --git a/docs/images/lock-dark.png b/docs/images/lock-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6f19111ba654b29b9df53bf2ac1248759b9fd310 GIT binary patch literal 2870 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 IW8rt_--8A31poj5 literal 0 HcmV?d00001 diff --git a/src/cards/lock-card/const.ts b/src/cards/lock-card/const.ts new file mode 100644 index 000000000..707b89e84 --- /dev/null +++ b/src/cards/lock-card/const.ts @@ -0,0 +1,4 @@ +import { PREFIX_NAME } from "../../const"; + +export const LOCK_CARD_NAME = `${PREFIX_NAME}-lock-card`; +export const LOCK_CARD_EDITOR_NAME = `${LOCK_CARD_NAME}-editor`; diff --git a/src/cards/lock-card/controls/lock-buttons-control.ts b/src/cards/lock-card/controls/lock-buttons-control.ts new file mode 100644 index 000000000..bd0ba7f13 --- /dev/null +++ b/src/cards/lock-card/controls/lock-buttons-control.ts @@ -0,0 +1,69 @@ +import { computeRTL, HomeAssistant } from "custom-card-helpers"; +import { html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { isAvailable } from "../../../ha/data/entity"; +import { LockEntity } from "../../../ha/data/lock"; +import { isActionPending, isLocked, isUnlocked } from "../utils"; + +interface LockButton { + icon: string; + serviceName?: string; + isVisible: (entity: LockEntity) => boolean; + isDisabled: (entity: LockEntity) => boolean; +} + +export const LOCK_BUTTONS: LockButton[] = [ + { + icon: "mdi:lock", + serviceName: "lock", + isVisible: (entity) => isUnlocked(entity), + isDisabled: () => false, + }, + { + icon: "mdi:lock-open", + serviceName: "unlock", + isVisible: (entity) => isLocked(entity), + isDisabled: () => false, + }, + { + icon: "mdi:lock-clock", + isVisible: (entity) => isActionPending(entity), + isDisabled: () => true, + }, +]; + +@customElement("mushroom-lock-buttons-control") +export class LockButtonsControl extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entity!: LockEntity; + + @property() public fill: boolean = false; + + private callService(e: CustomEvent) { + e.stopPropagation(); + const entry = (e.target! as any).entry as LockButton; + this.hass.callService("lock", entry.serviceName!, { + entity_id: this.entity!.entity_id, + }); + } + + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); + + return html` + ${LOCK_BUTTONS.filter((item) => item.isVisible(this.entity)).map( + (item) => html` + + ` + )} + `; + } +} diff --git a/src/cards/lock-card/lock-card-config.ts b/src/cards/lock-card/lock-card-config.ts new file mode 100644 index 000000000..2c1946f23 --- /dev/null +++ b/src/cards/lock-card/lock-card-config.ts @@ -0,0 +1,32 @@ +import { ActionConfig, LovelaceCardConfig } from "custom-card-helpers"; +import { assign, boolean, object, optional, string } from "superstruct"; +import { actionConfigStruct } from "../../utils/action-struct"; +import { baseLovelaceCardConfig } from "../../utils/editor-styles"; +import { Layout, layoutStruct } from "../../utils/layout"; + +export interface LockCardConfig extends LovelaceCardConfig { + entity?: string; + icon?: string; + name?: string; + icon_color?: string; + layout?: Layout; + hide_state?: boolean; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; +} + +export const lockCardConfigStruct = assign( + baseLovelaceCardConfig, + object({ + entity: optional(string()), + name: optional(string()), + icon: optional(string()), + icon_color: optional(string()), + layout: optional(layoutStruct), + hide_state: optional(boolean()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), + }) +); diff --git a/src/cards/lock-card/lock-card-editor.ts b/src/cards/lock-card/lock-card-editor.ts new file mode 100644 index 000000000..08e75c0df --- /dev/null +++ b/src/cards/lock-card/lock-card-editor.ts @@ -0,0 +1,93 @@ +import { fireEvent, HomeAssistant, LovelaceCardEditor } from "custom-card-helpers"; +import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import memoizeOne from "memoize-one"; +import { assert } from "superstruct"; +import { LOCK_ENTITY_DOMAINS } from "../../ha/data/lock"; +import setupCustomlocalize from "../../localize"; +import { configElementStyle } from "../../utils/editor-styles"; +import { GENERIC_FIELDS } from "../../utils/form/fields"; +import { HaFormSchema } from "../../utils/form/ha-form"; +import { stateIcon } from "../../utils/icons/state-icon"; +import { loadHaComponents } from "../../utils/loader"; +import { LOCK_CARD_EDITOR_NAME } from "./const"; +import { LockCardConfig, lockCardConfigStruct } from "./lock-card-config"; + +const computeSchema = memoizeOne((icon?: string): HaFormSchema[] => [ + { name: "entity", selector: { entity: { domain : LOCK_ENTITY_DOMAINS} } }, + { name: "name", selector: { text: {} } }, + { + type: "grid", + name: "", + schema: [ + { name: "icon", selector: { icon: { placeholder: icon } } }, + { name: "icon_color", selector: { "mush-color": {} } }, + ], + }, + { + type: "grid", + name: "", + schema: [ + { name: "layout", selector: { "mush-layout": {} } }, + { name: "hide_state", selector: { boolean: {} } }, + ], + }, + { name: "tap_action", selector: { "mush-action": {} } }, + { name: "hold_action", selector: { "mush-action": {} } }, + { name: "double_tap_action", selector: { "mush-action": {} } }, +]); + +@customElement(LOCK_CARD_EDITOR_NAME) +export class LockCardEditor extends LitElement implements LovelaceCardEditor { + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: LockCardConfig; + + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } + + public setConfig(config: LockCardConfig): void { + assert(config, lockCardConfigStruct); + this._config = config; + } + + private _computeLabelCallback = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); + + if (GENERIC_FIELDS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + }; + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + const entityState = this._config.entity ? this.hass.states[this._config.entity] : undefined; + const entityIcon = entityState ? stateIcon(entityState) : undefined; + const icon = this._config.icon || entityIcon; + const schema = computeSchema(icon); + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + static get styles(): CSSResultGroup { + return configElementStyle; + } +} diff --git a/src/cards/lock-card/lock-card.ts b/src/cards/lock-card/lock-card.ts new file mode 100644 index 000000000..1b514ea8b --- /dev/null +++ b/src/cards/lock-card/lock-card.ts @@ -0,0 +1,171 @@ +import { + ActionHandlerEvent, + computeRTL, + computeStateDisplay, + handleAction, + hasAction, + HomeAssistant, + LovelaceCard, + LovelaceCardEditor, +} from "custom-card-helpers"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { isActive, isAvailable } from "../../ha/data/entity"; +import { LOCK_ENTITY_DOMAINS } from "../../ha/data/lock"; +import "../../shared/badge-icon"; +import "../../shared/card"; +import "../../shared/shape-icon"; +import "../../shared/state-info"; +import "../../shared/state-item"; +import { cardStyle } from "../../utils/card-styles"; +import { computeRgbColor } from "../../utils/colors"; +import { registerCustomCard } from "../../utils/custom-cards"; +import { actionHandler } from "../../utils/directives/action-handler-directive"; +import { stateIcon } from "../../utils/icons/state-icon"; +import { getLayoutFromConfig } from "../../utils/layout"; +import { LOCK_CARD_EDITOR_NAME, LOCK_CARD_NAME } from "./const"; +import { LockCardConfig } from "./lock-card-config"; +import "./controls/lock-buttons-control"; + +registerCustomCard({ + type: LOCK_CARD_NAME, + name: "Mushroom Lock Card", + description: "Card for all lock entities", +}); + +@customElement(LOCK_CARD_NAME) +export class LockCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import("./lock-card-editor"); + return document.createElement(LOCK_CARD_EDITOR_NAME) as LovelaceCardEditor; + } + + public static async getStubConfig(hass: HomeAssistant): Promise { + const entities = Object.keys(hass.states); + const locks = entities.filter((e) => LOCK_ENTITY_DOMAINS.includes(e.split(".")[0])); + return { + type: `custom:${LOCK_CARD_NAME}`, + entity: locks[0], + }; + } + + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _config?: LockCardConfig; + + getCardSize(): number | Promise { + return 1; + } + + setConfig(config: LockCardConfig): void { + this._config = { + tap_action: { + action: "more-info", + }, + hold_action: { + action: "more-info", + }, + double_tap_action: { + action: "more-info", + }, + ...config, + }; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass || !this._config.entity) { + return html``; + } + + const entityId = this._config.entity; + const entity = this.hass.states[entityId]; + + const name = this._config.name || entity.attributes.friendly_name || ""; + const icon = this._config.icon || stateIcon(entity); + const hideState = this._config.hide_state; + const layout = getLayoutFromConfig(this._config); + + const stateDisplay = computeStateDisplay(this.hass.localize, entity, this.hass.locale); + + const iconColor = this._config.icon_color; + + const rtl = computeRTL(this.hass); + + return html` + + + ${this.renderIcon(icon, iconColor, isActive(entity))} + ${!isAvailable(entity) + ? html` + + ` + : null} + + +
+ + +
+
+ `; + } + + renderIcon(icon: string, iconColor: string | undefined, active: boolean): TemplateResult { + const iconStyle = { + "--icon-color": "rgb(var(--rgb-state-lock))", + "--shape-color": "rgba(var(--rgb-state-lock), 0.2)", + }; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; + iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; + } + return html` + + `; + } + + static get styles(): CSSResultGroup { + return [ + cardStyle, + css` + mushroom-state-item { + cursor: pointer; + } + mushroom-lock-buttons-control { + flex: 1; + } + `, + ]; + } +} diff --git a/src/cards/lock-card/utils.ts b/src/cards/lock-card/utils.ts new file mode 100644 index 000000000..874dbd65b --- /dev/null +++ b/src/cards/lock-card/utils.ts @@ -0,0 +1,25 @@ +import { + LockEntity, + LOCK_STATE_LOCKED, + LOCK_STATE_LOCKING, + LOCK_STATE_UNLOCKED, + LOCK_STATE_UNLOCKING, +} from "../../ha/data/lock"; + +export function isUnlocked(entity: LockEntity) { + return entity.state === LOCK_STATE_UNLOCKED; +} + +export function isLocked(entity: LockEntity) { + return entity.state === LOCK_STATE_LOCKED; +} + +export function isActionPending(entity: LockEntity) { + switch (entity.state) { + case LOCK_STATE_LOCKING: + case LOCK_STATE_UNLOCKING: + return true; + default: + return false; + } +} diff --git a/src/cards/vacuum-card/controls/vacuum-commands-control.ts b/src/cards/vacuum-card/controls/vacuum-commands-control.ts index 9c265f7e9..0856f4204 100644 --- a/src/cards/vacuum-card/controls/vacuum-commands-control.ts +++ b/src/cards/vacuum-card/controls/vacuum-commands-control.ts @@ -108,7 +108,7 @@ export class CoverButtonsControl extends LitElement { const rtl = computeRTL(this.hass); return html` - + ${VACUUM_BUTTONS.filter((item) => item.isVisible(this.entity, this.commands)).map( (item) => html`