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 { HomeAssistant } from '../../ha/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity';
|
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||||
import { Entity } from '../../utils/ha/registry/entity/types';
|
|
||||||
import { Camera, CameraInitializationOptions } from '../camera';
|
import { Camera, CameraInitializationOptions } from '../camera';
|
||||||
import { CameraInitializationError } from '../error';
|
import { CameraInitializationError } from '../error';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { HomeAssistant } from '../../ha/types';
|
|||||||
import { canonicalizeHAURL } from '../../utils/ha';
|
import { canonicalizeHAURL } from '../../utils/ha';
|
||||||
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
||||||
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../utils/ha/browse-media/types';
|
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 { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
import { RequestCache } from '../cache';
|
import { RequestCache } from '../cache';
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { ActionsExecutor } from '../card-controller/actions/types';
|
||||||
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
||||||
|
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
||||||
import { CameraConfig } from '../config/schema/cameras';
|
import { CameraConfig } from '../config/schema/cameras';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import { HassStateDifference, isTriggeredState } from '../utils/ha';
|
import { HassStateDifference, isTriggeredState } from '../utils/ha';
|
||||||
@@ -6,6 +8,7 @@ import { Capabilities } from './capabilities';
|
|||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
import { CameraNoIDError } from './error';
|
import { CameraNoIDError } from './error';
|
||||||
import { CameraEventCallback, CameraProxyConfig } from './types';
|
import { CameraEventCallback, CameraProxyConfig } from './types';
|
||||||
|
import { getConfiguredPTZAction } from './utils/ptz';
|
||||||
|
|
||||||
export interface CameraInitializationOptions {
|
export interface CameraInitializationOptions {
|
||||||
stateWatcher: StateWatcherSubscriptionInterface;
|
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 => {
|
protected _stateChangeHandler = (difference: HassStateDifference): void => {
|
||||||
this._eventCallback?.({
|
this._eventCallback?.({
|
||||||
cameraID: this.getID(),
|
cameraID: this.getID(),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CameraConfig } from '../config/schema/cameras';
|
|||||||
import { HomeAssistant } from '../ha/types';
|
import { HomeAssistant } from '../ha/types';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager';
|
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 { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||||
import { RecordingSegmentsCache, RequestCache } from './cache';
|
import { RecordingSegmentsCache, RequestCache } from './cache';
|
||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
|
||||||
import { CameraConfig } from '../config/schema/cameras';
|
import { CameraConfig } from '../config/schema/cameras';
|
||||||
import { HomeAssistant } from '../ha/types';
|
import { HomeAssistant } from '../ha/types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
@@ -128,14 +127,4 @@ export interface CameraManagerEngine {
|
|||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
context?: CameraEndpointsContext,
|
context?: CameraEndpointsContext,
|
||||||
): CameraEndpoints | null;
|
): 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 uniq from 'lodash-es/uniq';
|
||||||
|
import { ActionsExecutor } from '../../card-controller/actions/types';
|
||||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||||
|
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||||
import { CameraConfig } from '../../config/schema/cameras';
|
import { CameraConfig } from '../../config/schema/cameras';
|
||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||||
import { errorToConsole } from '../../utils/basic';
|
import { errorToConsole } from '../../utils/basic';
|
||||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity';
|
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||||
import { Entity } from '../../utils/ha/registry/entity/types';
|
|
||||||
import { Camera, CameraInitializationOptions } from '../camera';
|
import { Camera, CameraInitializationOptions } from '../camera';
|
||||||
import { Capabilities } from '../capabilities';
|
import { Capabilities } from '../capabilities';
|
||||||
import { CameraManagerEngine } from '../engine';
|
import { CameraManagerEngine } from '../engine';
|
||||||
@@ -57,6 +58,55 @@ export class FrigateCamera extends Camera {
|
|||||||
return await super.initialize(options);
|
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(
|
protected async _initializeConfig(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistryManager: EntityRegistryManager,
|
entityRegistryManager: EntityRegistryManager,
|
||||||
@@ -194,10 +244,10 @@ export class FrigateCamera extends Camera {
|
|||||||
// Note: The Frigate integration only supports continuous PTZ movements
|
// Note: The Frigate integration only supports continuous PTZ movements
|
||||||
// (regardless of the actual underlying camera capability).
|
// (regardless of the actual underlying camera capability).
|
||||||
const panTilt: PTZMovementType[] = [
|
const panTilt: PTZMovementType[] = [
|
||||||
...(ptzInfo.features?.includes('pt') ? ['continuous' as const] : []),
|
...(ptzInfo.features?.includes('pt') ? [PTZMovementType.Continuous] : []),
|
||||||
];
|
];
|
||||||
const zoom: PTZMovementType[] = [
|
const zoom: PTZMovementType[] = [
|
||||||
...(ptzInfo.features?.includes('zoom') ? ['continuous' as const] : []),
|
...(ptzInfo.features?.includes('zoom') ? [PTZMovementType.Continuous] : []),
|
||||||
];
|
];
|
||||||
const presets = ptzInfo.presets;
|
const presets = ptzInfo.presets;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import orderBy from 'lodash-es/orderBy';
|
|||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import uniqWith from 'lodash-es/uniqWith';
|
import uniqWith from 'lodash-es/uniqWith';
|
||||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
|
||||||
import { CameraConfig } from '../../config/schema/cameras';
|
import { CameraConfig } from '../../config/schema/cameras';
|
||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +13,7 @@ import {
|
|||||||
runWhenIdleIfSupported,
|
runWhenIdleIfSupported,
|
||||||
} from '../../utils/basic';
|
} from '../../utils/basic';
|
||||||
import { getEntityTitle } from '../../utils/ha';
|
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 { ViewMedia } from '../../view/media';
|
||||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||||
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
||||||
@@ -1005,44 +1004,4 @@ export class FrigateCameraManagerEngine
|
|||||||
...(jsmpeg && { jsmpeg: jsmpeg }),
|
...(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;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async executePTZAction(
|
|
||||||
_hass: HomeAssistant,
|
|
||||||
_cameraConfig: CameraConfig,
|
|
||||||
_action: PTZAction,
|
|
||||||
_options?: {
|
|
||||||
phase?: PTZActionPhase;
|
|
||||||
preset?: string;
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
|
||||||
// Pass.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import {
|
|||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
ResultsMap,
|
ResultsMap,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { getConfiguredPTZAction } from './utils/ptz.js';
|
|
||||||
import { sortMedia } from './utils/sort-media.js';
|
import { sortMedia } from './utils/sort-media.js';
|
||||||
|
|
||||||
export class QueryClassifier {
|
export class QueryClassifier {
|
||||||
@@ -789,23 +788,12 @@ export class CameraManager {
|
|||||||
preset?: string;
|
preset?: string;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
const camera = this._store.getCamera(cameraID);
|
||||||
if (!cameraConfig) {
|
if (!camera) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const configuredAction = getConfiguredPTZAction(cameraConfig, action, options);
|
await this._requestLimit.add(() =>
|
||||||
if (configuredAction) {
|
camera.executePTZAction(this._api.getActionsManager(), action, options),
|
||||||
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),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { HomeAssistant } from '../../ha/types';
|
||||||
import { localize } from '../../localize/localize';
|
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 { BrowseMediaCamera } from '../browse-media/camera';
|
||||||
import { Camera, CameraInitializationOptions } from '../camera';
|
import { Camera, CameraInitializationOptions } from '../camera';
|
||||||
|
import { Capabilities } from '../capabilities';
|
||||||
import { CameraInitializationError } from '../error';
|
import { CameraInitializationError } from '../error';
|
||||||
import { CameraProxyConfig } from '../types';
|
import { CameraProxyConfig } from '../types';
|
||||||
|
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||||
|
|
||||||
interface ReolinkCameraInitializationOptions extends CameraInitializationOptions {
|
interface ReolinkCameraInitializationOptions extends CameraInitializationOptions {
|
||||||
entityRegistryManager: EntityRegistryManager;
|
entityRegistryManager: EntityRegistryManager;
|
||||||
@@ -13,27 +18,138 @@ interface ReolinkCameraInitializationOptions extends CameraInitializationOptions
|
|||||||
|
|
||||||
class ReolinkInitializationError extends CameraInitializationError {}
|
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 {
|
export class ReolinkCamera extends BrowseMediaCamera {
|
||||||
protected _channel: number | null = null;
|
protected _channel: number | null = null;
|
||||||
|
protected _reolinkUniqueID: string | null = null;
|
||||||
|
protected _ptzButtons: PTZActionToButtonEntity | null = null;
|
||||||
|
|
||||||
public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> {
|
public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> {
|
||||||
await super.initialize(options);
|
await super.initialize(options);
|
||||||
this._initializeChannel();
|
this._initializeChannel();
|
||||||
|
await this._initializeCapabilities(options.hass, options.entityRegistryManager);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _initializeChannel(): void {
|
protected _initializeChannel(): void {
|
||||||
const uniqueID = this._entity?.unique_id;
|
const uniqueID = this._entity?.unique_id;
|
||||||
const match = uniqueID ? String(uniqueID).match(/(.*)_(?<channel>\d+)/) : null;
|
const match = uniqueID
|
||||||
const channel = match && match.groups?.channel ? Number(match.groups.channel) : null;
|
? 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(
|
throw new ReolinkInitializationError(
|
||||||
localize('error.camera_initialization_reolink'),
|
localize('error.camera_initialization_reolink'),
|
||||||
this.getConfig(),
|
this.getConfig(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._channel = channel;
|
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 {
|
public getChannel(): number | null {
|
||||||
@@ -61,4 +177,38 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
|||||||
: this._config.proxy.ssl_ciphers,
|
: 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 { isMediaWithinDates } from '../browse-media/utils/within-dates';
|
||||||
import { MemoryRequestCache } from '../cache';
|
import { MemoryRequestCache } from '../cache';
|
||||||
import { Camera } from '../camera';
|
import { Camera } from '../camera';
|
||||||
import { Capabilities } from '../capabilities';
|
|
||||||
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
||||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||||
import {
|
import {
|
||||||
@@ -37,7 +36,6 @@ import {
|
|||||||
QueryResultsType,
|
QueryResultsType,
|
||||||
QueryReturnType,
|
QueryReturnType,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
|
||||||
import { ReolinkCamera } from './camera';
|
import { ReolinkCamera } from './camera';
|
||||||
import { BrowseMediaReolinkCameraMetadata, ReolinkEventQueryResults } from './types';
|
import { BrowseMediaReolinkCameraMetadata, ReolinkEventQueryResults } from './types';
|
||||||
|
|
||||||
@@ -144,26 +142,6 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
|||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): Promise<Camera> {
|
): Promise<Camera> {
|
||||||
const camera = new ReolinkCamera(cameraConfig, this, {
|
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,
|
eventCallback: this._eventCallback,
|
||||||
});
|
});
|
||||||
return await camera.initialize({
|
return await camera.initialize({
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export const getConfiguredPTZMovementType = (
|
|||||||
|
|
||||||
return continuous || relative
|
return continuous || relative
|
||||||
? [
|
? [
|
||||||
...(continuous ? ['continuous' as const] : []),
|
...(continuous ? [PTZMovementType.Continuous] : []),
|
||||||
...(relative ? ['relative' as const] : []),
|
...(relative ? [PTZMovementType.Relative] : []),
|
||||||
]
|
]
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { ActionContext } from 'action';
|
import { ActionContext } from 'action';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ConditionsTriggerData } from '../../conditions/types.js';
|
|
||||||
import {
|
import {
|
||||||
ActionConfig,
|
ActionConfig,
|
||||||
Actions,
|
Actions,
|
||||||
ActionsConfig,
|
ActionsConfig,
|
||||||
AuxillaryActionConfig,
|
|
||||||
} from '../../config/schema/actions/types.js';
|
} from '../../config/schema/actions/types.js';
|
||||||
import { forwardHaptic } from '../../ha/haptic.js';
|
import { forwardHaptic } from '../../ha/haptic.js';
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +14,7 @@ import { allPromises } from '../../utils/basic.js';
|
|||||||
import { TemplateRenderer } from '../templates/index.js';
|
import { TemplateRenderer } from '../templates/index.js';
|
||||||
import { CardActionsManagerAPI } from '../types.js';
|
import { CardActionsManagerAPI } from '../types.js';
|
||||||
import { ActionSet } from './actions/set.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;
|
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
|
||||||
export type InteractionName = (typeof INTERACTIONS)[number];
|
export type InteractionName = (typeof INTERACTIONS)[number];
|
||||||
@@ -30,7 +28,7 @@ const interactionEventSchema = z.object({
|
|||||||
detail: interactionSchema,
|
detail: interactionSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class ActionsManager {
|
export class ActionsManager implements ActionsExecutor {
|
||||||
protected _api: CardActionsManagerAPI;
|
protected _api: CardActionsManagerAPI;
|
||||||
protected _actionsInFlight: ActionSet[] = [];
|
protected _actionsInFlight: ActionSet[] = [];
|
||||||
protected _actionContext: ActionContext = {};
|
protected _actionContext: ActionContext = {};
|
||||||
@@ -86,7 +84,7 @@ export class ActionsManager {
|
|||||||
// actions).
|
// actions).
|
||||||
actionConfig
|
actionConfig
|
||||||
) {
|
) {
|
||||||
await this.executeActions(actionConfig, { config });
|
await this.executeActions({ actions: actionConfig, config });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,7 +111,7 @@ export class ActionsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.executeActions(action);
|
await this.executeActions({ actions: action });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,11 +119,9 @@ export class ActionsManager {
|
|||||||
* Card itself (e.g. menu, PTZ controller).
|
* Card itself (e.g. menu, PTZ controller).
|
||||||
*/
|
*/
|
||||||
public handleActionExecutionRequestEvent = async (
|
public handleActionExecutionRequestEvent = async (
|
||||||
ev: CustomEvent<ActionExecutionRequest>,
|
ev: CustomEvent<ActionsExecutionRequest>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await this.executeActions(ev.detail.action, {
|
await this.executeActions(ev.detail);
|
||||||
config: ev.detail.config,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public async uninitialize(): Promise<void> {
|
public async uninitialize(): Promise<void> {
|
||||||
@@ -133,24 +129,18 @@ export class ActionsManager {
|
|||||||
await allPromises(this._actionsInFlight, (actionSet) => actionSet.stop());
|
await allPromises(this._actionsInFlight, (actionSet) => actionSet.stop());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async executeActions(
|
public async executeActions(request: ActionsExecutionRequest): Promise<void> {
|
||||||
action: ActionConfig | ActionConfig[],
|
|
||||||
options?: {
|
|
||||||
config?: AuxillaryActionConfig;
|
|
||||||
triggerData?: ConditionsTriggerData;
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
const renderedAction: ActionConfig | ActionConfig[] =
|
const renderedAction: ActionConfig | ActionConfig[] =
|
||||||
hass && this._templateRenderer
|
hass && this._templateRenderer
|
||||||
? (this._templateRenderer.renderRecursively(hass, action, {
|
? (this._templateRenderer.renderRecursively(hass, request.actions, {
|
||||||
conditionState: this._api.getConditionStateManager().getState(),
|
conditionState: this._api.getConditionStateManager().getState(),
|
||||||
triggerData: options?.triggerData,
|
triggerData: request?.triggerData,
|
||||||
}) as ActionConfig | ActionConfig[])
|
}) as ActionConfig | ActionConfig[])
|
||||||
: action;
|
: request.actions;
|
||||||
|
|
||||||
const actionSet = new ActionSet(this._actionContext, renderedAction, {
|
const actionSet = new ActionSet(this._actionContext, renderedAction, {
|
||||||
config: options?.config,
|
config: request.config,
|
||||||
cardID: this._api.getConfigManager().getConfig()?.card_id,
|
cardID: this._api.getConfigManager().getConfig()?.card_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PTZActionConfig } from '../../../config/schema/actions/custom/ptz';
|
import { PTZActionConfig } from '../../../config/schema/actions/custom/ptz';
|
||||||
|
import { PTZMovementType } from '../../../types';
|
||||||
import { getPTZTarget, ptzActionToCapabilityKey } from '../../../utils/ptz';
|
import { getPTZTarget, ptzActionToCapabilityKey } from '../../../utils/ptz';
|
||||||
import { Timer } from '../../../utils/timer';
|
import { Timer } from '../../../utils/timer';
|
||||||
import { CardActionsAPI } from '../../types';
|
import { CardActionsAPI } from '../../types';
|
||||||
@@ -66,7 +67,7 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
|||||||
if (
|
if (
|
||||||
(capabilityKey &&
|
(capabilityKey &&
|
||||||
ptzCapabilities[capabilityKey]?.includes(
|
ptzCapabilities[capabilityKey]?.includes(
|
||||||
this._action.ptz_phase ? 'continuous' : 'relative',
|
this._action.ptz_phase ? PTZMovementType.Continuous : PTZMovementType.Relative,
|
||||||
)) ||
|
)) ||
|
||||||
this._action.ptz_action === 'preset'
|
this._action.ptz_action === 'preset'
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ConditionsTriggerData } from '../../conditions/types.js';
|
||||||
import {
|
import {
|
||||||
ActionConfig,
|
ActionConfig,
|
||||||
AuxillaryActionConfig,
|
AuxillaryActionConfig,
|
||||||
@@ -10,9 +11,14 @@ export interface Action {
|
|||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionExecutionRequest {
|
export interface ActionsExecutionRequest {
|
||||||
action: ActionConfig[] | ActionConfig;
|
actions: ActionConfig[] | ActionConfig;
|
||||||
config?: AuxillaryActionConfig;
|
config?: AuxillaryActionConfig;
|
||||||
|
triggerData?: ConditionsTriggerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionsExecutor {
|
||||||
|
executeActions(request: ActionsExecutionRequest): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TargetedActionContext {
|
export interface TargetedActionContext {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { fireAdvancedCameraCardEvent } from '../../../utils/fire-advanced-camera-card-event';
|
import { fireAdvancedCameraCardEvent } from '../../../utils/fire-advanced-camera-card-event';
|
||||||
import { ActionExecutionRequest } from '../types';
|
import { ActionsExecutionRequest } from '../types';
|
||||||
|
|
||||||
export const dispatchActionExecutionRequest = (
|
export const dispatchActionExecutionRequest = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
request: ActionExecutionRequest,
|
request: ActionsExecutionRequest,
|
||||||
) => {
|
) => {
|
||||||
fireAdvancedCameraCardEvent(element, 'action:execution-request', request);
|
fireAdvancedCameraCardEvent(element, 'action:execution-request', request);
|
||||||
};
|
};
|
||||||
@@ -13,7 +13,7 @@ export interface ActionExecutionRequestEventTarget extends EventTarget {
|
|||||||
event: 'advanced-camera-card:action:execution-request',
|
event: 'advanced-camera-card:action:execution-request',
|
||||||
listener: (
|
listener: (
|
||||||
this: ActionExecutionRequestEventTarget,
|
this: ActionExecutionRequestEventTarget,
|
||||||
ev: CustomEvent<ActionExecutionRequest>,
|
ev: CustomEvent<ActionsExecutionRequest>,
|
||||||
) => void,
|
) => void,
|
||||||
options?: AddEventListenerOptions | boolean,
|
options?: AddEventListenerOptions | boolean,
|
||||||
): void;
|
): void;
|
||||||
@@ -26,7 +26,7 @@ export interface ActionExecutionRequestEventTarget extends EventTarget {
|
|||||||
event: 'advanced-camera-card:action:execution-request',
|
event: 'advanced-camera-card:action:execution-request',
|
||||||
listener: (
|
listener: (
|
||||||
this: ActionExecutionRequestEventTarget,
|
this: ActionExecutionRequestEventTarget,
|
||||||
ev: CustomEvent<ActionExecutionRequest>,
|
ev: CustomEvent<ActionsExecutionRequest>,
|
||||||
) => void,
|
) => void,
|
||||||
options?: boolean | EventListenerOptions,
|
options?: boolean | EventListenerOptions,
|
||||||
): void;
|
): void;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class AutomationsManager {
|
|||||||
|
|
||||||
await this._api
|
await this._api
|
||||||
.getActionsManager()
|
.getActionsManager()
|
||||||
.executeActions(actions, { triggerData: result.triggerData });
|
.executeActions({ actions, triggerData: result.triggerData });
|
||||||
|
|
||||||
--this._nestedAutomationExecutions;
|
--this._nestedAutomationExecutions;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
} from '../utils/ha/registry/device';
|
} from '../utils/ha/registry/device';
|
||||||
import {
|
import {
|
||||||
createEntityRegistryCache,
|
createEntityRegistryCache,
|
||||||
EntityRegistryManager,
|
EntityRegistryManagerLive,
|
||||||
} from '../utils/ha/registry/entity';
|
} from '../utils/ha/registry/entity';
|
||||||
|
import { EntityRegistryManager } from '../utils/ha/registry/entity/types';
|
||||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||||
import { ActionsManager } from './actions/actions-manager';
|
import { ActionsManager } from './actions/actions-manager';
|
||||||
import { AutomationsManager } from './automations-manager';
|
import { AutomationsManager } from './automations-manager';
|
||||||
@@ -100,7 +101,7 @@ export class CardController
|
|||||||
protected _deviceRegistryManager = new DeviceRegistryManager(
|
protected _deviceRegistryManager = new DeviceRegistryManager(
|
||||||
createDeviceRegistryCache(),
|
createDeviceRegistryCache(),
|
||||||
);
|
);
|
||||||
protected _entityRegistryManager = new EntityRegistryManager(
|
protected _entityRegistryManager = new EntityRegistryManagerLive(
|
||||||
createEntityRegistryCache(),
|
createEntityRegistryCache(),
|
||||||
);
|
);
|
||||||
protected _resolvedMediaCache = new ResolvedMediaCache();
|
protected _resolvedMediaCache = new ResolvedMediaCache();
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class QueryStringManager {
|
|||||||
|
|
||||||
protected async _executeNonViewRelated(intent: QueryStringViewIntent): Promise<void> {
|
protected async _executeNonViewRelated(intent: QueryStringViewIntent): Promise<void> {
|
||||||
if (intent.other) {
|
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 { CameraManager } from '../camera-manager/manager';
|
||||||
import type { ConditionStateManager } from '../conditions/state-manager';
|
import type { ConditionStateManager } from '../conditions/state-manager';
|
||||||
import type { Automation } from '../config/schema/automations';
|
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 { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||||
import type { ActionsManager } from './actions/actions-manager';
|
import type { ActionsManager } from './actions/actions-manager';
|
||||||
import type { AutomationsManager } from './automations-manager';
|
import type { AutomationsManager } from './automations-manager';
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export class MenuController {
|
|||||||
|
|
||||||
if (toggleLessActions.length) {
|
if (toggleLessActions.length) {
|
||||||
dispatchActionExecutionRequest(this._host, {
|
dispatchActionExecutionRequest(this._host, {
|
||||||
action: actions,
|
actions: actions,
|
||||||
config: config,
|
config: config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class PTZController {
|
|||||||
const action = getActionConfigGivenAction(interaction, config);
|
const action = getActionConfigGivenAction(interaction, config);
|
||||||
if (action) {
|
if (action) {
|
||||||
dispatchActionExecutionRequest(this._host, {
|
dispatchActionExecutionRequest(this._host, {
|
||||||
action: action,
|
actions: action,
|
||||||
...(config && { config: config }),
|
...(config && { config: config }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export class StatusBarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatchActionExecutionRequest(this._host, {
|
dispatchActionExecutionRequest(this._host, {
|
||||||
action: arrayify(action),
|
actions: arrayify(action),
|
||||||
config: config,
|
config: config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { HomeAssistant } from '../ha/types.js';
|
|||||||
import menuStyle from '../scss/menu.scss';
|
import menuStyle from '../scss/menu.scss';
|
||||||
import { hasAction } from '../utils/action.js';
|
import { hasAction } from '../utils/action.js';
|
||||||
import { getEntityTitle } from '../utils/ha/index.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 './icon.js';
|
||||||
import './submenu/select-button.js';
|
import './submenu/select-button.js';
|
||||||
import './submenu/submenu-button';
|
import './submenu/submenu-button';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import menuButtonStyle from '../../scss/menu-button.scss';
|
|||||||
import { Icon } from '../../types.js';
|
import { Icon } from '../../types.js';
|
||||||
import { getEntityTitle, isHassDifferent } from '../../utils/ha';
|
import { getEntityTitle, isHassDifferent } from '../../utils/ha';
|
||||||
import { getEntityStateTranslation } from '../../utils/ha/entity-state-translation.js';
|
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 '../icon.js';
|
||||||
import './index.js';
|
import './index.js';
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -81,7 +81,10 @@ export interface CardHelpers {
|
|||||||
createCardElement(config: LovelaceCardConfig): Promise<LovelaceCardWithEditor>;
|
createCardElement(config: LovelaceCardConfig): Promise<LovelaceCardWithEditor>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PTZMovementType = 'relative' | 'continuous';
|
export enum PTZMovementType {
|
||||||
|
Relative = 'relative',
|
||||||
|
Continuous = 'continuous',
|
||||||
|
}
|
||||||
|
|
||||||
export interface PTZCapabilities {
|
export interface PTZCapabilities {
|
||||||
left?: PTZMovementType[];
|
left?: PTZMovementType[];
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { HomeAssistant } from '../../../../ha/types.js';
|
|||||||
import { errorToConsole } from '../../../basic.js';
|
import { errorToConsole } from '../../../basic.js';
|
||||||
import { homeAssistantWSRequest } from '../../ws-request.js';
|
import { homeAssistantWSRequest } from '../../ws-request.js';
|
||||||
import { RegistryCache } from '../cache.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> => {
|
export const createEntityRegistryCache = (): RegistryCache<Entity> => {
|
||||||
return new RegistryCache<Entity>((entity) => entity.entity_id);
|
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
|
// as necessary. Some calls require every entity to be fetched, which may be
|
||||||
// non-trivial in size (after which they are cached forever).
|
// non-trivial in size (after which they are cached forever).
|
||||||
|
|
||||||
export class EntityRegistryManager {
|
export class EntityRegistryManagerLive implements EntityRegistryManager {
|
||||||
protected _cache: RegistryCache<Entity>;
|
protected _cache: RegistryCache<Entity>;
|
||||||
protected _fetchedEntityList = false;
|
protected _fetchedEntityList = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { HomeAssistant } from '../../../../ha/types';
|
||||||
|
|
||||||
export const entitySchema = z.object({
|
export const entitySchema = z.object({
|
||||||
config_entry_id: z.string().nullable(),
|
config_entry_id: z.string().nullable(),
|
||||||
@@ -17,3 +18,13 @@ export type Entity = z.infer<typeof entitySchema>;
|
|||||||
|
|
||||||
export const entityListSchema = entitySchema.array();
|
export const entityListSchema = entitySchema.array();
|
||||||
export type EntityList = z.infer<typeof entityListSchema>;
|
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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { mock } from 'vitest-mock-extended';
|
|||||||
import { BrowseMediaCamera } from '../../../src/camera-manager/browse-media/camera';
|
import { BrowseMediaCamera } from '../../../src/camera-manager/browse-media/camera';
|
||||||
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
import {
|
||||||
import { Entity } from '../../../src/utils/ha/registry/entity/types';
|
Entity,
|
||||||
|
EntityRegistryManager,
|
||||||
|
} from '../../../src/utils/ha/registry/entity/types';
|
||||||
import { createCameraConfig, createHASS } from '../../test-utils';
|
import { createCameraConfig, createHASS } from '../../test-utils';
|
||||||
|
|
||||||
describe('BrowseMediaCamera', () => {
|
describe('BrowseMediaCamera', () => {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||||
import { CapabilityKey, PTZCapabilities, capabilityKeys } from '../../src/types';
|
import {
|
||||||
|
CapabilityKey,
|
||||||
|
PTZCapabilities,
|
||||||
|
PTZMovementType,
|
||||||
|
capabilityKeys,
|
||||||
|
} from '../../src/types';
|
||||||
|
|
||||||
describe('Capabilities', () => {
|
describe('Capabilities', () => {
|
||||||
it('default capabilities', () => {
|
it('default capabilities', () => {
|
||||||
@@ -73,7 +78,7 @@ describe('Capabilities', () => {
|
|||||||
|
|
||||||
it('when set', () => {
|
it('when set', () => {
|
||||||
const ptz: PTZCapabilities = {
|
const ptz: PTZCapabilities = {
|
||||||
left: ['continuous' as const],
|
left: [PTZMovementType.Continuous],
|
||||||
presets: ['1', '2'],
|
presets: ['1', '2'],
|
||||||
};
|
};
|
||||||
const capabilities = new Capabilities({
|
const capabilities = new Capabilities({
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import { ReolinkCameraManagerEngine } from '../../src/camera-manager/reolink/eng
|
|||||||
import { Engine } from '../../src/camera-manager/types.js';
|
import { Engine } from '../../src/camera-manager/types.js';
|
||||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||||
import { CardWideConfig } from '../../src/config/schema/types.js';
|
import { CardWideConfig } from '../../src/config/schema/types.js';
|
||||||
import {
|
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/types.js';
|
||||||
createEntityRegistryCache,
|
|
||||||
EntityRegistryManager,
|
|
||||||
} from '../../src/utils/ha/registry/entity/index.js';
|
|
||||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
@@ -19,6 +16,7 @@ import {
|
|||||||
createRegistryEntity,
|
createRegistryEntity,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
import { EntityRegistryManagerMock } from '../utils/ha/registry/entity/mock.js';
|
||||||
|
|
||||||
vi.mock('../../src/utils/ha/entity-registry');
|
vi.mock('../../src/utils/ha/entity-registry');
|
||||||
vi.mock('../../src/utils/ha/entity-registry/cache');
|
vi.mock('../../src/utils/ha/entity-registry/cache');
|
||||||
@@ -28,8 +26,7 @@ const createFactory = (options?: {
|
|||||||
cardWideConfig?: CardWideConfig;
|
cardWideConfig?: CardWideConfig;
|
||||||
}): CameraManagerEngineFactory => {
|
}): CameraManagerEngineFactory => {
|
||||||
return new CameraManagerEngineFactory(
|
return new CameraManagerEngineFactory(
|
||||||
options?.entityRegistryManager ??
|
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
|
||||||
new EntityRegistryManager(createEntityRegistryCache()),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,15 +41,9 @@ describe('getEngineForCamera()', () => {
|
|||||||
|
|
||||||
it('from auto detection', async () => {
|
it('from auto detection', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
createEntityRegistryCache(),
|
createRegistryEntity({ entity_id: 'camera.foo', platform: 'frigate' }),
|
||||||
);
|
]);
|
||||||
|
|
||||||
entityRegistryManager.getEntity = vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(
|
|
||||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'frigate' }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await createFactory({
|
await createFactory({
|
||||||
@@ -81,15 +72,9 @@ describe('getEngineForCamera()', () => {
|
|||||||
|
|
||||||
it('from auto detection', async () => {
|
it('from auto detection', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
createEntityRegistryCache(),
|
createRegistryEntity({ entity_id: 'camera.foo', platform: 'motioneye' }),
|
||||||
);
|
]);
|
||||||
|
|
||||||
entityRegistryManager.getEntity = vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(
|
|
||||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'motioneye' }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await createFactory({
|
await createFactory({
|
||||||
@@ -109,15 +94,9 @@ describe('getEngineForCamera()', () => {
|
|||||||
|
|
||||||
it('from auto detection', async () => {
|
it('from auto detection', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
createEntityRegistryCache(),
|
createRegistryEntity({ entity_id: 'camera.foo', platform: 'reolink' }),
|
||||||
);
|
]);
|
||||||
|
|
||||||
entityRegistryManager.getEntity = vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(
|
|
||||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'reolink' }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await createFactory({
|
await createFactory({
|
||||||
@@ -137,15 +116,9 @@ describe('getEngineForCamera()', () => {
|
|||||||
|
|
||||||
it('from auto detection', async () => {
|
it('from auto detection', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
createEntityRegistryCache(),
|
createRegistryEntity({ entity_id: 'camera.foo', platform: 'generic' }),
|
||||||
);
|
]);
|
||||||
|
|
||||||
entityRegistryManager.getEntity = vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(
|
|
||||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'generic' }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await createFactory({
|
await createFactory({
|
||||||
@@ -159,11 +132,7 @@ describe('getEngineForCamera()', () => {
|
|||||||
engine: 'auto',
|
engine: 'auto',
|
||||||
webrtc_card: { entity: 'camera.foo' },
|
webrtc_card: { entity: 'camera.foo' },
|
||||||
});
|
});
|
||||||
const entityRegistryManager = new EntityRegistryManager(
|
const entityRegistryManager = new EntityRegistryManagerMock();
|
||||||
createEntityRegistryCache(),
|
|
||||||
);
|
|
||||||
|
|
||||||
entityRegistryManager.getEntity = vi.fn().mockResolvedValue(null);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await createFactory({
|
await createFactory({
|
||||||
@@ -182,10 +151,7 @@ describe('getEngineForCamera()', () => {
|
|||||||
engine: 'auto',
|
engine: 'auto',
|
||||||
webrtc_card: { entity: 'camera.foo' },
|
webrtc_card: { entity: 'camera.foo' },
|
||||||
});
|
});
|
||||||
const entityRegistryManager = new EntityRegistryManager(
|
const entityRegistryManager = new EntityRegistryManagerMock();
|
||||||
createEntityRegistryCache(),
|
|
||||||
);
|
|
||||||
entityRegistryManager.getEntity = vi.fn().mockResolvedValue(null);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
async () =>
|
async () =>
|
||||||
@@ -225,9 +191,8 @@ describe('getEngineForCamera()', () => {
|
|||||||
|
|
||||||
it('should throw error on invalid entity', async () => {
|
it('should throw error on invalid entity', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(createEntityRegistryCache());
|
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||||
|
entityRegistryManager.getEntity.mockRejectedValue(new Error());
|
||||||
entityRegistryManager.getEntity = vi.fn().mockRejectedValue(new Error());
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
createFactory({
|
createFactory({
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ import { FrigateCamera } from '../../../src/camera-manager/frigate/camera';
|
|||||||
import { FrigateEventWatcher } from '../../../src/camera-manager/frigate/event-watcher';
|
import { FrigateEventWatcher } from '../../../src/camera-manager/frigate/event-watcher';
|
||||||
import { getPTZInfo } from '../../../src/camera-manager/frigate/requests';
|
import { getPTZInfo } from '../../../src/camera-manager/frigate/requests';
|
||||||
import { FrigateEventChange } from '../../../src/camera-manager/frigate/types';
|
import { FrigateEventChange } from '../../../src/camera-manager/frigate/types';
|
||||||
|
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||||
|
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
|
||||||
import { CameraTriggerEventType } from '../../../src/config/schema/cameras';
|
import { CameraTriggerEventType } from '../../../src/config/schema/cameras';
|
||||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
import {
|
||||||
import { Entity } from '../../../src/utils/ha/registry/entity/types';
|
Entity,
|
||||||
|
EntityRegistryManager,
|
||||||
|
} from '../../../src/utils/ha/registry/entity/types';
|
||||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||||
|
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||||
|
|
||||||
vi.mock('../../../src/camera-manager/frigate/requests');
|
vi.mock('../../../src/camera-manager/frigate/requests');
|
||||||
|
|
||||||
@@ -53,8 +58,7 @@ describe('FrigateCamera', () => {
|
|||||||
}),
|
}),
|
||||||
mock<CameraManagerEngine>(),
|
mock<CameraManagerEngine>(),
|
||||||
);
|
);
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock();
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(null);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
async () =>
|
async () =>
|
||||||
@@ -74,12 +78,13 @@ describe('FrigateCamera', () => {
|
|||||||
}),
|
}),
|
||||||
mock<CameraManagerEngine>(),
|
mock<CameraManagerEngine>(),
|
||||||
);
|
);
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
const entity = createRegistryEntity({
|
createRegistryEntity({
|
||||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
entity_id: 'camera.front_door',
|
||||||
platform: 'frigate',
|
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
||||||
});
|
platform: 'frigate',
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: createHASS(),
|
hass: createHASS(),
|
||||||
@@ -97,12 +102,13 @@ describe('FrigateCamera', () => {
|
|||||||
}),
|
}),
|
||||||
mock<CameraManagerEngine>(),
|
mock<CameraManagerEngine>(),
|
||||||
);
|
);
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
const entity = createRegistryEntity({
|
createRegistryEntity({
|
||||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:WRONG:fnt_dr',
|
entity_id: 'camera.front_door',
|
||||||
platform: 'frigate',
|
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:WRONG:fnt_dr',
|
||||||
});
|
platform: 'frigate',
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: createHASS(),
|
hass: createHASS(),
|
||||||
@@ -120,12 +126,13 @@ describe('FrigateCamera', () => {
|
|||||||
}),
|
}),
|
||||||
mock<CameraManagerEngine>(),
|
mock<CameraManagerEngine>(),
|
||||||
);
|
);
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
const entity = createRegistryEntity({
|
createRegistryEntity({
|
||||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
entity_id: 'camera.front_door',
|
||||||
platform: 'something_else',
|
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
||||||
});
|
platform: 'something_else',
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: createHASS(),
|
hass: createHASS(),
|
||||||
@@ -656,6 +663,7 @@ describe('FrigateCamera', () => {
|
|||||||
describe('should handle triggers', () => {
|
describe('should handle triggers', () => {
|
||||||
const cameraEntity: Partial<Entity> = {
|
const cameraEntity: Partial<Entity> = {
|
||||||
config_entry_id: 'config_entry_id',
|
config_entry_id: 'config_entry_id',
|
||||||
|
entity_id: 'camera.front_door',
|
||||||
};
|
};
|
||||||
|
|
||||||
const occupancySensorEntityAll: Partial<Entity> = {
|
const occupancySensorEntityAll: Partial<Entity> = {
|
||||||
@@ -674,11 +682,8 @@ describe('FrigateCamera', () => {
|
|||||||
|
|
||||||
describe('should detect motion sensor', () => {
|
describe('should detect motion sensor', () => {
|
||||||
it('without a camera name', async () => {
|
it('without a camera name', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
createRegistryEntity(cameraEntity),
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity(motionSensorEntity),
|
createRegistryEntity(motionSensorEntity),
|
||||||
]);
|
]);
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
@@ -702,11 +707,8 @@ describe('FrigateCamera', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with camera entity and name', async () => {
|
it('with camera entity and name', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
createRegistryEntity(cameraEntity),
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity(motionSensorEntity),
|
createRegistryEntity(motionSensorEntity),
|
||||||
]);
|
]);
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
@@ -733,43 +735,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(camera.getConfig().triggers.entities).toEqual(['binary_sensor.foo']);
|
expect(camera.getConfig().triggers.entities).toEqual(['binary_sensor.foo']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with matching entity', async () => {
|
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity(motionSensorEntity),
|
|
||||||
]);
|
|
||||||
const camera = new FrigateCamera(
|
|
||||||
createCameraConfig({
|
|
||||||
frigate: {
|
|
||||||
camera_name: 'front_door',
|
|
||||||
},
|
|
||||||
triggers: {
|
|
||||||
motion: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
mock<CameraManagerEngine>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const hass = createHASS();
|
|
||||||
await camera.initialize({
|
|
||||||
hass: hass,
|
|
||||||
entityRegistryManager: entityRegistryManager,
|
|
||||||
stateWatcher: mock<StateWatcher>(),
|
|
||||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(camera.getConfig().triggers.entities).toEqual(['binary_sensor.foo']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without matching entity', async () => {
|
it('without matching entity', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([]);
|
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
createCameraConfig({
|
createCameraConfig({
|
||||||
frigate: {
|
frigate: {
|
||||||
@@ -784,7 +750,7 @@ describe('FrigateCamera', () => {
|
|||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: hass,
|
hass: hass,
|
||||||
entityRegistryManager: entityRegistryManager,
|
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||||
stateWatcher: mock<StateWatcher>(),
|
stateWatcher: mock<StateWatcher>(),
|
||||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||||
});
|
});
|
||||||
@@ -795,11 +761,8 @@ describe('FrigateCamera', () => {
|
|||||||
|
|
||||||
describe('should detect occupancy sensor', () => {
|
describe('should detect occupancy sensor', () => {
|
||||||
it('without a camera name', async () => {
|
it('without a camera name', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
createRegistryEntity(cameraEntity),
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity(occupancySensorEntityAll),
|
createRegistryEntity(occupancySensorEntityAll),
|
||||||
]);
|
]);
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
@@ -822,11 +785,8 @@ describe('FrigateCamera', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('without a camera name but with occupancy trigger', async () => {
|
it('without a camera name but with occupancy trigger', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
createRegistryEntity(cameraEntity),
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity(occupancySensorEntityAll),
|
createRegistryEntity(occupancySensorEntityAll),
|
||||||
]);
|
]);
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
@@ -848,42 +808,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(camera.getConfig().triggers.entities).toEqual([]);
|
expect(camera.getConfig().triggers.entities).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with matching entity', async () => {
|
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity(occupancySensorEntityAll),
|
|
||||||
]);
|
|
||||||
const camera = new FrigateCamera(
|
|
||||||
createCameraConfig({
|
|
||||||
frigate: {
|
|
||||||
camera_name: 'front_door',
|
|
||||||
},
|
|
||||||
triggers: {
|
|
||||||
occupancy: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
mock<CameraManagerEngine>(),
|
|
||||||
);
|
|
||||||
const hass = createHASS();
|
|
||||||
await camera.initialize({
|
|
||||||
hass: hass,
|
|
||||||
entityRegistryManager: entityRegistryManager,
|
|
||||||
stateWatcher: mock<StateWatcher>(),
|
|
||||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(camera.getConfig().triggers.entities).toEqual(['binary_sensor.foo']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without matching entity', async () => {
|
it('without matching entity', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([]);
|
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
createCameraConfig({
|
createCameraConfig({
|
||||||
frigate: {
|
frigate: {
|
||||||
@@ -898,7 +823,7 @@ describe('FrigateCamera', () => {
|
|||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: hass,
|
hass: hass,
|
||||||
entityRegistryManager: entityRegistryManager,
|
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||||
stateWatcher: mock<StateWatcher>(),
|
stateWatcher: mock<StateWatcher>(),
|
||||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||||
});
|
});
|
||||||
@@ -907,11 +832,8 @@ describe('FrigateCamera', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with zones', async () => {
|
it('with zones', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
createRegistryEntity(cameraEntity),
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity({
|
createRegistryEntity({
|
||||||
...occupancySensorEntityAll,
|
...occupancySensorEntityAll,
|
||||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:occupancy_sensor:zone_all',
|
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:occupancy_sensor:zone_all',
|
||||||
@@ -919,6 +841,7 @@ describe('FrigateCamera', () => {
|
|||||||
]);
|
]);
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
createCameraConfig({
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.front_door',
|
||||||
frigate: {
|
frigate: {
|
||||||
camera_name: 'front_door',
|
camera_name: 'front_door',
|
||||||
zones: ['zone'],
|
zones: ['zone'],
|
||||||
@@ -941,19 +864,18 @@ describe('FrigateCamera', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with labels', async () => {
|
it('with labels', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
|
||||||
createRegistryEntity(cameraEntity),
|
createRegistryEntity(cameraEntity),
|
||||||
);
|
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
|
||||||
createRegistryEntity({
|
createRegistryEntity({
|
||||||
...occupancySensorEntityAll,
|
...occupancySensorEntityAll,
|
||||||
unique_id:
|
unique_id:
|
||||||
'8c4e19d258359e82bc0cf9d47b021c46:occupancy_sensor:front_door_car',
|
'8c4e19d258359e82bc0cf9d47b021c46:occupancy_sensor:front_door_car',
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
createCameraConfig({
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.front_door',
|
||||||
frigate: {
|
frigate: {
|
||||||
camera_name: 'front_door',
|
camera_name: 'front_door',
|
||||||
labels: ['car'],
|
labels: ['car'],
|
||||||
@@ -976,74 +898,194 @@ describe('FrigateCamera', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter entities with correct function', async () => {
|
describe('should execute PTZ action', () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
it('should ignore preset action without a preset', async () => {
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(
|
const camera = new FrigateCamera(
|
||||||
createRegistryEntity(cameraEntity),
|
createCameraConfig(),
|
||||||
);
|
mock<CameraManagerEngine>(),
|
||||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
);
|
||||||
createRegistryEntity({
|
|
||||||
...occupancySensorEntityAll,
|
const hass = createHASS();
|
||||||
}),
|
await camera.initialize({
|
||||||
]);
|
hass: hass,
|
||||||
const camera = new FrigateCamera(
|
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||||
createCameraConfig({
|
stateWatcher: mock<StateWatcher>(),
|
||||||
camera_entity: 'camera.foo',
|
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||||
triggers: {
|
});
|
||||||
occupancy: true,
|
|
||||||
},
|
const executor = mock<ActionsExecutor>();
|
||||||
}),
|
await camera.executePTZAction(executor, 'preset');
|
||||||
mock<CameraManagerEngine>(),
|
|
||||||
);
|
expect(executor.executeActions).not.toBeCalled();
|
||||||
const hass = createHASS();
|
|
||||||
await camera.initialize({
|
|
||||||
hass: hass,
|
|
||||||
entityRegistryManager: entityRegistryManager,
|
|
||||||
stateWatcher: mock<StateWatcher>(),
|
|
||||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterFunc = entityRegistryManager.getMatchingEntities.mock.calls[0][1];
|
it('should ignore actions with configured action', async () => {
|
||||||
|
const camera = new FrigateCamera(
|
||||||
expect(
|
createCameraConfig({
|
||||||
filterFunc(
|
camera_entity: 'camera.office_frigate',
|
||||||
createRegistryEntity({
|
ptz: {
|
||||||
config_entry_id: cameraEntity.config_entry_id,
|
actions_left_start: {
|
||||||
disabled_by: '',
|
action: 'perform-action',
|
||||||
entity_id: 'binary_sensor.foo',
|
perform_action: 'button.press',
|
||||||
|
target: {
|
||||||
|
entity_id: 'button.foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
),
|
mock<CameraManagerEngine>(),
|
||||||
).toBeTruthy();
|
);
|
||||||
|
|
||||||
expect(
|
await camera.initialize({
|
||||||
filterFunc(
|
hass: createHASS(),
|
||||||
createRegistryEntity({
|
entityRegistryManager: new EntityRegistryManagerMock([
|
||||||
config_entry_id: cameraEntity.config_entry_id,
|
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||||
disabled_by: 'user',
|
]),
|
||||||
entity_id: 'binary_sensor.foo',
|
stateWatcher: mock<StateWatcher>(),
|
||||||
}),
|
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||||
),
|
});
|
||||||
).toBeFalsy();
|
|
||||||
|
|
||||||
expect(
|
const executor = mock<ActionsExecutor>();
|
||||||
filterFunc(
|
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||||
createRegistryEntity({
|
|
||||||
config_entry_id: cameraEntity.config_entry_id,
|
|
||||||
disabled_by: '',
|
|
||||||
entity_id: 'camera.is_not_a_binary_sensor',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBeFalsy();
|
|
||||||
|
|
||||||
expect(
|
expect(executor.executeActions).toBeCalledTimes(1);
|
||||||
filterFunc(
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
createRegistryEntity({
|
actions: {
|
||||||
config_entry_id: 'not_a_matching_config_entry_id',
|
action: 'perform-action',
|
||||||
disabled_by: '',
|
perform_action: 'button.press',
|
||||||
entity_id: 'binary_sensor.foo',
|
target: {
|
||||||
|
entity_id: 'button.foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['left' as const],
|
||||||
|
['right' as const],
|
||||||
|
['up' as const],
|
||||||
|
['down' as const],
|
||||||
|
])('should execute action %s', async (action: PTZAction) => {
|
||||||
|
const camera = new FrigateCamera(
|
||||||
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_frigate',
|
||||||
}),
|
}),
|
||||||
),
|
mock<CameraManagerEngine>(),
|
||||||
).toBeFalsy();
|
);
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: new EntityRegistryManagerMock([
|
||||||
|
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||||
|
]),
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, action, { phase: 'start' });
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: {
|
||||||
|
action: 'perform-action',
|
||||||
|
data: {
|
||||||
|
action: 'move',
|
||||||
|
argument: action,
|
||||||
|
},
|
||||||
|
perform_action: 'frigate.ptz',
|
||||||
|
target: {
|
||||||
|
entity_id: 'camera.office_frigate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, action, { phase: 'stop' });
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: {
|
||||||
|
action: 'perform-action',
|
||||||
|
data: {
|
||||||
|
action: 'stop',
|
||||||
|
},
|
||||||
|
perform_action: 'frigate.ptz',
|
||||||
|
target: {
|
||||||
|
entity_id: 'camera.office_frigate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['zoom_in' as const, 'in' as const],
|
||||||
|
['zoom_out' as const, 'out' as const],
|
||||||
|
])('should execute action %s', async (action: PTZAction, zoom: 'in' | 'out') => {
|
||||||
|
const camera = new FrigateCamera(
|
||||||
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_frigate',
|
||||||
|
}),
|
||||||
|
mock<CameraManagerEngine>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: new EntityRegistryManagerMock([
|
||||||
|
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||||
|
]),
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, action, { phase: 'start' });
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: {
|
||||||
|
action: 'perform-action',
|
||||||
|
data: {
|
||||||
|
action: 'zoom',
|
||||||
|
argument: zoom,
|
||||||
|
},
|
||||||
|
perform_action: 'frigate.ptz',
|
||||||
|
target: {
|
||||||
|
entity_id: 'camera.office_frigate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute preset', async () => {
|
||||||
|
const camera = new FrigateCamera(
|
||||||
|
createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_frigate',
|
||||||
|
}),
|
||||||
|
mock<CameraManagerEngine>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: new EntityRegistryManagerMock([
|
||||||
|
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||||
|
]),
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, 'preset', { preset: 'foo' });
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: {
|
||||||
|
action: 'perform-action',
|
||||||
|
data: {
|
||||||
|
action: 'preset',
|
||||||
|
argument: 'foo',
|
||||||
|
},
|
||||||
|
perform_action: 'frigate.ptz',
|
||||||
|
target: {
|
||||||
|
entity_id: 'camera.office_frigate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,20 +7,16 @@ import {
|
|||||||
} from '../../../src/camera-manager/frigate/media';
|
} from '../../../src/camera-manager/frigate/media';
|
||||||
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
||||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||||
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
|
|
||||||
import { CameraConfig } from '../../../src/config/schema/cameras';
|
import { CameraConfig } from '../../../src/config/schema/cameras';
|
||||||
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
|
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
|
||||||
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
||||||
import {
|
|
||||||
EntityRegistryManager,
|
|
||||||
createEntityRegistryCache,
|
|
||||||
} from '../../../src/utils/ha/registry/entity';
|
|
||||||
import { ViewMedia } from '../../../src/view/media';
|
import { ViewMedia } from '../../../src/view/media';
|
||||||
import { TestViewMedia, createCameraConfig, createHASS } from '../../test-utils';
|
import { TestViewMedia, createCameraConfig, createHASS } from '../../test-utils';
|
||||||
|
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||||
|
|
||||||
const createEngine = (): FrigateCameraManagerEngine => {
|
const createEngine = (): FrigateCameraManagerEngine => {
|
||||||
return new FrigateCameraManagerEngine(
|
return new FrigateCameraManagerEngine(
|
||||||
new EntityRegistryManager(createEntityRegistryCache()),
|
new EntityRegistryManagerMock(),
|
||||||
new StateWatcher(),
|
new StateWatcher(),
|
||||||
new RecordingSegmentsCache(),
|
new RecordingSegmentsCache(),
|
||||||
new RequestCache(),
|
new RequestCache(),
|
||||||
@@ -401,101 +397,3 @@ describe('getCameraEndpoints', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('executePTZAction', () => {
|
|
||||||
describe('preset', () => {
|
|
||||||
it('should reject without preset argument', () => {
|
|
||||||
const hass = createHASS();
|
|
||||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
|
||||||
|
|
||||||
createEngine().executePTZAction(hass, cameraConfig, 'preset');
|
|
||||||
|
|
||||||
expect(hass.callService).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should succeed', () => {
|
|
||||||
const hass = createHASS();
|
|
||||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
|
||||||
|
|
||||||
createEngine().executePTZAction(hass, cameraConfig, 'preset', {
|
|
||||||
preset: 'preset-foo',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
|
||||||
entity_id: 'camera.office',
|
|
||||||
action: 'preset',
|
|
||||||
argument: 'preset-foo',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('zoom', () => {
|
|
||||||
describe.each([['zoom_in' as const], ['zoom_out' as const]])(
|
|
||||||
'%s',
|
|
||||||
(actionName: PTZAction) => {
|
|
||||||
it('start', () => {
|
|
||||||
const hass = createHASS();
|
|
||||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
|
||||||
|
|
||||||
createEngine().executePTZAction(hass, cameraConfig, actionName);
|
|
||||||
|
|
||||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
|
||||||
entity_id: 'camera.office',
|
|
||||||
action: 'zoom',
|
|
||||||
argument: actionName === 'zoom_in' ? 'in' : 'out',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('stop', () => {
|
|
||||||
const hass = createHASS();
|
|
||||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
|
||||||
|
|
||||||
createEngine().executePTZAction(hass, cameraConfig, actionName, {
|
|
||||||
phase: 'stop',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
|
||||||
entity_id: 'camera.office',
|
|
||||||
action: 'stop',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('move', () => {
|
|
||||||
describe.each([
|
|
||||||
['left' as const],
|
|
||||||
['right' as const],
|
|
||||||
['up' as const],
|
|
||||||
['down' as const],
|
|
||||||
])('%s', (actionName: PTZAction) => {
|
|
||||||
it('start', () => {
|
|
||||||
const hass = createHASS();
|
|
||||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
|
||||||
|
|
||||||
createEngine().executePTZAction(hass, cameraConfig, actionName);
|
|
||||||
|
|
||||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
|
||||||
entity_id: 'camera.office',
|
|
||||||
action: 'move',
|
|
||||||
argument: actionName,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('stop', () => {
|
|
||||||
const hass = createHASS();
|
|
||||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
|
||||||
|
|
||||||
createEngine().executePTZAction(hass, cameraConfig, actionName, {
|
|
||||||
phase: 'stop',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
|
||||||
entity_id: 'camera.office',
|
|
||||||
action: 'stop',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -331,10 +331,4 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute PTZ action', () => {
|
|
||||||
const hass = createHASS();
|
|
||||||
createEngine().executePTZAction(hass, createCameraConfig(), 'left');
|
|
||||||
expect(hass.callService).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { sortMedia } from '../../src/camera-manager/utils/sort-media.js';
|
|||||||
import { CardController } from '../../src/card-controller/controller.js';
|
import { CardController } from '../../src/card-controller/controller.js';
|
||||||
import { CameraConfig } from '../../src/config/schema/cameras.js';
|
import { CameraConfig } from '../../src/config/schema/cameras.js';
|
||||||
import { HomeAssistant } from '../../src/ha/types.js';
|
import { HomeAssistant } from '../../src/ha/types.js';
|
||||||
|
import { PTZMovementType } from '../../src/types.js';
|
||||||
import { ViewMedia } from '../../src/view/media.js';
|
import { ViewMedia } from '../../src/view/media.js';
|
||||||
import {
|
import {
|
||||||
TestViewMedia,
|
TestViewMedia,
|
||||||
@@ -1069,7 +1070,7 @@ describe('CameraManager', async () => {
|
|||||||
snapshots: true,
|
snapshots: true,
|
||||||
|
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['continuous'],
|
left: [PTZMovementType.Continuous],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -1098,25 +1099,10 @@ describe('CameraManager', async () => {
|
|||||||
|
|
||||||
manager.executePTZAction('id', 'left', {});
|
manager.executePTZAction('id', 'left', {});
|
||||||
|
|
||||||
expect(engine.executePTZAction).not.toBeCalled();
|
// No visible action.
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without hass', async () => {
|
it('successfully', async () => {
|
||||||
const api = createCardAPI();
|
|
||||||
const engine = mock<CameraManagerEngine>();
|
|
||||||
const manager = createCameraManager(api, engine);
|
|
||||||
|
|
||||||
const hass = createHASS();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
||||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
|
||||||
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
||||||
manager.executePTZAction('id', 'left');
|
|
||||||
|
|
||||||
expect(engine.executePTZAction).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully from config', async () => {
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const engine = mock<CameraManagerEngine>();
|
const engine = mock<CameraManagerEngine>();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
@@ -1140,29 +1126,7 @@ describe('CameraManager', async () => {
|
|||||||
|
|
||||||
manager.executePTZAction('another', 'left');
|
manager.executePTZAction('another', 'left');
|
||||||
|
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledWith(action);
|
expect(api.getActionsManager().executeActions).toBeCalledWith({ actions: action });
|
||||||
expect(engine.executePTZAction).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully from engine', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const engine = mock<CameraManagerEngine>();
|
|
||||||
const hass = createHASS();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
||||||
const manager = createCameraManager(api, engine);
|
|
||||||
|
|
||||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
|
||||||
|
|
||||||
const action = 'left';
|
|
||||||
const options = {};
|
|
||||||
manager.executePTZAction('id', action, options);
|
|
||||||
|
|
||||||
expect(engine.executePTZAction).toBeCalledWith(
|
|
||||||
hass,
|
|
||||||
expect.anything(),
|
|
||||||
action,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,79 @@ import { mock } from 'vitest-mock-extended';
|
|||||||
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||||
import { ReolinkCamera } from '../../../src/camera-manager/reolink/camera';
|
import { ReolinkCamera } from '../../../src/camera-manager/reolink/camera';
|
||||||
import { CameraProxyConfig } from '../../../src/camera-manager/types';
|
import { CameraProxyConfig } from '../../../src/camera-manager/types';
|
||||||
|
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||||
import { ProxyConfig } from '../../../src/config/schema/cameras';
|
import { ProxyConfig } from '../../../src/config/schema/cameras';
|
||||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
import { EntityRegistryManagerLive } from '../../../src/utils/ha/registry/entity';
|
||||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||||
|
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||||
|
|
||||||
describe('ReolinkCamera', () => {
|
describe('ReolinkCamera', () => {
|
||||||
|
const cameraEntity = createRegistryEntity({
|
||||||
|
entity_id: 'camera.office_reolink',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_main',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
const buttonEntityPTZLeft = createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_left',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_left',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
const buttonEntityPTZRight = createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_right',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_right',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
const buttonEntityPTZUp = createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_up',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_up',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
const buttonEntityPTZDown = createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_down',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_down',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
const buttonEntityPTZZoomIn = createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_zoom_in',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_zoom_in',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
const buttonEntityPTZZoomOut = createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_zoom_out',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_zoom_out',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
const buttonEntityPTZStop = createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_stop',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_stop',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ptzPopulatedEntityRegistryManager = new EntityRegistryManagerMock([
|
||||||
|
cameraEntity,
|
||||||
|
buttonEntityPTZLeft,
|
||||||
|
buttonEntityPTZRight,
|
||||||
|
buttonEntityPTZUp,
|
||||||
|
buttonEntityPTZDown,
|
||||||
|
buttonEntityPTZZoomIn,
|
||||||
|
buttonEntityPTZZoomOut,
|
||||||
|
buttonEntityPTZStop,
|
||||||
|
|
||||||
|
// Unrelated button.
|
||||||
|
createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_foo',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_foo',
|
||||||
|
platform: 'reolink',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Unrelated button without unique_id.
|
||||||
|
createRegistryEntity({
|
||||||
|
entity_id: 'button.office_reolink_ptz_bar',
|
||||||
|
platform: 'reolink',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
describe('should initialize config', () => {
|
describe('should initialize config', () => {
|
||||||
describe('should detect channel', () => {
|
describe('should detect channel', () => {
|
||||||
it('without a camera_entity', async () => {
|
it('without a camera_entity', async () => {
|
||||||
@@ -19,7 +86,7 @@ describe('ReolinkCamera', () => {
|
|||||||
async () =>
|
async () =>
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: createHASS(),
|
hass: createHASS(),
|
||||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
entityRegistryManager: mock<EntityRegistryManagerLive>(),
|
||||||
stateWatcher: mock<StateWatcher>(),
|
stateWatcher: mock<StateWatcher>(),
|
||||||
}),
|
}),
|
||||||
).rejects.toThrowError('Could not find camera entity');
|
).rejects.toThrowError('Could not find camera entity');
|
||||||
@@ -31,17 +98,18 @@ describe('ReolinkCamera', () => {
|
|||||||
});
|
});
|
||||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
const entity = createRegistryEntity({
|
createRegistryEntity({
|
||||||
platform: 'reolink',
|
entity_id: 'camera.office_reolink',
|
||||||
});
|
platform: 'reolink',
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
async () =>
|
async () =>
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: createHASS(),
|
hass: createHASS(),
|
||||||
entityRegistryManager: entityRegistryManager,
|
entityRegistryManager,
|
||||||
stateWatcher: mock<StateWatcher>(),
|
stateWatcher: mock<StateWatcher>(),
|
||||||
}),
|
}),
|
||||||
).rejects.toThrowError('Could not initialize Reolink camera');
|
).rejects.toThrowError('Could not initialize Reolink camera');
|
||||||
@@ -52,19 +120,19 @@ describe('ReolinkCamera', () => {
|
|||||||
camera_entity: 'camera.office_reolink',
|
camera_entity: 'camera.office_reolink',
|
||||||
});
|
});
|
||||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
createRegistryEntity({
|
||||||
const entity = createRegistryEntity({
|
entity_id: 'camera.office_reolink',
|
||||||
unique_id: 'invalid',
|
unique_id: 'invalid',
|
||||||
platform: 'reolink',
|
platform: 'reolink',
|
||||||
});
|
}),
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
async () =>
|
async () =>
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: createHASS(),
|
hass: createHASS(),
|
||||||
entityRegistryManager: entityRegistryManager,
|
entityRegistryManager,
|
||||||
stateWatcher: mock<StateWatcher>(),
|
stateWatcher: mock<StateWatcher>(),
|
||||||
}),
|
}),
|
||||||
).rejects.toThrowError('Could not initialize Reolink camera');
|
).rejects.toThrowError('Could not initialize Reolink camera');
|
||||||
@@ -76,22 +144,71 @@ describe('ReolinkCamera', () => {
|
|||||||
camera_entity: 'camera.office_reolink',
|
camera_entity: 'camera.office_reolink',
|
||||||
});
|
});
|
||||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
const entityRegistryManager = new EntityRegistryManagerMock([cameraEntity]);
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
const entity = createRegistryEntity({
|
|
||||||
unique_id: '85270002TS7D4RUP_0_main',
|
|
||||||
platform: 'reolink',
|
|
||||||
});
|
|
||||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
|
||||||
|
|
||||||
await camera.initialize({
|
await camera.initialize({
|
||||||
hass: createHASS(),
|
hass: createHASS(),
|
||||||
entityRegistryManager: entityRegistryManager,
|
entityRegistryManager,
|
||||||
stateWatcher: mock<StateWatcher>(),
|
stateWatcher: mock<StateWatcher>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(camera.getChannel()).toBe(0);
|
expect(camera.getChannel()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('successfully with PTZ', () => {
|
||||||
|
it('should find PTZ button entities', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||||
|
left: ['continuous'],
|
||||||
|
right: ['continuous'],
|
||||||
|
up: ['continuous'],
|
||||||
|
down: ['continuous'],
|
||||||
|
zoomIn: ['continuous'],
|
||||||
|
zoomOut: ['continuous'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow configured PTZ actions to override', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
ptz: {
|
||||||
|
actions_left: {
|
||||||
|
action: 'perform-action',
|
||||||
|
perform_action: 'homeassistant.toggle',
|
||||||
|
target: {
|
||||||
|
entity_id: 'switch.camera_move_left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||||
|
left: ['relative'],
|
||||||
|
right: ['continuous'],
|
||||||
|
up: ['continuous'],
|
||||||
|
down: ['continuous'],
|
||||||
|
zoomIn: ['continuous'],
|
||||||
|
zoomOut: ['continuous'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should get proxy config', () => {
|
describe('should get proxy config', () => {
|
||||||
@@ -213,4 +330,103 @@ describe('ReolinkCamera', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should execute PTZ action', () => {
|
||||||
|
it('should ignore actions without matching button', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, 'left');
|
||||||
|
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||||
|
|
||||||
|
expect(executor.executeActions).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore actions with configured action', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
ptz: {
|
||||||
|
actions_left_start: {
|
||||||
|
action: 'perform-action',
|
||||||
|
perform_action: 'button.press',
|
||||||
|
target: {
|
||||||
|
entity_id: 'button.foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: new EntityRegistryManagerMock([
|
||||||
|
cameraEntity,
|
||||||
|
buttonEntityPTZLeft,
|
||||||
|
]),
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||||
|
|
||||||
|
expect(executor.executeActions).toBeCalledTimes(1);
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: {
|
||||||
|
action: 'perform-action',
|
||||||
|
perform_action: 'button.press',
|
||||||
|
target: {
|
||||||
|
entity_id: 'button.foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute action with matching button', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'perform-action',
|
||||||
|
perform_action: 'button.press',
|
||||||
|
target: {
|
||||||
|
entity_id: 'button.office_reolink_ptz_left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, 'left', { phase: 'stop' });
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'perform-action',
|
||||||
|
perform_action: 'button.press',
|
||||||
|
target: {
|
||||||
|
entity_id: 'button.office_reolink_ptz_stop',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
BrowseMedia,
|
BrowseMedia,
|
||||||
browseMediaSchema,
|
browseMediaSchema,
|
||||||
} from '../../../src/utils/ha/browse-media/types';
|
} from '../../../src/utils/ha/browse-media/types';
|
||||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity/types';
|
||||||
import { ResolvedMediaCache } from '../../../src/utils/ha/resolved-media';
|
import { ResolvedMediaCache } from '../../../src/utils/ha/resolved-media';
|
||||||
import { homeAssistantWSRequest } from '../../../src/utils/ha/ws-request';
|
import { homeAssistantWSRequest } from '../../../src/utils/ha/ws-request';
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
createRegistryEntity,
|
createRegistryEntity,
|
||||||
createStore,
|
createStore,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
|
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||||
|
|
||||||
vi.mock('../../../src/utils/ha/ws-request');
|
vi.mock('../../../src/utils/ha/ws-request');
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@ const createEngine = (options?: {
|
|||||||
entityRegistryManager?: EntityRegistryManager;
|
entityRegistryManager?: EntityRegistryManager;
|
||||||
}): ReolinkCameraManagerEngine => {
|
}): ReolinkCameraManagerEngine => {
|
||||||
return new ReolinkCameraManagerEngine(
|
return new ReolinkCameraManagerEngine(
|
||||||
options?.entityRegistryManager ?? mock<EntityRegistryManager>(),
|
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
|
||||||
mock<StateWatcher>(),
|
mock<StateWatcher>(),
|
||||||
options?.browseMediaManager ?? new BrowseMediaManager(),
|
options?.browseMediaManager ?? new BrowseMediaManager(),
|
||||||
new ResolvedMediaCache(),
|
new ResolvedMediaCache(),
|
||||||
@@ -192,15 +193,15 @@ const createEngine = (options?: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPopulatedEngine = (): ReolinkCameraManagerEngine => {
|
const cameraEntity = createRegistryEntity({
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
entity_id: 'camera.office',
|
||||||
const entity = createRegistryEntity({
|
unique_id: '85270002TS7D4RUP_0_main',
|
||||||
unique_id: '85270002TS7D4RUP_0_main',
|
platform: 'reolink',
|
||||||
platform: 'reolink',
|
config_entry_id: '01J8XHYTNH77WE3C654K03KX1F',
|
||||||
config_entry_id: '01J8XHYTNH77WE3C654K03KX1F',
|
});
|
||||||
});
|
|
||||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
|
||||||
|
|
||||||
|
const createPopulatedEngine = (): ReolinkCameraManagerEngine => {
|
||||||
|
const entityRegistryManager = new EntityRegistryManagerMock([cameraEntity]);
|
||||||
return createEngine({ entityRegistryManager });
|
return createEngine({ entityRegistryManager });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,14 +244,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create camera', async () => {
|
it('should create camera', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const engine = createPopulatedEngine();
|
||||||
const entity = createRegistryEntity({
|
|
||||||
unique_id: '85270002TS7D4RUP_0_main',
|
|
||||||
platform: 'reolink',
|
|
||||||
});
|
|
||||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
|
||||||
|
|
||||||
const engine = createEngine({ entityRegistryManager });
|
|
||||||
const config = createCameraConfig({
|
const config = createCameraConfig({
|
||||||
camera_entity: 'camera.office',
|
camera_entity: 'camera.office',
|
||||||
unique_id: 'office',
|
unique_id: 'office',
|
||||||
@@ -589,15 +583,15 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
|
|
||||||
describe('should ignore invalid cameras', () => {
|
describe('should ignore invalid cameras', () => {
|
||||||
it('camera without a config entry id', async () => {
|
it('camera without a config entry id', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
const entity = createRegistryEntity({
|
const entity = createRegistryEntity({
|
||||||
|
entity_id: 'camera.office',
|
||||||
unique_id: '85270002TS7D4RUP_0_main',
|
unique_id: '85270002TS7D4RUP_0_main',
|
||||||
platform: 'reolink',
|
platform: 'reolink',
|
||||||
|
|
||||||
// Cannot fetch events without a config_entry_id.
|
// Cannot fetch events without a config_entry_id.
|
||||||
config_entry_id: null,
|
config_entry_id: null,
|
||||||
});
|
});
|
||||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
const entityRegistryManager = new EntityRegistryManagerMock([entity]);
|
||||||
|
|
||||||
const engine = createEngine({ entityRegistryManager });
|
const engine = createEngine({ entityRegistryManager });
|
||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
@@ -1098,15 +1092,15 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
|
|
||||||
describe('should ignore invalid cameras', () => {
|
describe('should ignore invalid cameras', () => {
|
||||||
it('should get no metdata for camera without a config entry id', async () => {
|
it('should get no metdata for camera without a config entry id', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
const entity = createRegistryEntity({
|
const entity = createRegistryEntity({
|
||||||
|
entity_id: 'camera.office',
|
||||||
unique_id: '85270002TS7D4RUP_0_main',
|
unique_id: '85270002TS7D4RUP_0_main',
|
||||||
platform: 'reolink',
|
platform: 'reolink',
|
||||||
|
|
||||||
// Cannot fetch events without a config_entry_id.
|
// Cannot fetch events without a config_entry_id.
|
||||||
config_entry_id: null,
|
config_entry_id: null,
|
||||||
});
|
});
|
||||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
const entityRegistryManager = new EntityRegistryManagerMock([entity]);
|
||||||
|
|
||||||
const engine = createEngine({ entityRegistryManager });
|
const engine = createEngine({ entityRegistryManager });
|
||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-fact
|
|||||||
import { CameraManagerStore } from '../../src/camera-manager/store.js';
|
import { CameraManagerStore } from '../../src/camera-manager/store.js';
|
||||||
import { Engine } from '../../src/camera-manager/types.js';
|
import { Engine } from '../../src/camera-manager/types.js';
|
||||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||||
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/index.js';
|
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/types.js';
|
||||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
|
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
|
||||||
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
|
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
|
||||||
|
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ describe('ActionsManager', () => {
|
|||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||||
await manager.handleActionExecutionRequestEvent(
|
await manager.handleActionExecutionRequestEvent(
|
||||||
new CustomEvent('advanced-camera-card:action:execution-request', {
|
new CustomEvent('advanced-camera-card:action:execution-request', {
|
||||||
detail: { action: createLogAction('Hello, world!') },
|
detail: { actions: createLogAction('Hello, world!') },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(consoleSpy).toBeCalled();
|
expect(consoleSpy).toBeCalled();
|
||||||
@@ -271,7 +271,7 @@ describe('ActionsManager', () => {
|
|||||||
const manager = new ActionsManager(api);
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||||
await manager.executeActions(createLogAction('Hello, world!'));
|
await manager.executeActions({ actions: createLogAction('Hello, world!') });
|
||||||
expect(consoleSpy).toBeCalled();
|
expect(consoleSpy).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ describe('ActionsManager', () => {
|
|||||||
const manager = new ActionsManager(api);
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||||
await manager.executeActions(createLogAction('Hello, world!'));
|
await manager.executeActions({ actions: createLogAction('Hello, world!') });
|
||||||
expect(consoleSpy).toBeCalled();
|
expect(consoleSpy).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,10 +303,7 @@ describe('ActionsManager', () => {
|
|||||||
const config = { entity: 'light.office' };
|
const config = { entity: 'light.office' };
|
||||||
const triggerData = { view: { from: 'previous-view', to: 'view' } };
|
const triggerData = { view: { from: 'previous-view', to: 'view' } };
|
||||||
|
|
||||||
await manager.executeActions(action, {
|
await manager.executeActions({ actions: action, config, triggerData });
|
||||||
config,
|
|
||||||
triggerData,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
||||||
conditionState,
|
conditionState,
|
||||||
@@ -326,7 +323,7 @@ describe('ActionsManager', () => {
|
|||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new ActionsManager(api);
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
await manager.executeActions({ action: 'none' });
|
await manager.executeActions({ actions: { action: 'none' } });
|
||||||
|
|
||||||
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'success' }));
|
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'success' }));
|
||||||
});
|
});
|
||||||
@@ -340,7 +337,9 @@ describe('ActionsManager', () => {
|
|||||||
|
|
||||||
vi.stubGlobal('confirm', vi.fn().mockReturnValue(false));
|
vi.stubGlobal('confirm', vi.fn().mockReturnValue(false));
|
||||||
|
|
||||||
await manager.executeActions({ action: 'none', confirmation: true });
|
await manager.executeActions({
|
||||||
|
actions: { action: 'none', confirmation: true },
|
||||||
|
});
|
||||||
|
|
||||||
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'warning' }));
|
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'warning' }));
|
||||||
});
|
});
|
||||||
@@ -360,16 +359,18 @@ describe('ActionsManager', () => {
|
|||||||
const manager = new ActionsManager(api);
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||||
const promise = manager.executeActions([
|
const promise = manager.executeActions({
|
||||||
{
|
actions: [
|
||||||
action: 'fire-dom-event',
|
{
|
||||||
advanced_camera_card_action: 'sleep',
|
action: 'fire-dom-event',
|
||||||
duration: {
|
advanced_camera_card_action: 'sleep',
|
||||||
m: 1,
|
duration: {
|
||||||
|
m: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
createLogAction('Hello, world!'),
|
||||||
createLogAction('Hello, world!'),
|
],
|
||||||
]);
|
});
|
||||||
|
|
||||||
// Stop inflight actions.
|
// Stop inflight actions.
|
||||||
await manager.uninitialize();
|
await manager.uninitialize();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { Capabilities } from '../../../../src/camera-manager/capabilities';
|
||||||
import { PTZAction } from '../../../../src/card-controller/actions/actions/ptz';
|
import { PTZAction } from '../../../../src/card-controller/actions/actions/ptz';
|
||||||
|
import { PTZMovementType } from '../../../../src/types';
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
@@ -7,7 +9,6 @@ import {
|
|||||||
createStore,
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
} from '../../../test-utils';
|
} from '../../../test-utils';
|
||||||
import { Capabilities } from '../../../../src/camera-manager/capabilities';
|
|
||||||
|
|
||||||
describe('should handle ptz action', () => {
|
describe('should handle ptz action', () => {
|
||||||
it('should execute simple action', async () => {
|
it('should execute simple action', async () => {
|
||||||
@@ -20,7 +21,7 @@ describe('should handle ptz action', () => {
|
|||||||
const store = createStore([
|
const store = createStore([
|
||||||
{
|
{
|
||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
||||||
@@ -53,7 +54,7 @@ describe('should handle ptz action', () => {
|
|||||||
const store = createStore([
|
const store = createStore([
|
||||||
{
|
{
|
||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
||||||
@@ -93,7 +94,7 @@ describe('should handle ptz action', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
cameraID: 'camera.office_hd',
|
cameraID: 'camera.office_hd',
|
||||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
||||||
@@ -210,7 +211,7 @@ describe('should handle ptz action', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['relative'],
|
left: [PTZMovementType.Relative],
|
||||||
presets: [],
|
presets: [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -281,7 +282,7 @@ describe('should handle ptz action', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['continuous'],
|
left: [PTZMovementType.Continuous],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -329,7 +330,7 @@ describe('should handle ptz action', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['continuous'],
|
left: [PTZMovementType.Continuous],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -375,7 +376,7 @@ describe('should handle ptz action', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['continuous'],
|
left: [PTZMovementType.Continuous],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -419,7 +420,7 @@ describe('should handle ptz action', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['relative'],
|
left: [PTZMovementType.Relative],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -473,7 +474,7 @@ describe('should handle ptz action', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['relative'],
|
left: [PTZMovementType.Relative],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { ActionsExecutionRequest } from '../../src/card-controller/actions/types.js';
|
||||||
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
|
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
|
||||||
import { ConditionStateManager } from '../../src/conditions/state-manager.js';
|
import { ConditionStateManager } from '../../src/conditions/state-manager.js';
|
||||||
import { ActionConfig } from '../../src/config/schema/actions/types.js';
|
|
||||||
import { createCardAPI } from '../test-utils.js';
|
import { createCardAPI } from '../test-utils.js';
|
||||||
|
|
||||||
describe('AutomationsManager', () => {
|
describe('AutomationsManager', () => {
|
||||||
@@ -150,9 +150,7 @@ describe('AutomationsManager', () => {
|
|||||||
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
||||||
async (
|
async (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_action: ActionConfig | ActionConfig[],
|
_request: ActionsExecutionRequest,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
_options?: unknown,
|
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
fullscreen = !fullscreen;
|
fullscreen = !fullscreen;
|
||||||
stateManager.setState({ fullscreen: fullscreen });
|
stateManager.setState({ fullscreen: fullscreen });
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { ViewManager } from '../../src/card-controller/view/view-manager';
|
|||||||
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||||
import { AdvancedCameraCardEditor } from '../../src/editor';
|
import { AdvancedCameraCardEditor } from '../../src/editor';
|
||||||
import { DeviceRegistryManager } from '../../src/utils/ha/registry/device';
|
import { DeviceRegistryManager } from '../../src/utils/ha/registry/device';
|
||||||
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity';
|
import { EntityRegistryManagerLive } from '../../src/utils/ha/registry/entity';
|
||||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager');
|
vi.mock('../../src/camera-manager/manager');
|
||||||
@@ -160,7 +160,7 @@ describe('CardController', () => {
|
|||||||
|
|
||||||
it('getEntityRegistryManager', () => {
|
it('getEntityRegistryManager', () => {
|
||||||
expect(createController().getEntityRegistryManager()).toBe(
|
expect(createController().getEntityRegistryManager()).toBe(
|
||||||
vi.mocked(EntityRegistryManager).mock.instances[0],
|
vi.mocked(EntityRegistryManagerLive).mock.instances[0],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
MEDIA_PLAYER_SUPPORT_TURN_OFF,
|
MEDIA_PLAYER_SUPPORT_TURN_OFF,
|
||||||
} from '../../src/const';
|
} from '../../src/const';
|
||||||
import { HomeAssistant } from '../../src/ha/types.js';
|
import { HomeAssistant } from '../../src/ha/types.js';
|
||||||
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/index.js';
|
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/types.js';
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
createStore,
|
createStore,
|
||||||
TestViewMedia,
|
TestViewMedia,
|
||||||
} from '../test-utils.js';
|
} from '../test-utils.js';
|
||||||
|
import { EntityRegistryManagerMock } from '../utils/ha/registry/entity/mock.js';
|
||||||
|
|
||||||
const createHASSWithMediaPlayers = (): HomeAssistant => {
|
const createHASSWithMediaPlayers = (): HomeAssistant => {
|
||||||
const attributesSupported = {
|
const attributesSupported = {
|
||||||
@@ -68,13 +69,16 @@ describe('MediaPlayerManager', () => {
|
|||||||
|
|
||||||
describe('should initialize', () => {
|
describe('should initialize', () => {
|
||||||
it('correctly', async () => {
|
it('correctly', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntities.mockResolvedValue(
|
createRegistryEntity({
|
||||||
new Map([
|
entity_id: 'media_player.ok1',
|
||||||
['media_player.ok1', createRegistryEntity({ hidden_by: '' })],
|
hidden_by: '',
|
||||||
['media_player.ok2', createRegistryEntity({ hidden_by: 'user' })],
|
}),
|
||||||
]),
|
createRegistryEntity({
|
||||||
);
|
entity_id: 'media_player.ok2',
|
||||||
|
hidden_by: 'user',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||||
@@ -150,13 +154,16 @@ describe('MediaPlayerManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reinitialize when there is a config change', async () => {
|
it('should reinitialize when there is a config change', async () => {
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||||
entityRegistryManager.getEntities.mockResolvedValue(
|
createRegistryEntity({
|
||||||
new Map([
|
entity_id: 'media_player.ok1',
|
||||||
['media_player.ok1', createRegistryEntity({ hidden_by: '' })],
|
hidden_by: '',
|
||||||
['media_player.ok2', createRegistryEntity({ hidden_by: 'user' })],
|
}),
|
||||||
]),
|
createRegistryEntity({
|
||||||
);
|
entity_id: 'media_player.ok2',
|
||||||
|
hidden_by: 'user',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||||
|
|||||||
@@ -77,13 +77,15 @@ describe('QueryStringManager', () => {
|
|||||||
expect(manager.hasViewRelatedActionsToRun()).toBeFalsy();
|
expect(manager.hasViewRelatedActionsToRun()).toBeFalsy();
|
||||||
await manager.executeIfNecessary();
|
await manager.executeIfNecessary();
|
||||||
|
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledWith([
|
expect(api.getActionsManager().executeActions).toBeCalledWith({
|
||||||
{
|
actions: [
|
||||||
action: 'fire-dom-event',
|
{
|
||||||
card_id: 'id',
|
action: 'fire-dom-event',
|
||||||
advanced_camera_card_action: action,
|
card_id: 'id',
|
||||||
},
|
advanced_camera_card_action: action,
|
||||||
]);
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ describe('MenuController', () => {
|
|||||||
controller.handleAction(createInteractionActionEvent('tap'), tapActionConfig);
|
controller.handleAction(createInteractionActionEvent('tap'), tapActionConfig);
|
||||||
expect(handler).toBeCalledWith(
|
expect(handler).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
detail: { action: [action], config: tapActionConfig },
|
detail: { actions: [action], config: tapActionConfig },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(controller.isExpanded()).toBeFalsy();
|
expect(controller.isExpanded()).toBeFalsy();
|
||||||
@@ -402,7 +402,7 @@ describe('MenuController', () => {
|
|||||||
);
|
);
|
||||||
expect(handler).toBeCalledWith(
|
expect(handler).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
detail: { action: [action], config: tapActionConfig },
|
detail: { actions: [action], config: tapActionConfig },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -418,7 +418,7 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
expect(handler).toBeCalledWith(
|
expect(handler).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
detail: { action: [action, action, action], config: tapActionConfigMulti },
|
detail: { actions: [action, action, action], config: tapActionConfigMulti },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
PTZControlsConfig,
|
PTZControlsConfig,
|
||||||
ptzControlsConfigSchema,
|
ptzControlsConfigSchema,
|
||||||
} from '../../../src/config/schema/common/controls/ptz';
|
} from '../../../src/config/schema/common/controls/ptz';
|
||||||
|
import { PTZMovementType } from '../../../src/types';
|
||||||
import { createCameraManager, createCapabilities, createStore } from '../../test-utils';
|
import { createCameraManager, createCapabilities, createStore } from '../../test-utils';
|
||||||
|
|
||||||
const createConfig = (config?: Partial<PTZControlsConfig>): PTZControlsConfig => {
|
const createConfig = (config?: Partial<PTZControlsConfig>): PTZControlsConfig => {
|
||||||
@@ -122,14 +123,14 @@ describe('PTZController', () => {
|
|||||||
const store = createStore([
|
const store = createStore([
|
||||||
{
|
{
|
||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const cameraManager = createCameraManager(store);
|
const cameraManager = createCameraManager(store);
|
||||||
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||||
createCapabilities({
|
createCapabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['relative'],
|
left: [PTZMovementType.Relative],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -158,12 +159,12 @@ describe('PTZController', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['relative'],
|
left: [PTZMovementType.Relative],
|
||||||
right: ['relative'],
|
right: [PTZMovementType.Relative],
|
||||||
up: ['relative'],
|
up: [PTZMovementType.Relative],
|
||||||
down: ['relative'],
|
down: [PTZMovementType.Relative],
|
||||||
zoomIn: ['relative'],
|
zoomIn: [PTZMovementType.Relative],
|
||||||
zoomOut: ['relative'],
|
zoomOut: [PTZMovementType.Relative],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -197,12 +198,12 @@ describe('PTZController', () => {
|
|||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: new Capabilities({
|
capabilities: new Capabilities({
|
||||||
ptz: {
|
ptz: {
|
||||||
left: ['relative'],
|
left: [PTZMovementType.Relative],
|
||||||
right: ['relative'],
|
right: [PTZMovementType.Relative],
|
||||||
up: ['relative'],
|
up: [PTZMovementType.Relative],
|
||||||
down: ['relative'],
|
down: [PTZMovementType.Relative],
|
||||||
zoomIn: ['relative'],
|
zoomIn: [PTZMovementType.Relative],
|
||||||
zoomOut: ['relative'],
|
zoomOut: [PTZMovementType.Relative],
|
||||||
presets: ['door', 'window'],
|
presets: ['door', 'window'],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -387,7 +388,7 @@ describe('PTZController', () => {
|
|||||||
expect(handler).toBeCalledWith(
|
expect(handler).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
detail: {
|
detail: {
|
||||||
action: action,
|
actions: action,
|
||||||
config: config,
|
config: config,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ describe('StatusBarController', () => {
|
|||||||
|
|
||||||
expect(handler).toBeCalledWith(
|
expect(handler).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
detail: { action: [action], config: tapActionConfig },
|
detail: { actions: [action], config: tapActionConfig },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-2
@@ -54,8 +54,7 @@ import { CurrentUser, HomeAssistant } from '../src/ha/types';
|
|||||||
import { CapabilitiesRaw, Interaction, MediaLoadedInfo } from '../src/types';
|
import { CapabilitiesRaw, Interaction, MediaLoadedInfo } from '../src/types';
|
||||||
import { HassStateDifference } from '../src/utils/ha';
|
import { HassStateDifference } from '../src/utils/ha';
|
||||||
import { Device } from '../src/utils/ha/registry/device/types';
|
import { Device } from '../src/utils/ha/registry/device/types';
|
||||||
import { EntityRegistryManager } from '../src/utils/ha/registry/entity';
|
import { Entity, EntityRegistryManager } from '../src/utils/ha/registry/entity/types';
|
||||||
import { Entity } from '../src/utils/ha/registry/entity/types';
|
|
||||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||||
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
||||||
import { View, ViewParameters } from '../src/view/view';
|
import { View, ViewParameters } from '../src/view/view';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
createEntityRegistryCache,
|
createEntityRegistryCache,
|
||||||
EntityRegistryManager,
|
EntityRegistryManagerLive,
|
||||||
} from '../../../../../src/utils/ha/registry/entity';
|
} from '../../../../../src/utils/ha/registry/entity';
|
||||||
import { homeAssistantWSRequest } from '../../../../../src/utils/ha/ws-request';
|
import { homeAssistantWSRequest } from '../../../../../src/utils/ha/ws-request';
|
||||||
import { createHASS, createRegistryEntity } from '../../../../test-utils.js';
|
import { createHASS, createRegistryEntity } from '../../../../test-utils.js';
|
||||||
@@ -21,7 +21,7 @@ describe('EntityRegistryManager', () => {
|
|||||||
|
|
||||||
cache.add(testEntity);
|
cache.add(testEntity);
|
||||||
|
|
||||||
const manager = new EntityRegistryManager(cache);
|
const manager = new EntityRegistryManagerLive(cache);
|
||||||
expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity);
|
expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity);
|
||||||
|
|
||||||
expect(homeAssistantWSRequest).not.toHaveBeenCalled();
|
expect(homeAssistantWSRequest).not.toHaveBeenCalled();
|
||||||
@@ -30,7 +30,7 @@ describe('EntityRegistryManager', () => {
|
|||||||
it('should fetch and cache when not cached', async () => {
|
it('should fetch and cache when not cached', async () => {
|
||||||
const testEntity = createRegistryEntity({ entity_id: 'test' });
|
const testEntity = createRegistryEntity({ entity_id: 'test' });
|
||||||
|
|
||||||
const manager = new EntityRegistryManager(createEntityRegistryCache());
|
const manager = new EntityRegistryManagerLive(createEntityRegistryCache());
|
||||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(testEntity);
|
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(testEntity);
|
||||||
|
|
||||||
expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity);
|
expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity);
|
||||||
@@ -43,7 +43,7 @@ describe('EntityRegistryManager', () => {
|
|||||||
it('should return null when entity does not exist', async () => {
|
it('should return null when entity does not exist', async () => {
|
||||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found'));
|
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found'));
|
||||||
|
|
||||||
const manager = new EntityRegistryManager(createEntityRegistryCache());
|
const manager = new EntityRegistryManagerLive(createEntityRegistryCache());
|
||||||
expect(await manager.getEntity(createHASS(), 'missing')).toBeNull();
|
expect(await manager.getEntity(createHASS(), 'missing')).toBeNull();
|
||||||
|
|
||||||
vi.mocked(expect(console.warn)).toBeCalledWith('Not found');
|
vi.mocked(expect(console.warn)).toBeCalledWith('Not found');
|
||||||
@@ -57,7 +57,7 @@ describe('EntityRegistryManager', () => {
|
|||||||
const cache = createEntityRegistryCache();
|
const cache = createEntityRegistryCache();
|
||||||
cache.add(cachedEntity);
|
cache.add(cachedEntity);
|
||||||
|
|
||||||
const manager = new EntityRegistryManager(cache);
|
const manager = new EntityRegistryManagerLive(cache);
|
||||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(notCachedEntity);
|
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(notCachedEntity);
|
||||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found'));
|
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found'));
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ describe('EntityRegistryManager', () => {
|
|||||||
const entity = createRegistryEntity({ entity_id: 'cached' });
|
const entity = createRegistryEntity({ entity_id: 'cached' });
|
||||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([entity]);
|
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([entity]);
|
||||||
|
|
||||||
const manager = new EntityRegistryManager(createEntityRegistryCache());
|
const manager = new EntityRegistryManagerLive(createEntityRegistryCache());
|
||||||
|
|
||||||
await manager.fetchEntityList(hass);
|
await manager.fetchEntityList(hass);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ describe('EntityRegistryManager', () => {
|
|||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Fetch error'));
|
vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Fetch error'));
|
||||||
|
|
||||||
const manager = new EntityRegistryManager(createEntityRegistryCache());
|
const manager = new EntityRegistryManagerLive(createEntityRegistryCache());
|
||||||
|
|
||||||
await manager.fetchEntityList(hass);
|
await manager.fetchEntityList(hass);
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ describe('EntityRegistryManager', () => {
|
|||||||
notMatchingEntity,
|
notMatchingEntity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const manager = new EntityRegistryManager(createEntityRegistryCache());
|
const manager = new EntityRegistryManagerLive(createEntityRegistryCache());
|
||||||
expect(
|
expect(
|
||||||
await manager.getMatchingEntities(
|
await manager.getMatchingEntities(
|
||||||
hass,
|
hass,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { HomeAssistant } from '../../../../../src/ha/types';
|
||||||
|
import { RegistryCache } from '../../../../../src/utils/ha/registry/cache';
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
EntityRegistryManager,
|
||||||
|
} from '../../../../../src/utils/ha/registry/entity/types';
|
||||||
|
|
||||||
|
export class EntityRegistryManagerMock implements EntityRegistryManager {
|
||||||
|
protected _cache: RegistryCache<Entity>;
|
||||||
|
protected _fetchedEntityList = false;
|
||||||
|
|
||||||
|
constructor(data?: Entity[]) {
|
||||||
|
this._cache = new RegistryCache<Entity>((ent) => ent.entity_id);
|
||||||
|
this._cache.add(data ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEntity(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
entityID: string,
|
||||||
|
): Promise<Entity | null> {
|
||||||
|
return this._cache.get(entityID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMatchingEntities(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
func: (arg: Entity) => boolean,
|
||||||
|
): Promise<Entity[]> {
|
||||||
|
return this._cache.getMatches(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEntities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityIDs: string[],
|
||||||
|
): Promise<Map<string, Entity>> {
|
||||||
|
const output: Map<string, Entity> = new Map();
|
||||||
|
for (const entityID of entityIDs) {
|
||||||
|
const entityData = await this.getEntity(hass, entityID);
|
||||||
|
if (entityData) {
|
||||||
|
output.set(entityID, entityData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async fetchEntityList(_hass: HomeAssistant): Promise<void> {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user