From 552eeeddf6981395fd194a287d6c8b9cb0d23340 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 19 Mar 2024 14:29:34 +0100 Subject: [PATCH] `conditional` & `entity-filter`: add ability to filter through `entity_id` & add `entity-filter` `conditional`'s conditions (#19182) * entity-filter: add ability to filter through entity_id value * review: test filter value against undefined Co-authored-by: karwosts <32912880+karwosts@users.noreply.github.com> * review: better handle state values that could be mixed with an entity_id * Add multiple filter/condition types * Fix automation's NumericStateCondition above/below types * Replace operator condition by state for string or number * Move to condition: type & attr * Remove enable attr * fix condition state array * Remove necessary undefined check * Move to condition: use same codebase as conditionnal card * Fix entities error 'read properties of undefined' + conditions first * Fix lint * Merge condition to set the entity to filter on Co-authored-by: Paul Bottein * review: make numeric_state below & above working together again, with entity_id support * shorthand getValueFromEntityId * review: states are string * Split legacy state filter and condition logic * Fix types * Fix type * Update gallery doc * Fix operator in while numaric array * Rename condition card header in gallery * Don't use real gas station name * Update gallery * Update card is entity in condition change * Don't check for entity id in state condition * Improve check * Update condition card demo * Revert "Don't check for entity id in state condition" This reverts commit f5e6a65a370c108cb68876aeeafe348eead6e8be. * Use set instead of list * Update demo --------- Co-authored-by: karwosts <32912880+karwosts@users.noreply.github.com> Co-authored-by: Paul Bottein --- .../pages/automation/describe-condition.ts | 12 +- .../src/pages/lovelace/entity-filter-card.ts | 173 ++++++++++++++++-- src/data/automation.ts | 4 +- .../badges/hui-entity-filter-badge.ts | 52 ++++-- src/panels/lovelace/badges/types.ts | 11 +- .../lovelace/cards/hui-entity-filter-card.ts | 70 +++++-- src/panels/lovelace/cards/types.ts | 4 +- src/panels/lovelace/common/evaluate-filter.ts | 55 +++++- .../lovelace/common/validate-condition.ts | 172 ++++++++++++++--- src/panels/lovelace/entity-rows/types.ts | 12 +- 10 files changed, 461 insertions(+), 104 deletions(-) diff --git a/gallery/src/pages/automation/describe-condition.ts b/gallery/src/pages/automation/describe-condition.ts index a92fc724b4..6ec74c6794 100644 --- a/gallery/src/pages/automation/describe-condition.ts +++ b/gallery/src/pages/automation/describe-condition.ts @@ -21,10 +21,10 @@ const ENTITIES = [ }), ]; -const conditions = [ - { condition: "and" }, - { condition: "not" }, - { condition: "or" }, +const conditions: Condition[] = [ + { condition: "and", conditions: [] }, + { condition: "not", conditions: [] }, + { condition: "or", conditions: [] }, { condition: "state", entity_id: "light.kitchen", state: "on" }, { condition: "numeric_state", @@ -34,11 +34,11 @@ const conditions = [ above: 20, }, { condition: "sun", after: "sunset" }, - { condition: "sun", after: "sunrise", offset: "-01:00" }, + { condition: "sun", after: "sunrise", before_offset: 3600 }, { condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" }, { condition: "trigger", id: "motion" }, { condition: "time" }, - { condition: "template" }, + { condition: "template", value_template: "" }, ]; const initialCondition: Condition = { diff --git a/gallery/src/pages/lovelace/entity-filter-card.ts b/gallery/src/pages/lovelace/entity-filter-card.ts index 86b0b9cb93..45dc70881d 100644 --- a/gallery/src/pages/lovelace/entity-filter-card.ts +++ b/gallery/src/pages/lovelace/entity-filter-card.ts @@ -11,7 +11,7 @@ const ENTITIES = [ latitude: 32.877105, longitude: 117.232185, gps_accuracy: 91, - battery: 71, + battery: 25, friendly_name: "Paulus", }), getEntity("device_tracker", "demo_anne_therese", "school", { @@ -19,7 +19,7 @@ const ENTITIES = [ latitude: 32.877105, longitude: 117.232185, gps_accuracy: 91, - battery: 71, + battery: 50, friendly_name: "Anne Therese", }), getEntity("device_tracker", "demo_home_boy", "home", { @@ -27,7 +27,7 @@ const ENTITIES = [ latitude: 32.877105, longitude: 117.232185, gps_accuracy: 91, - battery: 71, + battery: 75, friendly_name: "Home Boy", }), getEntity("light", "bed_light", "on", { @@ -39,21 +39,53 @@ const ENTITIES = [ getEntity("light", "ceiling_lights", "off", { friendly_name: "Ceiling Lights", }), + getEntity("sensor", "battery_1", 20, { + device_class: "battery", + friendly_name: "Battery 1", + unit_of_measurement: "%", + }), + getEntity("sensor", "battery_2", 35, { + device_class: "battery", + friendly_name: "Battery 2", + unit_of_measurement: "%", + }), + getEntity("sensor", "battery_3", 40, { + device_class: "battery", + friendly_name: "Battery 3", + unit_of_measurement: "%", + }), + getEntity("sensor", "battery_4", 80, { + device_class: "battery", + friendly_name: "Battery 4", + unit_of_measurement: "%", + }), + getEntity("input_number", "min_battery_level", 30, { + mode: "slider", + step: 10, + min: 0, + max: 100, + icon: "mdi:battery-alert-variant", + friendly_name: "Minimum Battery Level", + unit_of_measurement: "%", + }), ]; const CONFIGS = [ { - heading: "Unfiltered controller", + heading: "Unfiltered entities", config: ` - type: entities entities: - - light.bed_light - - light.ceiling_lights - - light.kitchen_lights + - device_tracker.demo_anne_therese + - device_tracker.demo_home_boy + - device_tracker.demo_paulus + - light.bed_light + - light.ceiling_lights + - light.kitchen_lights `, }, { - heading: "Filtered entities card", + heading: "On and home entities", config: ` - type: entity-filter entities: @@ -63,9 +95,28 @@ const CONFIGS = [ - light.bed_light - light.ceiling_lights - light.kitchen_lights - state_filter: - - "on" - - home + conditions: + - condition: state + state: + - "on" + - home + `, + }, + { + heading: "Same state as Bed Light", + config: ` +- type: entity-filter + entities: + - device_tracker.demo_anne_therese + - device_tracker.demo_home_boy + - device_tracker.demo_paulus + - light.bed_light + - light.ceiling_lights + - light.kitchen_lights + conditions: + - condition: state + state: + - light.bed_light `, }, { @@ -79,9 +130,11 @@ const CONFIGS = [ - light.bed_light - light.ceiling_lights - light.kitchen_lights - state_filter: - - "on" - - not_home + conditions: + - condition: state + state: + - "on" + - home card: type: entities title: Custom Title @@ -99,15 +152,101 @@ const CONFIGS = [ - light.bed_light - light.ceiling_lights - light.kitchen_lights - state_filter: - - "on" - - not_home + conditions: + - condition: state + state: + - "on" + - home card: type: glance show_state: true title: Custom Title `, }, + { + heading: + "Filtered entities by battery attribute (< '30') using state filter", + config: ` +- type: entity-filter + entities: + - device_tracker.demo_anne_therese + - device_tracker.demo_home_boy + - device_tracker.demo_paulus + state_filter: + - operator: < + attribute: battery + value: "30" + `, + }, + { + heading: "Unfiltered number entities", + config: ` +- type: entities + entities: + - input_number.min_battery_level + - sensor.battery_1 + - sensor.battery_3 + - sensor.battery_2 + - sensor.battery_4 + `, + }, + { + heading: "Battery lower than 50%", + config: ` +- type: entity-filter + entities: + - sensor.battery_1 + - sensor.battery_3 + - sensor.battery_2 + - sensor.battery_4 + conditions: + - condition: numeric_state + below: 50 + `, + }, + { + heading: "Battery lower than min battery level", + config: ` +- type: entity-filter + entities: + - sensor.battery_1 + - sensor.battery_3 + - sensor.battery_2 + - sensor.battery_4 + conditions: + - condition: numeric_state + below: input_number.min_battery_level + `, + }, + { + heading: "Battery between min battery level and 70%", + config: ` +- type: entity-filter + entities: + - sensor.battery_1 + - sensor.battery_3 + - sensor.battery_2 + - sensor.battery_4 + conditions: + - condition: numeric_state + above: input_number.min_battery_level + below: 70 + `, + }, + { + heading: "Error: Entities must be specified", + config: ` +- type: entity-filter + `, + }, + { + heading: "Error: Incorrect filter config", + config: ` +- type: entity-filter + entities: + - sensor.gas_station_lowest_price + `, + }, ]; @customElement("demo-lovelace-entity-filter-card") diff --git a/src/data/automation.ts b/src/data/automation.ts index 31fd6d6e61..4b5292b213 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -219,8 +219,8 @@ export interface NumericStateCondition extends BaseCondition { condition: "numeric_state"; entity_id: string; attribute?: string; - above?: number; - below?: number; + above?: string | number; + below?: string | number; value_template?: string; } diff --git a/src/panels/lovelace/badges/hui-entity-filter-badge.ts b/src/panels/lovelace/badges/hui-entity-filter-badge.ts index 04cdd94a0f..e6a889febe 100644 --- a/src/panels/lovelace/badges/hui-entity-filter-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-filter-badge.ts @@ -1,8 +1,13 @@ import { PropertyValues, ReactiveElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { HomeAssistant } from "../../../types"; -import { evaluateFilter } from "../common/evaluate-filter"; +import { evaluateStateFilter } from "../common/evaluate-filter"; import { processConfigEntities } from "../common/process-config-entities"; +import { + addEntityToCondition, + checkConditionsMet, + extractConditionEntityIds, +} from "../common/validate-condition"; import { createBadgeElement } from "../create-element/create-badge-element"; import { EntityFilterEntityConfig } from "../entity-rows/types"; import { LovelaceBadge } from "../types"; @@ -29,7 +34,10 @@ export class HuiEntityFilterBadge } if ( - !(config.state_filter && Array.isArray(config.state_filter)) && + !( + (config.conditions && Array.isArray(config.conditions)) || + (config.state_filter && Array.isArray(config.state_filter)) + ) && !config.entities.every( (entity) => typeof entity === "object" && @@ -81,23 +89,19 @@ export class HuiEntityFilterBadge const entitiesList = this._configEntities.filter((entityConf) => { const stateObj = this.hass.states[entityConf.entity]; + if (!stateObj) return false; - if (!stateObj) { - return false; + const conditions = entityConf.conditions ?? this._config!.conditions; + if (conditions) { + const conditionWithEntity = conditions.map((condition) => + addEntityToCondition(condition, entityConf.entity) + ); + return checkConditionsMet(conditionWithEntity, this.hass!); } - if (entityConf.state_filter) { - for (const filter of entityConf.state_filter) { - if (evaluateFilter(stateObj, filter)) { - return true; - } - } - } else { - for (const filter of this._config!.state_filter) { - if (evaluateFilter(stateObj, filter)) { - return true; - } - } + const filters = entityConf.state_filter ?? this._config!.state_filter; + if (filters) { + return filters.some((filter) => evaluateStateFilter(stateObj, filter)); } return false; @@ -152,8 +156,24 @@ export class HuiEntityFilterBadge if (this.hass.states[config.entity] !== oldHass.states[config.entity]) { return true; } + if (config.conditions) { + const entityIds = extractConditionEntityIds(config.conditions); + for (const entityId of entityIds) { + if (this.hass.states[entityId] !== oldHass.states[entityId]) { + return true; + } + } + } } + if (this._config?.conditions) { + const entityIds = extractConditionEntityIds(this._config?.conditions); + for (const entityId of entityIds) { + if (this.hass.states[entityId] !== oldHass.states[entityId]) { + return true; + } + } + } return false; } } diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index c25c834539..345c1c3d8e 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -1,11 +1,14 @@ -import { ActionConfig } from "../../../data/lovelace/config/action"; -import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; -import { EntityFilterEntityConfig } from "../entity-rows/types"; +import type { ActionConfig } from "../../../data/lovelace/config/action"; +import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; +import type { LegacyStateFilter } from "../common/evaluate-filter"; +import type { Condition } from "../common/validate-condition"; +import type { EntityFilterEntityConfig } from "../entity-rows/types"; export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig { type: "entity-filter"; entities: Array; - state_filter: Array<{ key: string } | string>; + state_filter?: Array; + conditions?: Array; } export interface ErrorBadgeConfig extends LovelaceBadgeConfig { diff --git a/src/panels/lovelace/cards/hui-entity-filter-card.ts b/src/panels/lovelace/cards/hui-entity-filter-card.ts index 9b55fd7d14..ab6fb943b2 100644 --- a/src/panels/lovelace/cards/hui-entity-filter-card.ts +++ b/src/panels/lovelace/cards/hui-entity-filter-card.ts @@ -3,9 +3,14 @@ import { customElement, property, state } from "lit/decorators"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; -import { evaluateFilter } from "../common/evaluate-filter"; +import { evaluateStateFilter } from "../common/evaluate-filter"; import { findEntities } from "../common/find-entities"; import { processConfigEntities } from "../common/process-config-entities"; +import { + addEntityToCondition, + checkConditionsMet, + extractConditionEntityIds, +} from "../common/validate-condition"; import { createCardElement } from "../create-element/create-card-element"; import { EntityFilterEntityConfig } from "../entity-rows/types"; import { LovelaceCard } from "../types"; @@ -33,9 +38,14 @@ export class HuiEntityFilterCard return { type: "entity-filter", entities: foundEntities, - state_filter: [ - foundEntities[0] ? hass.states[foundEntities[0]].state : "", - ], + conditions: foundEntities[0] + ? [ + { + condition: "state", + state: hass.states[foundEntities[0]].state, + }, + ] + : [], card: { type: "entities" }, }; } @@ -61,12 +71,19 @@ export class HuiEntityFilterCard } public setConfig(config: EntityFilterCardConfig): void { - if (!config.entities.length || !Array.isArray(config.entities)) { + if ( + !config.entities || + !config.entities.length || + !Array.isArray(config.entities) + ) { throw new Error("Entities must be specified"); } if ( - !(config.state_filter && Array.isArray(config.state_filter)) && + !( + (config.conditions && Array.isArray(config.conditions)) || + (config.state_filter && Array.isArray(config.state_filter)) + ) && !config.entities.every( (entity) => typeof entity === "object" && @@ -127,23 +144,19 @@ export class HuiEntityFilterCard const entitiesList = this._configEntities.filter((entityConf) => { const stateObj = this.hass!.states[entityConf.entity]; + if (!stateObj) return false; - if (!stateObj) { - return false; + const conditions = entityConf.conditions ?? this._config!.conditions; + if (conditions) { + const conditionWithEntity = conditions.map((condition) => + addEntityToCondition(condition, entityConf.entity) + ); + return checkConditionsMet(conditionWithEntity, this.hass!); } - if (entityConf.state_filter) { - for (const filter of entityConf.state_filter) { - if (evaluateFilter(stateObj, filter)) { - return true; - } - } - } else { - for (const filter of this._config!.state_filter) { - if (evaluateFilter(stateObj, filter)) { - return true; - } - } + const filters = entityConf.state_filter ?? this._config!.state_filter; + if (filters) { + return filters.some((filter) => evaluateStateFilter(stateObj, filter)); } return false; @@ -202,6 +215,23 @@ export class HuiEntityFilterCard if (this.hass.states[config.entity] !== oldHass.states[config.entity]) { return true; } + if (config.conditions) { + const entityIds = extractConditionEntityIds(config.conditions); + for (const entityId of entityIds) { + if (this.hass.states[entityId] !== oldHass.states[entityId]) { + return true; + } + } + } + } + + if (this._config?.conditions) { + const entityIds = extractConditionEntityIds(this._config?.conditions); + for (const entityId of entityIds) { + if (this.hass.states[entityId] !== oldHass.states[entityId]) { + return true; + } + } } return false; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 9c353c02b6..8fcf28ab4e 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -5,6 +5,7 @@ import { Statistic, StatisticType } from "../../../data/recorder"; import { ForecastType } from "../../../data/weather"; import { FullCalendarView, TranslationDict } from "../../../types"; import { LovelaceCardFeatureConfig } from "../card-features/types"; +import { LegacyStateFilter } from "../common/evaluate-filter"; import { Condition, LegacyCondition } from "../common/validate-condition"; import { HuiImage } from "../components/hui-image"; import { TimestampRenderingFormat } from "../components/types"; @@ -201,7 +202,8 @@ export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig { export interface EntityFilterCardConfig extends LovelaceCardConfig { type: "entity-filter"; entities: Array; - state_filter: Array<{ key: string } | string>; + state_filter?: Array; + conditions: Array; card?: Partial; show_empty?: boolean; } diff --git a/src/panels/lovelace/common/evaluate-filter.ts b/src/panels/lovelace/common/evaluate-filter.ts index 9bc190e494..6e71695875 100644 --- a/src/panels/lovelace/common/evaluate-filter.ts +++ b/src/panels/lovelace/common/evaluate-filter.ts @@ -1,11 +1,45 @@ import { HassEntity } from "home-assistant-js-websocket"; -export const evaluateFilter = (stateObj: HassEntity, filter: any): boolean => { - const operator = filter.operator || "=="; - let value = filter.value ?? filter; - let state = filter.attribute - ? stateObj.attributes[filter.attribute] - : stateObj.state; +type FilterOperator = + | "==" + | "<=" + | "<" + | ">=" + | ">" + | "!=" + | "in" + | "not in" + | "regex"; + +// Legacy entity-filter badge & card condition +export type LegacyStateFilter = + | { + operator: FilterOperator; + attribute?: string; + value: string | number | (string | number)[]; + } + | number + | string; + +export const evaluateStateFilter = ( + stateObj: HassEntity, + filter: LegacyStateFilter +): boolean => { + let operator: FilterOperator; + let value: string | number | (string | number)[]; + let state: any; + + if (typeof filter === "object") { + operator = filter.operator; + value = filter.value; + state = filter.attribute + ? stateObj.attributes[filter.attribute] + : stateObj.state; + } else { + operator = "=="; + value = filter; + state = stateObj.state; + } if (operator === "==" || operator === "!=") { const valueIsNumeric = @@ -35,15 +69,24 @@ export const evaluateFilter = (stateObj: HassEntity, filter: any): boolean => { return state !== value; case "in": if (Array.isArray(value) || typeof value === "string") { + if (Array.isArray(value)) { + value = value.map((val) => `${val}`); + } return value.includes(state); } return false; case "not in": if (Array.isArray(value) || typeof value === "string") { + if (Array.isArray(value)) { + value = value.map((val) => `${val}`); + } return !value.includes(state); } return false; case "regex": { + if (typeof value !== "string") { + return false; + } if (state !== null && typeof state === "object") { return RegExp(value).test(JSON.stringify(state)); } diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index 861295064a..00294ff9fe 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -1,54 +1,76 @@ import { ensureArray } from "../../../common/array/ensure-array"; +import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; export type Condition = | NumericStateCondition - | ScreenCondition | StateCondition + | ScreenCondition | UserCondition | OrCondition | AndCondition; -export type LegacyCondition = { +// Legacy conditional card condition +export interface LegacyCondition { entity?: string; state?: string | string[]; state_not?: string | string[]; -}; +} -export type NumericStateCondition = { +interface BaseCondition { + condition: string; +} + +export interface NumericStateCondition extends BaseCondition { condition: "numeric_state"; entity?: string; - below?: number; - above?: number; -}; + below?: string | number; + above?: string | number; +} -export type StateCondition = { +export interface StateCondition extends BaseCondition { condition: "state"; entity?: string; state?: string | string[]; state_not?: string | string[]; -}; +} -export type ScreenCondition = { +export interface ScreenCondition extends BaseCondition { condition: "screen"; media_query?: string; -}; +} -export type UserCondition = { +export interface UserCondition extends BaseCondition { condition: "user"; users?: string[]; -}; +} -export type OrCondition = { +export interface OrCondition extends BaseCondition { condition: "or"; conditions?: Condition[]; -}; +} -export type AndCondition = { +export interface AndCondition extends BaseCondition { condition: "and"; conditions?: Condition[]; -}; +} + +function getValueFromEntityId( + hass: HomeAssistant, + value: string | string[] +): string | string[] { + if ( + typeof value === "string" && + isValidEntityId(value) && + hass.states[value] + ) { + value = hass.states[value]?.state; + } else if (Array.isArray(value)) { + value = value.map((v) => getValueFromEntityId(hass, v) as string); + } + return value; +} function checkStateCondition( condition: StateCondition | LegacyCondition, @@ -58,32 +80,50 @@ function checkStateCondition( condition.entity && hass.states[condition.entity] ? hass.states[condition.entity].state : UNAVAILABLE; + let value = condition.state ?? condition.state_not; + + // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) + if (Array.isArray(value) || typeof value === "string") { + value = getValueFromEntityId(hass, value); + } return condition.state != null - ? ensureArray(condition.state).includes(state) - : !ensureArray(condition.state_not).includes(state); + ? ensureArray(value).includes(state) + : !ensureArray(value).includes(state); } function checkStateNumericCondition( condition: NumericStateCondition, hass: HomeAssistant ) { - const entity = - (condition.entity ? hass.states[condition.entity] : undefined) ?? undefined; + const state = (condition.entity ? hass.states[condition.entity] : undefined) + ?.state; + let above = condition.above; + let below = condition.below; - if (!entity) { - return false; + // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) + if (typeof above === "string") { + above = getValueFromEntityId(hass, above) as string; + } + if (typeof below === "string") { + below = getValueFromEntityId(hass, below) as string; } - const numericState = Number(entity.state); + const numericState = Number(state); + const numericAbove = Number(above); + const numericBelow = Number(below); if (isNaN(numericState)) { return false; } return ( - (condition.above == null || condition.above < numericState) && - (condition.below == null || condition.below > numericState) + (condition.above == null || + isNaN(numericAbove) || + numericAbove < numericState) && + (condition.below == null || + isNaN(numericBelow) || + numericBelow > numericState) ); } @@ -109,6 +149,12 @@ function checkOrCondition(condition: OrCondition, hass: HomeAssistant) { return condition.conditions.some((c) => checkConditionsMet([c], hass)); } +/** + * Return the result of applying conditions + * @param conditions conditions to apply + * @param hass Home Assistant object + * @returns true if conditions are respected + */ export function checkConditionsMet( conditions: (Condition | LegacyCondition)[], hass: HomeAssistant @@ -134,6 +180,43 @@ export function checkConditionsMet( }); } +export function extractConditionEntityIds( + conditions: Condition[] +): Set { + const entityIds: Set = new Set(); + for (const condition of conditions) { + if (condition.condition === "numeric_state") { + if ( + typeof condition.above === "string" && + isValidEntityId(condition.above) + ) { + entityIds.add(condition.above); + } + if ( + typeof condition.below === "string" && + isValidEntityId(condition.below) + ) { + entityIds.add(condition.below); + } + } else if (condition.condition === "state") { + [ + ...(ensureArray(condition.state) ?? []), + ...(ensureArray(condition.state_not) ?? []), + ].forEach((state) => { + if (!!state && isValidEntityId(state)) { + entityIds.add(state); + } + }); + } else if ("conditions" in condition && condition.conditions) { + return new Set([ + ...entityIds, + ...extractConditionEntityIds(condition.conditions), + ]); + } + } + return entityIds; +} + function validateStateCondition(condition: StateCondition | LegacyCondition) { return ( condition.entity != null && @@ -163,7 +246,11 @@ function validateNumericStateCondition(condition: NumericStateCondition) { (condition.above != null || condition.below != null) ); } - +/** + * Validate the conditions config for the UI + * @param conditions conditions to apply + * @returns true if conditions are validated + */ export function validateConditionalConfig( conditions: (Condition | LegacyCondition)[] ): boolean { @@ -187,3 +274,34 @@ export function validateConditionalConfig( return validateStateCondition(c); }); } + +/** + * Build a condition for filters + * @param condition condition to apply + * @param entityId base the condition on that entity + * @returns a new condition with entity id + */ +export function addEntityToCondition( + condition: Condition, + entityId: string +): Condition { + if ("conditions" in condition && condition.conditions) { + return { + ...condition, + conditions: condition.conditions.map((c) => + addEntityToCondition(c, entityId) + ), + }; + } + + if ( + condition.condition === "state" || + condition.condition === "numeric_state" + ) { + return { + ...condition, + entity: entityId, + }; + } + return condition; +} diff --git a/src/panels/lovelace/entity-rows/types.ts b/src/panels/lovelace/entity-rows/types.ts index d7657ec87e..9ec1bd75ca 100644 --- a/src/panels/lovelace/entity-rows/types.ts +++ b/src/panels/lovelace/entity-rows/types.ts @@ -1,7 +1,8 @@ -import { ActionConfig } from "../../../data/lovelace/config/action"; -import { HomeAssistant } from "../../../types"; -import { Condition } from "../common/validate-condition"; -import { TimestampRenderingFormat } from "../components/types"; +import type { ActionConfig } from "../../../data/lovelace/config/action"; +import type { HomeAssistant } from "../../../types"; +import type { LegacyStateFilter } from "../common/evaluate-filter"; +import type { Condition } from "../common/validate-condition"; +import type { TimestampRenderingFormat } from "../components/types"; export interface EntityConfig { entity: string; @@ -14,7 +15,8 @@ export interface ActionRowConfig extends EntityConfig { action_name?: string; } export interface EntityFilterEntityConfig extends EntityConfig { - state_filter?: Array<{ key: string } | string>; + state_filter?: Array; + conditions?: Array; } export interface DividerConfig { type: "divider";