From fdddc182912c669046f05f48dd406182af22b34a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 23 Oct 2023 16:17:23 +0200 Subject: [PATCH] Add numeric state condition for conditional card (#18288) * Add numeric state condition for conditional card * Add validate ui * Clean entity data * Check for numeric state --- src/panels/lovelace/common/icon-condition.ts | 8 +- .../lovelace/common/validate-condition.ts | 47 +++++++- .../conditions/ha-card-conditions-editor.ts | 2 + .../types/ha-card-condition-numeric_state.ts | 113 ++++++++++++++++++ .../types/ha-card-condition-state.ts | 12 +- src/translations/en.json | 3 + 6 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts diff --git a/src/panels/lovelace/common/icon-condition.ts b/src/panels/lovelace/common/icon-condition.ts index c21b2bf342..fa6cd4cc2f 100644 --- a/src/panels/lovelace/common/icon-condition.ts +++ b/src/panels/lovelace/common/icon-condition.ts @@ -1,7 +1,13 @@ -import { mdiAccount, mdiResponsive, mdiStateMachine } from "@mdi/js"; +import { + mdiAccount, + mdiNumeric, + mdiResponsive, + mdiStateMachine, +} from "@mdi/js"; import { Condition } from "./validate-condition"; export const ICON_CONDITION: Record = { + numeric_state: mdiNumeric, 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 b4912788b0..57c2eab65e 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -2,7 +2,11 @@ import { ensureArray } from "../../../common/array/ensure-array"; import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; -export type Condition = StateCondition | ScreenCondition | UserCondition; +export type Condition = + | NumericStateCondition + | ScreenCondition + | StateCondition + | UserCondition; export type LegacyCondition = { entity?: string; @@ -10,6 +14,13 @@ export type LegacyCondition = { state_not?: string | string[]; }; +export type NumericStateCondition = { + condition: "numeric_state"; + entity?: string; + below?: number; + above?: number; +}; + export type StateCondition = { condition: "state"; entity?: string; @@ -41,6 +52,29 @@ function checkStateCondition( : ensureArray(condition.state_not).includes(state); } +function checkStateNumericCondition( + condition: NumericStateCondition, + hass: HomeAssistant +) { + const entity = + (condition.entity ? hass.states[condition.entity] : undefined) ?? undefined; + + if (!entity) { + return false; + } + + const numericState = Number(entity.state); + + if (isNaN(numericState)) { + return false; + } + + return ( + (condition.above == null || condition.above < numericState) && + (condition.below == null || condition.below >= numericState) + ); +} + function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) { return condition.media_query ? matchMedia(condition.media_query).matches @@ -64,6 +98,8 @@ export function checkConditionsMet( return checkScreenCondition(c, hass); case "user": return checkUserCondition(c, hass); + case "numeric_state": + return checkStateNumericCondition(c, hass); default: return checkStateCondition(c, hass); } @@ -87,6 +123,13 @@ function validateUserCondition(condition: UserCondition) { return condition.users != null; } +function validateNumericStateCondition(condition: NumericStateCondition) { + return ( + condition.entity != null && + (condition.above != null || condition.below != null) + ); +} + export function validateConditionalConfig( conditions: (Condition | LegacyCondition)[] ): boolean { @@ -97,6 +140,8 @@ export function validateConditionalConfig( return validateScreenCondition(c); case "user": return validateUserCondition(c); + case "numeric_state": + return validateNumericStateCondition(c); default: 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 c2c8803845..de0d8b8a0b 100644 --- a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts +++ b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts @@ -12,11 +12,13 @@ import { ICON_CONDITION } from "../../common/icon-condition"; import { Condition, LegacyCondition } from "../../common/validate-condition"; import "./ha-card-condition-editor"; import { LovelaceConditionEditorConstructor } from "./types"; +import "./types/ha-card-condition-numeric_state"; import "./types/ha-card-condition-screen"; import "./types/ha-card-condition-state"; import "./types/ha-card-condition-user"; const UI_CONDITION = [ + "numeric_state", "state", "screen", "user", diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts new file mode 100644 index 0000000000..001fb38bd3 --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts @@ -0,0 +1,113 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { assert, literal, number, object, optional, string } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-form/ha-form"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import { HaFormSchema } from "../../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../../types"; +import { + NumericStateCondition, + StateCondition, +} from "../../../common/validate-condition"; + +const numericStateConditionStruct = object({ + condition: literal("numeric_state"), + entity: optional(string()), + above: optional(number()), + below: optional(number()), +}); + +@customElement("ha-card-condition-numeric_state") +export class HaCardConditionNumericState extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public condition!: NumericStateCondition; + + @property({ type: Boolean }) public disabled = false; + + public static get defaultConfig(): NumericStateCondition { + return { condition: "numeric_state", entity: "" }; + } + + protected static validateUIConfig(condition: StateCondition) { + return assert(condition, numericStateConditionStruct); + } + + private _schema = memoizeOne( + (stateObj?: HassEntity) => + [ + { name: "entity", selector: { entity: {} } }, + { + name: "", + type: "grid", + schema: [ + { + name: "above", + selector: { + number: { + mode: "box", + unit_of_measurement: stateObj?.attributes.unit_of_measurement, + }, + }, + }, + { + name: "below", + selector: { + number: { + mode: "box", + unit_of_measurement: stateObj?.attributes.unit_of_measurement, + }, + }, + }, + ], + }, + ] as const satisfies readonly HaFormSchema[] + ); + + protected render() { + const stateObj = this.condition.entity + ? this.hass.states[this.condition.entity] + : undefined; + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const condition = ev.detail.value as NumericStateCondition; + fireEvent(this, "value-changed", { value: condition }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ): string => { + switch (schema.name) { + case "entity": + return this.hass.localize("ui.components.entity.entity-picker.entity"); + case "below": + return "Below"; + case "above": + return "Above"; + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-card-condition-numeric_state": HaCardConditionNumericState; + } +} diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-state.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-state.ts index 8c9234d221..e0594af3f6 100644 --- a/src/panels/lovelace/editor/conditions/types/ha-card-condition-state.ts +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-state.ts @@ -19,7 +19,7 @@ const stateConditionStruct = object({ type StateConditionData = { condition: "state"; - entity: string; + entity?: string; invert: "true" | "false"; state?: string | string[]; }; @@ -101,12 +101,9 @@ export class HaCardConditionState extends LitElement { const data: StateConditionData = { ...content, - entity: this.condition.entity ?? "", + entity: this.condition.entity, invert: this.condition.state_not ? "true" : "false", - state: - (this.condition.state_not as string | string[] | undefined) ?? - (this.condition.state as string | string[] | undefined) ?? - "", + state: this.condition.state_not ?? this.condition.state, }; return html` @@ -125,12 +122,11 @@ export class HaCardConditionState extends LitElement { ev.stopPropagation(); const data = ev.detail.value as StateConditionData; - const { invert, state, entity, condition: _, ...content } = data; + const { invert, state, condition: _, ...content } = data; const condition: StateCondition = { condition: "state", ...content, - entity: entity ?? "", state: invert === "false" ? state ?? "" : undefined, state_not: invert === "true" ? state ?? "" : undefined, }; diff --git a/src/translations/en.json b/src/translations/en.json index 23aedd73ae..e25d7fefe9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4758,6 +4758,9 @@ "explanation": "The card will be shown when ALL conditions below are fulfilled.", "add": "Add condition", "condition": { + "numeric_state": { + "label": "Entity numeric state" + }, "screen": { "label": "Screen", "breakpoints": "Screen sizes",