feat: Add out of the box support for Reolink PTZ (#1982)
- Closes: #1964
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity';
|
||||
import { Entity } from '../../utils/ha/registry/entity/types';
|
||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { Camera, CameraInitializationOptions } from '../camera';
|
||||
import { CameraInitializationError } from '../error';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HomeAssistant } from '../../ha/types';
|
||||
import { canonicalizeHAURL } from '../../utils/ha';
|
||||
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
||||
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../utils/ha/browse-media/types';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { RequestCache } from '../cache';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ActionsExecutor } from '../card-controller/actions/types';
|
||||
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { localize } from '../localize/localize';
|
||||
import { HassStateDifference, isTriggeredState } from '../utils/ha';
|
||||
@@ -6,6 +8,7 @@ import { Capabilities } from './capabilities';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraNoIDError } from './error';
|
||||
import { CameraEventCallback, CameraProxyConfig } from './types';
|
||||
import { getConfiguredPTZAction } from './utils/ptz';
|
||||
|
||||
export interface CameraInitializationOptions {
|
||||
stateWatcher: StateWatcherSubscriptionInterface;
|
||||
@@ -83,6 +86,22 @@ export class Camera {
|
||||
};
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
executor: ActionsExecutor,
|
||||
action: PTZAction,
|
||||
options?: {
|
||||
phase?: PTZActionPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<boolean> {
|
||||
const configuredAction = getConfiguredPTZAction(this.getConfig(), action, options);
|
||||
if (configuredAction) {
|
||||
await executor.executeActions({ actions: configuredAction });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected _stateChangeHandler = (difference: HassStateDifference): void => {
|
||||
this._eventCallback?.({
|
||||
cameraID: this.getID(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CameraConfig } from '../config/schema/cameras';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import { RecordingSegmentsCache, RequestCache } from './cache';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
@@ -128,14 +127,4 @@ export interface CameraManagerEngine {
|
||||
cameraConfig: CameraConfig,
|
||||
context?: CameraEndpointsContext,
|
||||
): CameraEndpoints | null;
|
||||
|
||||
executePTZAction(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
action: PTZAction,
|
||||
options?: {
|
||||
phase?: PTZActionPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import { ActionsExecutor } from '../../card-controller/actions/types';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||
import { errorToConsole } from '../../utils/basic';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity';
|
||||
import { Entity } from '../../utils/ha/registry/entity/types';
|
||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { Camera, CameraInitializationOptions } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
@@ -57,6 +58,55 @@ export class FrigateCamera extends Camera {
|
||||
return await super.initialize(options);
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
executor: ActionsExecutor,
|
||||
action: PTZAction,
|
||||
options?: {
|
||||
phase?: PTZActionPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<boolean> {
|
||||
if (await super.executePTZAction(executor, action, options)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cameraEntity = this.getConfig().camera_entity;
|
||||
if ((action === 'preset' && !options?.preset) || !cameraEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Awkward translation between card action and service parameters:
|
||||
// https://github.com/blakeblackshear/frigate-hass-integration/blob/dev/custom_components/frigate/services.yaml
|
||||
await executor.executeActions({
|
||||
actions: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'frigate.ptz',
|
||||
data: {
|
||||
action:
|
||||
options?.phase === 'stop'
|
||||
? 'stop'
|
||||
: action === 'zoom_in' || action === 'zoom_out'
|
||||
? 'zoom'
|
||||
: action === 'preset'
|
||||
? 'preset'
|
||||
: 'move',
|
||||
...(options?.phase !== 'stop' && {
|
||||
argument:
|
||||
action === 'zoom_in'
|
||||
? 'in'
|
||||
: action === 'zoom_out'
|
||||
? 'out'
|
||||
: action === 'preset'
|
||||
? options?.preset
|
||||
: action,
|
||||
}),
|
||||
},
|
||||
target: { entity_id: cameraEntity },
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async _initializeConfig(
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
@@ -194,10 +244,10 @@ export class FrigateCamera extends Camera {
|
||||
// Note: The Frigate integration only supports continuous PTZ movements
|
||||
// (regardless of the actual underlying camera capability).
|
||||
const panTilt: PTZMovementType[] = [
|
||||
...(ptzInfo.features?.includes('pt') ? ['continuous' as const] : []),
|
||||
...(ptzInfo.features?.includes('pt') ? [PTZMovementType.Continuous] : []),
|
||||
];
|
||||
const zoom: PTZMovementType[] = [
|
||||
...(ptzInfo.features?.includes('zoom') ? ['continuous' as const] : []),
|
||||
...(ptzInfo.features?.includes('zoom') ? [PTZMovementType.Continuous] : []),
|
||||
];
|
||||
const presets = ptzInfo.presets;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import orderBy from 'lodash-es/orderBy';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import uniqWith from 'lodash-es/uniqWith';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import {
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
runWhenIdleIfSupported,
|
||||
} from '../../utils/basic';
|
||||
import { getEntityTitle } from '../../utils/ha';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
||||
@@ -1005,44 +1004,4 @@ export class FrigateCameraManagerEngine
|
||||
...(jsmpeg && { jsmpeg: jsmpeg }),
|
||||
};
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
action: PTZAction,
|
||||
options?: {
|
||||
phase?: PTZActionPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const cameraEntity = cameraConfig.camera_entity;
|
||||
|
||||
if (action === 'preset' && !options?.preset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Awkward translation between card action and service parameters:
|
||||
// https://github.com/blakeblackshear/frigate-hass-integration/blob/dev/custom_components/frigate/services.yaml
|
||||
await hass.callService('frigate', 'ptz', {
|
||||
entity_id: cameraEntity,
|
||||
action:
|
||||
options?.phase === 'stop'
|
||||
? 'stop'
|
||||
: action === 'zoom_in' || action === 'zoom_out'
|
||||
? 'zoom'
|
||||
: action === 'preset'
|
||||
? 'preset'
|
||||
: 'move',
|
||||
...(options?.phase !== 'stop' && {
|
||||
argument:
|
||||
action === 'zoom_in'
|
||||
? 'in'
|
||||
: action === 'zoom_out'
|
||||
? 'out'
|
||||
: action === 'preset'
|
||||
? options?.preset
|
||||
: action,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,16 +235,4 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
_hass: HomeAssistant,
|
||||
_cameraConfig: CameraConfig,
|
||||
_action: PTZAction,
|
||||
_options?: {
|
||||
phase?: PTZActionPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
// Pass.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
ResultsMap,
|
||||
} from './types.js';
|
||||
import { getConfiguredPTZAction } from './utils/ptz.js';
|
||||
import { sortMedia } from './utils/sort-media.js';
|
||||
|
||||
export class QueryClassifier {
|
||||
@@ -789,23 +788,12 @@ export class CameraManager {
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||
if (!cameraConfig) {
|
||||
const camera = this._store.getCamera(cameraID);
|
||||
if (!camera) {
|
||||
return;
|
||||
}
|
||||
const configuredAction = getConfiguredPTZAction(cameraConfig, action, options);
|
||||
if (configuredAction) {
|
||||
return await this._api.getActionsManager().executeActions(configuredAction);
|
||||
}
|
||||
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
const engine = this._store.getEngineForCameraID(cameraID);
|
||||
|
||||
if (!engine || !hass) {
|
||||
return;
|
||||
}
|
||||
return await this._requestLimit.add(() =>
|
||||
engine.executePTZAction(hass, cameraConfig, action, options),
|
||||
await this._requestLimit.add(() =>
|
||||
camera.executePTZAction(this._api.getActionsManager(), action, options),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { ActionsExecutor } from '../../card-controller/actions/types';
|
||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity';
|
||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { BrowseMediaCamera } from '../browse-media/camera';
|
||||
import { Camera, CameraInitializationOptions } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CameraInitializationError } from '../error';
|
||||
import { CameraProxyConfig } from '../types';
|
||||
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
|
||||
interface ReolinkCameraInitializationOptions extends CameraInitializationOptions {
|
||||
entityRegistryManager: EntityRegistryManager;
|
||||
@@ -13,27 +18,138 @@ interface ReolinkCameraInitializationOptions extends CameraInitializationOptions
|
||||
|
||||
class ReolinkInitializationError extends CameraInitializationError {}
|
||||
|
||||
interface PTZActionToButtonEntity {
|
||||
stop?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
up?: string;
|
||||
down?: string;
|
||||
zoomIn?: string;
|
||||
zoomOut?: string;
|
||||
}
|
||||
|
||||
export class ReolinkCamera extends BrowseMediaCamera {
|
||||
protected _channel: number | null = null;
|
||||
protected _reolinkUniqueID: string | null = null;
|
||||
protected _ptzButtons: PTZActionToButtonEntity | null = null;
|
||||
|
||||
public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> {
|
||||
await super.initialize(options);
|
||||
this._initializeChannel();
|
||||
await this._initializeCapabilities(options.hass, options.entityRegistryManager);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected _initializeChannel(): void {
|
||||
const uniqueID = this._entity?.unique_id;
|
||||
const match = uniqueID ? String(uniqueID).match(/(.*)_(?<channel>\d+)/) : null;
|
||||
const channel = match && match.groups?.channel ? Number(match.groups.channel) : null;
|
||||
const match = uniqueID
|
||||
? String(uniqueID).match(/(?<uniqueid>.*)_(?<channel>\d+)/)
|
||||
: null;
|
||||
|
||||
if (channel === null) {
|
||||
const channel = match && match.groups?.channel ? Number(match.groups.channel) : null;
|
||||
const reolinkUniqueID = match?.groups?.uniqueid ?? null;
|
||||
|
||||
if (channel === null || reolinkUniqueID === null) {
|
||||
throw new ReolinkInitializationError(
|
||||
localize('error.camera_initialization_reolink'),
|
||||
this.getConfig(),
|
||||
);
|
||||
}
|
||||
this._channel = channel;
|
||||
this._reolinkUniqueID = reolinkUniqueID;
|
||||
}
|
||||
|
||||
protected async _initializeCapabilities(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryManager,
|
||||
): Promise<void> {
|
||||
const config = this.getConfig();
|
||||
|
||||
const ptzButtonMap = await this._getPTZButtonEntities(hass, entityRegistry);
|
||||
const configPTZCapabilities = getPTZCapabilitiesFromCameraConfig(this.getConfig());
|
||||
const reolinkPTZCapabilities = ptzButtonMap
|
||||
? Object.keys(ptzButtonMap).reduce(
|
||||
(acc, key) =>
|
||||
key === 'stop' ? acc : { [key]: [PTZMovementType.Continuous], ...acc },
|
||||
{},
|
||||
)
|
||||
: null;
|
||||
|
||||
const combinedPTZCapabilities: PTZCapabilities | null =
|
||||
configPTZCapabilities || reolinkPTZCapabilities
|
||||
? {
|
||||
...reolinkPTZCapabilities,
|
||||
...configPTZCapabilities,
|
||||
}
|
||||
: null;
|
||||
|
||||
this._capabilities = new Capabilities(
|
||||
{
|
||||
'favorite-events': false,
|
||||
'favorite-recordings': false,
|
||||
'remote-control-entity': true,
|
||||
clips: true,
|
||||
live: true,
|
||||
menu: true,
|
||||
recordings: false,
|
||||
seek: false,
|
||||
snapshots: false,
|
||||
substream: true,
|
||||
trigger: true,
|
||||
...(combinedPTZCapabilities && { ptz: combinedPTZCapabilities }),
|
||||
},
|
||||
{
|
||||
disable: config.capabilities?.disable,
|
||||
disableExcept: config.capabilities?.disable_except,
|
||||
},
|
||||
);
|
||||
this._ptzButtons = ptzButtonMap;
|
||||
}
|
||||
|
||||
protected async _getPTZButtonEntities(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryManager,
|
||||
): Promise<PTZActionToButtonEntity | null> {
|
||||
/* istanbul ignore next: this path cannot be reached as an exception is
|
||||
thrown in initialize() if this value is not found -- @preserve */
|
||||
if (!this._reolinkUniqueID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueIDPrefix = `${this._reolinkUniqueID}_${this._channel}_`;
|
||||
const buttonEntities = await entityRegistry.getMatchingEntities(
|
||||
hass,
|
||||
(ent: Entity) =>
|
||||
ent.config_entry_id === this._entity?.config_entry_id &&
|
||||
!!ent.unique_id &&
|
||||
String(ent.unique_id).startsWith(uniqueIDPrefix) &&
|
||||
!ent.disabled_by &&
|
||||
ent.entity_id.startsWith('button.'),
|
||||
);
|
||||
|
||||
const capabilityMap = {
|
||||
_ptz_stop: 'stop',
|
||||
_ptz_left: 'left',
|
||||
_ptz_right: 'right',
|
||||
_ptz_up: 'up',
|
||||
_ptz_down: 'down',
|
||||
_ptz_zoom_in: 'zoomIn',
|
||||
_ptz_zoom_out: 'zoomOut',
|
||||
};
|
||||
|
||||
const buttonMap: PTZActionToButtonEntity = {};
|
||||
for (const buttonEntity of buttonEntities) {
|
||||
for (const [uniqueIDSuffix, capability] of Object.entries(capabilityMap)) {
|
||||
if (
|
||||
buttonEntity.unique_id &&
|
||||
String(buttonEntity.unique_id).endsWith(uniqueIDSuffix)
|
||||
) {
|
||||
buttonMap[capability] = buttonEntity.entity_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(buttonMap).length ? buttonMap : null;
|
||||
}
|
||||
|
||||
public getChannel(): number | null {
|
||||
@@ -61,4 +177,38 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
||||
: this._config.proxy.ssl_ciphers,
|
||||
};
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
executor: ActionsExecutor,
|
||||
action: PTZAction,
|
||||
options?: {
|
||||
phase?: PTZActionPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<boolean> {
|
||||
if (await super.executePTZAction(executor, action, options)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const entityID =
|
||||
options?.phase === 'start'
|
||||
? this._ptzButtons?.[action]
|
||||
: options?.phase === 'stop'
|
||||
? this._ptzButtons?.stop
|
||||
: null;
|
||||
if (!entityID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await executor.executeActions({
|
||||
actions: [
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: { entity_id: entityID },
|
||||
},
|
||||
],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import { getViewMediaFromBrowseMediaArray } from '../browse-media/utils/browse-m
|
||||
import { isMediaWithinDates } from '../browse-media/utils/within-dates';
|
||||
import { MemoryRequestCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
QueryResultsType,
|
||||
QueryReturnType,
|
||||
} from '../types';
|
||||
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
import { ReolinkCamera } from './camera';
|
||||
import { BrowseMediaReolinkCameraMetadata, ReolinkEventQueryResults } from './types';
|
||||
|
||||
@@ -144,26 +142,6 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
const camera = new ReolinkCamera(cameraConfig, this, {
|
||||
capabilities: new Capabilities(
|
||||
{
|
||||
'favorite-events': false,
|
||||
'favorite-recordings': false,
|
||||
'remote-control-entity': true,
|
||||
clips: true,
|
||||
live: true,
|
||||
menu: true,
|
||||
recordings: false,
|
||||
seek: false,
|
||||
snapshots: false,
|
||||
substream: true,
|
||||
trigger: true,
|
||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||
},
|
||||
{
|
||||
disable: cameraConfig.capabilities?.disable,
|
||||
disableExcept: cameraConfig.capabilities?.disable_except,
|
||||
},
|
||||
),
|
||||
eventCallback: this._eventCallback,
|
||||
});
|
||||
return await camera.initialize({
|
||||
|
||||
@@ -48,8 +48,8 @@ export const getConfiguredPTZMovementType = (
|
||||
|
||||
return continuous || relative
|
||||
? [
|
||||
...(continuous ? ['continuous' as const] : []),
|
||||
...(relative ? ['relative' as const] : []),
|
||||
...(continuous ? [PTZMovementType.Continuous] : []),
|
||||
...(relative ? [PTZMovementType.Relative] : []),
|
||||
]
|
||||
: null;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ActionContext } from 'action';
|
||||
import { z } from 'zod';
|
||||
import { ConditionsTriggerData } from '../../conditions/types.js';
|
||||
import {
|
||||
ActionConfig,
|
||||
Actions,
|
||||
ActionsConfig,
|
||||
AuxillaryActionConfig,
|
||||
} from '../../config/schema/actions/types.js';
|
||||
import { forwardHaptic } from '../../ha/haptic.js';
|
||||
import {
|
||||
@@ -16,7 +14,7 @@ import { allPromises } from '../../utils/basic.js';
|
||||
import { TemplateRenderer } from '../templates/index.js';
|
||||
import { CardActionsManagerAPI } from '../types.js';
|
||||
import { ActionSet } from './actions/set.js';
|
||||
import { ActionExecutionRequest } from './types.js';
|
||||
import { ActionsExecutionRequest, ActionsExecutor } from './types.js';
|
||||
|
||||
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
|
||||
export type InteractionName = (typeof INTERACTIONS)[number];
|
||||
@@ -30,7 +28,7 @@ const interactionEventSchema = z.object({
|
||||
detail: interactionSchema,
|
||||
});
|
||||
|
||||
export class ActionsManager {
|
||||
export class ActionsManager implements ActionsExecutor {
|
||||
protected _api: CardActionsManagerAPI;
|
||||
protected _actionsInFlight: ActionSet[] = [];
|
||||
protected _actionContext: ActionContext = {};
|
||||
@@ -86,7 +84,7 @@ export class ActionsManager {
|
||||
// actions).
|
||||
actionConfig
|
||||
) {
|
||||
await this.executeActions(actionConfig, { config });
|
||||
await this.executeActions({ actions: actionConfig, config });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,7 +111,7 @@ export class ActionsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.executeActions(action);
|
||||
await this.executeActions({ actions: action });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -121,11 +119,9 @@ export class ActionsManager {
|
||||
* Card itself (e.g. menu, PTZ controller).
|
||||
*/
|
||||
public handleActionExecutionRequestEvent = async (
|
||||
ev: CustomEvent<ActionExecutionRequest>,
|
||||
ev: CustomEvent<ActionsExecutionRequest>,
|
||||
): Promise<void> => {
|
||||
await this.executeActions(ev.detail.action, {
|
||||
config: ev.detail.config,
|
||||
});
|
||||
await this.executeActions(ev.detail);
|
||||
};
|
||||
|
||||
public async uninitialize(): Promise<void> {
|
||||
@@ -133,24 +129,18 @@ export class ActionsManager {
|
||||
await allPromises(this._actionsInFlight, (actionSet) => actionSet.stop());
|
||||
}
|
||||
|
||||
public async executeActions(
|
||||
action: ActionConfig | ActionConfig[],
|
||||
options?: {
|
||||
config?: AuxillaryActionConfig;
|
||||
triggerData?: ConditionsTriggerData;
|
||||
},
|
||||
): Promise<void> {
|
||||
public async executeActions(request: ActionsExecutionRequest): Promise<void> {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
const renderedAction: ActionConfig | ActionConfig[] =
|
||||
hass && this._templateRenderer
|
||||
? (this._templateRenderer.renderRecursively(hass, action, {
|
||||
? (this._templateRenderer.renderRecursively(hass, request.actions, {
|
||||
conditionState: this._api.getConditionStateManager().getState(),
|
||||
triggerData: options?.triggerData,
|
||||
triggerData: request?.triggerData,
|
||||
}) as ActionConfig | ActionConfig[])
|
||||
: action;
|
||||
: request.actions;
|
||||
|
||||
const actionSet = new ActionSet(this._actionContext, renderedAction, {
|
||||
config: options?.config,
|
||||
config: request.config,
|
||||
cardID: this._api.getConfigManager().getConfig()?.card_id,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PTZActionConfig } from '../../../config/schema/actions/custom/ptz';
|
||||
import { PTZMovementType } from '../../../types';
|
||||
import { getPTZTarget, ptzActionToCapabilityKey } from '../../../utils/ptz';
|
||||
import { Timer } from '../../../utils/timer';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
@@ -66,7 +67,7 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||
if (
|
||||
(capabilityKey &&
|
||||
ptzCapabilities[capabilityKey]?.includes(
|
||||
this._action.ptz_phase ? 'continuous' : 'relative',
|
||||
this._action.ptz_phase ? PTZMovementType.Continuous : PTZMovementType.Relative,
|
||||
)) ||
|
||||
this._action.ptz_action === 'preset'
|
||||
) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ConditionsTriggerData } from '../../conditions/types.js';
|
||||
import {
|
||||
ActionConfig,
|
||||
AuxillaryActionConfig,
|
||||
@@ -10,9 +11,14 @@ export interface Action {
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ActionExecutionRequest {
|
||||
action: ActionConfig[] | ActionConfig;
|
||||
export interface ActionsExecutionRequest {
|
||||
actions: ActionConfig[] | ActionConfig;
|
||||
config?: AuxillaryActionConfig;
|
||||
triggerData?: ConditionsTriggerData;
|
||||
}
|
||||
|
||||
export interface ActionsExecutor {
|
||||
executeActions(request: ActionsExecutionRequest): Promise<void>;
|
||||
}
|
||||
|
||||
export interface TargetedActionContext {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { fireAdvancedCameraCardEvent } from '../../../utils/fire-advanced-camera-card-event';
|
||||
import { ActionExecutionRequest } from '../types';
|
||||
import { ActionsExecutionRequest } from '../types';
|
||||
|
||||
export const dispatchActionExecutionRequest = (
|
||||
element: HTMLElement,
|
||||
request: ActionExecutionRequest,
|
||||
request: ActionsExecutionRequest,
|
||||
) => {
|
||||
fireAdvancedCameraCardEvent(element, 'action:execution-request', request);
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export interface ActionExecutionRequestEventTarget extends EventTarget {
|
||||
event: 'advanced-camera-card:action:execution-request',
|
||||
listener: (
|
||||
this: ActionExecutionRequestEventTarget,
|
||||
ev: CustomEvent<ActionExecutionRequest>,
|
||||
ev: CustomEvent<ActionsExecutionRequest>,
|
||||
) => void,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
@@ -26,7 +26,7 @@ export interface ActionExecutionRequestEventTarget extends EventTarget {
|
||||
event: 'advanced-camera-card:action:execution-request',
|
||||
listener: (
|
||||
this: ActionExecutionRequestEventTarget,
|
||||
ev: CustomEvent<ActionExecutionRequest>,
|
||||
ev: CustomEvent<ActionsExecutionRequest>,
|
||||
) => void,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
|
||||
@@ -75,7 +75,7 @@ export class AutomationsManager {
|
||||
|
||||
await this._api
|
||||
.getActionsManager()
|
||||
.executeActions(actions, { triggerData: result.triggerData });
|
||||
.executeActions({ actions, triggerData: result.triggerData });
|
||||
|
||||
--this._nestedAutomationExecutions;
|
||||
};
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
} from '../utils/ha/registry/device';
|
||||
import {
|
||||
createEntityRegistryCache,
|
||||
EntityRegistryManager,
|
||||
EntityRegistryManagerLive,
|
||||
} from '../utils/ha/registry/entity';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import { ActionsManager } from './actions/actions-manager';
|
||||
import { AutomationsManager } from './automations-manager';
|
||||
@@ -100,7 +101,7 @@ export class CardController
|
||||
protected _deviceRegistryManager = new DeviceRegistryManager(
|
||||
createDeviceRegistryCache(),
|
||||
);
|
||||
protected _entityRegistryManager = new EntityRegistryManager(
|
||||
protected _entityRegistryManager = new EntityRegistryManagerLive(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
protected _resolvedMediaCache = new ResolvedMediaCache();
|
||||
|
||||
@@ -69,7 +69,7 @@ export class QueryStringManager {
|
||||
|
||||
protected async _executeNonViewRelated(intent: QueryStringViewIntent): Promise<void> {
|
||||
if (intent.other) {
|
||||
await this._api.getActionsManager().executeActions(intent.other);
|
||||
await this._api.getActionsManager().executeActions({ actions: intent.other });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CameraManager } from '../camera-manager/manager';
|
||||
import type { ConditionStateManager } from '../conditions/state-manager';
|
||||
import type { Automation } from '../config/schema/automations';
|
||||
import type { EntityRegistryManager } from '../utils/ha/registry/entity';
|
||||
import type { EntityRegistryManager } from '../utils/ha/registry/entity/types';
|
||||
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import type { ActionsManager } from './actions/actions-manager';
|
||||
import type { AutomationsManager } from './automations-manager';
|
||||
|
||||
@@ -116,7 +116,7 @@ export class MenuController {
|
||||
|
||||
if (toggleLessActions.length) {
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
action: actions,
|
||||
actions: actions,
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export class PTZController {
|
||||
const action = getActionConfigGivenAction(interaction, config);
|
||||
if (action) {
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
action: action,
|
||||
actions: action,
|
||||
...(config && { config: config }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export class StatusBarController {
|
||||
}
|
||||
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
action: arrayify(action),
|
||||
actions: arrayify(action),
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { HomeAssistant } from '../ha/types.js';
|
||||
import menuStyle from '../scss/menu.scss';
|
||||
import { hasAction } from '../utils/action.js';
|
||||
import { getEntityTitle } from '../utils/ha/index.js';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/index.js';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/types.js';
|
||||
import './icon.js';
|
||||
import './submenu/select-button.js';
|
||||
import './submenu/submenu-button';
|
||||
|
||||
@@ -15,7 +15,7 @@ import menuButtonStyle from '../../scss/menu-button.scss';
|
||||
import { Icon } from '../../types.js';
|
||||
import { getEntityTitle, isHassDifferent } from '../../utils/ha';
|
||||
import { getEntityStateTranslation } from '../../utils/ha/entity-state-translation.js';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/index.js';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types.js';
|
||||
import '../icon.js';
|
||||
import './index.js';
|
||||
|
||||
|
||||
+4
-1
@@ -81,7 +81,10 @@ export interface CardHelpers {
|
||||
createCardElement(config: LovelaceCardConfig): Promise<LovelaceCardWithEditor>;
|
||||
}
|
||||
|
||||
export type PTZMovementType = 'relative' | 'continuous';
|
||||
export enum PTZMovementType {
|
||||
Relative = 'relative',
|
||||
Continuous = 'continuous',
|
||||
}
|
||||
|
||||
export interface PTZCapabilities {
|
||||
left?: PTZMovementType[];
|
||||
|
||||
@@ -2,7 +2,13 @@ import { HomeAssistant } from '../../../../ha/types.js';
|
||||
import { errorToConsole } from '../../../basic.js';
|
||||
import { homeAssistantWSRequest } from '../../ws-request.js';
|
||||
import { RegistryCache } from '../cache.js';
|
||||
import { Entity, EntityList, entityListSchema, entitySchema } from './types.js';
|
||||
import {
|
||||
Entity,
|
||||
EntityList,
|
||||
entityListSchema,
|
||||
EntityRegistryManager,
|
||||
entitySchema,
|
||||
} from './types.js';
|
||||
|
||||
export const createEntityRegistryCache = (): RegistryCache<Entity> => {
|
||||
return new RegistryCache<Entity>((entity) => entity.entity_id);
|
||||
@@ -12,7 +18,7 @@ export const createEntityRegistryCache = (): RegistryCache<Entity> => {
|
||||
// as necessary. Some calls require every entity to be fetched, which may be
|
||||
// non-trivial in size (after which they are cached forever).
|
||||
|
||||
export class EntityRegistryManager {
|
||||
export class EntityRegistryManagerLive implements EntityRegistryManager {
|
||||
protected _cache: RegistryCache<Entity>;
|
||||
protected _fetchedEntityList = false;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { HomeAssistant } from '../../../../ha/types';
|
||||
|
||||
export const entitySchema = z.object({
|
||||
config_entry_id: z.string().nullable(),
|
||||
@@ -17,3 +18,13 @@ export type Entity = z.infer<typeof entitySchema>;
|
||||
|
||||
export const entityListSchema = entitySchema.array();
|
||||
export type EntityList = z.infer<typeof entityListSchema>;
|
||||
|
||||
export interface EntityRegistryManager {
|
||||
getEntity(hass: HomeAssistant, entityID: string): Promise<Entity | null>;
|
||||
getEntities(hass: HomeAssistant, entityIDs: string[]): Promise<Map<string, Entity>>;
|
||||
getMatchingEntities(
|
||||
hass: HomeAssistant,
|
||||
func: (arg: Entity) => boolean,
|
||||
): Promise<Entity[]>;
|
||||
fetchEntityList(hass: HomeAssistant): Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user