Convert conditions to HA style.

This commit is contained in:
Dermot Duffy
2024-03-10 18:09:17 -07:00
parent ff78cc51a1
commit 829f411acc
11 changed files with 1334 additions and 476 deletions
+95 -93
View File
@@ -5,13 +5,17 @@ import { copyConfig } from '../config-mgmt';
import {
FrigateCardCondition,
frigateConditionalSchema,
MicrophoneConditionState,
OverrideConfigurationKey,
RawFrigateCardConfig,
ViewDisplayMode,
} from '../config/types';
import { CardConditionAPI } from './types';
interface MicrophoneConditionState {
connected?: boolean;
muted?: boolean;
}
interface ConditionState {
view?: string;
fullscreen?: boolean;
@@ -26,13 +30,13 @@ interface ConditionState {
user?: CurrentUser;
}
export class ConditionEvaluateRequestEvent extends Event {
public condition: FrigateCardCondition;
export class ConditionsEvaluateRequestEvent extends Event {
public conditions: FrigateCardCondition[];
public evaluation?: boolean;
constructor(condition: FrigateCardCondition, eventInitDict?: EventInit) {
super('frigate-card:condition:evaluate', eventInitDict);
this.condition = condition;
constructor(conditions: FrigateCardCondition[], eventInitDict?: EventInit) {
super('frigate-card:conditions:evaluate', eventInitDict);
this.conditions = conditions;
}
}
@@ -42,13 +46,13 @@ export class ConditionEvaluateRequestEvent extends Event {
*/
export function evaluateConditionViaEvent(
element: HTMLElement,
condition?: FrigateCardCondition,
conditions?: FrigateCardCondition[],
): boolean {
if (!condition) {
if (!conditions) {
return true;
}
const evaluateEvent = new ConditionEvaluateRequestEvent(condition, {
const evaluateEvent = new ConditionsEvaluateRequestEvent(conditions, {
bubbles: true,
composed: true,
});
@@ -70,7 +74,7 @@ export function evaluateConditionViaEvent(
}
type RawOverrides = {
conditions: FrigateCardCondition;
conditions: FrigateCardCondition[];
overrides: RawFrigateCardConfig;
}[];
@@ -84,7 +88,7 @@ export function getOverriddenConfig(
let overridden = false;
if (configOverrides) {
for (const override of configOverrides) {
if (manager.evaluateCondition(override.conditions, stateOverrides)) {
if (manager.evaluateConditions(override.conditions, stateOverrides)) {
merge(output, override.overrides);
overridden = true;
}
@@ -156,7 +160,7 @@ export class ConditionsManager {
const getAllConditions = (): FrigateCardCondition[] => {
const config = this._api.getConfigManager().getConfig();
const conditions: FrigateCardCondition[] = [];
config?.overrides?.forEach((override) => conditions.push(override.conditions));
config?.overrides?.forEach((override) => conditions.push(...override.conditions));
// Element conditions can be arbitrarily nested underneath conditionals and
// custom elements that this card may not known. Here we recursively parse
@@ -164,7 +168,7 @@ export class ConditionsManager {
const getElementsConditions = (data: unknown): void => {
const parseResult = frigateConditionalSchema.safeParse(data);
if (parseResult.success) {
conditions.push(parseResult.data.conditions);
conditions.push(...parseResult.data.conditions);
parseResult.data.elements?.forEach(getElementsConditions);
} else if (data && typeof data === 'object') {
Object.keys(data).forEach((key) => getElementsConditions(data[key]));
@@ -175,15 +179,12 @@ export class ConditionsManager {
};
const conditions = getAllConditions();
this._hasHAStateConditions = conditions.some(
(condition) =>
!!condition.state?.length ||
!!condition.numeric_state?.length ||
!!condition.users?.length,
this._hasHAStateConditions = conditions.some((conditionObj) =>
['state', 'numeric_state', 'user'].includes(conditionObj.condition),
);
conditions.forEach((condition) => {
if (condition.media_query) {
const mql = window.matchMedia(condition.media_query);
conditions.forEach((conditionObj) => {
if (conditionObj.condition === 'screen') {
const mql = window.matchMedia(conditionObj.media_query);
mql.addEventListener('change', this._mediaQueryTrigger);
this._mediaQueries.push(mql);
}
@@ -210,8 +211,17 @@ export class ConditionsManager {
return this._epoch;
}
public evaluateCondition(
condition: Readonly<FrigateCardCondition>,
public evaluateConditions(
conditions: Readonly<FrigateCardCondition>[],
stateOverrides?: Partial<ConditionState>,
): boolean {
return conditions.every((conditionObj) =>
this._evaluateCondition(conditionObj, stateOverrides),
);
}
protected _evaluateCondition(
conditionObj: Readonly<FrigateCardCondition>,
stateOverrides?: Partial<ConditionState>,
): boolean {
const state = {
@@ -219,79 +229,71 @@ export class ConditionsManager {
...stateOverrides,
};
let result = true;
if (condition.view?.length) {
result &&= !!state?.view && condition.view.includes(state.view);
}
if (condition.fullscreen !== undefined) {
result &&=
state.fullscreen !== undefined && condition.fullscreen === state.fullscreen;
}
if (condition.expand !== undefined) {
result &&= state.expand !== undefined && condition.expand === state.expand;
}
if (condition.camera?.length) {
result &&= !!state.camera && condition.camera.includes(state.camera);
}
if (condition.state?.length) {
for (const stateTest of condition.state) {
result &&=
switch (conditionObj.condition) {
case 'view':
return !!state?.view && conditionObj.views.includes(state.view);
case 'fullscreen':
return (
state.fullscreen !== undefined && conditionObj.fullscreen === state.fullscreen
);
case 'expand':
return state.expand !== undefined && conditionObj.expand === state.expand;
case 'camera':
return !!state.camera && conditionObj.cameras.includes(state.camera);
case 'state':
return (
!!state.state &&
((!stateTest.state && !stateTest.state_not) ||
(stateTest.entity in state.state &&
(!stateTest.state ||
(Array.isArray(stateTest.state)
? stateTest.state.includes(state.state[stateTest.entity].state)
: stateTest.state === state.state[stateTest.entity].state)) &&
(!stateTest.state_not ||
(Array.isArray(stateTest.state_not)
? !stateTest.state_not.includes(state.state[stateTest.entity].state)
: stateTest.state_not !== state.state[stateTest.entity].state))));
}
}
if (condition.numeric_state?.length) {
for (const stateTest of condition.numeric_state) {
result &&=
((!conditionObj.state && !conditionObj.state_not) ||
(conditionObj.entity in state.state &&
(!conditionObj.state ||
(Array.isArray(conditionObj.state)
? conditionObj.state.includes(state.state[conditionObj.entity].state)
: conditionObj.state === state.state[conditionObj.entity].state)) &&
(!conditionObj.state_not ||
(Array.isArray(conditionObj.state_not)
? !conditionObj.state_not.includes(
state.state[conditionObj.entity].state,
)
: conditionObj.state_not !== state.state[conditionObj.entity].state))))
);
case 'numeric_state':
return (
!!state.state &&
stateTest.entity in state.state &&
state.state[stateTest.entity].state !== undefined &&
(stateTest.above === undefined ||
Number(state.state[stateTest.entity].state) > stateTest.above) &&
(stateTest.below === undefined ||
Number(state.state[stateTest.entity].state) < stateTest.below);
}
conditionObj.entity in state.state &&
state.state[conditionObj.entity].state !== undefined &&
(conditionObj.above === undefined ||
Number(state.state[conditionObj.entity].state) > conditionObj.above) &&
(conditionObj.below === undefined ||
Number(state.state[conditionObj.entity].state) < conditionObj.below)
);
case 'user':
return !!state.user && conditionObj.users.includes(state.user.id);
case 'media_loaded':
return (
state.media_loaded !== undefined &&
conditionObj.media_loaded === state.media_loaded
);
case 'screen':
return window.matchMedia(conditionObj.media_query).matches;
case 'display_mode':
return !!state.displayMode && conditionObj.display_mode === state.displayMode;
case 'triggered':
return conditionObj.triggered.some((triggeredCameraID) =>
state.triggered?.has(triggeredCameraID),
);
case 'interaction':
return (
state.interaction !== undefined &&
conditionObj.interaction === state.interaction
);
case 'microphone':
return (
(conditionObj.connected === undefined ||
state.microphone?.connected === conditionObj.connected) &&
(conditionObj.muted === undefined ||
state.microphone?.muted === conditionObj.muted)
);
}
if (condition.users?.length) {
result &&= !!state.user && condition.users.includes(state.user.id);
}
if (condition.media_loaded !== undefined) {
result &&=
state.media_loaded !== undefined &&
condition.media_loaded === state.media_loaded;
}
if (condition.media_query) {
result &&= window.matchMedia(condition.media_query).matches;
}
if (condition.display_mode) {
result &&= !!state.displayMode && condition.display_mode === state.displayMode;
}
if (condition.triggered?.length) {
result &&= condition.triggered.some((triggeredCameraID) =>
state.triggered?.has(triggeredCameraID),
);
}
if (condition.interaction !== undefined) {
result &&=
state.interaction !== undefined && condition.interaction === state.interaction;
}
if (condition.microphone) {
result &&=
(condition.microphone.connected === undefined ||
state.microphone?.connected === condition.microphone.connected) &&
(condition.microphone.muted === undefined ||
state.microphone?.muted === condition.microphone.muted);
}
return result;
}
protected _createEpoch(): ConditionsManagerEpoch {