From eedb42b2f3c94d65aef3288a7452d18eafceebd9 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 23 Oct 2023 15:49:27 +0200 Subject: [PATCH] Add user condition to conditional card (#18265) * Add user condition to conditional card * Refactor user fetch * Add validate ui * Use ha-check-list-item --- src/panels/lovelace/common/icon-condition.ts | 3 +- .../lovelace/common/validate-condition.ts | 44 +++++-- .../conditions/ha-card-conditions-editor.ts | 2 + .../types/ha-card-condition-user.ts | 122 ++++++++++++++++++ src/translations/en.json | 3 + 5 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 src/panels/lovelace/editor/conditions/types/ha-card-condition-user.ts diff --git a/src/panels/lovelace/common/icon-condition.ts b/src/panels/lovelace/common/icon-condition.ts index 11a530f7fa..c21b2bf342 100644 --- a/src/panels/lovelace/common/icon-condition.ts +++ b/src/panels/lovelace/common/icon-condition.ts @@ -1,7 +1,8 @@ -import { mdiResponsive, mdiStateMachine } from "@mdi/js"; +import { mdiAccount, mdiResponsive, mdiStateMachine } from "@mdi/js"; import { Condition } from "./validate-condition"; export const ICON_CONDITION: Record = { state: mdiStateMachine, screen: mdiResponsive, + user: mdiAccount, }; diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index 1d17f44b10..442e40921b 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -1,7 +1,7 @@ import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; -export type Condition = StateCondition | ScreenCondition; +export type Condition = StateCondition | ScreenCondition | UserCondition; export type LegacyCondition = { entity?: string; @@ -21,6 +21,11 @@ export type ScreenCondition = { media_query?: string; }; +export type UserCondition = { + condition: "user"; + users?: string[]; +}; + function checkStateCondition( condition: StateCondition | LegacyCondition, hass: HomeAssistant @@ -35,30 +40,38 @@ function checkStateCondition( : state !== condition.state_not; } -function checkScreenCondition( - condition: ScreenCondition, - _hass: HomeAssistant -) { +function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) { return condition.media_query ? matchMedia(condition.media_query).matches : false; } +function checkUserCondition(condition: UserCondition, hass: HomeAssistant) { + return condition.users && hass.user?.id + ? condition.users.includes(hass.user.id) + : false; +} + export function checkConditionsMet( conditions: (Condition | LegacyCondition)[], hass: HomeAssistant ): boolean { return conditions.every((c) => { if ("condition" in c) { - if (c.condition === "screen") { - return checkScreenCondition(c, hass); + switch (c.condition) { + case "screen": + return checkScreenCondition(c, hass); + case "user": + return checkUserCondition(c, hass); + default: + return checkStateCondition(c, hass); } } return checkStateCondition(c, hass); }); } -function valideStateCondition(condition: StateCondition | LegacyCondition) { +function validateStateCondition(condition: StateCondition | LegacyCondition) { return ( condition.entity != null && (condition.state != null || condition.state_not != null) @@ -69,15 +82,24 @@ function validateScreenCondition(condition: ScreenCondition) { return condition.media_query != null; } +function validateUserCondition(condition: UserCondition) { + return condition.users != null; +} + export function validateConditionalConfig( conditions: (Condition | LegacyCondition)[] ): boolean { return conditions.every((c) => { if ("condition" in c) { - if (c.condition === "screen") { - return validateScreenCondition(c); + switch (c.condition) { + case "screen": + return validateScreenCondition(c); + case "user": + return validateUserCondition(c); + default: + return validateStateCondition(c); } } - return valideStateCondition(c); + return validateStateCondition(c); }); } diff --git a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts index 9d6486de6b..c2c8803845 100644 --- a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts +++ b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts @@ -14,10 +14,12 @@ import "./ha-card-condition-editor"; import { LovelaceConditionEditorConstructor } from "./types"; import "./types/ha-card-condition-screen"; import "./types/ha-card-condition-state"; +import "./types/ha-card-condition-user"; const UI_CONDITION = [ "state", "screen", + "user", ] as const satisfies readonly Condition["condition"][]; @customElement("ha-card-conditions-editor") diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-user.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-user.ts new file mode 100644 index 0000000000..68dfc63b67 --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-user.ts @@ -0,0 +1,122 @@ +import "@material/mwc-list"; +import { LitElement, PropertyValues, css, html } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { array, assert, literal, object, string } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { stringCompare } from "../../../../../common/string/compare"; +import "../../../../../components/ha-check-list-item"; +import "../../../../../components/ha-switch"; +import "../../../../../components/user/ha-user-badge"; +import { User, fetchUsers } from "../../../../../data/user"; +import type { HomeAssistant } from "../../../../../types"; +import { UserCondition } from "../../../common/validate-condition"; + +const userConditionStruct = object({ + condition: literal("user"), + users: array(string()), +}); + +@customElement("ha-card-condition-user") +export class HaCardConditionUser extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public condition!: UserCondition; + + @property({ type: Boolean }) public disabled = false; + + public static get defaultConfig(): UserCondition { + return { condition: "user", users: [] }; + } + + @state() private _users: User[] = []; + + protected static validateUIConfig(condition: UserCondition) { + return assert(condition, userConditionStruct); + } + + private _sortedUsers = memoizeOne((users: User[]) => + users.sort((a, b) => + stringCompare(a.name, b.name, this.hass.locale.language) + ) + ); + + protected async firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._fetchUsers(); + } + + private async _fetchUsers() { + const users = await fetchUsers(this.hass); + this._users = users.filter((user) => !user.system_generated); + } + + protected render() { + const selectedUsers = this.condition.users ?? []; + + return html` + + ${this._sortedUsers(this._users).map( + (user) => html` + + + ${user.name} + + ` + )} + + `; + } + + private _userChanged(ev) { + ev.stopPropagation(); + const selectedUsers = this.condition.users ?? []; + const userId = ev.currentTarget.userId as string; + const checked = ev.detail.selected as boolean; + + if (checked === selectedUsers.includes(userId)) { + return; + } + + let users = selectedUsers; + if (checked) { + users = [...users, userId]; + } else { + users = users.filter((user) => user !== userId); + } + + const condition: UserCondition = { + ...this.condition, + users, + }; + + fireEvent(this, "value-changed", { value: condition }); + } + + static get styles() { + return css` + :host { + display: block; + } + mwc-list { + --mdc-list-vertical-padding: 0; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-card-condition-user": HaCardConditionUser; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 513cc4e8d8..23aedd73ae 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4774,6 +4774,9 @@ "state_equal": "State is equal to", "state_not_equal": "State is not equal to", "current_state": "current" + }, + "user": { + "label": "User" } } },