## 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` |
150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { isEqual } from 'lodash-es';
|
|
import { RemoteControlEntityPriority } from '../../config/schema/remote-control';
|
|
import { createCameraAction, createInternalCallbackAction } from '../../utils/action';
|
|
import { CardActionsAPI, CardConfigLoaderAPI, TaggedAutomation } from '../types';
|
|
|
|
export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|
const automationTag = setRemoteControlEntityFromConfig;
|
|
|
|
api.getAutomationsManager().deleteAutomations(automationTag);
|
|
|
|
const remoteControlConfig = api.getConfigManager().getConfig()?.remote_control;
|
|
const cameraControlEntity = remoteControlConfig?.entities.camera;
|
|
if (!cameraControlEntity) {
|
|
return;
|
|
}
|
|
|
|
const cameraPriority: RemoteControlEntityPriority =
|
|
remoteControlConfig.entities.camera_priority;
|
|
|
|
// Control entities functionality is implemented entirely by populating
|
|
// automations.
|
|
|
|
const automations: TaggedAutomation[] = [
|
|
{
|
|
conditions: [
|
|
{
|
|
condition: 'config' as const,
|
|
paths: ['cameras', 'remote_control.entities.camera'],
|
|
},
|
|
],
|
|
actions: [
|
|
// Set the possible options on the entity to the camera IDs via a
|
|
// callback to `setCameraOptionsOnEntity` (below).
|
|
createInternalCallbackAction((api: CardActionsAPI) =>
|
|
setCameraOptionsOnEntity(cameraControlEntity, api),
|
|
),
|
|
],
|
|
tag: automationTag,
|
|
},
|
|
{
|
|
conditions: [
|
|
{
|
|
condition: 'camera' as const,
|
|
},
|
|
],
|
|
actions: [
|
|
// When the camera changes, update the entity to match (only if different
|
|
// to avoid race conditions when multiple cards share the same entity).
|
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2244
|
|
createInternalCallbackAction(async (api: CardActionsAPI) =>
|
|
selectOptionOnEntityIfDifferent(
|
|
api,
|
|
cameraControlEntity,
|
|
api.getViewManager().getView()?.camera ?? undefined,
|
|
),
|
|
),
|
|
],
|
|
tag: automationTag,
|
|
},
|
|
{
|
|
// Immediately on the start, the HA state for the entity will be updated.
|
|
// However, that will almost certainly not trigger the condition below
|
|
// this one, as automations only run *after* the card is initialized (and
|
|
// it very likely will not yet be). Instead, wait to be initialized, then
|
|
// set the camera.
|
|
conditions: [
|
|
{
|
|
condition: 'initialized' as const,
|
|
},
|
|
],
|
|
actions: [
|
|
cameraPriority === 'entity'
|
|
? // Set the currently selected camera to the state of the entity.
|
|
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;
|
|
return selectOptionOnEntityIfDifferent(api, cameraControlEntity, camera);
|
|
}),
|
|
],
|
|
tag: automationTag,
|
|
},
|
|
{
|
|
conditions: [
|
|
{
|
|
condition: 'state' as const,
|
|
entity: cameraControlEntity,
|
|
},
|
|
],
|
|
actions: [
|
|
// When the entity state changes, updated the selected option.
|
|
createCameraAction('{{ advanced_camera_card.trigger.state.to }}'),
|
|
],
|
|
tag: automationTag,
|
|
},
|
|
];
|
|
|
|
api.getAutomationsManager().addAutomations(automations);
|
|
};
|
|
|
|
const selectOptionOnEntityIfDifferent = async (
|
|
api: CardActionsAPI,
|
|
entity: string,
|
|
option?: string,
|
|
): Promise<void> => {
|
|
const hass = api.getHASSManager().getHASS();
|
|
const currentState = hass?.states[entity]?.state;
|
|
|
|
// Only update if the option is defined and different from current state.
|
|
// This prevents race conditions when multiple cards share the same entity.
|
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2244
|
|
if (!option || option === currentState) {
|
|
return;
|
|
}
|
|
|
|
await hass?.callService(
|
|
'input_select',
|
|
'select_option',
|
|
{
|
|
option: option,
|
|
},
|
|
{
|
|
entity_id: entity,
|
|
},
|
|
);
|
|
};
|
|
|
|
const setCameraOptionsOnEntity = async (entity: string, api: CardActionsAPI) => {
|
|
const hass = api.getHASSManager().getHASS();
|
|
const cameraIDs = api.getCameraManager().getStore().getCameraIDs();
|
|
|
|
const existingOptions = (hass?.states[entity]?.attributes?.options ?? []).sort();
|
|
const desiredOptions = [...cameraIDs].sort();
|
|
|
|
if (isEqual(existingOptions, desiredOptions)) {
|
|
return;
|
|
}
|
|
|
|
await hass?.callService(
|
|
'input_select',
|
|
'set_options',
|
|
{
|
|
options: desiredOptions,
|
|
},
|
|
{
|
|
entity_id: entity,
|
|
},
|
|
);
|
|
};
|