@@ -29,7 +29,7 @@ menu:
|
||||
|
||||
| Button name | Description |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `call` | The `call` menu button: starts or ends a [two-way audio](../usage/2-way-audio.md) call. |
|
||||
| `call` | The `call` menu button: starts a [two-way audio](../usage/2-way-audio.md) call, answers an inbound (ringing) call (hold to reject it instead), and ends an active call. |
|
||||
| `camera_ui` | The `camera_ui` menu button: brings the user to a context-appropriate page on the UI of their camera engine (e.g. the Frigate camera homepage). Will only appear if the camera engine supports a camera UI (e.g. if `frigate.url` option is set for `frigate` engine users). |
|
||||
| `cameras` | The camera selection submenu. Will only appear if multiple cameras are configured. |
|
||||
| `clips` | The `clips` view menu button: brings the user to the `clips` view on tap and the most-recent `clip` view on hold. |
|
||||
|
||||
+4
-21
@@ -570,10 +570,10 @@ By default on-screen controls will appear mid-card to handle a call. Setting
|
||||
controls so the call can be driven via some other mechanism. In this example,
|
||||
the card is configured to allow calls to be driven from the menu instead.
|
||||
|
||||
This wires up the menu equivalents of every overlay control. The menu `call`
|
||||
button starts, ends, and (while ringing) rejects calls, but it cannot _answer_
|
||||
an inbound ring -- so a conditional answer button is added that appears only
|
||||
while ringing.
|
||||
The single menu `call` button handles the whole call: it starts a call, answers
|
||||
an inbound ring (hold to reject it instead), and hangs up. It is shown by
|
||||
default, so only the microphone and audio buttons need enabling below to cover
|
||||
the full functionality of the (in this example: disabled) call controls.
|
||||
|
||||
```yaml
|
||||
type: custom:advanced-camera-card
|
||||
@@ -595,9 +595,6 @@ menu:
|
||||
# controls stay reachable.
|
||||
auto_hide: []
|
||||
buttons:
|
||||
# Starts a call, and becomes a hang-up button for the duration of a call.
|
||||
call:
|
||||
enabled: true
|
||||
# Mutes/unmutes your outbound microphone during a call.
|
||||
microphone:
|
||||
enabled: true
|
||||
@@ -605,20 +602,6 @@ menu:
|
||||
# Mutes/unmutes the inbound (caller's) audio during a call.
|
||||
mute:
|
||||
enabled: true
|
||||
elements:
|
||||
# The menu `call` button cannot answer an inbound (ringing) call, so this
|
||||
# answer button is shown only while ringing to provide that control.
|
||||
- type: custom:advanced-camera-card-conditional
|
||||
conditions:
|
||||
- condition: call
|
||||
call: ringing
|
||||
elements:
|
||||
- type: custom:advanced-camera-card-menu-icon
|
||||
icon: mdi:phone
|
||||
title: Answer call
|
||||
tap_action:
|
||||
action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: call_answer
|
||||
```
|
||||
|
||||
## Events from other cameras
|
||||
|
||||
@@ -53,6 +53,12 @@ enabled by default and appears in the `live` view whenever the selected camera
|
||||
[`view.triggers.actions.trigger: call`](../configuration/view.md?id=trigger-action-configuration)
|
||||
trigger -- e.g. a doorbell) open the overlay in a ringing state with only
|
||||
two buttons: a red **Reject** and a green **Answer**.
|
||||
- The **call** menu button itself tracks the call state: tap it to start or
|
||||
answer a call and to hang up an active one, and while an inbound call is
|
||||
ringing **hold** it to reject. This lets you drive the whole call from the
|
||||
menu when the standard call controls are hidden with
|
||||
[`live.controls.call.enabled: false`](../configuration/live.md?id=call) -- see
|
||||
[Driving calls from the menu](../examples.md?id=driving-calls-from-the-menu).
|
||||
- When a call is answered (outbound calls are answered by definition) the
|
||||
inbound audio is unmuted automatically, so the caller can be heard. The
|
||||
microphone stays muted by default (push-to-talk) -- tap the microphone button
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import type { MediaLoadedInfo } from '../types';
|
||||
import {
|
||||
createCallAnswerAction,
|
||||
createCallEndAction,
|
||||
createCallStartAction,
|
||||
createCameraAction,
|
||||
@@ -507,15 +508,26 @@ export class MenuButtonController {
|
||||
return null;
|
||||
}
|
||||
|
||||
// In a call: a single hang-up button, regardless of target count.
|
||||
if (callManager?.isActive()) {
|
||||
// In a call, a single button regardless of target count. An unanswered
|
||||
// inbound call (ringing) answers; an answered or outbound call hangs up.
|
||||
const call = callManager?.getCall();
|
||||
if (call) {
|
||||
const ringing = call.inbound && !call.answered;
|
||||
return {
|
||||
icon: 'mdi:phone-hangup',
|
||||
title: localize('config.live.controls.call.end'),
|
||||
style: this._getEmphasizedStyle(true),
|
||||
icon: ringing ? 'mdi:phone-ring' : 'mdi:phone-hangup',
|
||||
title: ringing
|
||||
? localize('config.live.controls.call.answer')
|
||||
: localize('config.live.controls.call.end'),
|
||||
style: ringing
|
||||
? this._getPulsingStyle(
|
||||
'var(--advanced-camera-card-menu-button-positive-color)',
|
||||
)
|
||||
: this._getEmphasizedStyle(true),
|
||||
...config.menu.buttons.call,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
tap_action: createCallEndAction(),
|
||||
tap_action: ringing ? createCallAnswerAction() : createCallEndAction(),
|
||||
// While ringing, tap answers and hold rejects.
|
||||
...(ringing && { hold_action: createCallEndAction() }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -915,16 +927,28 @@ export class MenuButtonController {
|
||||
*/
|
||||
private _getEmphasizedStyle(critical?: boolean): StyleInfo {
|
||||
if (critical) {
|
||||
return {
|
||||
animation: 'pulse 3s infinite',
|
||||
color: 'var(--advanced-camera-card-menu-button-critical-color)',
|
||||
};
|
||||
return this._getPulsingStyle(
|
||||
'var(--advanced-camera-card-menu-button-critical-color)',
|
||||
);
|
||||
}
|
||||
return {
|
||||
color: 'var(--advanced-camera-card-menu-button-active-color)',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pulsing style in the given color, e.g. to draw attention to a
|
||||
* critical or a ringing button.
|
||||
* @param color The CSS color to pulse.
|
||||
* @returns A StyleInfo.
|
||||
*/
|
||||
private _getPulsingStyle(color: string): StyleInfo {
|
||||
return {
|
||||
animation: 'pulse 3s infinite',
|
||||
color,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a button determine if the style should be emphasized by examining all
|
||||
* of the actions sequentially.
|
||||
|
||||
@@ -141,3 +141,19 @@ div.opposing {
|
||||
|
||||
// Further theme related styling is dynamically applied by `menu.ts`, see
|
||||
// `_renderPerInstanceStyle`.
|
||||
|
||||
/*****************
|
||||
* Pulse animation
|
||||
*****************/
|
||||
|
||||
// Referenced by the inline `animation` on emphasized button styles in
|
||||
// `menu-button-controller` (e.g. the ringing call button).
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
var(--advanced-camera-card-warning-color)
|
||||
);
|
||||
|
||||
// The color of a positive icon in the menu (e.g. answer an inbound call).
|
||||
--advanced-camera-card-menu-button-positive-color: var(
|
||||
--advanced-camera-card-success-color
|
||||
);
|
||||
|
||||
// The background color of the button "circles" in the menu.
|
||||
--advanced-camera-card-menu-button-background: color-mix(
|
||||
in oklab,
|
||||
|
||||
@@ -1289,7 +1289,7 @@ describe('MenuButtonController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('when a call is active', () => {
|
||||
it('when an answered call is active', () => {
|
||||
const cameraManager = createCameraManager(
|
||||
createStore([
|
||||
{
|
||||
@@ -1299,7 +1299,12 @@ describe('MenuButtonController', () => {
|
||||
]),
|
||||
);
|
||||
const callManager = mock<CallManager>();
|
||||
vi.mocked(callManager.isActive).mockReturnValue(true);
|
||||
vi.mocked(callManager.getCall).mockReturnValue({
|
||||
cameraID: 'camera-1',
|
||||
inbound: false,
|
||||
answered: true,
|
||||
previousView: createView({ camera: 'camera-1' }),
|
||||
});
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager,
|
||||
callManager,
|
||||
@@ -1325,6 +1330,53 @@ describe('MenuButtonController', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('when an inbound call is ringing', () => {
|
||||
const cameraManager = createCameraManager(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: createCapabilities({ '2-way-audio': true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const callManager = mock<CallManager>();
|
||||
vi.mocked(callManager.getCall).mockReturnValue({
|
||||
cameraID: 'camera-1',
|
||||
inbound: true,
|
||||
answered: false,
|
||||
previousView: createView({ camera: 'camera-1' }),
|
||||
});
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager,
|
||||
callManager,
|
||||
view: createView({ camera: 'camera-1' }),
|
||||
});
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
alignment: 'matching',
|
||||
state_color: true,
|
||||
permanent: false,
|
||||
icon: 'mdi:phone-ring',
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: 'Answer call',
|
||||
style: {
|
||||
animation: 'pulse 3s infinite',
|
||||
color: 'var(--advanced-camera-card-menu-button-positive-color)',
|
||||
},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'call_answer',
|
||||
},
|
||||
hold_action: {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'call_end',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should have microphone button', () => {
|
||||
|
||||
Reference in New Issue
Block a user