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:
committed by
dermotduffy
parent
15e335a647
commit
f18e4cd4b8
@@ -1,14 +1,14 @@
|
||||
import { GeneralActionConfig } from '../../../config/schema/actions/custom/general';
|
||||
import { SubstreamOffActionConfig } from '../../../config/schema/actions/custom/substream-off';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { SubstreamViewModifier } from '../../view/modifiers/substream';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class SubstreamOffAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||
export class SubstreamOffAction extends AdvancedCameraCardAction<SubstreamOffActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
api.getViewManager().setViewByParameters({
|
||||
modifiers: [new SubstreamViewModifier()],
|
||||
modifiers: [new SubstreamViewModifier({ camera: this._action.camera })],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CameraManager } from '../../../camera-manager/manager';
|
||||
import { SubstreamViewModifier } from '../../view/modifiers/substream';
|
||||
import { GeneralActionConfig } from '../../../config/schema/actions/custom/general';
|
||||
import { SubstreamOnActionConfig } from '../../../config/schema/actions/custom/substream-on';
|
||||
import { View } from '../../../view/view';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { SubstreamViewModifier } from '../../view/modifiers/substream';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class SubstreamOnAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||
export class SubstreamOnAction extends AdvancedCameraCardAction<SubstreamOnActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
@@ -14,35 +14,38 @@ export class SubstreamOnAction extends AdvancedCameraCardAction<GeneralActionCon
|
||||
return;
|
||||
}
|
||||
|
||||
const cameraID = this._action.camera ?? view.camera;
|
||||
if (!cameraID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stream =
|
||||
this._action.stream ??
|
||||
this._getCycledSubstreamID(view, cameraID, api.getCameraManager());
|
||||
|
||||
api.getViewManager().setViewByParameters({
|
||||
modifiers: [
|
||||
new SubstreamViewModifier(
|
||||
this._getCycledSubstreamID(view, api.getCameraManager()),
|
||||
),
|
||||
],
|
||||
modifiers: [new SubstreamViewModifier({ stream, camera: cameraID })],
|
||||
});
|
||||
}
|
||||
|
||||
// The next substream in the selected camera's cycle: its `substream`
|
||||
// dependencies in order, wrapping back round. `undefined` means the camera's
|
||||
// own stream (no substream).
|
||||
// The next substream in the camera's cycle: its `substream` dependencies in
|
||||
// order, wrapping back round. `undefined` means the camera's own stream (no
|
||||
// substream).
|
||||
private _getCycledSubstreamID(
|
||||
view: View,
|
||||
cameraID: string,
|
||||
cameraManager: CameraManager,
|
||||
): string | undefined {
|
||||
if (!view.camera) {
|
||||
return undefined;
|
||||
}
|
||||
const dependencies = [
|
||||
...cameraManager.getStore().getAllDependentCameras(view.camera, 'substream'),
|
||||
...cameraManager.getStore().getAllDependentCameras(cameraID, 'substream'),
|
||||
];
|
||||
if (dependencies.length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
const current = view.context?.live?.overrides?.get(view.camera) ?? view.camera;
|
||||
const current = view.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
||||
const currentIndex = dependencies.indexOf(current);
|
||||
const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
|
||||
// Index 0 is the camera itself, i.e. no substream.
|
||||
return dependencies[nextIndex] === view.camera ? undefined : dependencies[nextIndex];
|
||||
return dependencies[nextIndex] === cameraID ? undefined : dependencies[nextIndex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { SubstreamSelectActionConfig } from '../../../config/schema/actions/custom/substream-select';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { SubstreamViewModifier } from '../../view/modifiers/substream';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class SubstreamSelectAction extends AdvancedCameraCardAction<SubstreamSelectActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
api.getViewManager().setViewByParameters({
|
||||
modifiers: [new SubstreamViewModifier(this._action.camera)],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,6 @@ import { SleepAction } from './actions/sleep';
|
||||
import { StatusBarAction } from './actions/status-bar';
|
||||
import { SubstreamOffAction } from './actions/substream-off';
|
||||
import { SubstreamOnAction } from './actions/substream-on';
|
||||
import { SubstreamSelectAction } from './actions/substream-select';
|
||||
import { ToggleAction } from './actions/toggle';
|
||||
import { UnmuteAction } from './actions/unmute';
|
||||
import { URLAction } from './actions/url';
|
||||
@@ -130,11 +129,9 @@ export class ActionFactory {
|
||||
return new CallEndAction(context, action, options?.config);
|
||||
case 'camera_select':
|
||||
return new CameraSelectAction(context, action, options?.config);
|
||||
case 'live_substream_select':
|
||||
return new SubstreamSelectAction(context, action, options?.config);
|
||||
case 'live_substream_off':
|
||||
case 'substream_off':
|
||||
return new SubstreamOffAction(context, action, options?.config);
|
||||
case 'live_substream_on':
|
||||
case 'substream_on':
|
||||
return new SubstreamOnAction(context, action, options?.config);
|
||||
case 'media_player':
|
||||
return new MediaPlayerAction(context, action, options?.config);
|
||||
|
||||
@@ -134,7 +134,7 @@ export class CallManager {
|
||||
...(needsNavigation && {
|
||||
params: { view: 'live', camera: parentID },
|
||||
}),
|
||||
modifiers: [new SubstreamViewModifier(callCameraID, parentID)],
|
||||
modifiers: [new SubstreamViewModifier({ stream: callCameraID, camera: parentID })],
|
||||
force: true,
|
||||
});
|
||||
this._api.getConditionStateManager().setState({ call: true });
|
||||
@@ -255,12 +255,11 @@ export class CallManager {
|
||||
const previousStream = getStreamCameraID(previousView, call.cameraID);
|
||||
viewManager.setViewByParameters({
|
||||
modifiers: [
|
||||
new SubstreamViewModifier(
|
||||
previousStream && previousStream !== call.cameraID
|
||||
? previousStream
|
||||
: undefined,
|
||||
call.cameraID,
|
||||
),
|
||||
new SubstreamViewModifier({
|
||||
...(previousStream &&
|
||||
previousStream !== call.cameraID && { stream: previousStream }),
|
||||
camera: call.cameraID,
|
||||
}),
|
||||
],
|
||||
force: true,
|
||||
});
|
||||
|
||||
@@ -71,10 +71,7 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
actions: [
|
||||
cameraPriority === 'entity'
|
||||
? // Set the currently selected camera to the state of the entity.
|
||||
createCameraAction(
|
||||
'camera_select',
|
||||
`{{ hass.states["${cameraControlEntity}"].state }}`,
|
||||
)
|
||||
createCameraAction(`{{ hass.states["${cameraControlEntity}"].state }}`)
|
||||
: // Set the selected option in the entity to the current camera ID.
|
||||
createInternalCallbackAction(async (api: CardActionsAPI) => {
|
||||
const camera = api.getViewManager().getView()?.camera ?? undefined;
|
||||
@@ -92,10 +89,7 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
],
|
||||
actions: [
|
||||
// When the entity state changes, updated the selected option.
|
||||
createCameraAction(
|
||||
'camera_select',
|
||||
'{{ advanced_camera_card.trigger.state.to }}',
|
||||
),
|
||||
createCameraAction('{{ advanced_camera_card.trigger.state.to }}'),
|
||||
],
|
||||
tag: automationTag,
|
||||
},
|
||||
|
||||
@@ -22,9 +22,8 @@ const CALL_DISRUPTIVE_ACTIONS: ReadonlySet<string> = new Set([
|
||||
'default',
|
||||
|
||||
// Substreams.
|
||||
'live_substream_select',
|
||||
'live_substream_on',
|
||||
'live_substream_off',
|
||||
'substream_on',
|
||||
'substream_off',
|
||||
|
||||
// Stream-disrupting actions. `play` is intentionally NOT here: it's the
|
||||
// recovery path from a paused state.
|
||||
|
||||
@@ -3,6 +3,8 @@ import { AdvancedCameraCardCustomActionConfig } from '../config/schema/actions/t
|
||||
import {
|
||||
createCameraAction,
|
||||
createGeneralAction,
|
||||
createSubstreamOffAction,
|
||||
createSubstreamOnAction,
|
||||
createViewAction,
|
||||
} from '../utils/action.js';
|
||||
import { CardQueryStringAPI } from './types';
|
||||
@@ -12,7 +14,12 @@ import { ViewParametersUserSpecified } from './view/types.js';
|
||||
interface QueryStringViewIntent {
|
||||
view?: ViewParametersUserSpecified & {
|
||||
default?: boolean;
|
||||
substream?: string;
|
||||
|
||||
// The substream change to apply alongside the view. Tri-state:
|
||||
// - `undefined`: no substream URL action present, no modifier issued.
|
||||
// - `string`: `substream_on=X` — engage stream X.
|
||||
// - `null`: `substream_off` — explicitly clear the override.
|
||||
stream?: string | null;
|
||||
};
|
||||
other?: AdvancedCameraCardCustomActionConfig[];
|
||||
}
|
||||
@@ -44,14 +51,20 @@ export class QueryStringManager {
|
||||
|
||||
private async _executeViewRelated(intent: QueryStringViewIntent): Promise<void> {
|
||||
if (intent.view) {
|
||||
const modifiers =
|
||||
intent.view.stream !== undefined
|
||||
? [
|
||||
new SubstreamViewModifier(
|
||||
intent.view.stream === null ? {} : { stream: intent.view.stream },
|
||||
),
|
||||
]
|
||||
: undefined;
|
||||
if (intent.view.default) {
|
||||
await this._api.getViewManager().setViewDefaultWithNewQuery({
|
||||
params: {
|
||||
camera: intent.view.camera,
|
||||
},
|
||||
...(intent.view.substream && {
|
||||
modifiers: [new SubstreamViewModifier(intent.view.substream)],
|
||||
}),
|
||||
...(modifiers && { modifiers }),
|
||||
});
|
||||
} else {
|
||||
await this._api.getViewManager().setViewByParametersWithNewQuery({
|
||||
@@ -59,9 +72,7 @@ export class QueryStringManager {
|
||||
...(intent.view.view && { view: intent.view.view }),
|
||||
...(intent.view.camera && { camera: intent.view.camera }),
|
||||
},
|
||||
...(intent.view.substream && {
|
||||
modifiers: [new SubstreamViewModifier(intent.view.substream)],
|
||||
}),
|
||||
...(modifiers && { modifiers }),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -84,8 +95,13 @@ export class QueryStringManager {
|
||||
(result.view ??= {}).view = undefined;
|
||||
} else if (action.advanced_camera_card_action === 'camera_select') {
|
||||
(result.view ??= {}).camera = action.camera;
|
||||
} else if (action.advanced_camera_card_action === 'live_substream_select') {
|
||||
(result.view ??= {}).substream = action.camera;
|
||||
} else if (
|
||||
action.advanced_camera_card_action === 'substream_on' &&
|
||||
action.stream !== undefined
|
||||
) {
|
||||
(result.view ??= {}).stream = action.stream;
|
||||
} else if (action.advanced_camera_card_action === 'substream_off') {
|
||||
(result.view ??= {}).stream = null;
|
||||
} else {
|
||||
(result.other ??= []).push(action);
|
||||
}
|
||||
@@ -116,11 +132,19 @@ export class QueryStringManager {
|
||||
let action: AdvancedCameraCardCustomActionConfig | null = null;
|
||||
switch (actionName) {
|
||||
case 'camera_select':
|
||||
case 'live_substream_select':
|
||||
if (value) {
|
||||
action = createCameraAction(actionName, value, { cardID });
|
||||
action = createCameraAction(value, { cardID });
|
||||
}
|
||||
break;
|
||||
case 'substream_on':
|
||||
action = createSubstreamOnAction({
|
||||
...(value && { stream: value }),
|
||||
cardID,
|
||||
});
|
||||
break;
|
||||
case 'substream_off':
|
||||
action = createSubstreamOffAction({ cardID });
|
||||
break;
|
||||
case 'camera_ui':
|
||||
case 'default':
|
||||
case 'download':
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
import { View } from '../../../view/view';
|
||||
import { ViewModifier } from '../types';
|
||||
|
||||
// The single write path for the camera-keyed `live.overrides` map (the read
|
||||
// path being `getStreamCameraID` in `view/substream`): sets a camera's
|
||||
// substream override, or clears it when `substreamID` is absent so the
|
||||
// camera's own stream is used. `cameraID` defaults to the selected camera.
|
||||
export class SubstreamViewModifier implements ViewModifier {
|
||||
private _substreamID?: string;
|
||||
private _cameraID?: string;
|
||||
interface SubstreamViewModifierOptions {
|
||||
// The substream to engage. When absent, the override is cleared so the
|
||||
// camera's own stream is used.
|
||||
stream?: string;
|
||||
|
||||
constructor(substreamID?: string, cameraID?: string) {
|
||||
this._substreamID = substreamID;
|
||||
this._cameraID = cameraID;
|
||||
// The camera whose override to write. Defaults to the selected camera.
|
||||
camera?: string;
|
||||
}
|
||||
|
||||
// The single write path for the camera-keyed `live.overrides` map (the read
|
||||
// path being `getStreamCameraID` in `view/substream`).
|
||||
export class SubstreamViewModifier implements ViewModifier {
|
||||
private _options: SubstreamViewModifierOptions;
|
||||
|
||||
constructor(options?: SubstreamViewModifierOptions) {
|
||||
this._options = options ?? {};
|
||||
}
|
||||
|
||||
public modify(view: View): void {
|
||||
const cameraID = this._cameraID ?? view.camera;
|
||||
const cameraID = this._options.camera ?? view.camera;
|
||||
if (!cameraID) {
|
||||
return;
|
||||
}
|
||||
if (!this._substreamID) {
|
||||
// A stream equal to the camera itself is semantically "no substream";
|
||||
// normalise it to a cleared override so the map doesn't carry self-
|
||||
// referential entries.
|
||||
if (!this._options.stream || this._options.stream === cameraID) {
|
||||
view.context?.live?.overrides?.delete(cameraID);
|
||||
return;
|
||||
}
|
||||
const overrides = view.context?.live?.overrides ?? new Map<string, string>();
|
||||
overrides.set(cameraID, this._substreamID);
|
||||
overrides.set(cameraID, this._options.stream);
|
||||
view.mergeInContext({ live: { overrides } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
+37
-13
@@ -27,7 +27,8 @@ import {
|
||||
PTZActionPhase,
|
||||
} from '../config/schema/actions/custom/ptz.js';
|
||||
import { SetReviewActionConfig } from '../config/schema/actions/custom/set-review.js';
|
||||
import { SubstreamSelectActionConfig } from '../config/schema/actions/custom/substream-select.js';
|
||||
import { SubstreamOffActionConfig } from '../config/schema/actions/custom/substream-off.js';
|
||||
import { SubstreamOnActionConfig } from '../config/schema/actions/custom/substream-on.js';
|
||||
import { ViewActionConfig } from '../config/schema/actions/custom/view.js';
|
||||
import { PerformActionActionConfig } from '../config/schema/actions/stock/perform-action.js';
|
||||
import type { Notification } from '../config/schema/actions/types.js';
|
||||
@@ -72,20 +73,45 @@ export function createViewAction(
|
||||
}
|
||||
|
||||
export function createCameraAction(
|
||||
action: 'camera_select' | 'live_substream_select',
|
||||
camera: string,
|
||||
options?: {
|
||||
cardID?: string;
|
||||
},
|
||||
): CameraSelectActionConfig | SubstreamSelectActionConfig {
|
||||
): CameraSelectActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: action,
|
||||
advanced_camera_card_action: 'camera_select',
|
||||
camera: camera,
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createSubstreamOnAction(options?: {
|
||||
stream?: string;
|
||||
camera?: string;
|
||||
cardID?: string;
|
||||
}): SubstreamOnActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'substream_on',
|
||||
...(options?.stream && { stream: options.stream }),
|
||||
...(options?.camera && { camera: options.camera }),
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createSubstreamOffAction(options?: {
|
||||
camera?: string;
|
||||
cardID?: string;
|
||||
}): SubstreamOffActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'substream_off',
|
||||
...(options?.camera && { camera: options.camera }),
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createMediaPlayerAction(
|
||||
mediaPlayer: string,
|
||||
mediaPlayerAction: 'play' | 'stop',
|
||||
@@ -274,18 +300,16 @@ export function createSetReviewAction(reviewed?: boolean): SetReviewActionConfig
|
||||
};
|
||||
}
|
||||
|
||||
export function createCallStartAction(
|
||||
camera?: string,
|
||||
stream?: string,
|
||||
options?: {
|
||||
cardID?: string;
|
||||
},
|
||||
): CallStartActionConfig {
|
||||
export function createCallStartAction(options?: {
|
||||
camera?: string;
|
||||
stream?: string;
|
||||
cardID?: string;
|
||||
}): CallStartActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'call_start',
|
||||
...(camera && { camera }),
|
||||
...(stream && { stream }),
|
||||
...(options?.camera && { camera: options.camera }),
|
||||
...(options?.stream && { stream: options.stream }),
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user