From f1ef33c1a7732809a2339160e446b1fd9a7f3425 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 2 Feb 2024 15:40:15 +0100 Subject: [PATCH] Split legacy state filter and condition logic --- .../badges/hui-entity-filter-badge.ts | 38 +++-- src/panels/lovelace/badges/types.ts | 13 +- .../lovelace/cards/hui-entity-filter-card.ts | 38 +++-- src/panels/lovelace/common/evaluate-filter.ts | 92 ++++++++++ .../lovelace/common/validate-condition.ts | 157 ++++-------------- src/panels/lovelace/entity-rows/types.ts | 13 +- 6 files changed, 181 insertions(+), 170 deletions(-) create mode 100644 src/panels/lovelace/common/evaluate-filter.ts diff --git a/src/panels/lovelace/badges/hui-entity-filter-badge.ts b/src/panels/lovelace/badges/hui-entity-filter-badge.ts index 249cdb7a9e..8f76c99a1e 100644 --- a/src/panels/lovelace/badges/hui-entity-filter-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-filter-badge.ts @@ -1,15 +1,16 @@ import { PropertyValues, ReactiveElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { HomeAssistant } from "../../../types"; +import { evaluateStateFilter } from "../common/evaluate-filter"; import { processConfigEntities } from "../common/process-config-entities"; +import { + addEntityToCondition, + checkConditionsMet, +} from "../common/validate-condition"; import { createBadgeElement } from "../create-element/create-badge-element"; import { EntityFilterEntityConfig } from "../entity-rows/types"; import { LovelaceBadge } from "../types"; import { EntityFilterBadgeConfig } from "./types"; -import { - buildConditionForFilter, - checkConditionsMet, -} from "../common/validate-condition"; @customElement("hui-entity-filter-badge") export class HuiEntityFilterBadge @@ -86,20 +87,23 @@ export class HuiEntityFilterBadge } const entitiesList = this._configEntities.filter((entityConf) => { - const conditions = - entityConf.conditions ?? - this._config!.conditions ?? - entityConf.state_filter ?? - this._config!.state_filter; + const stateObj = this.hass.states[entityConf.entity]; + if (!stateObj) return false; - return ( - conditions - .map((condition) => - buildConditionForFilter(condition, entityConf.entity) - ) - .filter((condition) => checkConditionsMet([condition], this.hass!)) - .length > 0 - ); + const conditions = entityConf.conditions ?? this._config!.conditions; + if (conditions) { + const conditionWithEntity = conditions.map((condition) => + addEntityToCondition(condition, entityConf.entity) + ); + return checkConditionsMet(conditionWithEntity, this.hass!); + } + + const filters = entityConf.state_filter ?? this._config!.state_filter; + if (filters) { + return filters.some((filter) => evaluateStateFilter(stateObj, filter)); + } + + return false; }); if (entitiesList.length === 0) { diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index 0f89451da2..7416f8b705 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -1,13 +1,14 @@ -import { ActionConfig } from "../../../data/lovelace/config/action"; -import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; -import { Condition, LegacyFilterCondition } from "../common/validate-condition"; -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; - conditions: Array; + 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 6ee1b40285..edb7d418d1 100644 --- a/src/panels/lovelace/cards/hui-entity-filter-card.ts +++ b/src/panels/lovelace/cards/hui-entity-filter-card.ts @@ -3,16 +3,17 @@ 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 { evaluateStateFilter } from "../common/evaluate-filter"; import { findEntities } from "../common/find-entities"; import { processConfigEntities } from "../common/process-config-entities"; +import { + addEntityToCondition, + checkConditionsMet, +} from "../common/validate-condition"; import { createCardElement } from "../create-element/create-card-element"; import { EntityFilterEntityConfig } from "../entity-rows/types"; import { LovelaceCard } from "../types"; import { EntityFilterCardConfig } from "./types"; -import { - buildConditionForFilter, - checkConditionsMet, -} from "../common/validate-condition"; @customElement("hui-entity-filter-card") export class HuiEntityFilterCard @@ -141,20 +142,23 @@ export class HuiEntityFilterCard } const entitiesList = this._configEntities.filter((entityConf) => { - const conditions = - entityConf.conditions ?? - this._config!.conditions ?? - entityConf.state_filter ?? - this._config!.state_filter; + const stateObj = this.hass!.states[entityConf.entity]; + if (!stateObj) return false; - return ( - conditions - .map((condition) => - buildConditionForFilter(condition, entityConf.entity) - ) - .filter((condition) => checkConditionsMet([condition], this.hass!)) - .length > 0 - ); + const conditions = entityConf.conditions ?? this._config!.conditions; + if (conditions) { + const conditionWithEntity = conditions.map((condition) => + addEntityToCondition(condition, entityConf.entity) + ); + return checkConditionsMet(conditionWithEntity, this.hass!); + } + + const filters = entityConf.state_filter ?? this._config!.state_filter; + if (filters) { + return filters.some((filter) => evaluateStateFilter(stateObj, filter)); + } + + return false; }); if (entitiesList.length === 0 && this._config.show_empty === false) { diff --git a/src/panels/lovelace/common/evaluate-filter.ts b/src/panels/lovelace/common/evaluate-filter.ts new file mode 100644 index 0000000000..151b8022d8 --- /dev/null +++ b/src/panels/lovelace/common/evaluate-filter.ts @@ -0,0 +1,92 @@ +import { HassEntity } from "home-assistant-js-websocket"; + +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 = + typeof value === "number" || + (typeof value === "string" && value.trim() && !isNaN(Number(value))); + const stateIsNumeric = + typeof state === "number" || + (typeof state === "string" && state.trim() && !isNaN(Number(state))); + if (valueIsNumeric && stateIsNumeric) { + value = Number(value); + state = Number(state); + } + } + + switch (operator) { + case "==": + return state === value; + case "<=": + return state <= value; + case "<": + return state < value; + case ">=": + return state >= value; + case ">": + return state > value; + case "!=": + return state !== value; + case "in": + if (Array.isArray(value) || typeof value === "string") { + return value.includes(state); + } + return false; + case "not in": + if (Array.isArray(value) || typeof value === "string") { + 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)); + } + return RegExp(value).test(state); + } + default: + return false; + } +}; diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index ef0e4aecd6..87c2b5759e 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -1,8 +1,7 @@ -import { HassEntity } from "home-assistant-js-websocket"; import { ensureArray } from "../../../common/array/ensure-array"; +import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; -import { isValidEntityId } from "../../../common/entity/valid_entity_id"; export type Condition = | NumericStateCondition @@ -19,25 +18,6 @@ export interface LegacyCondition { state_not?: string | string[]; } -type FilterOperator = - | "==" - | "<=" - | "<" - | ">=" - | ">" - | "!=" - | "in" - | "not in" - | "regex"; - -// Legacy entity-filter badge & card condition -export interface LegacyFilterCondition { - operator: FilterOperator; - entity?: string | number; - attribute?: string; - value: string | number | string[]; -} - interface BaseCondition { condition: string; } @@ -92,72 +72,6 @@ function getValueFromEntityId( return value; } -function checkLegacyFilterCondition( - condition: LegacyFilterCondition, - hass: HomeAssistant -) { - const entity: HassEntity = hass.states[condition.entity!]; - - if (!entity) { - return false; - } - - let value = condition.value; - let state: string | number = condition.attribute - ? entity.attributes[condition.attribute] - : entity.state; - - if (Array.isArray(value) || typeof value === "string") { - value = getValueFromEntityId(hass, value); - } - - if (condition.operator === "==" || condition.operator === "!=") { - const valueIsNumeric = - typeof value === "number" || - (typeof value === "string" && !isNaN(Number(value.trim()))); - const stateIsNumeric = - typeof state === "number" || - (typeof state === "string" && !isNaN(Number(state.trim()))); - if (valueIsNumeric && stateIsNumeric) { - value = Number(value); - state = Number(state); - } - } - - switch (condition.operator) { - case "==": - return state === value; - case "<=": - return state <= value; - case "<": - return state < value; - case ">=": - return state >= value; - case ">": - return state > value; - case "!=": - return state !== value; - case "in": - if (Array.isArray(value) || typeof value === "string") { - return value.includes(`${state}`); - } - return false; - case "not in": - if (Array.isArray(value) || typeof value === "string") { - return !value.includes(`${state}`); - } - return false; - case "regex": { - if (state !== null && typeof state === "object") { - return RegExp(`${value}`).test(JSON.stringify(state)); - } - return RegExp(`${value}`).test(`${state}`); - } - default: - return false; - } -} - function checkStateCondition( condition: StateCondition | LegacyCondition, hass: HomeAssistant @@ -235,40 +149,6 @@ function checkOrCondition(condition: OrCondition, hass: HomeAssistant) { return condition.conditions.some((c) => checkConditionsMet([c], hass)); } -/** - * Build a condition for filters - * @param condition condition to apply - * @param entityId base the condition on that entity (current entity to filter) - * @returns a new condition that handled legacy filter conditions - */ -export function buildConditionForFilter( - condition: Condition | LegacyFilterCondition | string | number, - entityId: string -): Condition | LegacyFilterCondition { - let newCondition: Condition | LegacyFilterCondition; - - if (typeof condition === "string" || typeof condition === "number") { - newCondition = { - condition: "state", - state: `${condition}`, - }; - } else { - newCondition = condition; - } - - // Set the entity to filter on - if ( - ("condition" in newCondition && - (newCondition.condition === "numeric_state" || - newCondition.condition === "state")) || - "operator" in newCondition - ) { - newCondition.entity = entityId; - } - - return newCondition; -} - /** * Return the result of applying conditions * @param conditions conditions to apply @@ -276,7 +156,7 @@ export function buildConditionForFilter( * @returns true if conditions are respected */ export function checkConditionsMet( - conditions: (Condition | LegacyCondition | LegacyFilterCondition)[], + conditions: (Condition | LegacyCondition)[], hass: HomeAssistant ): boolean { return conditions.every((c) => { @@ -295,8 +175,6 @@ export function checkConditionsMet( default: return checkStateCondition(c, hass); } - } else if ("operator" in c) { - return checkLegacyFilterCondition(c, hass); } return checkStateCondition(c, hass); }); @@ -359,3 +237,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 473b5990e7..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, LegacyFilterCondition } 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,8 +15,8 @@ export interface ActionRowConfig extends EntityConfig { action_name?: string; } export interface EntityFilterEntityConfig extends EntityConfig { - state_filter?: Array; - conditions?: Array; + state_filter?: Array; + conditions?: Array; } export interface DividerConfig { type: "divider";