Split legacy state filter and condition logic

This commit is contained in:
Paul Bottein 2024-02-02 15:40:15 +01:00
parent e20bf741a4
commit f1ef33c1a7
No known key found for this signature in database
6 changed files with 181 additions and 170 deletions

View File

@ -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) {

View File

@ -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<EntityFilterEntityConfig | string>;
state_filter?: Array<Condition | LegacyFilterCondition | string | number>;
conditions: Array<Condition | LegacyFilterCondition | string | number>;
state_filter?: Array<LegacyStateFilter>;
conditions?: Array<Condition>;
}
export interface ErrorBadgeConfig extends LovelaceBadgeConfig {

View File

@ -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) {

View File

@ -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;
}
};

View File

@ -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;
}

View File

@ -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<Condition | LegacyFilterCondition | string | number>;
conditions?: Array<Condition | LegacyFilterCondition | string | number>;
state_filter?: Array<LegacyStateFilter>;
conditions?: Array<Condition>;
}
export interface DividerConfig {
type: "divider";