fix: Fix conditions re-triggering incorrectly in certain cases (#1950)
- Closes #1948 [skip ci]
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { ActionContext } from 'action';
|
import { ActionContext } from 'action';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ConditionsEvaluationData } from '../../conditions/types.js';
|
import { ConditionsTriggerData } from '../../conditions/types.js';
|
||||||
import { Actions, ActionsConfig, ActionType } from '../../config/types.js';
|
import { Actions, ActionsConfig, ActionType } from '../../config/types.js';
|
||||||
import { getActionConfigGivenAction } from '../../utils/action.js';
|
import { getActionConfigGivenAction } from '../../utils/action.js';
|
||||||
import { TemplateRenderer } from '../templates/index.js';
|
import { TemplateRenderer } from '../templates/index.js';
|
||||||
@@ -115,7 +115,7 @@ export class ActionsManager {
|
|||||||
action: ActionType | ActionType[],
|
action: ActionType | ActionType[],
|
||||||
options?: {
|
options?: {
|
||||||
config?: AuxillaryActionConfig;
|
config?: AuxillaryActionConfig;
|
||||||
triggerData?: ConditionsEvaluationData;
|
triggerData?: ConditionsTriggerData;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class AutomationsManager {
|
|||||||
|
|
||||||
await this._api
|
await this._api
|
||||||
.getActionsManager()
|
.getActionsManager()
|
||||||
.executeActions(actions, { triggerData: result.data });
|
.executeActions(actions, { triggerData: result.triggerData });
|
||||||
|
|
||||||
--this._nestedAutomationExecutions;
|
--this._nestedAutomationExecutions;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import { HASS, renderTemplate } from 'ha-nunjucks/dist';
|
import { HASS, renderTemplate } from 'ha-nunjucks/dist';
|
||||||
import { ConditionsEvaluationData, ConditionState } from '../../conditions/types';
|
import { ConditionsTriggerData, ConditionState } from '../../conditions/types';
|
||||||
import { ActionType } from '../../config/types';
|
import { ActionType } from '../../config/types';
|
||||||
|
|
||||||
interface TemplateContextInternal {
|
interface TemplateContextInternal {
|
||||||
camera?: string;
|
camera?: string;
|
||||||
view?: string;
|
view?: string;
|
||||||
trigger?: ConditionsEvaluationData;
|
trigger?: ConditionsTriggerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TemplateContext {
|
interface TemplateContext {
|
||||||
@@ -22,7 +22,7 @@ export class TemplateRenderer {
|
|||||||
data: unknown,
|
data: unknown,
|
||||||
options?: {
|
options?: {
|
||||||
conditionState?: ConditionState;
|
conditionState?: ConditionState;
|
||||||
triggerData?: ConditionsEvaluationData;
|
triggerData?: ConditionsTriggerData;
|
||||||
},
|
},
|
||||||
): ActionType => {
|
): ActionType => {
|
||||||
return this._renderTemplateRecursively(
|
return this._renderTemplateRecursively(
|
||||||
@@ -37,7 +37,7 @@ export class TemplateRenderer {
|
|||||||
|
|
||||||
protected _conditionStateToTemplateContext(
|
protected _conditionStateToTemplateContext(
|
||||||
conditionState?: ConditionState,
|
conditionState?: ConditionState,
|
||||||
triggerData?: ConditionsEvaluationData,
|
triggerData?: ConditionsTriggerData,
|
||||||
): TemplateContext | undefined {
|
): TemplateContext | undefined {
|
||||||
if (!conditionState?.camera && !conditionState?.view && !triggerData) {
|
if (!conditionState?.camera && !conditionState?.view && !triggerData) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import { getConfigValue } from '../config/management';
|
import { getConfigValue } from '../config/management';
|
||||||
import { AdvancedCameraCardCondition } from '../config/types';
|
import { AdvancedCameraCardCondition } from '../config/types';
|
||||||
import { isCompanionApp } from '../utils/companion';
|
import { isCompanionApp } from '../utils/companion';
|
||||||
import {
|
import {
|
||||||
ConditionsEvaluationData,
|
|
||||||
ConditionsEvaluationResult,
|
ConditionsEvaluationResult,
|
||||||
ConditionsListener,
|
ConditionsListener,
|
||||||
ConditionsManagerReadonlyInterface,
|
ConditionsManagerReadonlyInterface,
|
||||||
ConditionState,
|
ConditionState,
|
||||||
ConditionStateChange,
|
ConditionStateChange,
|
||||||
ConditionStateManagerReadonlyInterface,
|
ConditionStateManagerReadonlyInterface,
|
||||||
|
ConditionsTriggerData,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +103,7 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
const state = options?.stateChange?.new ?? this._stateManager?.getState();
|
const state = options?.stateChange?.new ?? this._stateManager?.getState();
|
||||||
|
|
||||||
let result = true;
|
let result = true;
|
||||||
let data: ConditionsEvaluationData = {};
|
let triggerData: ConditionsTriggerData = {};
|
||||||
|
|
||||||
for (const condition of this._conditions) {
|
for (const condition of this._conditions) {
|
||||||
const evaluation = this._evaluateCondition(
|
const evaluation = this._evaluateCondition(
|
||||||
@@ -116,17 +115,20 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
result = false;
|
result = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
data = {
|
triggerData = {
|
||||||
...data,
|
...triggerData,
|
||||||
...evaluation.data,
|
...evaluation.triggerData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluation: ConditionsEvaluationResult = result
|
const evaluation: ConditionsEvaluationResult = result
|
||||||
? { result, data }
|
? { result, triggerData }
|
||||||
: { result };
|
: { result };
|
||||||
|
|
||||||
if (!isEqual(evaluation, this._evaluation)) {
|
if (
|
||||||
|
evaluation.result !== this._evaluation.result ||
|
||||||
|
(evaluation.triggerData && Object.keys(evaluation.triggerData).length)
|
||||||
|
) {
|
||||||
this._evaluation = evaluation;
|
this._evaluation = evaluation;
|
||||||
if (options?.callListeners ?? true) {
|
if (options?.callListeners ?? true) {
|
||||||
this._listeners.forEach(
|
this._listeners.forEach(
|
||||||
@@ -143,44 +145,44 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
): ConditionsEvaluationResult {
|
): ConditionsEvaluationResult {
|
||||||
switch (condition.condition) {
|
switch (condition.condition) {
|
||||||
case undefined:
|
case undefined:
|
||||||
case 'state':
|
case 'state': {
|
||||||
|
const fromState = oldState?.state?.[condition.entity]?.state;
|
||||||
|
const toState = newState?.state?.[condition.entity]?.state;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result:
|
result:
|
||||||
!!newState?.state &&
|
(!condition.state && !condition.state_not && toState !== fromState) ||
|
||||||
((!condition.state &&
|
|
||||||
!condition.state_not &&
|
|
||||||
newState.state[condition.entity]?.state !==
|
|
||||||
oldState?.state?.[condition.entity]?.state) ||
|
|
||||||
((!!condition.state || !!condition.state_not) &&
|
((!!condition.state || !!condition.state_not) &&
|
||||||
condition.entity in newState.state &&
|
!!toState &&
|
||||||
(!condition.state ||
|
(!condition.state ||
|
||||||
(Array.isArray(condition.state)
|
(Array.isArray(condition.state)
|
||||||
? condition.state.includes(newState.state[condition.entity].state)
|
? condition.state.includes(toState)
|
||||||
: condition.state === newState.state[condition.entity].state)) &&
|
: condition.state === toState)) &&
|
||||||
(!condition.state_not ||
|
(!condition.state_not ||
|
||||||
(Array.isArray(condition.state_not)
|
(Array.isArray(condition.state_not)
|
||||||
? !condition.state_not.includes(
|
? !condition.state_not.includes(toState)
|
||||||
newState.state[condition.entity].state,
|
: condition.state_not !== toState))),
|
||||||
)
|
...(fromState !== toState && {
|
||||||
: condition.state_not !== newState.state[condition.entity].state)))),
|
triggerData: {
|
||||||
data: {
|
|
||||||
state: {
|
state: {
|
||||||
entity: condition.entity,
|
entity: condition.entity,
|
||||||
...(oldState?.state?.[condition.entity]?.state && {
|
...(fromState && { from: fromState }),
|
||||||
from: oldState?.state?.[condition.entity]?.state,
|
...(toState && { to: toState }),
|
||||||
}),
|
|
||||||
...(newState?.state?.[condition.entity]?.state && {
|
|
||||||
to: newState?.state?.[condition.entity]?.state,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
case 'view':
|
}
|
||||||
|
case 'view': {
|
||||||
|
const oldView = oldState?.view;
|
||||||
|
const newView = newState?.view;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result:
|
result:
|
||||||
(!!newState?.view && condition.views?.includes(newState.view)) ||
|
(!!newView && condition.views?.includes(newView)) ||
|
||||||
(newState?.view !== oldState?.view && !condition.views?.length),
|
(newView !== oldView && !condition.views?.length),
|
||||||
data: {
|
...(oldView !== newView && {
|
||||||
|
triggerData: {
|
||||||
...((oldState?.view || newState?.view) && {
|
...((oldState?.view || newState?.view) && {
|
||||||
view: {
|
view: {
|
||||||
...(oldState?.view && { from: oldState.view }),
|
...(oldState?.view && { from: oldState.view }),
|
||||||
@@ -188,7 +190,9 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case 'fullscreen':
|
case 'fullscreen':
|
||||||
return {
|
return {
|
||||||
result:
|
result:
|
||||||
@@ -199,12 +203,16 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
return {
|
return {
|
||||||
result: newState?.expand !== undefined && condition.expand === newState.expand,
|
result: newState?.expand !== undefined && condition.expand === newState.expand,
|
||||||
};
|
};
|
||||||
case 'camera':
|
case 'camera': {
|
||||||
|
const oldCamera = oldState?.camera;
|
||||||
|
const newCamera = newState?.camera;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result:
|
result:
|
||||||
(!!newState?.camera && !!condition.cameras?.includes(newState.camera)) ||
|
(!!newCamera && !!condition.cameras?.includes(newCamera)) ||
|
||||||
(newState?.camera !== oldState?.camera && !condition.cameras?.length),
|
(newCamera !== oldCamera && !condition.cameras?.length),
|
||||||
data: {
|
...(newCamera !== oldCamera && {
|
||||||
|
triggerData: {
|
||||||
...((oldState?.camera || newState?.camera) && {
|
...((oldState?.camera || newState?.camera) && {
|
||||||
camera: {
|
camera: {
|
||||||
...(oldState?.camera && { from: oldState?.camera }),
|
...(oldState?.camera && { from: oldState?.camera }),
|
||||||
@@ -212,7 +220,9 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case 'numeric_state':
|
case 'numeric_state':
|
||||||
return {
|
return {
|
||||||
result:
|
result:
|
||||||
@@ -287,17 +297,21 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
new RegExp(condition.user_agent_re).test(newState.userAgent)),
|
new RegExp(condition.user_agent_re).test(newState.userAgent)),
|
||||||
};
|
};
|
||||||
case 'config': {
|
case 'config': {
|
||||||
|
const newConfig = newState?.config;
|
||||||
|
const oldConfig = oldState?.config;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result:
|
result:
|
||||||
!!newState?.config &&
|
!!newConfig &&
|
||||||
newState.config !== oldState?.config &&
|
newConfig !== oldConfig &&
|
||||||
(!condition.paths?.length ||
|
(!condition.paths?.length ||
|
||||||
condition.paths.some(
|
condition.paths.some(
|
||||||
(key) =>
|
(key) =>
|
||||||
getConfigValue(newState.config!, key) !==
|
getConfigValue(newConfig, key) !==
|
||||||
(oldState?.config ? getConfigValue(oldState?.config, key) : undefined),
|
(oldConfig ? getConfigValue(oldConfig, key) : undefined),
|
||||||
)),
|
)),
|
||||||
data: {
|
...(newConfig !== oldConfig && {
|
||||||
|
triggerData: {
|
||||||
config: {
|
config: {
|
||||||
...((oldState?.config || newState?.config) && {
|
...((oldState?.config || newState?.config) && {
|
||||||
...(oldState?.config && { from: oldState?.config }),
|
...(oldState?.config && { from: oldState?.config }),
|
||||||
@@ -305,6 +319,7 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'initialized':
|
case 'initialized':
|
||||||
|
|||||||
+16
-9
@@ -36,29 +36,36 @@ export interface ConditionStateManagerReadonlyInterface {
|
|||||||
getState(): ConditionState;
|
getState(): ConditionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConditionsEvaluationDataFromTo {
|
interface ConditionsTriggerDataFromTo {
|
||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConditionsEvaluationDataState extends ConditionsEvaluationDataFromTo {
|
interface ConditionsTriggerDataState extends ConditionsTriggerDataFromTo {
|
||||||
entity: string;
|
entity: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConditionsEvaluationDataConfig {
|
interface ConditionsTriggerDataConfig {
|
||||||
from?: AdvancedCameraCardConfig;
|
from?: AdvancedCameraCardConfig;
|
||||||
to?: AdvancedCameraCardConfig;
|
to?: AdvancedCameraCardConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConditionsEvaluationData {
|
export interface ConditionsTriggerData {
|
||||||
camera?: ConditionsEvaluationDataFromTo;
|
camera?: ConditionsTriggerDataFromTo;
|
||||||
view?: ConditionsEvaluationDataFromTo;
|
view?: ConditionsTriggerDataFromTo;
|
||||||
state?: ConditionsEvaluationDataState;
|
state?: ConditionsTriggerDataState;
|
||||||
config?: ConditionsEvaluationDataConfig;
|
config?: ConditionsTriggerDataConfig;
|
||||||
}
|
}
|
||||||
export interface ConditionsEvaluationResult {
|
export interface ConditionsEvaluationResult {
|
||||||
result: boolean;
|
result: boolean;
|
||||||
data?: ConditionsEvaluationData;
|
|
||||||
|
// Trigger data is only provided if there was a real change of state (For
|
||||||
|
// example: if a state condition that matches 'on' previously evaluated to
|
||||||
|
// true, and a call from the state manager arrives for an unrelated hass state
|
||||||
|
// update, the condition will still evaluate true, but there won't be any
|
||||||
|
// trigger data provided since the state relevant to the condition did not
|
||||||
|
// change).
|
||||||
|
triggerData?: ConditionsTriggerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConditionsListener = (result: ConditionsEvaluationResult) => void;
|
export type ConditionsListener = (result: ConditionsEvaluationResult) => void;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe('ConditionsManager', () => {
|
|||||||
stateManager.setState({ view: 'clips' });
|
stateManager.setState({ view: 'clips' });
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
view: {
|
view: {
|
||||||
to: 'clips',
|
to: 'clips',
|
||||||
},
|
},
|
||||||
@@ -52,7 +52,7 @@ describe('ConditionsManager', () => {
|
|||||||
stateManager.setState({ view: 'timeline' });
|
stateManager.setState({ view: 'timeline' });
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
view: {
|
view: {
|
||||||
from: 'clips',
|
from: 'clips',
|
||||||
to: 'timeline',
|
to: 'timeline',
|
||||||
@@ -62,6 +62,23 @@ describe('ConditionsManager', () => {
|
|||||||
|
|
||||||
expect(listener).toBeCalledTimes(2);
|
expect(listener).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not re-trigger without a real change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'view' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'clips' });
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({ view: 'clips' });
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with fullscreen condition', () => {
|
it('with fullscreen condition', () => {
|
||||||
@@ -120,7 +137,7 @@ describe('ConditionsManager', () => {
|
|||||||
stateManager.setState({ camera: 'bar' });
|
stateManager.setState({ camera: 'bar' });
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
camera: {
|
camera: {
|
||||||
to: 'bar',
|
to: 'bar',
|
||||||
},
|
},
|
||||||
@@ -130,7 +147,7 @@ describe('ConditionsManager', () => {
|
|||||||
stateManager.setState({ camera: 'foo' });
|
stateManager.setState({ camera: 'foo' });
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
camera: {
|
camera: {
|
||||||
from: 'bar',
|
from: 'bar',
|
||||||
to: 'foo',
|
to: 'foo',
|
||||||
@@ -140,6 +157,23 @@ describe('ConditionsManager', () => {
|
|||||||
|
|
||||||
expect(listener).toBeCalledTimes(2);
|
expect(listener).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not re-trigger without a real change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'camera' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ camera: 'bar' });
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({ camera: 'bar' });
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with stock HA conditions', () => {
|
describe('with stock HA conditions', () => {
|
||||||
@@ -163,7 +197,7 @@ describe('ConditionsManager', () => {
|
|||||||
});
|
});
|
||||||
expect(listener).toBeCalledWith({
|
expect(listener).toBeCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
state: {
|
state: {
|
||||||
entity: 'binary_sensor.foo',
|
entity: 'binary_sensor.foo',
|
||||||
to: 'on',
|
to: 'on',
|
||||||
@@ -177,7 +211,7 @@ describe('ConditionsManager', () => {
|
|||||||
});
|
});
|
||||||
expect(listener).toBeCalledWith({
|
expect(listener).toBeCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
state: {
|
state: {
|
||||||
entity: 'binary_sensor.foo',
|
entity: 'binary_sensor.foo',
|
||||||
from: 'on',
|
from: 'on',
|
||||||
@@ -336,7 +370,7 @@ describe('ConditionsManager', () => {
|
|||||||
});
|
});
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
// Only the last matching state will be included in the data.
|
// Only the last matching state will be included in the data.
|
||||||
state: {
|
state: {
|
||||||
entity: 'switch.two',
|
entity: 'switch.two',
|
||||||
@@ -354,7 +388,7 @@ describe('ConditionsManager', () => {
|
|||||||
|
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
// Only the last matching state will be included in the data.
|
// Only the last matching state will be included in the data.
|
||||||
state: {
|
state: {
|
||||||
entity: 'switch.two',
|
entity: 'switch.two',
|
||||||
@@ -366,6 +400,41 @@ describe('ConditionsManager', () => {
|
|||||||
|
|
||||||
expect(listener).toBeCalledTimes(2);
|
expect(listener).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not re-trigger without a real change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'state' as const, entity: 'switch.one' }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({
|
||||||
|
state: {
|
||||||
|
'switch.one': createStateEntity({ state: 'on' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
expect(manager.getEvaluation()).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
// Only the last matching state will be included in the data.
|
||||||
|
state: {
|
||||||
|
entity: 'switch.one',
|
||||||
|
to: 'on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
stateManager.setState({
|
||||||
|
state: {
|
||||||
|
'switch.one': createStateEntity({ state: 'on' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with numeric state condition', () => {
|
describe('with numeric state condition', () => {
|
||||||
@@ -526,7 +595,7 @@ describe('ConditionsManager', () => {
|
|||||||
addEventListener.mock.calls[0][1]();
|
addEventListener.mock.calls[0][1]();
|
||||||
|
|
||||||
// This should result in a callback to our state listener.
|
// This should result in a callback to our state listener.
|
||||||
expect(callback).toBeCalledWith({ result: true, data: {} });
|
expect(callback).toBeCalledWith({ result: true, triggerData: {} });
|
||||||
|
|
||||||
// Destroy the manager and ensure the event listener is removed.
|
// Destroy the manager and ensure the event listener is removed.
|
||||||
manager.destroy();
|
manager.destroy();
|
||||||
@@ -887,7 +956,7 @@ describe('ConditionsManager', () => {
|
|||||||
stateManager.setState({ config: config_1 });
|
stateManager.setState({ config: config_1 });
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
config: {
|
config: {
|
||||||
to: config_1,
|
to: config_1,
|
||||||
},
|
},
|
||||||
@@ -897,7 +966,7 @@ describe('ConditionsManager', () => {
|
|||||||
stateManager.setState({ config: config_2 });
|
stateManager.setState({ config: config_2 });
|
||||||
expect(listener).toHaveBeenLastCalledWith({
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
result: true,
|
result: true,
|
||||||
data: {
|
triggerData: {
|
||||||
config: {
|
config: {
|
||||||
from: config_1,
|
from: config_1,
|
||||||
to: config_2,
|
to: config_2,
|
||||||
@@ -971,6 +1040,23 @@ describe('ConditionsManager', () => {
|
|||||||
stateManager.setState({ fullscreen: true });
|
stateManager.setState({ fullscreen: true });
|
||||||
expect(listener).toBeCalledTimes(2);
|
expect(listener).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not re-trigger without a real change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'config' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ config: config_1 });
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
stateManager.setState({ config: config_1 });
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with initialized condition', () => {
|
it('with initialized condition', () => {
|
||||||
@@ -1001,7 +1087,7 @@ describe('ConditionsManager', () => {
|
|||||||
|
|
||||||
stateManager.setState({ fullscreen: true });
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
expect(listener).toBeCalledWith({ result: true, data: {} });
|
expect(listener).toBeCalledWith({ result: true, triggerData: {} });
|
||||||
expect(listener).toBeCalledTimes(1);
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
stateManager.setState({ fullscreen: false });
|
stateManager.setState({ fullscreen: false });
|
||||||
@@ -1013,7 +1099,7 @@ describe('ConditionsManager', () => {
|
|||||||
|
|
||||||
stateManager.setState({ fullscreen: true });
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
expect(listener).toBeCalledWith({ result: true, data: {} });
|
expect(listener).toBeCalledWith({ result: true, triggerData: {} });
|
||||||
expect(listener).toBeCalledTimes(3);
|
expect(listener).toBeCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { renderTemplate } from 'ha-nunjucks';
|
import { renderTemplate } from 'ha-nunjucks';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { TemplateRenderer } from '../../src/card-controller/templates';
|
import { TemplateRenderer } from '../../src/card-controller/templates';
|
||||||
import { ConditionsEvaluationData, ConditionState } from '../../src/conditions/types';
|
import { ConditionsTriggerData, ConditionState } from '../../src/conditions/types';
|
||||||
import { createHASS } from '../test-utils';
|
import { createHASS } from '../test-utils';
|
||||||
|
|
||||||
// ha-nunjucks attempts to make websocket calls initially so mock it out.
|
// ha-nunjucks attempts to make websocket calls initially so mock it out.
|
||||||
@@ -46,7 +46,7 @@ describe('TemplateRenderer', () => {
|
|||||||
camera: 'camera',
|
camera: 'camera',
|
||||||
view: 'view',
|
view: 'view',
|
||||||
};
|
};
|
||||||
const triggerData: ConditionsEvaluationData = {
|
const triggerData: ConditionsTriggerData = {
|
||||||
camera: {
|
camera: {
|
||||||
to: 'camera',
|
to: 'camera',
|
||||||
from: 'previous-camera',
|
from: 'previous-camera',
|
||||||
|
|||||||
Reference in New Issue
Block a user