638 lines
21 KiB
TypeScript
638 lines
21 KiB
TypeScript
import type { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
|
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
|
|
|
import { ConditionStateManager } from '../../../../src/condition-trigger/conditions/state-manager';
|
|
import { StateTrigger } from '../../../../src/condition-trigger/triggers/triggers/state';
|
|
import type { TriggerOfType } from '../../../../src/condition-trigger/triggers/triggers/types';
|
|
import { createHASS, createStateEntity } from '../../../test-utils';
|
|
import { createTriggerEvaluatorContext } from './test-utils';
|
|
|
|
const ENTITY = 'binary_sensor.door';
|
|
const ENTITY_TWO = 'binary_sensor.window';
|
|
|
|
// @vitest-environment jsdom
|
|
describe('StateTrigger', () => {
|
|
const create = (
|
|
config: TriggerOfType<'state'>,
|
|
): {
|
|
trigger: StateTrigger;
|
|
stateManager: ConditionStateManager;
|
|
callback: Mock;
|
|
} => {
|
|
const stateManager = new ConditionStateManager();
|
|
const callback = vi.fn();
|
|
const trigger = new StateTrigger(
|
|
config,
|
|
createTriggerEvaluatorContext({ stateManager }),
|
|
);
|
|
return { trigger, stateManager, callback };
|
|
};
|
|
|
|
const setHass = (
|
|
manager: ConditionStateManager,
|
|
states: Record<string, Partial<HassEntity>>,
|
|
): void => {
|
|
const entities: HassEntities = {};
|
|
for (const [id, partial] of Object.entries(states)) {
|
|
entities[id] = createStateEntity(partial);
|
|
}
|
|
manager.setState({ hass: createHASS(entities) });
|
|
};
|
|
|
|
it('should trigger on a transition to a matching state', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
expect(callback).toHaveBeenCalledWith(
|
|
expect.objectContaining({ platform: 'state', entity_id: ENTITY, entity: ENTITY }),
|
|
);
|
|
});
|
|
|
|
it('should not trigger on a transition to a non-matching state', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'unavailable' } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should require the from state to match', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
from: 'off',
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'unknown' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
|
|
// From 'unknown', not 'off'.
|
|
expect(callback).not.toHaveBeenCalled();
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should match not_to', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
not_to: 'unavailable',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
// Triggers (initial -> off, not unavailable).
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// To unavailable -> excluded.
|
|
setHass(stateManager, { [ENTITY]: { state: 'unavailable' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should exclude transitions from a not_from state', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
not_from: 'unavailable',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
// From undefined (not 'unavailable') -> triggers.
|
|
setHass(stateManager, { [ENTITY]: { state: 'unavailable' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// From 'unavailable' -> excluded.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// From 'on' -> triggers.
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should match a list of to-states', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: ['armed_home', 'armed_away'],
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'disarmed' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'armed_home' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
setHass(stateManager, { [ENTITY]: { state: 'armed_away' } });
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should match a transition to an empty-string state', () => {
|
|
// HA compares `state == ''`; the empty string is a real value to match.
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: '',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
setHass(stateManager, { [ENTITY]: { state: '' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should fan out independently over a list of entities', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: [ENTITY, ENTITY_TWO],
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'off' },
|
|
[ENTITY_TWO]: { state: 'off' },
|
|
});
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' }, [ENTITY_TWO]: { state: 'off' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
expect(callback).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({ entity_id: ENTITY }),
|
|
);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' }, [ENTITY_TWO]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
expect(callback).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({ entity_id: ENTITY_TWO }),
|
|
);
|
|
});
|
|
|
|
it('should not trigger for a watched entity that did not change', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
|
|
// An unrelated entity changes; ENTITY is unchanged so must not trigger.
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'off' },
|
|
[ENTITY_TWO]: { state: 'on' },
|
|
});
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should provide the full from_state/to_state objects', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
from_state: expect.objectContaining({ state: 'off' }),
|
|
to_state: expect.objectContaining({ state: 'on' }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should accept the entity alias', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity: ENTITY,
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
describe('attribute-only changes', () => {
|
|
it('should trigger on an attribute-only change when no from/to is set', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
// Initial change.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on', attributes: { a: 1 } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// Attribute-only change still triggers.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on', attributes: { a: 2 } } });
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should not trigger on an attribute-only change when a to is set', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
|
|
// Off -> on.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on', attributes: { a: 1 } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// Attribute-only -> suppressed.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on', attributes: { a: 2 } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
it('should treat a null to as matching any state change but not attributes', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: null,
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// Any state change.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
|
|
// Attribute-only suppressed (key present).
|
|
setHass(stateManager, { [ENTITY]: { state: 'on', attributes: { a: 1 } } });
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should match against an attribute', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'device_class',
|
|
to: 'door',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'on', attributes: { device_class: 'window' } },
|
|
});
|
|
expect(callback).not.toHaveBeenCalled();
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'on', attributes: { device_class: 'door' } },
|
|
});
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not trigger when the watched attribute is unchanged', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'device_class',
|
|
to: 'door',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'on', attributes: { device_class: 'window' } },
|
|
});
|
|
|
|
// The state changes but the watched attribute does not -> ignored.
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'off', attributes: { device_class: 'window' } },
|
|
});
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should treat an absent watched attribute as no value', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'device_class',
|
|
to: 'door',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
// The entity has no `device_class` attribute, so there is no value to match.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
describe('attribute matching by type', () => {
|
|
it('should match a numeric attribute against an unquoted number', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'battery_level',
|
|
to: 50,
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 20 } } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 50 } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not match a numeric attribute against a stringified number', () => {
|
|
// `50 == "50"` is false in Home Assistant.
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'battery_level',
|
|
to: '50',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 20 } } });
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 50 } } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should treat a boolean attribute as equal to its integer form', () => {
|
|
// Python's `bool` is a subtype of `int`, so `true == 1`.
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'charging',
|
|
to: 1,
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { attributes: { charging: false } } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
setHass(stateManager, { [ENTITY]: { attributes: { charging: true } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should match a list of numeric values by membership', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'battery_level',
|
|
to: [50, 80],
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 20 } } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 80 } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should match a falsy attribute value of zero', () => {
|
|
// `0` is a real attribute value, not the "no value" sentinel, and must
|
|
// not be dropped (e.g. by `arrayify`).
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'battery_level',
|
|
to: 0,
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 5 } } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
setHass(stateManager, { [ENTITY]: { attributes: { battery_level: 0 } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not trigger when the raw attribute value is unchanged', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'battery_level',
|
|
to: 50,
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'on', attributes: { battery_level: 50 } },
|
|
});
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// The state changes but the watched numeric attribute does not -> ignored.
|
|
setHass(stateManager, {
|
|
[ENTITY]: { state: 'off', attributes: { battery_level: 50 } },
|
|
});
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should match a null attribute value via to: [null]', () => {
|
|
// HA collapses a missing/None attribute to `None`; `to: [null]` matches it.
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'bar',
|
|
to: [null],
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { attributes: { bar: 'x' } } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
setHass(stateManager, { [ENTITY]: { attributes: { bar: null } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should exclude a null attribute value via not_to: [null]', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
attribute: 'bar',
|
|
not_to: [null],
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
// To a real value -> fires.
|
|
setHass(stateManager, { [ENTITY]: { attributes: { bar: 'x' } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
// To null -> excluded.
|
|
setHass(stateManager, { [ENTITY]: { attributes: { bar: null } } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
it('should trigger without a to_state when the entity is removed', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// Remove the entity from hass entirely.
|
|
stateManager.setState({ hass: createHASS({}) });
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
expect(callback).toHaveBeenLastCalledWith({
|
|
platform: 'state',
|
|
entity_id: ENTITY,
|
|
entity: ENTITY,
|
|
from_state: expect.objectContaining({ state: 'on' }),
|
|
});
|
|
});
|
|
|
|
describe('for', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should trigger only after the state has been held for the duration', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
for: '00:00:05',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
vi.advanceTimersByTime(4999);
|
|
expect(callback).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should cancel the pending trigger when the state leaves the match', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
for: '00:00:05',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
vi.advanceTimersByTime(3000);
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
vi.advanceTimersByTime(5000);
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should clear a pending for: timer on destroy', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
for: '00:00:05',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
|
|
// Arms the for: timer.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
trigger.destroy();
|
|
vi.advanceTimersByTime(5000);
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reset the for: timer on a fresh matching transition', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: ['on', 'off'],
|
|
for: '00:00:05',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
// Arms (to includes 'off').
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
vi.advanceTimersByTime(3000);
|
|
|
|
// Re-arms (resets the 5s).
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
vi.advanceTimersByTime(4000);
|
|
|
|
// Would have triggered at 5s without the reset.
|
|
expect(callback).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1000);
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not cancel the for: hold on an attribute-only change', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
for: '00:00:05',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
|
|
// Arms.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on', attributes: { a: 1 } } });
|
|
vi.advanceTimersByTime(3000);
|
|
|
|
// The state stays 'on'; only an attribute moves -- the hold must continue.
|
|
setHass(stateManager, { [ENTITY]: { state: 'on', attributes: { a: 2 } } });
|
|
|
|
// 5s total since arming.
|
|
vi.advanceTimersByTime(2000);
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not trigger when for is unparseable', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
for: 'not-a-duration',
|
|
});
|
|
trigger.subscribe(callback);
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
vi.advanceTimersByTime(100000);
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('should stop triggering after destroy', () => {
|
|
const { trigger, stateManager, callback } = create({
|
|
trigger: 'state',
|
|
entity_id: ENTITY,
|
|
to: 'on',
|
|
});
|
|
trigger.subscribe(callback);
|
|
trigger.destroy();
|
|
|
|
setHass(stateManager, { [ENTITY]: { state: 'off' } });
|
|
setHass(stateManager, { [ENTITY]: { state: 'on' } });
|
|
expect(callback).not.toHaveBeenCalled();
|
|
});
|
|
});
|