`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 f5e6a65a37
.
* 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:
parent
cbc150bad2
commit
552eeeddf6
|
@ -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 = {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<EntityFilterEntityConfig | string>;
|
||||
state_filter: Array<{ key: string } | string>;
|
||||
state_filter?: Array<LegacyStateFilter>;
|
||||
conditions?: Array<Condition>;
|
||||
}
|
||||
|
||||
export interface ErrorBadgeConfig extends LovelaceBadgeConfig {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<EntityFilterEntityConfig | string>;
|
||||
state_filter: Array<{ key: string } | string>;
|
||||
state_filter?: Array<LegacyStateFilter>;
|
||||
conditions: Array<Condition>;
|
||||
card?: Partial<LovelaceCardConfig>;
|
||||
show_empty?: boolean;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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<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) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<LegacyStateFilter>;
|
||||
conditions?: Array<Condition>;
|
||||
}
|
||||
export interface DividerConfig {
|
||||
type: "divider";
|
||||
|
|
Loading…
Reference in New Issue