From d9a1e1e22a8bf60d197eee05afdd03924c50a7b0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 25 Oct 2023 17:39:34 +0200 Subject: [PATCH 1/3] Add and and or condition to conditional card --- src/panels/lovelace/common/icon-condition.ts | 4 ++ .../lovelace/common/validate-condition.ts | 40 +++++++++++- .../components/hui-conditional-base.ts | 32 ++++++++-- .../conditions/ha-card-conditions-editor.ts | 29 +++++++-- .../conditions/types/ha-card-condition-and.ts | 61 +++++++++++++++++++ .../conditions/types/ha-card-condition-or.ts | 61 +++++++++++++++++++ src/translations/en.json | 6 ++ 7 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 src/panels/lovelace/editor/conditions/types/ha-card-condition-and.ts create mode 100644 src/panels/lovelace/editor/conditions/types/ha-card-condition-or.ts diff --git a/src/panels/lovelace/common/icon-condition.ts b/src/panels/lovelace/common/icon-condition.ts index fa6cd4cc2f7b..5a9f1986cff4 100644 --- a/src/panels/lovelace/common/icon-condition.ts +++ b/src/panels/lovelace/common/icon-condition.ts @@ -1,5 +1,7 @@ import { mdiAccount, + mdiAmpersand, + mdiGateOr, mdiNumeric, mdiResponsive, mdiStateMachine, @@ -11,4 +13,6 @@ export const ICON_CONDITION: Record = { state: mdiStateMachine, screen: mdiResponsive, user: mdiAccount, + and: mdiAmpersand, + or: mdiGateOr, }; diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index b6059292dbaa..3a63aa071816 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -6,7 +6,9 @@ export type Condition = | NumericStateCondition | ScreenCondition | StateCondition - | UserCondition; + | UserCondition + | OrCondition + | AndCondition; export type LegacyCondition = { entity?: string; @@ -38,6 +40,16 @@ export type UserCondition = { users?: string[]; }; +export type OrCondition = { + condition: "or"; + conditions?: Condition[]; +}; + +export type AndCondition = { + condition: "and"; + conditions?: Condition[]; +}; + function checkStateCondition( condition: StateCondition | LegacyCondition, hass: HomeAssistant @@ -87,6 +99,16 @@ function checkUserCondition(condition: UserCondition, hass: HomeAssistant) { : false; } +function checkAndCondition(condition: AndCondition, hass: HomeAssistant) { + if (!condition.conditions) return true; + return checkConditionsMet(condition.conditions, hass); +} + +function checkOrCondition(condition: OrCondition, hass: HomeAssistant) { + if (!condition.conditions) return true; + return condition.conditions.some((c) => checkConditionsMet([c], hass)); +} + export function checkConditionsMet( conditions: (Condition | LegacyCondition)[], hass: HomeAssistant @@ -100,6 +122,10 @@ export function checkConditionsMet( return checkUserCondition(c, hass); case "numeric_state": return checkStateNumericCondition(c, hass); + case "and": + return checkAndCondition(c, hass); + case "or": + return checkOrCondition(c, hass); default: return checkStateCondition(c, hass); } @@ -123,6 +149,14 @@ function validateUserCondition(condition: UserCondition) { return condition.users != null; } +function validateAndCondition(condition: AndCondition) { + return condition.conditions != null; +} + +function validateOrCondition(condition: OrCondition) { + return condition.conditions != null; +} + function validateNumericStateCondition(condition: NumericStateCondition) { return ( condition.entity != null && @@ -142,6 +176,10 @@ export function validateConditionalConfig( return validateUserCondition(c); case "numeric_state": return validateNumericStateCondition(c); + case "and": + return validateAndCondition(c); + case "or": + return validateOrCondition(c); default: return validateStateCondition(c); } diff --git a/src/panels/lovelace/components/hui-conditional-base.ts b/src/panels/lovelace/components/hui-conditional-base.ts index 79fea6759e32..24256c9da612 100644 --- a/src/panels/lovelace/components/hui-conditional-base.ts +++ b/src/panels/lovelace/components/hui-conditional-base.ts @@ -1,16 +1,30 @@ import { PropertyValues, ReactiveElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { listenMediaQuery } from "../../../common/dom/media_query"; +import { deepEqual } from "../../../common/util/deep-equal"; import { HomeAssistant } from "../../../types"; import { ConditionalCardConfig } from "../cards/types"; import { + Condition, + LegacyCondition, ScreenCondition, checkConditionsMet, validateConditionalConfig, } from "../common/validate-condition"; import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; import { LovelaceCard } from "../types"; -import { listenMediaQuery } from "../../../common/dom/media_query"; -import { deepEqual } from "../../../common/util/deep-equal"; + +function flatConditions( + conditions: (Condition | LegacyCondition)[] +): (Condition | LegacyCondition)[] { + return conditions.reduce<(Condition | LegacyCondition)[]>((array, c) => { + if ("conditions" in c && c.conditions) { + array.push(...flatConditions(c.conditions)); + } + array.push(c); + return array; + }, []); +} @customElement("hui-conditional-base") export class HuiConditionalBase extends ReactiveElement { @@ -77,7 +91,9 @@ export class HuiConditionalBase extends ReactiveElement { return; } - const conditions = this._config.conditions.filter( + const flattenConditions = flatConditions(this._config.conditions); + + const conditions = flattenConditions.filter( (c) => "condition" in c && c.condition === "screen" ) as ScreenCondition[]; @@ -91,10 +107,15 @@ export class HuiConditionalBase extends ReactiveElement { while (this._mediaQueriesListeners.length) { this._mediaQueriesListeners.pop()!(); } + mediaQueries.forEach((query) => { const listener = listenMediaQuery(query, (matches) => { - // For performance, if there is only one condition, set the visibility directly - if (this._config!.conditions.length === 1) { + // For performance, if there is only one condition and it's a screen condition, set the visibility directly + if ( + this._config!.conditions.length === 1 && + "condition" in this._config!.conditions[0] && + this._config!.conditions[0].condition === "screen" + ) { this._setVisibility(matches); return; } @@ -128,6 +149,7 @@ export class HuiConditionalBase extends ReactiveElement { this._config!.conditions, this.hass! ); + this._setVisibility(conditionMet); } 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 f1ec54e28760..b0f5eaf851f5 100644 --- a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts +++ b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts @@ -1,5 +1,12 @@ import { mdiPlus } from "@mdi/js"; -import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; +import { + CSSResultGroup, + LitElement, + PropertyValues, + css, + html, + nothing, +} from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; @@ -18,12 +25,16 @@ 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"; +import "./types/ha-card-condition-or"; +import "./types/ha-card-condition-and"; const UI_CONDITION = [ "numeric_state", "state", "screen", "user", + "and", + "or", ] as const satisfies readonly Condition["condition"][]; @customElement("ha-card-conditions-editor") @@ -35,6 +46,8 @@ export class HaCardConditionsEditor extends LitElement { | LegacyCondition )[]; + @property({ attribute: true, type: Boolean }) public nested?: boolean; + private _focusLastConditionOnChange = false; protected firstUpdated() { @@ -70,11 +83,15 @@ export class HaCardConditionsEditor extends LitElement { protected render() { return html`
- - ${this.hass!.localize( - "ui.panel.lovelace.editor.condition-editor.explanation" - )} - + ${!this.nested + ? html` + + ${this.hass!.localize( + "ui.panel.lovelace.editor.condition-editor.explanation" + )} + + ` + : nothing} ${this.conditions.map( (cond, idx) => html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const conditions = ev.detail.value as Condition[]; + const condition = { + ...this.condition, + conditions, + }; + fireEvent(this, "value-changed", { value: condition }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-card-condition-and": HaCardConditionNumericAnd; + } +} diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-or.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-or.ts new file mode 100644 index 000000000000..3f7efd094a08 --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-or.ts @@ -0,0 +1,61 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { any, array, assert, literal, object, optional } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-form/ha-form"; +import type { HomeAssistant } from "../../../../../types"; +import { + Condition, + OrCondition, + StateCondition, +} from "../../../common/validate-condition"; + +const orConditionStruct = object({ + condition: literal("or"), + conditions: optional(array(any())), +}); + +@customElement("ha-card-condition-or") +export class HaCardConditionOr extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public condition!: OrCondition; + + @property({ type: Boolean }) public disabled = false; + + public static get defaultConfig(): OrCondition { + return { condition: "or", conditions: [] }; + } + + protected static validateUIConfig(condition: StateCondition) { + return assert(condition, orConditionStruct); + } + + protected render() { + return html` + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const conditions = ev.detail.value as Condition[]; + const condition = { + ...this.condition, + conditions, + }; + fireEvent(this, "value-changed", { value: condition }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-card-condition-or": HaCardConditionOr; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 4b7444cb2de8..23ad7c04d1c1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4835,6 +4835,12 @@ }, "user": { "label": "User" + }, + "or": { + "label": "Or" + }, + "and": { + "label": "And" } } }, From eb2fc98d1c13fcb5aed6066f7118c87a8f98b720 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 25 Oct 2023 17:44:44 +0200 Subject: [PATCH 2/3] merge flat and filter --- .../components/hui-conditional-base.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/panels/lovelace/components/hui-conditional-base.ts b/src/panels/lovelace/components/hui-conditional-base.ts index 24256c9da612..b624a029c431 100644 --- a/src/panels/lovelace/components/hui-conditional-base.ts +++ b/src/panels/lovelace/components/hui-conditional-base.ts @@ -14,14 +14,16 @@ import { import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; import { LovelaceCard } from "../types"; -function flatConditions( +function extractScreenConditions( conditions: (Condition | LegacyCondition)[] -): (Condition | LegacyCondition)[] { - return conditions.reduce<(Condition | LegacyCondition)[]>((array, c) => { +): ScreenCondition[] { + return conditions.reduce((array, c) => { if ("conditions" in c && c.conditions) { - array.push(...flatConditions(c.conditions)); + array.push(...extractScreenConditions(c.conditions)); + } + if ("condition" in c && c.condition === "screen") { + array.push(c); } - array.push(c); return array; }, []); } @@ -91,13 +93,9 @@ export class HuiConditionalBase extends ReactiveElement { return; } - const flattenConditions = flatConditions(this._config.conditions); - - const conditions = flattenConditions.filter( - (c) => "condition" in c && c.condition === "screen" - ) as ScreenCondition[]; + const screenConditions = extractScreenConditions(this._config.conditions); - const mediaQueries = conditions + const mediaQueries = screenConditions .filter((c) => c.media_query) .map((c) => c.media_query as string); From c7411436e919bc88c03630053209357871694cb5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 25 Oct 2023 17:46:25 +0200 Subject: [PATCH 3/3] merge media query filter and screen filter --- .../components/hui-conditional-base.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/panels/lovelace/components/hui-conditional-base.ts b/src/panels/lovelace/components/hui-conditional-base.ts index b624a029c431..3ffa839fdd7e 100644 --- a/src/panels/lovelace/components/hui-conditional-base.ts +++ b/src/panels/lovelace/components/hui-conditional-base.ts @@ -7,22 +7,21 @@ import { ConditionalCardConfig } from "../cards/types"; import { Condition, LegacyCondition, - ScreenCondition, checkConditionsMet, validateConditionalConfig, } from "../common/validate-condition"; import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; import { LovelaceCard } from "../types"; -function extractScreenConditions( +function extractMediaQueries( conditions: (Condition | LegacyCondition)[] -): ScreenCondition[] { - return conditions.reduce((array, c) => { +): string[] { + return conditions.reduce((array, c) => { if ("conditions" in c && c.conditions) { - array.push(...extractScreenConditions(c.conditions)); + array.push(...extractMediaQueries(c.conditions)); } - if ("condition" in c && c.condition === "screen") { - array.push(c); + if ("condition" in c && c.condition === "screen" && c.media_query) { + array.push(c.media_query); } return array; }, []); @@ -93,11 +92,7 @@ export class HuiConditionalBase extends ReactiveElement { return; } - const screenConditions = extractScreenConditions(this._config.conditions); - - const mediaQueries = screenConditions - .filter((c) => c.media_query) - .map((c) => c.media_query as string); + const mediaQueries = extractMediaQueries(this._config.conditions); if (deepEqual(mediaQueries, this._mediaQueries)) return;