feat: Trigger cameras on HA bus events (#2512)
This commit is contained in:
committed by
dermotduffy
parent
406176eebc
commit
0a36358394
@@ -1,3 +1,4 @@
|
||||
import { EventWatcherSubscriptionInterface } from '../../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../ha/browse-media/types';
|
||||
@@ -37,12 +38,13 @@ export class BrowseMediaCameraManagerEngine
|
||||
public constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
browseMediaManager: BrowseMediaWalker,
|
||||
resolvedMediaCache: ResolvedMediaCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, entityRegistryManager, eventCallback);
|
||||
super(stateWatcher, eventWatcher, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._browseMediaWalker = browseMediaManager;
|
||||
this._resolvedMediaCache = resolvedMediaCache;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { uniq } from 'lodash-es';
|
||||
import { ActionsExecutor } from '../card-controller/actions/types';
|
||||
import {
|
||||
EventSubscriptionRequest,
|
||||
EventWatcherSubscriptionInterface,
|
||||
} from '../card-controller/hass/event-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, TriggerEvent } from '../config/schema/cameras';
|
||||
import { EnabledProxyConfig, resolveProxyConfig } from '../config/schema/common/proxy';
|
||||
import { computeDomain } from '../ha/compute-domain';
|
||||
import { matchesEventData } from '../ha/event-data-match';
|
||||
import { getTriggerEventType } from '../ha/get-trigger-event-type';
|
||||
import { Entity, EntityRegistryManager } from '../ha/registry/entity/types';
|
||||
import { HassStateDifference, HomeAssistant } from '../ha/types';
|
||||
@@ -40,6 +45,7 @@ interface CapabilityOptions {
|
||||
export interface CameraInitializationOptions {
|
||||
hass: HomeAssistant;
|
||||
stateWatcher: StateWatcherSubscriptionInterface;
|
||||
eventWatcher: EventWatcherSubscriptionInterface;
|
||||
capabilityOptions?: CapabilityOptions;
|
||||
entityRegistryManager?: EntityRegistryManager;
|
||||
}
|
||||
@@ -85,16 +91,41 @@ export class Camera {
|
||||
await this._getTriggerEntities(options);
|
||||
this._config.triggers.entities = uniq(this._config.triggers.entities);
|
||||
|
||||
// Subscribe to state based triggers.
|
||||
options.stateWatcher.subscribe(
|
||||
this._stateChangeHandler,
|
||||
this._config.triggers.entities,
|
||||
);
|
||||
this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler));
|
||||
|
||||
// Subscribe to event based triggers.
|
||||
for (const event of this._config.triggers.events) {
|
||||
const request = this._buildEventSubscriptionRequest(event);
|
||||
await options.eventWatcher.subscribe(options.hass, request);
|
||||
this._onDestroy(() => options.eventWatcher.unsubscribe(request));
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private _buildEventSubscriptionRequest(event: TriggerEvent): EventSubscriptionRequest {
|
||||
const filter = event.event_data;
|
||||
return {
|
||||
event_type: event.event_type,
|
||||
...(filter && { matcher: (data) => matchesEventData(filter, data) }),
|
||||
callback: () => this._momentaryEventHandler(event.event_type),
|
||||
};
|
||||
}
|
||||
|
||||
private _momentaryEventHandler(eventType: string): void {
|
||||
this._eventCallback?.({
|
||||
cameraID: this.getID(),
|
||||
id: `event:${eventType}`,
|
||||
type: 'momentary',
|
||||
});
|
||||
}
|
||||
|
||||
private async _resolveEntity(
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<Entity | null> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EventWatcherSubscriptionInterface } from '../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { BrowseMediaWalker } from '../ha/browse-media/walker';
|
||||
@@ -13,6 +14,7 @@ import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
|
||||
|
||||
interface CameraManagerEngineFactoryOptions {
|
||||
stateWatcher: StateWatcherSubscriptionInterface;
|
||||
eventWatcher: EventWatcherSubscriptionInterface;
|
||||
resolvedMediaCache: ResolvedMediaCache;
|
||||
eventCallback?: CameraEventCallback;
|
||||
}
|
||||
@@ -39,6 +41,7 @@ export class CameraManagerEngineFactory {
|
||||
const { GenericCameraManagerEngine } = await import('./generic/engine-generic');
|
||||
cameraManagerEngine = new GenericCameraManagerEngine(
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
this._entityRegistryManager,
|
||||
options.eventCallback,
|
||||
);
|
||||
@@ -48,6 +51,7 @@ export class CameraManagerEngineFactory {
|
||||
cameraManagerEngine = new FrigateCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
new RecordingSegmentsCache(),
|
||||
new CameraManagerRequestCache(),
|
||||
options.eventCallback,
|
||||
@@ -60,6 +64,7 @@ export class CameraManagerEngineFactory {
|
||||
cameraManagerEngine = new MotionEyeCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
new BrowseMediaWalker(),
|
||||
options.resolvedMediaCache,
|
||||
new CameraManagerRequestCache(),
|
||||
@@ -72,6 +77,7 @@ export class CameraManagerEngineFactory {
|
||||
this._entityRegistryManager,
|
||||
this._deviceRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
new BrowseMediaWalker(),
|
||||
options.resolvedMediaCache,
|
||||
new CameraManagerRequestCache(),
|
||||
@@ -83,6 +89,7 @@ export class CameraManagerEngineFactory {
|
||||
cameraManagerEngine = new TPLinkCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
options.eventCallback,
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -457,7 +457,7 @@ export class FrigateCamera extends Camera {
|
||||
): Promise<void> {
|
||||
const config = this.getConfig();
|
||||
if (
|
||||
!config.triggers.events.length ||
|
||||
!config.triggers.media_events.length ||
|
||||
!config.frigate.camera_name ||
|
||||
!config.frigate.client_id
|
||||
) {
|
||||
@@ -500,12 +500,12 @@ export class FrigateCamera extends Camera {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventsToTriggerOn = config.triggers.events;
|
||||
const mediaEventsToTriggerOn = config.triggers.media_events;
|
||||
if (
|
||||
!(
|
||||
eventsToTriggerOn.includes('events') ||
|
||||
(eventsToTriggerOn.includes('snapshots') && snapshotChange) ||
|
||||
(eventsToTriggerOn.includes('clips') && clipChange)
|
||||
mediaEventsToTriggerOn.includes('events') ||
|
||||
(mediaEventsToTriggerOn.includes('snapshots') && snapshotChange) ||
|
||||
(mediaEventsToTriggerOn.includes('clips') && clipChange)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -518,8 +518,8 @@ export class FrigateCamera extends Camera {
|
||||
type: ev.type,
|
||||
// In cases where there are both clip and snapshot media, ensure to only
|
||||
// trigger on the media type that is allowed by the configuration.
|
||||
clip: clipChange && eventsToTriggerOn.includes('clips'),
|
||||
snapshot: snapshotChange && eventsToTriggerOn.includes('snapshots'),
|
||||
clip: clipChange && mediaEventsToTriggerOn.includes('clips'),
|
||||
snapshot: snapshotChange && mediaEventsToTriggerOn.includes('snapshots'),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { add, endOfHour, fromUnixTime, startOfHour } from 'date-fns';
|
||||
import { isEqual, orderBy, throttle, uniqWith } from 'lodash-es';
|
||||
import { EventWatcherSubscriptionInterface } from '../../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title';
|
||||
@@ -136,11 +137,12 @@ export class FrigateCameraManagerEngine
|
||||
constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
recordingSegmentsCache: RecordingSegmentsCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, entityRegistryManager, eventCallback);
|
||||
super(stateWatcher, eventWatcher, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._frigateEventWatcher = new FrigateEventWatcher();
|
||||
this._frigateReviewWatcher = new FrigateReviewWatcher();
|
||||
@@ -163,6 +165,7 @@ export class FrigateCameraManagerEngine
|
||||
hass,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
frigateEventWatcher: this._frigateEventWatcher,
|
||||
frigateReviewWatcher: this._frigateReviewWatcher,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { HomeAssistant, SubscriptionUnsubscribe } from '../../ha/types';
|
||||
import {
|
||||
FrigateEventChange,
|
||||
FrigateReviewChange,
|
||||
@@ -20,8 +20,6 @@ export interface FrigateWatcherSubscriptionInterface<T> {
|
||||
unsubscribe(request: FrigateWatcherRequest<T>): void;
|
||||
}
|
||||
|
||||
type SubscriptionUnsubscribe = () => Promise<void>;
|
||||
|
||||
/**
|
||||
* Base class for Frigate WebSocket watchers.
|
||||
* Handles subscription management and message routing to callbacks.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { EventWatcherSubscriptionInterface } from '../../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title';
|
||||
@@ -41,14 +42,17 @@ import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
protected _eventCallback?: CameraEventCallback;
|
||||
protected _stateWatcher: StateWatcherSubscriptionInterface;
|
||||
protected _eventWatcher: EventWatcherSubscriptionInterface;
|
||||
protected _entityRegistryManager?: EntityRegistryManager;
|
||||
|
||||
constructor(
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
entityRegistryManager?: EntityRegistryManager,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
this._stateWatcher = stateWatcher;
|
||||
this._eventWatcher = eventWatcher;
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._eventCallback = eventCallback;
|
||||
}
|
||||
@@ -66,6 +70,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
}).initialize({
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
capabilityOptions: {
|
||||
raw: {
|
||||
|
||||
@@ -205,6 +205,7 @@ export class CameraManager {
|
||||
(await this._engineFactory.createEngine(engineType, {
|
||||
eventCallback: (ev) => this._api.getTriggersManager().handleCameraEvent(ev),
|
||||
stateWatcher: this._api.getHASSManager().getStateWatcher(),
|
||||
eventWatcher: this._api.getHASSManager().getEventWatcher(),
|
||||
resolvedMediaCache: this._api.getResolvedMediaCache(),
|
||||
}))
|
||||
: null;
|
||||
|
||||
@@ -78,6 +78,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { add, endOfDay, parse, startOfDay } from 'date-fns';
|
||||
import { orderBy } from 'lodash-es';
|
||||
import { EventWatcherSubscriptionInterface } from '../../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media';
|
||||
@@ -61,6 +62,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
deviceRegistryManager: DeviceRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
browseMediaManager: BrowseMediaWalker,
|
||||
resolvedMediaCache: ResolvedMediaCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
@@ -69,6 +71,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
super(
|
||||
entityRegistryManager,
|
||||
stateWatcher,
|
||||
eventWatcher,
|
||||
browseMediaManager,
|
||||
resolvedMediaCache,
|
||||
requestCache,
|
||||
@@ -176,6 +179,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
deviceRegistryManager: this._deviceRegistryManager,
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EventWatcherSubscriptionInterface } from '../../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
@@ -11,9 +12,10 @@ export class TPLinkCameraManagerEngine extends GenericCameraManagerEngine {
|
||||
constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, entityRegistryManager, eventCallback);
|
||||
super(stateWatcher, eventWatcher, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
}
|
||||
|
||||
@@ -32,6 +34,7 @@ export class TPLinkCameraManagerEngine extends GenericCameraManagerEngine {
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ export interface CameraEvent {
|
||||
| 'update' // An update for an event is available (except GenAI).
|
||||
| 'end' // An event has ended.
|
||||
| 'genai' // An AI based update is available.
|
||||
| 'signal'; // A momentary signal (no start/end, just an instant).
|
||||
| 'momentary'; // A momentary event (no start/end, just an instant).
|
||||
|
||||
// When fidelity is `high`, the engine is assumed to provide exact details of
|
||||
// what new media is available. Otherwise all media types are assumed to be
|
||||
|
||||
Reference in New Issue
Block a user