feat: Trigger automations on call answered, rejected and hung up (#2591)

This commit is contained in:
Dermot Duffy
2026-07-22 11:11:57 -07:00
committed by GitHub
parent 2400e20134
commit 87ecbc7e45
19 changed files with 737 additions and 97 deletions
@@ -10,27 +10,53 @@ describe('call condition', () => {
expect(() => callConditionSchema.parse({ condition: 'call' })).toThrow();
});
it('should match when call is true', () => {
const evaluator = createConditionEvaluator(
{ condition: 'call' as const, call: true },
createEvaluatorContext(),
);
expect(evaluator.evaluate({}).result).toBeFalsy();
expect(evaluator.evaluate({ call: true }).result).toBeTruthy();
expect(evaluator.evaluate({ call: false }).result).toBeFalsy();
it('should reject a phase that does not exist', () => {
expect(() =>
callConditionSchema.parse({ condition: 'call', call: 'hungup' }),
).toThrow();
});
it('should match when call is false', () => {
it('should match the ringing phase', () => {
const evaluator = createConditionEvaluator(
{ condition: 'call' as const, call: false },
{ condition: 'call' as const, call: 'ringing' as const },
createEvaluatorContext(),
);
expect(evaluator.evaluate({ call: 'ringing' }).result).toBeTruthy();
expect(evaluator.evaluate({ call: 'answered' }).result).toBeFalsy();
expect(evaluator.evaluate({ call: 'idle' }).result).toBeFalsy();
});
it('should match the answered phase', () => {
const evaluator = createConditionEvaluator(
{ condition: 'call' as const, call: 'answered' as const },
createEvaluatorContext(),
);
expect(evaluator.evaluate({ call: 'answered' }).result).toBeTruthy();
expect(evaluator.evaluate({ call: 'ringing' }).result).toBeFalsy();
});
it('should treat an absent call state as idle', () => {
const evaluator = createConditionEvaluator(
{ condition: 'call' as const, call: 'idle' as const },
createEvaluatorContext(),
);
// With no state.call published, the bare condition matches `false`,
// so `call: false` is satisfied.
expect(evaluator.evaluate({}).result).toBeTruthy();
expect(evaluator.evaluate({ call: true }).result).toBeFalsy();
expect(evaluator.evaluate({ call: false }).result).toBeTruthy();
expect(evaluator.evaluate({ call: 'idle' }).result).toBeTruthy();
expect(evaluator.evaluate({ call: 'ringing' }).result).toBeFalsy();
});
it('should match any phase in a list', () => {
const evaluator = createConditionEvaluator(
{ condition: 'call' as const, call: ['ringing', 'answered'] as const },
createEvaluatorContext(),
);
expect(evaluator.evaluate({ call: 'ringing' }).result).toBeTruthy();
expect(evaluator.evaluate({ call: 'answered' }).result).toBeTruthy();
expect(evaluator.evaluate({ call: 'idle' }).result).toBeFalsy();
expect(evaluator.evaluate({}).result).toBeFalsy();
});
});
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest';
import { CallConditionEvaluator } from '../../../src/condition-trigger/conditions/conditions/call';
import { CameraConditionEvaluator } from '../../../src/condition-trigger/conditions/conditions/camera';
import { DisplayModeConditionEvaluator } from '../../../src/condition-trigger/conditions/conditions/display-mode';
import { ExpandConditionEvaluator } from '../../../src/condition-trigger/conditions/conditions/expand';
@@ -19,7 +18,6 @@ type ConditionEvaluatorConstructor = new (...args: never[]) => ConditionEvaluato
describe('createConditionEvaluatorForTrigger', () => {
it.each<[Trigger, ConditionEvaluatorConstructor]>([
[{ trigger: 'call', call: true }, CallConditionEvaluator],
[{ trigger: 'camera', cameras: ['front'] }, CameraConditionEvaluator],
[{ trigger: 'display_mode', display_mode: 'single' }, DisplayModeConditionEvaluator],
[{ trigger: 'expand', expand: true }, ExpandConditionEvaluator],
@@ -40,6 +38,7 @@ describe('createConditionEvaluatorForTrigger', () => {
it.each<[string, Trigger]>([
['a valueless trigger fires on any change', { trigger: 'fullscreen' }],
['config has no matching condition', { trigger: 'config', paths: ['menu.style'] }],
['call matches the change itself', { trigger: 'call', from: 'ringing' }],
[
'stock triggers evaluate themselves',
{ trigger: 'state', entity_id: 'binary_sensor.x' },