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 <paul.bottein@gmail.com>

* 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 <paul.bottein@gmail.com>
This commit is contained in:
Quentame 2024-03-19 14:29:34 +01:00 committed by GitHub
parent cbc150bad2
commit 552eeeddf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 461 additions and 104 deletions

View File

@ -21,10 +21,10 @@ const ENTITIES = [
}), }),
]; ];
const conditions = [ const conditions: Condition[] = [
{ condition: "and" }, { condition: "and", conditions: [] },
{ condition: "not" }, { condition: "not", conditions: [] },
{ condition: "or" }, { condition: "or", conditions: [] },
{ condition: "state", entity_id: "light.kitchen", state: "on" }, { condition: "state", entity_id: "light.kitchen", state: "on" },
{ {
condition: "numeric_state", condition: "numeric_state",
@ -34,11 +34,11 @@ const conditions = [
above: 20, above: 20,
}, },
{ condition: "sun", after: "sunset" }, { 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: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
{ condition: "trigger", id: "motion" }, { condition: "trigger", id: "motion" },
{ condition: "time" }, { condition: "time" },
{ condition: "template" }, { condition: "template", value_template: "" },
]; ];
const initialCondition: Condition = { const initialCondition: Condition = {

View File

@ -11,7 +11,7 @@ const ENTITIES = [
latitude: 32.877105, latitude: 32.877105,
longitude: 117.232185, longitude: 117.232185,
gps_accuracy: 91, gps_accuracy: 91,
battery: 71, battery: 25,
friendly_name: "Paulus", friendly_name: "Paulus",
}), }),
getEntity("device_tracker", "demo_anne_therese", "school", { getEntity("device_tracker", "demo_anne_therese", "school", {
@ -19,7 +19,7 @@ const ENTITIES = [
latitude: 32.877105, latitude: 32.877105,
longitude: 117.232185, longitude: 117.232185,
gps_accuracy: 91, gps_accuracy: 91,
battery: 71, battery: 50,
friendly_name: "Anne Therese", friendly_name: "Anne Therese",
}), }),
getEntity("device_tracker", "demo_home_boy", "home", { getEntity("device_tracker", "demo_home_boy", "home", {
@ -27,7 +27,7 @@ const ENTITIES = [
latitude: 32.877105, latitude: 32.877105,
longitude: 117.232185, longitude: 117.232185,
gps_accuracy: 91, gps_accuracy: 91,
battery: 71, battery: 75,
friendly_name: "Home Boy", friendly_name: "Home Boy",
}), }),
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {
@ -39,21 +39,53 @@ const ENTITIES = [
getEntity("light", "ceiling_lights", "off", { getEntity("light", "ceiling_lights", "off", {
friendly_name: "Ceiling Lights", 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 = [ const CONFIGS = [
{ {
heading: "Unfiltered controller", heading: "Unfiltered entities",
config: ` config: `
- type: entities - type: entities
entities: entities:
- light.bed_light - device_tracker.demo_anne_therese
- light.ceiling_lights - device_tracker.demo_home_boy
- light.kitchen_lights - device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
`, `,
}, },
{ {
heading: "Filtered entities card", heading: "On and home entities",
config: ` config: `
- type: entity-filter - type: entity-filter
entities: entities:
@ -63,9 +95,28 @@ const CONFIGS = [
- light.bed_light - light.bed_light
- light.ceiling_lights - light.ceiling_lights
- light.kitchen_lights - light.kitchen_lights
state_filter: conditions:
- "on" - condition: state
- home 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.bed_light
- light.ceiling_lights - light.ceiling_lights
- light.kitchen_lights - light.kitchen_lights
state_filter: conditions:
- "on" - condition: state
- not_home state:
- "on"
- home
card: card:
type: entities type: entities
title: Custom Title title: Custom Title
@ -99,15 +152,101 @@ const CONFIGS = [
- light.bed_light - light.bed_light
- light.ceiling_lights - light.ceiling_lights
- light.kitchen_lights - light.kitchen_lights
state_filter: conditions:
- "on" - condition: state
- not_home state:
- "on"
- home
card: card:
type: glance type: glance
show_state: true show_state: true
title: Custom Title 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") @customElement("demo-lovelace-entity-filter-card")

View File

@ -219,8 +219,8 @@ export interface NumericStateCondition extends BaseCondition {
condition: "numeric_state"; condition: "numeric_state";
entity_id: string; entity_id: string;
attribute?: string; attribute?: string;
above?: number; above?: string | number;
below?: number; below?: string | number;
value_template?: string; value_template?: string;
} }

View File

@ -1,8 +1,13 @@
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 { evaluateFilter } from "../common/evaluate-filter"; import { evaluateStateFilter } from "../common/evaluate-filter";
import { processConfigEntities } from "../common/process-config-entities"; import { processConfigEntities } from "../common/process-config-entities";
import {
addEntityToCondition,
checkConditionsMet,
extractConditionEntityIds,
} 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";
@ -29,7 +34,10 @@ export class HuiEntityFilterBadge
} }
if ( 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( !config.entities.every(
(entity) => (entity) =>
typeof entity === "object" && typeof entity === "object" &&
@ -81,23 +89,19 @@ export class HuiEntityFilterBadge
const entitiesList = this._configEntities.filter((entityConf) => { const entitiesList = this._configEntities.filter((entityConf) => {
const stateObj = this.hass.states[entityConf.entity]; const stateObj = this.hass.states[entityConf.entity];
if (!stateObj) return false;
if (!stateObj) { const conditions = entityConf.conditions ?? this._config!.conditions;
return false; if (conditions) {
const conditionWithEntity = conditions.map((condition) =>
addEntityToCondition(condition, entityConf.entity)
);
return checkConditionsMet(conditionWithEntity, this.hass!);
} }
if (entityConf.state_filter) { const filters = entityConf.state_filter ?? this._config!.state_filter;
for (const filter of entityConf.state_filter) { if (filters) {
if (evaluateFilter(stateObj, filter)) { return filters.some((filter) => evaluateStateFilter(stateObj, filter));
return true;
}
}
} else {
for (const filter of this._config!.state_filter) {
if (evaluateFilter(stateObj, filter)) {
return true;
}
}
} }
return false; return false;
@ -152,8 +156,24 @@ export class HuiEntityFilterBadge
if (this.hass.states[config.entity] !== oldHass.states[config.entity]) { if (this.hass.states[config.entity] !== oldHass.states[config.entity]) {
return true; 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; return false;
} }
} }

View File

@ -1,11 +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 { EntityFilterEntityConfig } from "../entity-rows/types"; 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 { export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
type: "entity-filter"; type: "entity-filter";
entities: Array<EntityFilterEntityConfig | string>; entities: Array<EntityFilterEntityConfig | string>;
state_filter: Array<{ key: string } | string>; state_filter?: Array<LegacyStateFilter>;
conditions?: Array<Condition>;
} }
export interface ErrorBadgeConfig extends LovelaceBadgeConfig { export interface ErrorBadgeConfig extends LovelaceBadgeConfig {

View File

@ -3,9 +3,14 @@ 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 { evaluateFilter } from "../common/evaluate-filter"; 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,
extractConditionEntityIds,
} 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";
@ -33,9 +38,14 @@ export class HuiEntityFilterCard
return { return {
type: "entity-filter", type: "entity-filter",
entities: foundEntities, entities: foundEntities,
state_filter: [ conditions: foundEntities[0]
foundEntities[0] ? hass.states[foundEntities[0]].state : "", ? [
], {
condition: "state",
state: hass.states[foundEntities[0]].state,
},
]
: [],
card: { type: "entities" }, card: { type: "entities" },
}; };
} }
@ -61,12 +71,19 @@ export class HuiEntityFilterCard
} }
public setConfig(config: EntityFilterCardConfig): void { 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"); throw new Error("Entities must be specified");
} }
if ( 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( !config.entities.every(
(entity) => (entity) =>
typeof entity === "object" && typeof entity === "object" &&
@ -127,23 +144,19 @@ export class HuiEntityFilterCard
const entitiesList = this._configEntities.filter((entityConf) => { const entitiesList = this._configEntities.filter((entityConf) => {
const stateObj = this.hass!.states[entityConf.entity]; const stateObj = this.hass!.states[entityConf.entity];
if (!stateObj) return false;
if (!stateObj) { const conditions = entityConf.conditions ?? this._config!.conditions;
return false; if (conditions) {
const conditionWithEntity = conditions.map((condition) =>
addEntityToCondition(condition, entityConf.entity)
);
return checkConditionsMet(conditionWithEntity, this.hass!);
} }
if (entityConf.state_filter) { const filters = entityConf.state_filter ?? this._config!.state_filter;
for (const filter of entityConf.state_filter) { if (filters) {
if (evaluateFilter(stateObj, filter)) { return filters.some((filter) => evaluateStateFilter(stateObj, filter));
return true;
}
}
} else {
for (const filter of this._config!.state_filter) {
if (evaluateFilter(stateObj, filter)) {
return true;
}
}
} }
return false; return false;
@ -202,6 +215,23 @@ export class HuiEntityFilterCard
if (this.hass.states[config.entity] !== oldHass.states[config.entity]) { if (this.hass.states[config.entity] !== oldHass.states[config.entity]) {
return true; 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; return false;

View File

@ -5,6 +5,7 @@ import { Statistic, StatisticType } from "../../../data/recorder";
import { ForecastType } from "../../../data/weather"; import { ForecastType } from "../../../data/weather";
import { FullCalendarView, TranslationDict } from "../../../types"; import { FullCalendarView, TranslationDict } from "../../../types";
import { LovelaceCardFeatureConfig } from "../card-features/types"; import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LegacyStateFilter } from "../common/evaluate-filter";
import { Condition, LegacyCondition } from "../common/validate-condition"; import { Condition, LegacyCondition } from "../common/validate-condition";
import { HuiImage } from "../components/hui-image"; import { HuiImage } from "../components/hui-image";
import { TimestampRenderingFormat } from "../components/types"; import { TimestampRenderingFormat } from "../components/types";
@ -201,7 +202,8 @@ export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig {
export interface EntityFilterCardConfig extends LovelaceCardConfig { export interface EntityFilterCardConfig extends LovelaceCardConfig {
type: "entity-filter"; type: "entity-filter";
entities: Array<EntityFilterEntityConfig | string>; entities: Array<EntityFilterEntityConfig | string>;
state_filter: Array<{ key: string } | string>; state_filter?: Array<LegacyStateFilter>;
conditions: Array<Condition>;
card?: Partial<LovelaceCardConfig>; card?: Partial<LovelaceCardConfig>;
show_empty?: boolean; show_empty?: boolean;
} }

View File

@ -1,11 +1,45 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
export const evaluateFilter = (stateObj: HassEntity, filter: any): boolean => { type FilterOperator =
const operator = filter.operator || "=="; | "=="
let value = filter.value ?? filter; | "<="
let state = filter.attribute | "<"
? stateObj.attributes[filter.attribute] | ">="
: stateObj.state; | ">"
| "!="
| "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 === "!=") { if (operator === "==" || operator === "!=") {
const valueIsNumeric = const valueIsNumeric =
@ -35,15 +69,24 @@ export const evaluateFilter = (stateObj: HassEntity, filter: any): boolean => {
return state !== value; return state !== value;
case "in": case "in":
if (Array.isArray(value) || typeof value === "string") { if (Array.isArray(value) || typeof value === "string") {
if (Array.isArray(value)) {
value = value.map((val) => `${val}`);
}
return value.includes(state); return value.includes(state);
} }
return false; return false;
case "not in": case "not in":
if (Array.isArray(value) || typeof value === "string") { if (Array.isArray(value) || typeof value === "string") {
if (Array.isArray(value)) {
value = value.map((val) => `${val}`);
}
return !value.includes(state); return !value.includes(state);
} }
return false; return false;
case "regex": { case "regex": {
if (typeof value !== "string") {
return false;
}
if (state !== null && typeof state === "object") { if (state !== null && typeof state === "object") {
return RegExp(value).test(JSON.stringify(state)); return RegExp(value).test(JSON.stringify(state));
} }

View File

@ -1,54 +1,76 @@
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";
export type Condition = export type Condition =
| NumericStateCondition | NumericStateCondition
| ScreenCondition
| StateCondition | StateCondition
| ScreenCondition
| UserCondition | UserCondition
| OrCondition | OrCondition
| AndCondition; | AndCondition;
export type LegacyCondition = { // Legacy conditional card condition
export interface LegacyCondition {
entity?: string; entity?: string;
state?: string | string[]; state?: string | string[];
state_not?: string | string[]; state_not?: string | string[];
}; }
export type NumericStateCondition = { interface BaseCondition {
condition: string;
}
export interface NumericStateCondition extends BaseCondition {
condition: "numeric_state"; condition: "numeric_state";
entity?: string; entity?: string;
below?: number; below?: string | number;
above?: number; above?: string | number;
}; }
export type StateCondition = { export interface StateCondition extends BaseCondition {
condition: "state"; condition: "state";
entity?: string; entity?: string;
state?: string | string[]; state?: string | string[];
state_not?: string | string[]; state_not?: string | string[];
}; }
export type ScreenCondition = { export interface ScreenCondition extends BaseCondition {
condition: "screen"; condition: "screen";
media_query?: string; media_query?: string;
}; }
export type UserCondition = { export interface UserCondition extends BaseCondition {
condition: "user"; condition: "user";
users?: string[]; users?: string[];
}; }
export type OrCondition = { export interface OrCondition extends BaseCondition {
condition: "or"; condition: "or";
conditions?: Condition[]; conditions?: Condition[];
}; }
export type AndCondition = { export interface AndCondition extends BaseCondition {
condition: "and"; condition: "and";
conditions?: Condition[]; 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( function checkStateCondition(
condition: StateCondition | LegacyCondition, condition: StateCondition | LegacyCondition,
@ -58,32 +80,50 @@ function checkStateCondition(
condition.entity && hass.states[condition.entity] condition.entity && hass.states[condition.entity]
? hass.states[condition.entity].state ? hass.states[condition.entity].state
: UNAVAILABLE; : 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 return condition.state != null
? ensureArray(condition.state).includes(state) ? ensureArray(value).includes(state)
: !ensureArray(condition.state_not).includes(state); : !ensureArray(value).includes(state);
} }
function checkStateNumericCondition( function checkStateNumericCondition(
condition: NumericStateCondition, condition: NumericStateCondition,
hass: HomeAssistant hass: HomeAssistant
) { ) {
const entity = const state = (condition.entity ? hass.states[condition.entity] : undefined)
(condition.entity ? hass.states[condition.entity] : undefined) ?? undefined; ?.state;
let above = condition.above;
let below = condition.below;
if (!entity) { // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
return false; 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)) { if (isNaN(numericState)) {
return false; return false;
} }
return ( return (
(condition.above == null || condition.above < numericState) && (condition.above == null ||
(condition.below == null || condition.below > numericState) 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 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( export function checkConditionsMet(
conditions: (Condition | LegacyCondition)[], conditions: (Condition | LegacyCondition)[],
hass: HomeAssistant hass: HomeAssistant
@ -134,6 +180,43 @@ export function checkConditionsMet(
}); });
} }
export function extractConditionEntityIds(
conditions: Condition[]
): Set<string> {
const entityIds: Set<string> = 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) { function validateStateCondition(condition: StateCondition | LegacyCondition) {
return ( return (
condition.entity != null && condition.entity != null &&
@ -163,7 +246,11 @@ function validateNumericStateCondition(condition: NumericStateCondition) {
(condition.above != null || condition.below != null) (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( export function validateConditionalConfig(
conditions: (Condition | LegacyCondition)[] conditions: (Condition | LegacyCondition)[]
): boolean { ): boolean {
@ -187,3 +274,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;
}

View File

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