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
+39
View File
@@ -827,6 +827,36 @@ const microphoneConnectedToCallTransform = (data: unknown): boolean => {
return true;
};
// Unify the legacy trio `live_substream_{on,off,select}` into the new
// `substream_{on,off}` pair. `live_substream_select` carried the substream ID
// in its `camera` field; that field becomes `stream` on `substream_on`.
const substreamActionsUnifyTransform = (data: RawAdvancedCameraCardConfig): boolean => {
if (
data['action'] !== 'fire-dom-event' &&
data['action'] !== 'custom:advanced-camera-card-action'
) {
return false;
}
const action = data['advanced_camera_card_action'];
if (action === 'live_substream_on') {
data['advanced_camera_card_action'] = 'substream_on';
return true;
}
if (action === 'live_substream_off') {
data['advanced_camera_card_action'] = 'substream_off';
return true;
}
if (action === 'live_substream_select') {
data['advanced_camera_card_action'] = 'substream_on';
if ('camera' in data) {
data['stream'] = data['camera'];
delete data['camera'];
}
return true;
}
return false;
};
const frigateCardToAdvancedCameraCardStyleTransform = (data: unknown): unknown => {
if (typeof data !== 'object' || !data || Array.isArray(data)) {
return data;
@@ -1051,4 +1081,13 @@ const UPGRADES = [
typeof data === 'object' && data ? data[CONF_AUTOMATIONS] : {},
);
},
// Unify `live_substream_{on,off,select}` actions. Walked over the entire
// tree because card actions can appear anywhere (menu buttons, elements,
// automations, view-action handlers, etc.).
(data: unknown): boolean => {
return upgradeObjectRecursively(substreamActionsUnifyTransform)(
typeof data === 'object' && data ? (data as RawAdvancedCameraCardConfig) : {},
);
},
];
@@ -8,8 +8,6 @@ const GENERAL_ACTIONS = [
'expand',
'fullscreen',
'info',
'live_substream_off',
'live_substream_on',
'menu_toggle',
'microphone_connect',
'microphone_disconnect',
@@ -0,0 +1,12 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const substreamOffActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('substream_off'),
// The camera whose substream override to clear. Defaults to the selected
// camera. A no-op if that camera has no substream engaged.
camera: z.string().optional(),
});
export type SubstreamOffActionConfig = z.infer<typeof substreamOffActionConfigSchema>;
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const substreamOnActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('substream_on'),
// The camera that owns the substream. Defaults to the selected camera.
camera: z.string().optional(),
// The substream to engage: one of `camera`'s `substream` dependencies. When
// omitted, repeated calls advance through `camera`'s `substream`
// dependencies in order, wrapping back to no substream engaged.
stream: z.string().optional(),
});
export type SubstreamOnActionConfig = z.infer<typeof substreamOnActionConfigSchema>;
@@ -1,11 +0,0 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const substreamSelectActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('live_substream_select'),
camera: z.string(),
});
export type SubstreamSelectActionConfig = z.infer<
typeof substreamSelectActionConfigSchema
>;
+4 -2
View File
@@ -18,7 +18,8 @@ import { ptzDigitalActionConfigSchema } from './custom/ptz-digital';
import { ptzMultiActionSchema } from './custom/ptz-multi';
import { setReviewActionConfigSchema } from './custom/set-review';
import { sleepActionConfigSchema } from './custom/sleep';
import { substreamSelectActionConfigSchema } from './custom/substream-select';
import { substreamOffActionConfigSchema } from './custom/substream-off';
import { substreamOnActionConfigSchema } from './custom/substream-on';
import { viewActionConfigSchema } from './custom/view';
import { stockActionSchema } from './stock/types';
@@ -76,7 +77,8 @@ const advancedCameraCardCustomActionSchema = z.union([
setReviewActionConfigSchema,
sleepActionConfigSchema,
statusBarActionConfigSchema,
substreamSelectActionConfigSchema,
substreamOffActionConfigSchema,
substreamOnActionConfigSchema,
viewActionConfigSchema,
viewDisplayModeActionConfigSchema,
]);