mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
* Add view columns visibility condition * Use max column, not column count * Rename * Remove editor
469 lines
12 KiB
TypeScript
469 lines
12 KiB
TypeScript
import { ensureArray } from "../../../common/array/ensure-array";
|
|
import {
|
|
checkTimeInRange,
|
|
isValidTimeString,
|
|
} from "../../../common/datetime/check_time";
|
|
import {
|
|
WEEKDAYS_SHORT,
|
|
type WeekdayShort,
|
|
} from "../../../common/datetime/weekday";
|
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
|
import { UNKNOWN } from "../../../data/entity/entity";
|
|
import { getUserPerson } from "../../../data/person";
|
|
import type { HomeAssistant } from "../../../types";
|
|
|
|
export type Condition =
|
|
| ViewColumnsCondition
|
|
| LocationCondition
|
|
| NumericStateCondition
|
|
| StateCondition
|
|
| ScreenCondition
|
|
| TimeCondition
|
|
| UserCondition
|
|
| OrCondition
|
|
| AndCondition
|
|
| NotCondition;
|
|
|
|
// Legacy conditional card condition
|
|
export interface LegacyCondition {
|
|
entity?: string;
|
|
state?: string | string[];
|
|
state_not?: string | string[];
|
|
}
|
|
|
|
interface BaseCondition {
|
|
condition: string;
|
|
}
|
|
|
|
export interface ConditionContext {
|
|
max_columns?: number;
|
|
}
|
|
|
|
export interface ViewColumnsCondition extends BaseCondition {
|
|
condition: "view_columns";
|
|
min?: number;
|
|
max?: number;
|
|
}
|
|
|
|
export interface LocationCondition extends BaseCondition {
|
|
condition: "location";
|
|
locations?: string[];
|
|
}
|
|
|
|
export interface NumericStateCondition extends BaseCondition {
|
|
condition: "numeric_state";
|
|
entity?: string;
|
|
below?: string | number;
|
|
above?: string | number;
|
|
}
|
|
|
|
export interface StateCondition extends BaseCondition {
|
|
condition: "state";
|
|
entity?: string;
|
|
state?: string | string[];
|
|
state_not?: string | string[];
|
|
}
|
|
|
|
export interface ScreenCondition extends BaseCondition {
|
|
condition: "screen";
|
|
media_query?: string;
|
|
}
|
|
|
|
export interface TimeCondition extends BaseCondition {
|
|
condition: "time";
|
|
after?: string;
|
|
before?: string;
|
|
weekdays?: WeekdayShort[];
|
|
}
|
|
|
|
export interface UserCondition extends BaseCondition {
|
|
condition: "user";
|
|
users?: string[];
|
|
}
|
|
|
|
export interface OrCondition extends BaseCondition {
|
|
condition: "or";
|
|
conditions?: Condition[];
|
|
}
|
|
|
|
export interface AndCondition extends BaseCondition {
|
|
condition: "and";
|
|
conditions?: Condition[];
|
|
}
|
|
|
|
export interface NotCondition extends BaseCondition {
|
|
condition: "not";
|
|
conditions?: Condition[];
|
|
}
|
|
|
|
function getValueFromEntityId(
|
|
hass: HomeAssistant,
|
|
value: string
|
|
): string | undefined {
|
|
if (isValidEntityId(value) && hass.states[value]) {
|
|
return hass.states[value]?.state;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function checkStateCondition(
|
|
condition: StateCondition | LegacyCondition,
|
|
hass: HomeAssistant
|
|
) {
|
|
const state =
|
|
condition.entity && hass.states[condition.entity]
|
|
? hass.states[condition.entity].state
|
|
: UNKNOWN;
|
|
let value = condition.state ?? condition.state_not;
|
|
|
|
// Guard against invalid/incomplete condition configuration
|
|
if (value === undefined) {
|
|
return false;
|
|
}
|
|
|
|
// Handle entity_id, UI should be updated for conditional card (filters does not have UI for now)
|
|
if (Array.isArray(value)) {
|
|
const entityValues = value
|
|
.map((v) => getValueFromEntityId(hass, v))
|
|
.filter((v): v is string => v !== undefined);
|
|
value = [...value, ...entityValues];
|
|
} else if (typeof value === "string") {
|
|
const entityValue = getValueFromEntityId(hass, value);
|
|
value = [value];
|
|
if (entityValue) {
|
|
value.push(entityValue);
|
|
}
|
|
}
|
|
|
|
return condition.state != null
|
|
? ensureArray(value).includes(state)
|
|
: !ensureArray(value).includes(state);
|
|
}
|
|
|
|
function checkStateNumericCondition(
|
|
condition: NumericStateCondition,
|
|
hass: HomeAssistant
|
|
) {
|
|
const state = (condition.entity ? hass.states[condition.entity] : undefined)
|
|
?.state;
|
|
let above = condition.above;
|
|
let below = condition.below;
|
|
|
|
// Handle entity_id, UI should be updated for conditional card (filters does not have UI for now)
|
|
if (typeof above === "string") {
|
|
above = getValueFromEntityId(hass, above) ?? above;
|
|
}
|
|
if (typeof below === "string") {
|
|
below = getValueFromEntityId(hass, below) ?? below;
|
|
}
|
|
|
|
const numericState = Number(state);
|
|
const numericAbove = Number(above);
|
|
const numericBelow = Number(below);
|
|
|
|
if (isNaN(numericState)) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
(condition.above == null ||
|
|
isNaN(numericAbove) ||
|
|
numericAbove < numericState) &&
|
|
(condition.below == null ||
|
|
isNaN(numericBelow) ||
|
|
numericBelow > numericState)
|
|
);
|
|
}
|
|
|
|
function checkViewColumnsCondition(
|
|
condition: ViewColumnsCondition,
|
|
context: ConditionContext
|
|
) {
|
|
if (!context.max_columns) return true;
|
|
return (
|
|
(condition.min == null || context.max_columns >= condition.min) &&
|
|
(condition.max == null || context.max_columns <= condition.max)
|
|
);
|
|
}
|
|
|
|
function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
|
|
return condition.media_query
|
|
? matchMedia(condition.media_query).matches
|
|
: false;
|
|
}
|
|
|
|
function checkTimeCondition(
|
|
condition: Omit<TimeCondition, "condition">,
|
|
hass: HomeAssistant
|
|
) {
|
|
return checkTimeInRange(hass, condition);
|
|
}
|
|
|
|
function checkLocationCondition(
|
|
condition: LocationCondition,
|
|
hass: HomeAssistant
|
|
) {
|
|
const stateObj = getUserPerson(hass);
|
|
if (!stateObj) {
|
|
return false;
|
|
}
|
|
return condition.locations?.includes(stateObj.state);
|
|
}
|
|
|
|
function checkUserCondition(condition: UserCondition, hass: HomeAssistant) {
|
|
return condition.users && hass.user?.id
|
|
? condition.users.includes(hass.user.id)
|
|
: false;
|
|
}
|
|
|
|
function checkAndCondition(
|
|
condition: AndCondition,
|
|
hass: HomeAssistant,
|
|
context: ConditionContext
|
|
) {
|
|
if (!condition.conditions) return true;
|
|
return checkConditionsMet(condition.conditions, hass, context);
|
|
}
|
|
|
|
function checkNotCondition(
|
|
condition: NotCondition,
|
|
hass: HomeAssistant,
|
|
context: ConditionContext
|
|
) {
|
|
if (!condition.conditions) return true;
|
|
return !checkConditionsMet(condition.conditions, hass, context);
|
|
}
|
|
|
|
function checkOrCondition(
|
|
condition: OrCondition,
|
|
hass: HomeAssistant,
|
|
context: ConditionContext
|
|
) {
|
|
if (!condition.conditions) return true;
|
|
return condition.conditions.some((c) =>
|
|
checkConditionsMet([c], hass, context)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return the result of applying conditions
|
|
* @param conditions conditions to apply
|
|
* @param hass Home Assistant object
|
|
* @param context optional context for conditions that need runtime information
|
|
* @returns true if conditions are respected
|
|
*/
|
|
export function checkConditionsMet(
|
|
conditions: (Condition | LegacyCondition)[],
|
|
hass: HomeAssistant,
|
|
context: ConditionContext
|
|
): boolean {
|
|
return conditions.every((c) => {
|
|
if ("condition" in c) {
|
|
switch (c.condition) {
|
|
case "view_columns":
|
|
return checkViewColumnsCondition(c, context);
|
|
case "time":
|
|
return checkTimeCondition(c, hass);
|
|
case "screen":
|
|
return checkScreenCondition(c, hass);
|
|
case "user":
|
|
return checkUserCondition(c, hass);
|
|
case "location":
|
|
return checkLocationCondition(c, hass);
|
|
case "numeric_state":
|
|
return checkStateNumericCondition(c, hass);
|
|
case "and":
|
|
return checkAndCondition(c, hass, context);
|
|
case "not":
|
|
return checkNotCondition(c, hass, context);
|
|
case "or":
|
|
return checkOrCondition(c, hass, context);
|
|
default:
|
|
return checkStateCondition(c, hass);
|
|
}
|
|
}
|
|
return checkStateCondition(c, hass);
|
|
});
|
|
}
|
|
|
|
export function extractConditionEntityIds(
|
|
conditions: Condition[]
|
|
): Set<string> {
|
|
const entityIds = new Set<string>();
|
|
for (const condition of conditions) {
|
|
if (condition.condition === "numeric_state") {
|
|
if (condition.entity) {
|
|
entityIds.add(condition.entity);
|
|
}
|
|
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") {
|
|
if (condition.entity) {
|
|
entityIds.add(condition.entity);
|
|
}
|
|
[
|
|
...(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 &&
|
|
(condition.state != null || condition.state_not != null)
|
|
);
|
|
}
|
|
|
|
function validateScreenCondition(condition: ScreenCondition) {
|
|
return condition.media_query != null;
|
|
}
|
|
|
|
function validateTimeCondition(condition: TimeCondition) {
|
|
// Check if time strings are present and non-empty
|
|
const hasAfter = condition.after != null && condition.after !== "";
|
|
const hasBefore = condition.before != null && condition.before !== "";
|
|
const hasTime = hasAfter || hasBefore;
|
|
|
|
const hasWeekdays =
|
|
condition.weekdays != null && condition.weekdays.length > 0;
|
|
const weekdaysValid =
|
|
!hasWeekdays ||
|
|
condition.weekdays!.every((w: WeekdayShort) => WEEKDAYS_SHORT.includes(w));
|
|
|
|
// Validate time string formats if present
|
|
const timeStringsValid =
|
|
(!hasAfter || isValidTimeString(condition.after!)) &&
|
|
(!hasBefore || isValidTimeString(condition.before!));
|
|
|
|
// Prevent after and before being identical (creates zero-length interval)
|
|
const timeRangeValid =
|
|
!hasAfter || !hasBefore || condition.after !== condition.before;
|
|
|
|
return (
|
|
(hasTime || hasWeekdays) &&
|
|
weekdaysValid &&
|
|
timeStringsValid &&
|
|
timeRangeValid
|
|
);
|
|
}
|
|
|
|
function validateUserCondition(condition: UserCondition) {
|
|
return condition.users != null;
|
|
}
|
|
|
|
function validateLocationCondition(condition: LocationCondition) {
|
|
return condition.locations != null;
|
|
}
|
|
|
|
function validateAndCondition(condition: AndCondition) {
|
|
return condition.conditions != null;
|
|
}
|
|
|
|
function validateNotCondition(condition: NotCondition) {
|
|
return condition.conditions != null;
|
|
}
|
|
|
|
function validateOrCondition(condition: OrCondition) {
|
|
return condition.conditions != null;
|
|
}
|
|
|
|
function validateViewColumnsCondition(condition: ViewColumnsCondition) {
|
|
return condition.min != null || condition.max != null;
|
|
}
|
|
|
|
function validateNumericStateCondition(condition: NumericStateCondition) {
|
|
return (
|
|
condition.entity != 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(
|
|
conditions: (Condition | LegacyCondition)[]
|
|
): boolean {
|
|
return conditions.every((c) => {
|
|
if ("condition" in c) {
|
|
switch (c.condition) {
|
|
case "view_columns":
|
|
return validateViewColumnsCondition(c);
|
|
case "screen":
|
|
return validateScreenCondition(c);
|
|
case "time":
|
|
return validateTimeCondition(c);
|
|
case "user":
|
|
return validateUserCondition(c);
|
|
case "location":
|
|
return validateLocationCondition(c);
|
|
case "numeric_state":
|
|
return validateNumericStateCondition(c);
|
|
case "and":
|
|
return validateAndCondition(c);
|
|
case "not":
|
|
return validateNotCondition(c);
|
|
case "or":
|
|
return validateOrCondition(c);
|
|
default:
|
|
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 {
|
|
entity: entityId,
|
|
...condition,
|
|
};
|
|
}
|
|
return condition;
|
|
}
|