feat: Trigger automations on call answered, rejected and hung up (#2591)
This commit is contained in:
1 parent
2400e20134
commit
87ecbc7e45
19 files changed
+737
-97
No files matched your search
@@ -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' },
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('createTriggerEvaluator', () => {
|
||||
[{ trigger: 'state', entity_id: 'binary_sensor.x' }, StateTrigger],
|
||||
[{ trigger: 'numeric_state', entity_id: 'sensor.x', above: 5 }, NumericStateTrigger],
|
||||
[{ trigger: 'template', value_template: '{{ true }}' }, TemplateTrigger],
|
||||
[{ trigger: 'call', call: true }, CallTrigger],
|
||||
[{ trigger: 'call', to: 'answered' }, CallTrigger],
|
||||
[{ trigger: 'camera', cameras: ['front'] }, CameraTrigger],
|
||||
[{ trigger: 'config' }, ConfigTrigger],
|
||||
[{ trigger: 'display_mode', display_mode: 'single' }, DisplayModeTrigger],
|
||||
|
||||
@@ -18,31 +18,152 @@ describe('CallTrigger', () => {
|
||||
return { stateManager, callback };
|
||||
};
|
||||
|
||||
it('should treat an absent call state as not-in-call', () => {
|
||||
it('should treat an absent call state as idle', () => {
|
||||
const { stateManager, callback } = create({ trigger: 'call' });
|
||||
|
||||
// Absent (undefined) is equivalent to false, so this is not a change.
|
||||
stateManager.setState({ call: false });
|
||||
// Absent (undefined) is equivalent to idle, so this is not a change.
|
||||
stateManager.setState({ call: 'idle' });
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
stateManager.setState({ call: true });
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should trigger only on changes to the given value', () => {
|
||||
const { stateManager, callback } = create({ trigger: 'call', call: true });
|
||||
stateManager.setState({ call: true });
|
||||
stateManager.setState({ call: false });
|
||||
it('should trigger on any phase change without from or to', () => {
|
||||
const { stateManager, callback } = create({ trigger: 'call' });
|
||||
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
stateManager.setState({ call: 'answered' });
|
||||
stateManager.setState({ call: 'idle' });
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not trigger when the phase is unchanged', () => {
|
||||
const { stateManager, callback } = create({ trigger: 'call' });
|
||||
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
stateManager.setState({ call: 'ringing', camera: 'camera.office' });
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should trigger on the falling edge to a false value', () => {
|
||||
const { stateManager, callback } = create({ trigger: 'call', call: false });
|
||||
it('should trigger when an inbound call is answered', () => {
|
||||
const { stateManager, callback } = create({
|
||||
trigger: 'call',
|
||||
from: 'ringing',
|
||||
to: 'answered',
|
||||
});
|
||||
|
||||
stateManager.setState({ call: true });
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
stateManager.setState({ call: false });
|
||||
stateManager.setState({ call: 'answered' });
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not trigger on an outbound call when from is ringing', () => {
|
||||
const { stateManager, callback } = create({
|
||||
trigger: 'call',
|
||||
from: 'ringing',
|
||||
to: 'answered',
|
||||
});
|
||||
|
||||
// An outbound call is answered by construction, so it moves from idle
|
||||
// straight to answered without ringing.
|
||||
stateManager.setState({ call: 'answered' });
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger on an outbound call when only to is given', () => {
|
||||
const { stateManager, callback } = create({ trigger: 'call', to: 'answered' });
|
||||
|
||||
stateManager.setState({ call: 'answered' });
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should trigger when a ringing call is rejected', () => {
|
||||
const { stateManager, callback } = create({
|
||||
trigger: 'call',
|
||||
from: 'ringing',
|
||||
to: 'idle',
|
||||
});
|
||||
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
stateManager.setState({ call: 'idle' });
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not trigger a reject when an answered call ends', () => {
|
||||
const { stateManager, callback } = create({
|
||||
trigger: 'call',
|
||||
from: 'ringing',
|
||||
to: 'idle',
|
||||
});
|
||||
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
stateManager.setState({ call: 'answered' });
|
||||
stateManager.setState({ call: 'idle' });
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger when an answered call is hung up', () => {
|
||||
const { stateManager, callback } = create({
|
||||
trigger: 'call',
|
||||
from: 'answered',
|
||||
to: 'idle',
|
||||
});
|
||||
|
||||
stateManager.setState({ call: 'answered' });
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
stateManager.setState({ call: 'idle' });
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should trigger on both answer and reject with from alone', () => {
|
||||
const answered = create({ trigger: 'call', from: 'ringing' });
|
||||
const rejected = create({ trigger: 'call', from: 'ringing' });
|
||||
|
||||
answered.stateManager.setState({ call: 'ringing' });
|
||||
answered.stateManager.setState({ call: 'answered' });
|
||||
|
||||
rejected.stateManager.setState({ call: 'ringing' });
|
||||
rejected.stateManager.setState({ call: 'idle' });
|
||||
|
||||
expect(answered.callback).toHaveBeenCalledTimes(1);
|
||||
expect(rejected.callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should trigger on any end with to alone', () => {
|
||||
const { stateManager, callback } = create({ trigger: 'call', to: 'idle' });
|
||||
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
stateManager.setState({ call: 'idle' });
|
||||
stateManager.setState({ call: 'answered' });
|
||||
stateManager.setState({ call: 'idle' });
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should match any phase in a from or to list', () => {
|
||||
const { stateManager, callback } = create({
|
||||
trigger: 'call',
|
||||
from: ['ringing', 'answered'],
|
||||
to: ['idle'],
|
||||
});
|
||||
|
||||
stateManager.setState({ call: 'ringing' });
|
||||
stateManager.setState({ call: 'idle' });
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
stateManager.setState({ call: 'answered' });
|
||||
stateManager.setState({ call: 'idle' });
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user