feat: Allow disabling the call controls (#2615)

- For: #2587
This commit is contained in:
Dermot Duffy
2026-07-26 11:35:18 -07:00
committed by GitHub
parent 0c3d46ad3d
commit 3b8835ee7c
7 changed files with 87 additions and 8 deletions
+7 -6
View File
@@ -55,12 +55,13 @@ live:
# [...]
```
| Option | Default | Description |
| ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `button_size` | `40` | The size of the call control buttons in pixels. Must be >= `20`. |
| `lock` | `true` | Whether to lock the rest of the card controls/actions while a call is in progress. Prevents an accidental tap, swipe or action mid-call. Set to `false` to allow interactions regardless of call state. |
| `ringtone` | | The audible chime played while an inbound call (e.g. one started by [`view.triggers.actions.trigger: call`](./view.md?id=trigger-action-configuration)) is ringing. Stops as soon as the call is answered or ended. Manual calls never ring. See [`ringtone`](#ringtone). |
| `unanswered_timeout_seconds` | `60` | The number of seconds an inbound call may ring unanswered before it is automatically ended. The timer is cancelled the moment the call is answered. Set to `0` to disable the timeout. |
| Option | Default | Description |
| ---------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `button_size` | `40` | The size of the call control buttons in pixels. Must be >= `20`. |
| `enabled` | `true` | Whether the on-screen call controls are shown during a call. Set to `false` to hide them and drive calls from the [`menu`](./menu.md) instead. When hiding the controls, be sure to also remove `call` from `menu.auto_hide` so the menu stays visible allowing the call to be controlled. See [Driving calls from the menu](../examples.md?id=driving-calls-from-the-menu) for a complete example. |
| `lock` | `true` | Whether to lock the rest of the card controls/actions while a call is in progress. Prevents an accidental tap, swipe or action mid-call. Set to `false` to allow interactions regardless of call state. |
| `ringtone` | | The audible chime played while an inbound call (e.g. one started by [`view.triggers.actions.trigger: call`](./view.md?id=trigger-action-configuration)) is ringing. Stops as soon as the call is answered or ended. Manual calls never ring. See [`ringtone`](#ringtone). |
| `unanswered_timeout_seconds` | `60` | The number of seconds an inbound call may ring unanswered before it is automatically ended. The timer is cancelled the moment the call is answered. Set to `0` to disable the timeout. |
> [!NOTE] Browser autoplay restrictions may prevent the ringtone from playing
> until the page has received a user gesture (e.g. a tap or click). When that
+58
View File
@@ -563,6 +563,64 @@ profiles:
> the view-only camera to `live_provider: ha` (HLS / receive-only WebRTC, which
> never offers a backchannel) instead.
### Driving calls from the menu
By default on-screen controls will appear mid-card to handle a call. Setting
[`live.controls.call.enabled: false`](configuration/live.md?id=call) hides those
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.
```yaml
type: custom:advanced-camera-card
cameras:
- camera_entity: camera.front_door
live_provider: go2rtc
go2rtc:
modes:
- webrtc
profiles:
- doorbell
live:
controls:
call:
# Hide the on-screen call controls overlay -- the menu drives the call.
enabled: false
menu:
# The menu hides during a call by default; keep it visible so its call
# 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
type: toggle
# 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
`dependencies.cameras` allows events/recordings for other cameras to be shown
+1
View File
@@ -25,6 +25,7 @@ const getCallSchema = (): HAFormExpandableSchema => ({
title: localize('config.live.controls.call.editor_label'),
icon: 'mdi:phone',
schema: [
{ name: 'enabled', selector: { boolean: {} } },
{ name: 'lock', selector: { boolean: {} } },
{
name: 'ringtone',
+3 -2
View File
@@ -435,7 +435,8 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
!gesturesPTZActive &&
!this.locked;
const isCallActive = this.call?.cameraID === carouselCameraID;
const isCallControlsActive =
this.liveConfig.controls.call.enabled && this.call?.cameraID === carouselCameraID;
const callMediaPlayerController =
this._mediaLoadedInfoSinkController.get()?.mediaPlayerController ?? null;
@@ -470,7 +471,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
>
</advanced-camera-card-ptz>
<advanced-camera-card-call-controls
.active=${isCallActive}
.active=${isCallControlsActive}
.answered=${this.call?.answered ?? true}
.microphoneState=${this.microphoneState}
.muted=${callMediaPlayerController?.isMuted()}
+2
View File
@@ -50,6 +50,7 @@ export type RingtoneConfig = z.infer<typeof ringtoneConfigSchema>;
const callConfigDefault = {
button_size: 40,
enabled: true,
lock: true,
ringtone: { ...ringtoneConfigDefault },
unanswered_timeout_seconds: 60,
@@ -57,6 +58,7 @@ const callConfigDefault = {
const callConfigSchema = z.object({
button_size: z.number().min(BUTTON_SIZE_MIN).default(callConfigDefault.button_size),
enabled: z.boolean().default(callConfigDefault.enabled),
lock: z.boolean().default(callConfigDefault.lock),
ringtone: ringtoneConfigSchema.default(callConfigDefault.ringtone),
+1
View File
@@ -459,6 +459,7 @@
"answer": "Answer call",
"button_size": "Call control button size",
"editor_label": "Two-way audio call",
"enabled": "Show on-screen call controls",
"end": "End 2-way audio call",
"lock": "Lock UI during an active call",
"mute_audio": "Mute audio",
+15
View File
@@ -99,6 +99,7 @@ describe('config defaults', () => {
builtin: true,
call: {
button_size: 40,
enabled: true,
lock: true,
ringtone: { type: 'chime', repeat: 0 },
unanswered_timeout_seconds: 60,
@@ -1848,6 +1849,20 @@ it('should not require title controls to specify all options', () => {
).toBeTruthy();
});
it('should allow the on-screen call controls overlay to be disabled', () => {
const config = createConfig({
cameras: [{}],
live: {
controls: {
call: {
enabled: false,
},
},
},
});
expect(config.live.controls.call.enabled).toBe(false);
});
it('should strip trailing slashes from go2rtc url', () => {
const config = createConfig({
cameras: [