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
+229 -10
View File
@@ -1897,7 +1897,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -1919,7 +1919,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2023,7 +2023,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2045,7 +2045,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2154,7 +2154,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2176,7 +2176,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2323,7 +2323,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2351,7 +2351,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2455,7 +2455,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -2477,7 +2477,7 @@ describe('should handle version specific upgrades', () => {
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'live_substream_on' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
@@ -4051,5 +4051,224 @@ describe('should handle version specific upgrades', () => {
postUpgradeChecks(config);
});
});
describe('live_substream_{on,off,select} → substream_{on,off}', () => {
it('rewrites live_substream_on to substream_on in an automation', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_on',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
},
]);
postUpgradeChecks(config);
});
it('rewrites live_substream_off to substream_off in an automation', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_off',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_off',
},
]);
postUpgradeChecks(config);
});
it('rewrites live_substream_select to substream_on with camera → stream', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
stream: 'camera.office_hd',
},
]);
postUpgradeChecks(config);
});
it('rewrites a malformed live_substream_select with no camera field', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
},
]);
postUpgradeChecks(config);
});
it('migrates actions on elements and overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:video-input-component',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
},
],
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
merge: {
menu: {
buttons: {
substreams: {
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_on',
},
},
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements[0].tap_action).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
stream: 'camera.office_hd',
});
expect(config.overrides[0].merge.menu.buttons.substreams.tap_action).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
});
postUpgradeChecks(config);
});
it('leaves opaque user payloads alone', () => {
// A perform-action service payload that happens to mention the legacy
// action string in its `data` block must not be rewritten — `data` is
// opaque, not an action.
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'perform-action',
perform_action: 'script.bogus',
data: {
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.automations[0].actions[0]).toEqual({
action: 'perform-action',
perform_action: 'script.bogus',
data: {
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
});
});
it('is idempotent', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
// Running upgradeConfig again should not change anything.
expect(upgradeConfig(config)).toBeFalsy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
stream: 'camera.office_hd',
},
]);
postUpgradeChecks(config);
});
});
});
});
+4 -7
View File
@@ -1054,16 +1054,13 @@ describe('config defaults', () => {
},
{
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'live_substream_off',
advanced_camera_card_action: 'substream_off',
},
{
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'live_substream_on',
},
{
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.front_door_hd',
advanced_camera_card_action: 'substream_on',
camera: 'camera.front_door',
stream: 'camera.front_door_hd',
},
{
action: 'custom:advanced-camera-card-action',