Files
advanced-camera-card/tests/condition-trigger/conditions/conditions/state.test.ts
T

474 lines
13 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createConditionEvaluator } from '../../../../src/condition-trigger/conditions/factory';
import { createHASS, createStateEntity } from '../../../test-utils';
import { createEvaluatorContext } from './test-utils';
// @vitest-environment jsdom
describe('state condition', () => {
it('should match any transition when neither state nor state_not is set', () => {
const evaluator = createConditionEvaluator(
{ condition: 'state' as const, entity_id: 'binary_sensor.foo' },
createEvaluatorContext(),
);
expect(
evaluator.evaluate(
{
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'on' }) }),
},
{},
),
).toEqual({ result: true });
expect(
evaluator.evaluate(
{
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
},
{
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'on' }) }),
},
),
).toEqual({ result: true });
});
it('should match a single positive state', () => {
const evaluator = createConditionEvaluator(
{ condition: 'state' as const, entity_id: 'binary_sensor.foo', state: 'on' },
createEvaluatorContext(),
);
expect(evaluator.evaluate({}).result).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
}).result,
).toBeTruthy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
}).result,
).toBeFalsy();
});
it('should match multiple positive states', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state: ['active', 'on'],
},
createEvaluatorContext(),
);
expect(evaluator.evaluate({}).result).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
}).result,
).toBeTruthy();
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'active' }),
}),
}).result,
).toBeTruthy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
}).result,
).toBeFalsy();
});
it('should match a single negative state', () => {
const evaluator = createConditionEvaluator(
{ condition: 'state' as const, entity_id: 'binary_sensor.foo', state_not: 'on' },
createEvaluatorContext(),
);
expect(evaluator.evaluate({}).result).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
}).result,
).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
}).result,
).toBeTruthy();
});
it('should match multiple negative states', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state_not: ['active', 'on'],
},
createEvaluatorContext(),
);
expect(evaluator.evaluate({}).result).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
}).result,
).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'active' }),
}),
}).result,
).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
}).result,
).toBeTruthy();
});
it('should match an implicit state condition', () => {
const evaluator = createConditionEvaluator(
{ entity_id: 'binary_sensor.foo', state: 'on' },
createEvaluatorContext(),
);
expect(evaluator.evaluate({}).result).toBeFalsy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
}).result,
).toBeTruthy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
}).result,
).toBeFalsy();
});
it('should not match when no entity is set', () => {
const evaluator = createConditionEvaluator(
{ condition: 'state' as const, state: 'on' },
createEvaluatorContext(),
);
const result = evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'on' }) }),
});
expect(result.result).toBeFalsy();
});
it('should accept the entity field', () => {
const evaluator = createConditionEvaluator(
{ condition: 'state' as const, entity: 'binary_sensor.foo', state: 'on' },
createEvaluatorContext(),
);
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'on' }) }),
}).result,
).toBeTruthy();
expect(
evaluator.evaluate({
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
}).result,
).toBeFalsy();
});
it('should match a list of entities only when all match by default', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: ['binary_sensor.foo', 'binary_sensor.bar'],
state: 'on',
},
createEvaluatorContext(),
);
// Both on -> match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'on' }),
'binary_sensor.bar': createStateEntity({ state: 'on' }),
}),
}).result,
).toBeTruthy();
// One off -> no match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'on' }),
'binary_sensor.bar': createStateEntity({ state: 'off' }),
}),
}).result,
).toBeFalsy();
// One absent -> no match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'on' }),
}),
}).result,
).toBeFalsy();
});
it('should match a list of entities when any matches and match is any', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: ['binary_sensor.foo', 'binary_sensor.bar'],
state: 'on',
match: 'any' as const,
},
createEvaluatorContext(),
);
// One on -> match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'on' }),
'binary_sensor.bar': createStateEntity({ state: 'off' }),
}),
}).result,
).toBeTruthy();
// None on -> no match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'off' }),
'binary_sensor.bar': createStateEntity({ state: 'off' }),
}),
}).result,
).toBeFalsy();
});
it('should match against an attribute instead of the state', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
attribute: 'device_class',
state: 'door',
},
createEvaluatorContext(),
);
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({
state: 'on',
attributes: { device_class: 'door' },
}),
}),
}).result,
).toBeTruthy();
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({
state: 'on',
attributes: { device_class: 'window' },
}),
}),
}).result,
).toBeFalsy();
// Attribute absent on the entity -> no value -> no match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'on', attributes: {} }),
}),
}).result,
).toBeFalsy();
});
describe('for', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-05T22:56:56Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('should not match until the state has been held long enough', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state: 'on',
for: '00:00:05',
},
createEvaluatorContext(),
);
// Held 2s (< 5s) -> no match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({
state: 'on',
last_changed: '2026-06-05T22:56:54Z',
}),
}),
}).result,
).toBeFalsy();
// Held 8s (>= 5s) -> match.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({
state: 'on',
last_changed: '2026-06-05T22:56:48Z',
}),
}),
}).result,
).toBeTruthy();
});
it('should render a templated "for" before comparing', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state: 'on',
for: "{{ states('input_number.delay') }}",
},
createEvaluatorContext(),
);
const evaluateHeldSince = (lastChanged: string): boolean =>
!!evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({
state: 'on',
last_changed: lastChanged,
}),
'input_number.delay': createStateEntity({ state: '5' }),
}),
}).result;
// `for` renders to 5s: held 2s -> no match, held 8s -> match.
expect(evaluateHeldSince('2026-06-05T22:56:54Z')).toBe(false);
expect(evaluateHeldSince('2026-06-05T22:56:48Z')).toBe(true);
});
it('should not match when last_changed is unavailable', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state: 'on',
for: '00:00:05',
},
createEvaluatorContext(),
);
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'on', last_changed: '' }),
}),
}).result,
).toBeFalsy();
});
it('should not match when for is unparseable', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state: 'on',
for: 'not-a-duration',
},
createEvaluatorContext(),
);
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({
state: 'on',
last_changed: '2026-06-05T22:56:46Z',
}),
}),
}).result,
).toBeFalsy();
});
});
it('should resolve an expected state that names another entity', () => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state: 'input_text.expected',
},
createEvaluatorContext(),
);
// The entity's state matches the resolved state of `input_text.expected`.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'armed' }),
'input_text.expected': createStateEntity({ state: 'armed' }),
}),
}).result,
).toBeTruthy();
// It does not match when the resolved state differs.
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'disarmed' }),
'input_text.expected': createStateEntity({ state: 'armed' }),
}),
}).result,
).toBeFalsy();
});
it('should match a state_not against an empty-string entity state', () => {
const evaluator = createConditionEvaluator(
{ condition: 'state' as const, entity_id: 'sensor.foo', state_not: 'on' },
createEvaluatorContext(),
);
expect(
evaluator.evaluate({
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '' }) }),
}).result,
).toBeTruthy();
});
it('should match an empty-string expected state', () => {
const evaluator = createConditionEvaluator(
{ condition: 'state' as const, entity_id: 'sensor.foo', state: '' },
createEvaluatorContext(),
);
// An entity state of "" matches the configured empty `state`.
expect(
evaluator.evaluate({
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '' }) }),
}).result,
).toBeTruthy();
// A non-empty entity state does not.
expect(
evaluator.evaluate({
hass: createHASS({ 'sensor.foo': createStateEntity({ state: 'on' }) }),
}).result,
).toBeFalsy();
});
});