From ffc14df62948f44bccc185d88d298a136ee79094 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 19 Oct 2023 16:02:59 +0200 Subject: [PATCH] Add numeric state condition for conditional card --- src/panels/lovelace/common/icon-condition.ts | 3 +- .../lovelace/common/validate-condition.ts | 60 +++++++-- .../types/ha-card-condition-numeric_state.ts | 117 ++++++++++++++++++ .../hui-conditional-card-editor.ts | 2 + src/translations/en.json | 3 + 5 files changed, 172 insertions(+), 13 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 11a530f7fa70..c924cfbb727e 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 { mdiNumeric, mdiResponsive, mdiStateMachine } from "@mdi/js"; import { Condition } from "./validate-condition"; export const ICON_CONDITION: Record = { state: mdiStateMachine, screen: mdiResponsive, + numeric_state: mdiNumeric, }; diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index c68950509055..c6a6a6955f7c 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -1,7 +1,10 @@ import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; -export type Condition = StateCondition | ScreenCondition; +export type Condition = + | StateCondition + | ScreenCondition + | NumericStateCondition; export type LegacyCondition = { entity?: string; @@ -9,6 +12,13 @@ export type LegacyCondition = { state_not?: string; }; +export type NumericStateCondition = { + condition: "numeric_state"; + entity?: string; + below?: number; + above?: number; +}; + export type StateCondition = { condition: "state"; entity?: string; @@ -32,10 +42,25 @@ function checkStateCondition(condition: StateCondition, hass: HomeAssistant) { : state !== condition.state_not; } -function checkScreenCondition( - condition: ScreenCondition, - _hass: HomeAssistant +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); + 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 : false; @@ -46,15 +71,18 @@ export function checkConditionsMet( hass: HomeAssistant ): boolean { return conditions.every((c) => { - if (c.condition === "screen") { - return checkScreenCondition(c, hass); + switch (c.condition) { + case "screen": + return checkScreenCondition(c, hass); + case "numeric_state": + return checkStateNumericCondition(c, hass); + default: + return checkStateCondition(c, hass); } - - return checkStateCondition(c, hass); }); } -function valideStateCondition(condition: StateCondition) { +function validateStateCondition(condition: StateCondition) { return ( condition.entity != null && (condition.state != null || condition.state_not != null) @@ -65,11 +93,19 @@ function validateScreenCondition(condition: ScreenCondition) { return condition.media_query != null; } +function validateNumericStateCondition(condition: NumericStateCondition) { + return condition.entity != null; +} + export function validateConditionalConfig(conditions: Condition[]): boolean { return conditions.every((c) => { - if (c.condition === "screen") { - return validateScreenCondition(c); + switch (c.condition) { + case "screen": + return validateScreenCondition(c); + case "numeric_state": + return validateNumericStateCondition(c); + default: + return validateStateCondition(c); } - return valideStateCondition(c); }); } 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 000000000000..3823c9a549ec --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts @@ -0,0 +1,117 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement, PropertyValues } 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 } from "../../../common/validate-condition"; + +const numericStateConditionStruct = object({ + condition: literal("state"), + entity: 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 willUpdate(changedProperties: PropertyValues): void { + if (!changedProperties.has("condition")) { + return; + } + try { + assert(this.condition, numericStateConditionStruct); + } catch (err: any) { + fireEvent(this, "ui-mode-not-available", err); + } + } + + 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/config-elements/hui-conditional-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts index 4c67d934c5c0..cf77f88032d7 100644 --- a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts @@ -35,6 +35,7 @@ import "../conditions/ha-card-condition-editor"; import { LovelaceConditionEditorConstructor } from "../conditions/types"; import "../conditions/types/ha-card-condition-screen"; import "../conditions/types/ha-card-condition-state"; +import "../conditions/types/ha-card-condition-numeric_state"; import "../hui-element-editor"; import type { ConfigChangedEvent } from "../hui-element-editor"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; @@ -43,6 +44,7 @@ import { configElementStyle } from "./config-elements-style"; const UI_CONDITION = [ "state", + "numeric_state", "screen", ] as const satisfies readonly Condition["condition"][]; diff --git a/src/translations/en.json b/src/translations/en.json index d48e089221ac..ed64c531c038 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4801,6 +4801,9 @@ }, "state": { "label": "Entity state" + }, + "numeric_state": { + "label": "Entity numeric state" } } },