refactor: Restructure condition evaluation into per-type evaluator classes (#2518)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent 3480a55a87
commit 12ae3b216c
50 changed files with 1902 additions and 1572 deletions
+17 -266
View File
@@ -1,13 +1,11 @@
import { TemplateRenderer } from '../card-controller/templates';
import { getConfigValue } from '../config/management';
import { AdvancedCameraCardCondition } from '../config/schema/conditions/types';
import { isBeingCasted } from '../utils/casting';
import { isCompanionApp } from '../utils/companion';
import { ConditionEvaluator } from './conditions/types';
import { createConditionEvaluator } from './factory';
import {
ConditionsEvaluationResult,
ConditionsListener,
ConditionsManagerReadonlyInterface,
ConditionState,
ConditionStateChange,
ConditionStateManagerReadonlyInterface,
ConditionsTriggerData,
@@ -19,29 +17,29 @@ import {
* associated with a result).
*/
export class ConditionsManager implements ConditionsManagerReadonlyInterface {
private _conditions: AdvancedCameraCardCondition[];
private _stateManager: ConditionStateManagerReadonlyInterface | null;
private _evaluators: ConditionEvaluator[];
private _listeners: ConditionsListener[] = [];
private _mediaQueries: MediaQueryList[] = [];
private _evaluation: ConditionsEvaluationResult = { result: false };
private _templateRenderer: TemplateRenderer = new TemplateRenderer();
constructor(
conditions: AdvancedCameraCardCondition[],
stateManager?: ConditionStateManagerReadonlyInterface | null,
) {
this._conditions = conditions;
conditions.forEach((condition) => {
if (condition.condition === 'screen') {
const mql = window.matchMedia(condition.media_query);
mql.addEventListener('change', this._mediaQueryHandler);
this._mediaQueries.push(mql);
}
});
const context = { templateRenderer: new TemplateRenderer() };
this._evaluators = conditions.map((condition) =>
createConditionEvaluator(condition, context),
);
this._stateManager = stateManager ?? null;
// Subscribe evaluators that have external change sources (currently
// `screen`), including nested evaluators inside composites.
this._evaluators.forEach((evaluator) =>
evaluator.subscribe?.(() => this._evaluate()),
);
// Do an initial condition evaluation, but without calling listeners.
this._evaluate({ callListeners: false });
@@ -53,11 +51,8 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
this._listeners.forEach((l) => this.removeListener(l));
this._mediaQueries.forEach((mql) =>
mql.removeEventListener('change', this._mediaQueryHandler),
);
this._mediaQueries = [];
this._conditions = [];
this._evaluators.forEach((evaluator) => evaluator.destroy?.());
this._evaluators = [];
}
public addListener(listener: ConditionsListener): void {
@@ -74,8 +69,6 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
return this._evaluation;
}
private _mediaQueryHandler = () => this._evaluate();
private _stateManagerHandler = (stateChange: ConditionStateChange): void => {
this._evaluate({ stateChange });
};
@@ -89,12 +82,8 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
let result = true;
let triggerData: ConditionsTriggerData = {};
for (const condition of this._conditions) {
const evaluation = this._evaluateCondition(
condition,
state,
options?.stateChange?.old,
);
for (const evaluator of this._evaluators) {
const evaluation = evaluator.evaluate(state, options?.stateChange?.old);
if (!evaluation.result) {
result = false;
break;
@@ -121,242 +110,4 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
}
}
}
private _evaluateCondition(
condition: AdvancedCameraCardCondition,
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
switch (condition.condition) {
case undefined:
case 'state': {
const fromState = oldState?.hass?.states?.[condition.entity]?.state;
const toState = newState?.hass?.states?.[condition.entity]?.state;
return {
result:
(!condition.state && !condition.state_not && toState !== fromState) ||
((!!condition.state || !!condition.state_not) &&
!!toState &&
(!condition.state ||
(Array.isArray(condition.state)
? condition.state.includes(toState)
: condition.state === toState)) &&
(!condition.state_not ||
(Array.isArray(condition.state_not)
? !condition.state_not.includes(toState)
: condition.state_not !== toState))),
...(fromState !== toState && {
triggerData: {
state: {
entity: condition.entity,
...(fromState && { from: fromState }),
...(toState && { to: toState }),
},
},
}),
};
}
case 'view': {
const oldView = oldState?.view;
const newView = newState?.view;
return {
result:
(!!newView && condition.views?.includes(newView)) ||
(newView !== oldView && !condition.views?.length),
...(oldView !== newView && {
triggerData: {
...((oldState?.view || newState?.view) && {
view: {
...(oldState?.view && { from: oldState.view }),
...(newState?.view && { to: newState.view }),
},
}),
},
}),
};
}
case 'fullscreen':
return {
result:
newState?.fullscreen !== undefined &&
condition.fullscreen === newState.fullscreen,
};
case 'expand':
return {
result: newState?.expand !== undefined && condition.expand === newState.expand,
};
case 'camera': {
const oldCamera = oldState?.camera;
const newCamera = newState?.camera;
return {
result:
(!!newCamera && !!condition.cameras?.includes(newCamera)) ||
(newCamera !== oldCamera && !condition.cameras?.length),
...(newCamera !== oldCamera && {
triggerData: {
...((oldState?.camera || newState?.camera) && {
camera: {
...(oldState?.camera && { from: oldState?.camera }),
...(newState?.camera && { to: newState?.camera }),
},
}),
},
}),
};
}
case 'numeric_state':
return {
result:
!!newState?.hass?.states &&
condition.entity in newState.hass?.states &&
newState.hass.states[condition.entity].state !== undefined &&
(condition.above === undefined ||
Number(newState.hass.states[condition.entity].state) > condition.above) &&
(condition.below === undefined ||
Number(newState.hass.states[condition.entity].state) < condition.below),
};
case 'user':
return {
result:
!!newState?.hass?.user && condition.users.includes(newState.hass.user.id),
};
case 'media_loaded':
return {
result:
newState?.mediaLoadedInfo !== undefined &&
condition.media_loaded === !!newState.mediaLoadedInfo,
};
case 'screen':
return { result: window.matchMedia(condition.media_query).matches };
case 'display_mode':
return {
result:
!!newState?.displayMode && condition.display_mode === newState.displayMode,
};
case 'triggered':
return {
result: condition.triggered.some((triggeredCameraID) =>
newState?.triggered?.has(triggeredCameraID),
),
};
case 'interaction':
return {
result:
newState?.interaction !== undefined &&
condition.interaction === newState.interaction,
};
case 'microphone':
return {
result: newState?.microphone?.muted === condition.muted,
};
case 'call':
return {
result: (condition.call ?? true) === (newState?.call ?? false),
};
case 'key':
return {
result:
!!newState?.keys &&
condition.key in newState.keys &&
(condition.state ?? 'down') === newState.keys[condition.key].state &&
(condition.ctrl === undefined ||
condition.ctrl === !!newState.keys[condition.key].ctrl) &&
(condition.alt === undefined ||
condition.alt === !!newState.keys[condition.key].alt) &&
(condition.meta === undefined ||
condition.meta === !!newState.keys[condition.key].meta) &&
(condition.shift === undefined ||
condition.shift === !!newState.keys[condition.key].shift),
};
case 'user_agent':
return {
result:
!!newState?.userAgent &&
(!condition.user_agent || condition.user_agent === newState.userAgent) &&
(condition.casting === undefined ||
condition.casting === isBeingCasted(newState.userAgent)) &&
(condition.companion === undefined ||
condition.companion === isCompanionApp(newState.userAgent)) &&
(condition.user_agent_re === undefined ||
new RegExp(condition.user_agent_re).test(newState.userAgent)),
};
case 'config': {
const newConfig = newState?.config;
const oldConfig = oldState?.config;
return {
result:
!!newConfig &&
newConfig !== oldConfig &&
(!condition.paths?.length ||
condition.paths.some(
(key) =>
getConfigValue(newConfig, key) !==
(oldConfig ? getConfigValue(oldConfig, key) : undefined),
)),
...(newConfig !== oldConfig && {
triggerData: {
config: {
...((oldState?.config || newState?.config) && {
...(oldState?.config && { from: oldState?.config }),
...(newState?.config && { to: newState?.config }),
}),
},
},
}),
};
}
case 'initialized':
return { result: !!newState?.initialized };
case 'or':
for (const subCondition of condition.conditions) {
const evaluation = this._evaluateCondition(subCondition, newState, oldState);
if (evaluation.result) {
return evaluation;
}
}
return { result: false };
case 'and': {
let triggerData: ConditionsTriggerData = {};
for (const subCondition of condition.conditions) {
const evaluation = this._evaluateCondition(subCondition, newState, oldState);
if (!evaluation.result) {
return { result: false };
}
triggerData = {
...triggerData,
...evaluation.triggerData,
};
}
return { result: true, triggerData };
}
case 'not': {
// "Not" is an inverted `or` (NOR). There is no trigger data for "not
// triggering".
return {
result: !this._evaluateCondition(
{
...condition,
condition: 'or',
},
newState,
oldState,
).result,
};
}
case 'template':
return {
result:
!!newState?.hass &&
this._templateRenderer.renderRecursively(
newState.hass,
condition.value_template,
{ conditionState: newState },
) === true,
};
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import {
ConditionsEvaluationResult,
ConditionState,
ConditionsTriggerData,
} from '../types';
import { CompositeConditionEvaluator } from './composite';
export class AndConditionEvaluator extends CompositeConditionEvaluator {
public evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
let triggerData: ConditionsTriggerData = {};
for (const child of this._children) {
const evaluation = child.evaluate(newState, oldState);
if (!evaluation.result) {
return { result: false };
}
triggerData = {
...triggerData,
...evaluation.triggerData,
};
}
return { result: true, triggerData };
}
}
+16
View File
@@ -0,0 +1,16 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class CallConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'call'>;
constructor(condition: ConditionOfType<'call'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result: (this._condition.call ?? true) === (newState?.call ?? false),
};
}
}
+34
View File
@@ -0,0 +1,34 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class CameraConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'camera'>;
constructor(condition: ConditionOfType<'camera'>) {
this._condition = condition;
}
public evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
const oldCamera = oldState?.camera;
const newCamera = newState?.camera;
return {
result:
(!!newCamera && !!this._condition.cameras?.includes(newCamera)) ||
(newCamera !== oldCamera && !this._condition.cameras?.length),
...(newCamera !== oldCamera && {
triggerData: {
...((oldState?.camera || newState?.camera) && {
camera: {
...(oldState?.camera && { from: oldState?.camera }),
...(newState?.camera && { to: newState?.camera }),
},
}),
},
}),
};
}
}
+27
View File
@@ -0,0 +1,27 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionEvaluatorSubscriptionCallback } from './types';
/**
* Base class for the `or`/`and`/`not` composites: each holds child evaluators
* and forwards subscription/teardown to them. Subclasses provide `evaluate`.
*/
export abstract class CompositeConditionEvaluator implements ConditionEvaluator {
protected _children: ConditionEvaluator[];
constructor(children: ConditionEvaluator[]) {
this._children = children;
}
public abstract evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult;
public subscribe(onChange: ConditionEvaluatorSubscriptionCallback): void {
this._children.forEach((child) => child.subscribe?.(onChange));
}
public destroy(): void {
this._children.forEach((child) => child.destroy?.());
}
}
+41
View File
@@ -0,0 +1,41 @@
import { getConfigValue } from '../../config/management';
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class ConfigConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'config'>;
constructor(condition: ConditionOfType<'config'>) {
this._condition = condition;
}
public evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
const newConfig = newState?.config;
const oldConfig = oldState?.config;
return {
result:
!!newConfig &&
newConfig !== oldConfig &&
(!this._condition.paths?.length ||
this._condition.paths.some(
(key) =>
getConfigValue(newConfig, key) !==
(oldConfig ? getConfigValue(oldConfig, key) : undefined),
)),
...(newConfig !== oldConfig && {
triggerData: {
config: {
...((oldState?.config || newState?.config) && {
...(oldState?.config && { from: oldState?.config }),
...(newState?.config && { to: newState?.config }),
}),
},
},
}),
};
}
}
+17
View File
@@ -0,0 +1,17 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class DisplayModeConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'display_mode'>;
constructor(condition: ConditionOfType<'display_mode'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result:
!!newState?.displayMode && this._condition.display_mode === newState.displayMode,
};
}
}
+17
View File
@@ -0,0 +1,17 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class ExpandConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'expand'>;
constructor(condition: ConditionOfType<'expand'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result:
newState?.expand !== undefined && this._condition.expand === newState.expand,
};
}
}
+18
View File
@@ -0,0 +1,18 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class FullscreenConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'fullscreen'>;
constructor(condition: ConditionOfType<'fullscreen'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result:
newState?.fullscreen !== undefined &&
this._condition.fullscreen === newState.fullscreen,
};
}
}
+8
View File
@@ -0,0 +1,8 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator } from './types';
export class InitializedConditionEvaluator implements ConditionEvaluator {
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return { result: !!newState?.initialized };
}
}
+18
View File
@@ -0,0 +1,18 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class InteractionConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'interaction'>;
constructor(condition: ConditionOfType<'interaction'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result:
newState?.interaction !== undefined &&
this._condition.interaction === newState.interaction,
};
}
}
+28
View File
@@ -0,0 +1,28 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class KeyConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'key'>;
constructor(condition: ConditionOfType<'key'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
const condition = this._condition;
return {
result:
!!newState?.keys &&
condition.key in newState.keys &&
(condition.state ?? 'down') === newState.keys[condition.key].state &&
(condition.ctrl === undefined ||
condition.ctrl === !!newState.keys[condition.key].ctrl) &&
(condition.alt === undefined ||
condition.alt === !!newState.keys[condition.key].alt) &&
(condition.meta === undefined ||
condition.meta === !!newState.keys[condition.key].meta) &&
(condition.shift === undefined ||
condition.shift === !!newState.keys[condition.key].shift),
};
}
}
+18
View File
@@ -0,0 +1,18 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class MediaLoadedConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'media_loaded'>;
constructor(condition: ConditionOfType<'media_loaded'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result:
newState?.mediaLoadedInfo !== undefined &&
this._condition.media_loaded === !!newState.mediaLoadedInfo,
};
}
}
+16
View File
@@ -0,0 +1,16 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class MicrophoneConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'microphone'>;
constructor(condition: ConditionOfType<'microphone'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result: newState?.microphone?.muted === this._condition.muted,
};
}
}
+15
View File
@@ -0,0 +1,15 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { CompositeConditionEvaluator } from './composite';
export class NotConditionEvaluator extends CompositeConditionEvaluator {
// "Not" is an inverted `or` (NOR). There is no trigger data for "not
// triggering".
public evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
return {
result: !this._children.some((child) => child.evaluate(newState, oldState).result),
};
}
}
@@ -0,0 +1,24 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class NumericStateConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'numeric_state'>;
constructor(condition: ConditionOfType<'numeric_state'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
const condition = this._condition;
return {
result:
!!newState?.hass?.states &&
condition.entity in newState.hass?.states &&
newState.hass.states[condition.entity].state !== undefined &&
(condition.above === undefined ||
Number(newState.hass.states[condition.entity].state) > condition.above) &&
(condition.below === undefined ||
Number(newState.hass.states[condition.entity].state) < condition.below),
};
}
}
+17
View File
@@ -0,0 +1,17 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { CompositeConditionEvaluator } from './composite';
export class OrConditionEvaluator extends CompositeConditionEvaluator {
public evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
for (const child of this._children) {
const evaluation = child.evaluate(newState, oldState);
if (evaluation.result) {
return evaluation;
}
}
return { result: false };
}
}
+34
View File
@@ -0,0 +1,34 @@
import { ConditionsEvaluationResult } from '../types';
import {
ConditionEvaluator,
ConditionEvaluatorSubscriptionCallback,
ConditionOfType,
} from './types';
export class ScreenConditionEvaluator implements ConditionEvaluator {
private _mediaQuery: MediaQueryList | null = null;
private _onChange: ConditionEvaluatorSubscriptionCallback | null = null;
private _condition: ConditionOfType<'screen'>;
constructor(condition: ConditionOfType<'screen'>) {
this._condition = condition;
}
public evaluate(): ConditionsEvaluationResult {
return { result: window.matchMedia(this._condition.media_query).matches };
}
public subscribe(onChange: ConditionEvaluatorSubscriptionCallback): void {
this._onChange = onChange;
this._mediaQuery = window.matchMedia(this._condition.media_query);
this._mediaQuery.addEventListener('change', this._handler);
}
public destroy(): void {
this._mediaQuery?.removeEventListener('change', this._handler);
this._mediaQuery = null;
}
private _handler = (): void => this._onChange?.();
}
+43
View File
@@ -0,0 +1,43 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class StateConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'state'>;
constructor(condition: ConditionOfType<'state'>) {
this._condition = condition;
}
public evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
const condition = this._condition;
const fromState = oldState?.hass?.states?.[condition.entity]?.state;
const toState = newState?.hass?.states?.[condition.entity]?.state;
return {
result:
(!condition.state && !condition.state_not && toState !== fromState) ||
((!!condition.state || !!condition.state_not) &&
!!toState &&
(!condition.state ||
(Array.isArray(condition.state)
? condition.state.includes(toState)
: condition.state === toState)) &&
(!condition.state_not ||
(Array.isArray(condition.state_not)
? !condition.state_not.includes(toState)
: condition.state_not !== toState))),
...(fromState !== toState && {
triggerData: {
state: {
entity: condition.entity,
...(fromState && { from: fromState }),
...(toState && { to: toState }),
},
},
}),
};
}
}
+24
View File
@@ -0,0 +1,24 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType, EvaluatorContext } from './types';
export class TemplateConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'template'>;
private _context: EvaluatorContext;
constructor(condition: ConditionOfType<'template'>, context: EvaluatorContext) {
this._condition = condition;
this._context = context;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result:
!!newState?.hass &&
this._context.templateRenderer.renderRecursively(
newState.hass,
this._condition.value_template,
{ conditionState: newState },
) === true,
};
}
}
+18
View File
@@ -0,0 +1,18 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class TriggeredConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'triggered'>;
constructor(condition: ConditionOfType<'triggered'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result: this._condition.triggered.some((triggeredCameraID) =>
newState?.triggered?.has(triggeredCameraID),
),
};
}
}
+32
View File
@@ -0,0 +1,32 @@
import { TemplateRenderer } from '../../card-controller/templates';
import { AdvancedCameraCardCondition } from '../../config/schema/conditions/types';
import { ConditionsEvaluationResult, ConditionState } from '../types';
export type ConditionEvaluatorSubscriptionCallback = () => void;
/**
* A single condition, constructed once with its configuration and evaluated
* repeatedly against incoming state.
*/
export interface ConditionEvaluator {
evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult;
// Optional hook for conditions with an external change source (e.g.
// `screen`). The owner passes a callback to request re-evaluation.
subscribe?(onChange: ConditionEvaluatorSubscriptionCallback): void;
destroy?(): void;
}
export interface EvaluatorContext {
templateRenderer: TemplateRenderer;
}
// The condition union member(s) carrying a given discriminator literal.
export type ConditionOfType<T extends string> = Extract<
AdvancedCameraCardCondition,
{ condition?: T }
>;
+27
View File
@@ -0,0 +1,27 @@
import { isBeingCasted } from '../../utils/casting';
import { isCompanionApp } from '../../utils/companion';
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class UserAgentConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'user_agent'>;
constructor(condition: ConditionOfType<'user_agent'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
const condition = this._condition;
return {
result:
!!newState?.userAgent &&
(!condition.user_agent || condition.user_agent === newState.userAgent) &&
(condition.casting === undefined ||
condition.casting === isBeingCasted(newState.userAgent)) &&
(condition.companion === undefined ||
condition.companion === isCompanionApp(newState.userAgent)) &&
(condition.user_agent_re === undefined ||
new RegExp(condition.user_agent_re).test(newState.userAgent)),
};
}
}
+17
View File
@@ -0,0 +1,17 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class UserConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'user'>;
constructor(condition: ConditionOfType<'user'>) {
this._condition = condition;
}
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
return {
result:
!!newState?.hass?.user && this._condition.users.includes(newState.hass.user.id),
};
}
}
+34
View File
@@ -0,0 +1,34 @@
import { ConditionsEvaluationResult, ConditionState } from '../types';
import { ConditionEvaluator, ConditionOfType } from './types';
export class ViewConditionEvaluator implements ConditionEvaluator {
private _condition: ConditionOfType<'view'>;
constructor(condition: ConditionOfType<'view'>) {
this._condition = condition;
}
public evaluate(
newState?: ConditionState,
oldState?: ConditionState,
): ConditionsEvaluationResult {
const oldView = oldState?.view;
const newView = newState?.view;
return {
result:
(!!newView && this._condition.views?.includes(newView)) ||
(newView !== oldView && !this._condition.views?.length),
...(oldView !== newView && {
triggerData: {
...((oldState?.view || newState?.view) && {
view: {
...(oldState?.view && { from: oldState.view }),
...(newState?.view && { to: newState.view }),
},
}),
},
}),
};
}
}
+83
View File
@@ -0,0 +1,83 @@
import { AdvancedCameraCardCondition } from '../config/schema/conditions/types';
import { AndConditionEvaluator } from './conditions/and';
import { CallConditionEvaluator } from './conditions/call';
import { CameraConditionEvaluator } from './conditions/camera';
import { ConfigConditionEvaluator } from './conditions/config';
import { DisplayModeConditionEvaluator } from './conditions/display-mode';
import { ExpandConditionEvaluator } from './conditions/expand';
import { FullscreenConditionEvaluator } from './conditions/fullscreen';
import { InitializedConditionEvaluator } from './conditions/initialized';
import { InteractionConditionEvaluator } from './conditions/interaction';
import { KeyConditionEvaluator } from './conditions/key';
import { MediaLoadedConditionEvaluator } from './conditions/media-loaded';
import { MicrophoneConditionEvaluator } from './conditions/microphone';
import { NotConditionEvaluator } from './conditions/not';
import { NumericStateConditionEvaluator } from './conditions/numeric-state';
import { OrConditionEvaluator } from './conditions/or';
import { ScreenConditionEvaluator } from './conditions/screen';
import { StateConditionEvaluator } from './conditions/state';
import { TemplateConditionEvaluator } from './conditions/template';
import { TriggeredConditionEvaluator } from './conditions/triggered';
import { ConditionEvaluator, EvaluatorContext } from './conditions/types';
import { UserConditionEvaluator } from './conditions/user';
import { UserAgentConditionEvaluator } from './conditions/user-agent';
import { ViewConditionEvaluator } from './conditions/view';
export const createConditionEvaluator = (
condition: AdvancedCameraCardCondition,
context: EvaluatorContext,
): ConditionEvaluator => {
switch (condition.condition) {
case undefined:
case 'state':
return new StateConditionEvaluator(condition);
case 'view':
return new ViewConditionEvaluator(condition);
case 'fullscreen':
return new FullscreenConditionEvaluator(condition);
case 'expand':
return new ExpandConditionEvaluator(condition);
case 'camera':
return new CameraConditionEvaluator(condition);
case 'numeric_state':
return new NumericStateConditionEvaluator(condition);
case 'user':
return new UserConditionEvaluator(condition);
case 'media_loaded':
return new MediaLoadedConditionEvaluator(condition);
case 'screen':
return new ScreenConditionEvaluator(condition);
case 'display_mode':
return new DisplayModeConditionEvaluator(condition);
case 'triggered':
return new TriggeredConditionEvaluator(condition);
case 'interaction':
return new InteractionConditionEvaluator(condition);
case 'microphone':
return new MicrophoneConditionEvaluator(condition);
case 'call':
return new CallConditionEvaluator(condition);
case 'key':
return new KeyConditionEvaluator(condition);
case 'user_agent':
return new UserAgentConditionEvaluator(condition);
case 'config':
return new ConfigConditionEvaluator(condition);
case 'initialized':
return new InitializedConditionEvaluator();
case 'template':
return new TemplateConditionEvaluator(condition, context);
case 'or':
return new OrConditionEvaluator(
condition.conditions.map((child) => createConditionEvaluator(child, context)),
);
case 'and':
return new AndConditionEvaluator(
condition.conditions.map((child) => createConditionEvaluator(child, context)),
);
case 'not':
return new NotConditionEvaluator(
condition.conditions.map((child) => createConditionEvaluator(child, context)),
);
}
};