mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-08 17:07:47 +00:00
Split legacy state filter and condition logic
This commit is contained in:
parent
e20bf741a4
commit
f1ef33c1a7
@ -1,15 +1,16 @@
|
|||||||
import { PropertyValues, ReactiveElement } from "lit";
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { evaluateStateFilter } from "../common/evaluate-filter";
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
|
import {
|
||||||
|
addEntityToCondition,
|
||||||
|
checkConditionsMet,
|
||||||
|
} from "../common/validate-condition";
|
||||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||||
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||||
import { LovelaceBadge } from "../types";
|
import { LovelaceBadge } from "../types";
|
||||||
import { EntityFilterBadgeConfig } from "./types";
|
import { EntityFilterBadgeConfig } from "./types";
|
||||||
import {
|
|
||||||
buildConditionForFilter,
|
|
||||||
checkConditionsMet,
|
|
||||||
} from "../common/validate-condition";
|
|
||||||
|
|
||||||
@customElement("hui-entity-filter-badge")
|
@customElement("hui-entity-filter-badge")
|
||||||
export class HuiEntityFilterBadge
|
export class HuiEntityFilterBadge
|
||||||
@ -86,20 +87,23 @@ export class HuiEntityFilterBadge
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entitiesList = this._configEntities.filter((entityConf) => {
|
const entitiesList = this._configEntities.filter((entityConf) => {
|
||||||
const conditions =
|
const stateObj = this.hass.states[entityConf.entity];
|
||||||
entityConf.conditions ??
|
if (!stateObj) return false;
|
||||||
this._config!.conditions ??
|
|
||||||
entityConf.state_filter ??
|
|
||||||
this._config!.state_filter;
|
|
||||||
|
|
||||||
return (
|
const conditions = entityConf.conditions ?? this._config!.conditions;
|
||||||
conditions
|
if (conditions) {
|
||||||
.map((condition) =>
|
const conditionWithEntity = conditions.map((condition) =>
|
||||||
buildConditionForFilter(condition, entityConf.entity)
|
addEntityToCondition(condition, entityConf.entity)
|
||||||
)
|
);
|
||||||
.filter((condition) => checkConditionsMet([condition], this.hass!))
|
return checkConditionsMet(conditionWithEntity, this.hass!);
|
||||||
.length > 0
|
}
|
||||||
);
|
|
||||||
|
const filters = entityConf.state_filter ?? this._config!.state_filter;
|
||||||
|
if (filters) {
|
||||||
|
return filters.some((filter) => evaluateStateFilter(stateObj, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (entitiesList.length === 0) {
|
if (entitiesList.length === 0) {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { ActionConfig } from "../../../data/lovelace/config/action";
|
import type { ActionConfig } from "../../../data/lovelace/config/action";
|
||||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
import { Condition, LegacyFilterCondition } from "../common/validate-condition";
|
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
||||||
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
import type { Condition } from "../common/validate-condition";
|
||||||
|
import type { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||||
|
|
||||||
export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
|
export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
|
||||||
type: "entity-filter";
|
type: "entity-filter";
|
||||||
entities: Array<EntityFilterEntityConfig | string>;
|
entities: Array<EntityFilterEntityConfig | string>;
|
||||||
state_filter?: Array<Condition | LegacyFilterCondition | string | number>;
|
state_filter?: Array<LegacyStateFilter>;
|
||||||
conditions: Array<Condition | LegacyFilterCondition | string | number>;
|
conditions?: Array<Condition>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorBadgeConfig extends LovelaceBadgeConfig {
|
export interface ErrorBadgeConfig extends LovelaceBadgeConfig {
|
||||||
|
@ -3,16 +3,17 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
|
import { evaluateStateFilter } from "../common/evaluate-filter";
|
||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
|
import {
|
||||||
|
addEntityToCondition,
|
||||||
|
checkConditionsMet,
|
||||||
|
} from "../common/validate-condition";
|
||||||
import { createCardElement } from "../create-element/create-card-element";
|
import { createCardElement } from "../create-element/create-card-element";
|
||||||
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||||
import { LovelaceCard } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
import { EntityFilterCardConfig } from "./types";
|
import { EntityFilterCardConfig } from "./types";
|
||||||
import {
|
|
||||||
buildConditionForFilter,
|
|
||||||
checkConditionsMet,
|
|
||||||
} from "../common/validate-condition";
|
|
||||||
|
|
||||||
@customElement("hui-entity-filter-card")
|
@customElement("hui-entity-filter-card")
|
||||||
export class HuiEntityFilterCard
|
export class HuiEntityFilterCard
|
||||||
@ -141,20 +142,23 @@ export class HuiEntityFilterCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entitiesList = this._configEntities.filter((entityConf) => {
|
const entitiesList = this._configEntities.filter((entityConf) => {
|
||||||
const conditions =
|
const stateObj = this.hass!.states[entityConf.entity];
|
||||||
entityConf.conditions ??
|
if (!stateObj) return false;
|
||||||
this._config!.conditions ??
|
|
||||||
entityConf.state_filter ??
|
|
||||||
this._config!.state_filter;
|
|
||||||
|
|
||||||
return (
|
const conditions = entityConf.conditions ?? this._config!.conditions;
|
||||||
conditions
|
if (conditions) {
|
||||||
.map((condition) =>
|
const conditionWithEntity = conditions.map((condition) =>
|
||||||
buildConditionForFilter(condition, entityConf.entity)
|
addEntityToCondition(condition, entityConf.entity)
|
||||||
)
|
);
|
||||||
.filter((condition) => checkConditionsMet([condition], this.hass!))
|
return checkConditionsMet(conditionWithEntity, this.hass!);
|
||||||
.length > 0
|
}
|
||||||
);
|
|
||||||
|
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) {
|
if (entitiesList.length === 0 && this._config.show_empty === false) {
|
||||||
|
92
src/panels/lovelace/common/evaluate-filter.ts
Normal file
92
src/panels/lovelace/common/evaluate-filter.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
@ -1,8 +1,7 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
import { UNAVAILABLE } from "../../../data/entity";
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
|
||||||
|
|
||||||
export type Condition =
|
export type Condition =
|
||||||
| NumericStateCondition
|
| NumericStateCondition
|
||||||
@ -19,25 +18,6 @@ export interface LegacyCondition {
|
|||||||
state_not?: string | string[];
|
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 {
|
interface BaseCondition {
|
||||||
condition: string;
|
condition: string;
|
||||||
}
|
}
|
||||||
@ -92,72 +72,6 @@ function getValueFromEntityId(
|
|||||||
return value;
|
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(
|
function checkStateCondition(
|
||||||
condition: StateCondition | LegacyCondition,
|
condition: StateCondition | LegacyCondition,
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
@ -235,40 +149,6 @@ function checkOrCondition(condition: OrCondition, hass: HomeAssistant) {
|
|||||||
return condition.conditions.some((c) => checkConditionsMet([c], hass));
|
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
|
* Return the result of applying conditions
|
||||||
* @param conditions conditions to apply
|
* @param conditions conditions to apply
|
||||||
@ -276,7 +156,7 @@ export function buildConditionForFilter(
|
|||||||
* @returns true if conditions are respected
|
* @returns true if conditions are respected
|
||||||
*/
|
*/
|
||||||
export function checkConditionsMet(
|
export function checkConditionsMet(
|
||||||
conditions: (Condition | LegacyCondition | LegacyFilterCondition)[],
|
conditions: (Condition | LegacyCondition)[],
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
): boolean {
|
): boolean {
|
||||||
return conditions.every((c) => {
|
return conditions.every((c) => {
|
||||||
@ -295,8 +175,6 @@ export function checkConditionsMet(
|
|||||||
default:
|
default:
|
||||||
return checkStateCondition(c, hass);
|
return checkStateCondition(c, hass);
|
||||||
}
|
}
|
||||||
} else if ("operator" in c) {
|
|
||||||
return checkLegacyFilterCondition(c, hass);
|
|
||||||
}
|
}
|
||||||
return checkStateCondition(c, hass);
|
return checkStateCondition(c, hass);
|
||||||
});
|
});
|
||||||
@ -359,3 +237,34 @@ export function validateConditionalConfig(
|
|||||||
return validateStateCondition(c);
|
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;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { ActionConfig } from "../../../data/lovelace/config/action";
|
import type { ActionConfig } from "../../../data/lovelace/config/action";
|
||||||
import { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { Condition, LegacyFilterCondition } from "../common/validate-condition";
|
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
||||||
import { TimestampRenderingFormat } from "../components/types";
|
import type { Condition } from "../common/validate-condition";
|
||||||
|
import type { TimestampRenderingFormat } from "../components/types";
|
||||||
|
|
||||||
export interface EntityConfig {
|
export interface EntityConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
@ -14,8 +15,8 @@ export interface ActionRowConfig extends EntityConfig {
|
|||||||
action_name?: string;
|
action_name?: string;
|
||||||
}
|
}
|
||||||
export interface EntityFilterEntityConfig extends EntityConfig {
|
export interface EntityFilterEntityConfig extends EntityConfig {
|
||||||
state_filter?: Array<Condition | LegacyFilterCondition | string | number>;
|
state_filter?: Array<LegacyStateFilter>;
|
||||||
conditions?: Array<Condition | LegacyFilterCondition | string | number>;
|
conditions?: Array<Condition>;
|
||||||
}
|
}
|
||||||
export interface DividerConfig {
|
export interface DividerConfig {
|
||||||
type: "divider";
|
type: "divider";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user