refactor: Unify substream actions into substream_{on,off} (#2497)

## Summary

- Collapse `live_substream_{on,off,select}` into a unified
`substream_{on,off}` pair, symmetric with `call_{start,end}`.
- Rename the `camera` field on the former `live_substream_select` to
`stream` (it always was a stream ID).
- Add optional `camera` field to both new actions for targeting a
non-selected base camera.
- YAML configs are migrated automatically; URL bookmarks must be updated
by hand.

## Migration

### Cycling between camera and substream (toggle button)

Before:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: live_substream_on
```

After:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: substream_on
```

### Selecting a specific substream

Before:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: live_substream_select
  camera: camera.front_door_hd
```

After:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: substream_on
  stream: camera.front_door_hd
```

### Turning the substream off

Before:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: live_substream_off
```

After:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: substream_off
```

### URL querystrings (manual update required)

| Before | After |
| --- | --- |
|
`?advanced-camera-card-action.live_substream_select=camera.front_door_hd`
| `?advanced-camera-card-action.substream_on=camera.front_door_hd` |
This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent 15e335a647
commit f18e4cd4b8
32 changed files with 878 additions and 386 deletions
+15 -15
View File
@@ -27,6 +27,8 @@ import {
createPTZControlsAction,
createPTZMultiAction,
createSetReviewAction,
createSubstreamOffAction,
createSubstreamOnAction,
createViewAction,
isAdvancedCameraCardCustomAction,
} from '../utils/action';
@@ -166,7 +168,6 @@ export class MenuButtonController {
const menuCameraIDs = cameraManager.getStore().getCameraIDsWithCapability('menu');
if (menuCameraIDs.size > 1) {
const submenuItems = Array.from(menuCameraIDs, (cameraID) => {
const action = createCameraAction('camera_select', cameraID);
const metadata = cameraManager.getCameraMetadata(cameraID);
return {
@@ -176,7 +177,7 @@ export class MenuButtonController {
state_color: true,
title: metadata?.title,
selected: view?.camera === cameraID,
...(action && { tap_action: action }),
tap_action: createCameraAction(cameraID),
};
});
@@ -221,13 +222,12 @@ export class MenuButtonController {
title: localize('config.menu.buttons.substreams'),
...config.menu.buttons.substreams,
type: 'custom:advanced-camera-card-menu-icon',
tap_action: createGeneralAction(
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
),
tap_action: hasSubstream(view)
? createSubstreamOffAction()
: createSubstreamOnAction(),
};
} else if (streams.length > 2) {
const menuItems = Array.from(streams, (streamID) => {
const action = createCameraAction('live_substream_select', streamID);
const metadata = cameraManager.getCameraMetadata(streamID) ?? undefined;
return {
enabled: true,
@@ -236,7 +236,7 @@ export class MenuButtonController {
state_color: true,
title: metadata?.title,
selected: substreamAwareCameraID === streamID,
...(action && { tap_action: action }),
tap_action: createSubstreamOnAction({ stream: streamID }),
};
});
@@ -538,10 +538,10 @@ export class MenuButtonController {
entity: metadata?.icon.entity,
state_color: true,
title: metadata?.title,
tap_action: createCallStartAction(
cameraID,
streamID === cameraID ? undefined : streamID,
),
tap_action: createCallStartAction({
camera: cameraID,
...(streamID !== cameraID && { stream: streamID }),
}),
};
});
@@ -676,8 +676,6 @@ export class MenuButtonController {
.map((playerEntityID) => {
const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
const state = hass.states[playerEntityID];
const playAction = createMediaPlayerAction(playerEntityID, 'play');
const stopAction = createMediaPlayerAction(playerEntityID, 'stop');
const disabled = !state || state.state === 'unavailable';
return {
@@ -687,8 +685,10 @@ export class MenuButtonController {
state_color: false,
title: title,
disabled: disabled,
...(!disabled && playAction && { tap_action: playAction }),
...(!disabled && stopAction && { hold_action: stopAction }),
...(!disabled && {
tap_action: createMediaPlayerAction(playerEntityID, 'play'),
hold_action: createMediaPlayerAction(playerEntityID, 'stop'),
}),
};
});