feat: Add event-based automation triggers (#2537)
This commit is contained in:
committed by
dermotduffy
parent
b701366762
commit
a31816c168
@@ -1,5 +1,4 @@
|
||||
import { EventWatcherSubscriptionInterface } from '../../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { HASSManagerReadonlyInterface } from '../../card-controller/hass/types';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../ha/browse-media/types';
|
||||
import { BrowseMediaWalker } from '../../ha/browse-media/walker';
|
||||
@@ -37,14 +36,13 @@ export class BrowseMediaCameraManagerEngine
|
||||
|
||||
public constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
hassManager: HASSManagerReadonlyInterface,
|
||||
browseMediaManager: BrowseMediaWalker,
|
||||
resolvedMediaCache: ResolvedMediaCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventWatcher, entityRegistryManager, eventCallback);
|
||||
super(hassManager, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._browseMediaWalker = browseMediaManager;
|
||||
this._resolvedMediaCache = resolvedMediaCache;
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
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 { EventSubscriptionRequest } from '../card-controller/hass/event-watcher';
|
||||
import { HASSManagerReadonlyInterface } from '../card-controller/hass/types';
|
||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig, TriggerEvent } from '../config/schema/cameras';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { HAEvent } from '../config/schema/common/ha-event';
|
||||
import { EnabledProxyConfig, resolveProxyConfig } from '../config/schema/common/proxy';
|
||||
import { computeDomain } from '../ha/compute-domain';
|
||||
import { matchesEventData } from '../ha/event-data-match';
|
||||
import { matchesEventContext, matchesEventData } from '../ha/event-match';
|
||||
import { getTriggerEventType } from '../ha/get-trigger-event-type';
|
||||
import { Entity, EntityRegistryManager } from '../ha/registry/entity/types';
|
||||
import { HassStateDifference, HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { CapabilitiesRaw, CapabilityKey, Endpoint } from '../types';
|
||||
import { arrayify } from '../utils/basic';
|
||||
import { liveProviderSupports2WayAudio } from '../utils/live-provider';
|
||||
import { Capabilities } from './capabilities';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
@@ -43,9 +42,7 @@ interface CapabilityOptions {
|
||||
}
|
||||
|
||||
export interface CameraInitializationOptions {
|
||||
hass: HomeAssistant;
|
||||
stateWatcher: StateWatcherSubscriptionInterface;
|
||||
eventWatcher: EventWatcherSubscriptionInterface;
|
||||
hassManager: HASSManagerReadonlyInterface;
|
||||
capabilityOptions?: CapabilityOptions;
|
||||
entityRegistryManager?: EntityRegistryManager;
|
||||
}
|
||||
@@ -58,7 +55,6 @@ export class Camera {
|
||||
protected _capabilities?: Capabilities;
|
||||
protected _eventCallback?: CameraEventCallback;
|
||||
protected _destroyCallbacks: DestroyCallback[] = [];
|
||||
protected _destroyed = false;
|
||||
protected _entity: Entity | null = null;
|
||||
|
||||
constructor(
|
||||
@@ -80,63 +76,57 @@ export class Camera {
|
||||
}
|
||||
|
||||
async initialize(options: CameraInitializationOptions): Promise<Camera> {
|
||||
this._entity = await this._resolveEntity(options);
|
||||
await this._initialize(options);
|
||||
// Freeze a single HASS snapshot for the whole (async, multi-step)
|
||||
// initialization so every step observes a consistent entity world; live
|
||||
// subscriptions below still use the manager's current watchers.
|
||||
const hass = options.hassManager.getHASS();
|
||||
if (!hass) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this._entity = await this._resolveEntity(hass, options);
|
||||
await this._initialize(hass, options);
|
||||
|
||||
this._capabilities =
|
||||
options.capabilityOptions?.capabilities ??
|
||||
this._capabilities ??
|
||||
(await this._buildCapabilities(options));
|
||||
(await this._buildCapabilities(hass, options));
|
||||
|
||||
if (this._capabilities.has('trigger')) {
|
||||
await this._getTriggerEntities(options);
|
||||
await this._getTriggerEntities(hass, options);
|
||||
this._config.triggers.entities = uniq(this._config.triggers.entities);
|
||||
|
||||
// Subscribe to state based triggers (sync; no race with destroy).
|
||||
options.stateWatcher.subscribe(
|
||||
this._stateChangeHandler,
|
||||
this._config.triggers.entities,
|
||||
);
|
||||
this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler));
|
||||
const stateWatcher = options.hassManager.getStateWatcher();
|
||||
stateWatcher.subscribe(this._stateChangeHandler, this._config.triggers.entities);
|
||||
this._onDestroy(() => stateWatcher.unsubscribe(this._stateChangeHandler));
|
||||
|
||||
// Subscribe to event based triggers.
|
||||
// Subscribe to event based triggers. List-form `event_type` expands into
|
||||
// one subscription per type sharing the same data/context matcher.
|
||||
const eventWatcher = options.hassManager.getEventWatcher();
|
||||
for (const event of this._config.triggers.events) {
|
||||
const request = this._buildEventSubscriptionRequest(event);
|
||||
await this._setupSubscription(
|
||||
() => options.eventWatcher.subscribe(options.hass, request),
|
||||
() => options.eventWatcher.unsubscribe(request),
|
||||
);
|
||||
for (const request of this._buildEventSubscriptionRequests(event)) {
|
||||
eventWatcher.subscribe(request);
|
||||
this._onDestroy(() => eventWatcher.unsubscribe(request));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up an async subscription with its teardown. Registers the unsubscribe
|
||||
* callback synchronously before awaiting subscribe, so a destroy during the
|
||||
* await reliably triggers cleanup; short-circuits if destroy has already
|
||||
* run, so the cleanup callback can't fire (and enqueue an unsubscribe)
|
||||
* before the subscribe runs.
|
||||
*/
|
||||
protected async _setupSubscription(
|
||||
subscribe: () => Promise<void>,
|
||||
unsubscribe: () => void | Promise<void>,
|
||||
): Promise<void> {
|
||||
if (this._destroyed) {
|
||||
return;
|
||||
}
|
||||
this._onDestroy(unsubscribe);
|
||||
await subscribe();
|
||||
}
|
||||
|
||||
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 _buildEventSubscriptionRequests(event: HAEvent): EventSubscriptionRequest[] {
|
||||
const dataFilter = event.event_data;
|
||||
const contextFilter = event.context;
|
||||
return uniq(arrayify(event.event_type)).map((eventType) => ({
|
||||
event_type: eventType,
|
||||
...((dataFilter || contextFilter) && {
|
||||
matcher: (evt) =>
|
||||
(!dataFilter || matchesEventData(dataFilter, evt.data)) &&
|
||||
(!contextFilter || matchesEventContext(contextFilter, evt.context)),
|
||||
}),
|
||||
callback: () => this._momentaryEventHandler(eventType),
|
||||
}));
|
||||
}
|
||||
|
||||
private _momentaryEventHandler(eventType: string): void {
|
||||
@@ -148,13 +138,14 @@ export class Camera {
|
||||
}
|
||||
|
||||
private async _resolveEntity(
|
||||
hass: HomeAssistant,
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<Entity | null> {
|
||||
const cameraEntityID = getCameraEntityFromConfig(this._config);
|
||||
if (!cameraEntityID || !options.entityRegistryManager) {
|
||||
return null;
|
||||
}
|
||||
return await options.entityRegistryManager.getEntity(options.hass, cameraEntityID);
|
||||
return await options.entityRegistryManager.getEntity(hass, cameraEntityID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,12 +153,14 @@ export class Camera {
|
||||
* to add engine-specific discovery; call `super` to keep the base discoveries.
|
||||
*/
|
||||
protected async _getTriggerEntities(
|
||||
hass: HomeAssistant,
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await this._getDoorbellEntities(options);
|
||||
await this._getDoorbellEntities(hass, options);
|
||||
}
|
||||
|
||||
private async _getDoorbellEntities(
|
||||
hass: HomeAssistant,
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
if (
|
||||
@@ -183,7 +176,7 @@ export class Camera {
|
||||
// narrow by `device_id` + domain first and filter by device_class against
|
||||
// `hass.states` second.
|
||||
const candidates = await options.entityRegistryManager.getMatchingEntities(
|
||||
options.hass,
|
||||
hass,
|
||||
(ent) =>
|
||||
ent.device_id === deviceID &&
|
||||
!ent.disabled_by &&
|
||||
@@ -192,8 +185,7 @@ export class Camera {
|
||||
|
||||
const doorbells = candidates
|
||||
.filter(
|
||||
(ent) =>
|
||||
options.hass.states[ent.entity_id]?.attributes?.device_class === 'doorbell',
|
||||
(ent) => hass.states[ent.entity_id]?.attributes?.device_class === 'doorbell',
|
||||
)
|
||||
.map((ent) => ent.entity_id);
|
||||
|
||||
@@ -204,16 +196,19 @@ export class Camera {
|
||||
* Subclass initialization hook. Override for async initialization work.
|
||||
*/
|
||||
protected async _initialize(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_hass: HomeAssistant,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_options: CameraInitializationOptions,
|
||||
): Promise<void> {}
|
||||
|
||||
protected async _buildCapabilities(
|
||||
hass: HomeAssistant,
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<Capabilities> {
|
||||
const rawCapabilities = await this._getRawCapabilities(options);
|
||||
const rawCapabilities = await this._getRawCapabilities(hass, options);
|
||||
const config = this.getConfig();
|
||||
const has2WayAudio = await this._has2WayAudioCapability(options.hass);
|
||||
const has2WayAudio = await this._has2WayAudioCapability(hass);
|
||||
|
||||
return new Capabilities(
|
||||
{
|
||||
@@ -254,6 +249,7 @@ export class Camera {
|
||||
* and call super._getRawCapabilities() to extend defaults.
|
||||
*/
|
||||
protected async _getRawCapabilities(
|
||||
_hass: HomeAssistant,
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<CapabilitiesRaw> {
|
||||
return {
|
||||
@@ -267,7 +263,6 @@ export class Camera {
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
this._destroyed = true;
|
||||
const callbacks = this._destroyCallbacks;
|
||||
this._destroyCallbacks = [];
|
||||
await Promise.all(callbacks.map((callback) => callback()));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { EventWatcherSubscriptionInterface } from '../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
||||
import { HASSManagerReadonlyInterface } from '../card-controller/hass/types';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { BrowseMediaWalker } from '../ha/browse-media/walker';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device';
|
||||
@@ -13,8 +12,7 @@ import { CameraEventCallback, CameraManagerRequestCache, Engine } from './types'
|
||||
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
|
||||
|
||||
interface CameraManagerEngineFactoryOptions {
|
||||
stateWatcher: StateWatcherSubscriptionInterface;
|
||||
eventWatcher: EventWatcherSubscriptionInterface;
|
||||
hassManager: HASSManagerReadonlyInterface;
|
||||
resolvedMediaCache: ResolvedMediaCache;
|
||||
eventCallback?: CameraEventCallback;
|
||||
}
|
||||
@@ -40,8 +38,7 @@ export class CameraManagerEngineFactory {
|
||||
case Engine.Generic:
|
||||
const { GenericCameraManagerEngine } = await import('./generic/engine-generic');
|
||||
cameraManagerEngine = new GenericCameraManagerEngine(
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
options.hassManager,
|
||||
this._entityRegistryManager,
|
||||
options.eventCallback,
|
||||
);
|
||||
@@ -50,8 +47,7 @@ export class CameraManagerEngineFactory {
|
||||
const { FrigateCameraManagerEngine } = await import('./frigate/engine-frigate');
|
||||
cameraManagerEngine = new FrigateCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
options.hassManager,
|
||||
new RecordingSegmentsCache(),
|
||||
new CameraManagerRequestCache(),
|
||||
options.eventCallback,
|
||||
@@ -63,8 +59,7 @@ export class CameraManagerEngineFactory {
|
||||
);
|
||||
cameraManagerEngine = new MotionEyeCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
options.hassManager,
|
||||
new BrowseMediaWalker(),
|
||||
options.resolvedMediaCache,
|
||||
new CameraManagerRequestCache(),
|
||||
@@ -76,8 +71,7 @@ export class CameraManagerEngineFactory {
|
||||
cameraManagerEngine = new ReolinkCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
this._deviceRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
options.hassManager,
|
||||
new BrowseMediaWalker(),
|
||||
options.resolvedMediaCache,
|
||||
new CameraManagerRequestCache(),
|
||||
@@ -88,8 +82,7 @@ export class CameraManagerEngineFactory {
|
||||
const { TPLinkCameraManagerEngine } = await import('./tplink/engine-tplink');
|
||||
cameraManagerEngine = new TPLinkCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
options.eventWatcher,
|
||||
options.hassManager,
|
||||
options.eventCallback,
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
||||
export interface CameraManagerEngine {
|
||||
getEngineType(): Engine;
|
||||
|
||||
createCamera(hass: HomeAssistant, cameraConfig: CameraConfig): Promise<Camera>;
|
||||
createCamera(cameraConfig: CameraConfig): Promise<Camera>;
|
||||
|
||||
/**
|
||||
* Get default query parameters for a camera based on its configuration.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { Camera, CameraInitializationOptions } from './camera';
|
||||
import { CameraNoEntityError } from './error';
|
||||
|
||||
@@ -9,11 +10,12 @@ import { CameraNoEntityError } from './error';
|
||||
*/
|
||||
export class EntityCamera extends Camera {
|
||||
protected override async _initialize(
|
||||
hass: HomeAssistant,
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
if (!this._entity) {
|
||||
throw new CameraNoEntityError(this.getConfig());
|
||||
}
|
||||
await super._initialize(options);
|
||||
await super._initialize(hass, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,17 +43,33 @@ export const isBirdseye = (cameraConfig: CameraConfig): boolean => {
|
||||
};
|
||||
|
||||
export class FrigateCamera extends Camera {
|
||||
// Short-circuits subscription when destroy() was invoked while base
|
||||
// initialization was still awaiting. Set BEFORE awaiting `super.destroy()` so
|
||||
// an in-flight initialize() sees the flip immediately.
|
||||
private _destroyed = false;
|
||||
|
||||
public async initialize(options: FrigateCameraInitializationOptions): Promise<Camera> {
|
||||
await super.initialize(options);
|
||||
|
||||
// A destroy() during the await above means the camera is being torn down;
|
||||
// it must not register live subscriptions afterward.
|
||||
if (this._destroyed) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (this._capabilities?.has('trigger')) {
|
||||
await this._subscribeToEvents(options.hass, options.frigateEventWatcher);
|
||||
await this._subscribeToReviews(options.hass, options.frigateReviewWatcher);
|
||||
this._subscribeToEvents(options.frigateEventWatcher);
|
||||
this._subscribeToReviews(options.frigateReviewWatcher);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override async destroy(): Promise<void> {
|
||||
this._destroyed = true;
|
||||
await super.destroy();
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
executor: ActionsExecutor,
|
||||
action: PTZAction,
|
||||
@@ -104,9 +120,7 @@ export class FrigateCamera extends Camera {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override async _initialize(
|
||||
options: FrigateCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
protected override async _initialize(hass: HomeAssistant): Promise<void> {
|
||||
const config = this.getConfig();
|
||||
const hasCameraName = !!config.frigate?.camera_name;
|
||||
const cameraEntity = getCameraEntityFromConfig(config);
|
||||
@@ -126,7 +140,7 @@ export class FrigateCamera extends Camera {
|
||||
}
|
||||
|
||||
if (!this._config.frigate.client_id) {
|
||||
const stateEntity = cameraEntity ? options.hass.states[cameraEntity] : undefined;
|
||||
const stateEntity = cameraEntity ? hass.states[cameraEntity] : undefined;
|
||||
const clientID = stateEntity?.attributes?.client_id;
|
||||
if (typeof clientID === 'string' && clientID) {
|
||||
this._config.frigate.client_id = clientID;
|
||||
@@ -137,13 +151,15 @@ export class FrigateCamera extends Camera {
|
||||
}
|
||||
|
||||
protected override async _getTriggerEntities(
|
||||
hass: HomeAssistant,
|
||||
options: FrigateCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await this._getFrigateMotionAndOccupancyEntities(options);
|
||||
await super._getTriggerEntities(options);
|
||||
await this._getFrigateMotionAndOccupancyEntities(hass, options);
|
||||
await super._getTriggerEntities(hass, options);
|
||||
}
|
||||
|
||||
private async _getFrigateMotionAndOccupancyEntities(
|
||||
hass: HomeAssistant,
|
||||
options: FrigateCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
const config = this.getConfig();
|
||||
@@ -162,7 +178,7 @@ export class FrigateCamera extends Camera {
|
||||
// searching via unique_id ensures this still works if the user renames
|
||||
// the entity_id.
|
||||
const binarySensorEntities = await options.entityRegistryManager.getMatchingEntities(
|
||||
options.hass,
|
||||
hass,
|
||||
(ent) =>
|
||||
ent.config_entry_id === this._entity?.config_entry_id &&
|
||||
!ent.disabled_by &&
|
||||
@@ -189,12 +205,13 @@ export class FrigateCamera extends Camera {
|
||||
}
|
||||
|
||||
protected async _getRawCapabilities(
|
||||
hass: HomeAssistant,
|
||||
options: FrigateCameraInitializationOptions,
|
||||
): Promise<CapabilitiesRaw> {
|
||||
const base = await super._getRawCapabilities(options);
|
||||
const base = await super._getRawCapabilities(hass, options);
|
||||
const config = this.getConfig();
|
||||
|
||||
const frigatePTZ = await this._getPTZCapabilities(options.hass, config);
|
||||
const frigatePTZ = await this._getPTZCapabilities(hass, config);
|
||||
const configPTZ = getPTZCapabilitiesFromCameraConfig(config);
|
||||
const combinedPTZ: PTZCapabilities | null =
|
||||
configPTZ || frigatePTZ ? { ...frigatePTZ, ...configPTZ } : null;
|
||||
@@ -451,10 +468,9 @@ export class FrigateCamera extends Camera {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async _subscribeToEvents(
|
||||
hass: HomeAssistant,
|
||||
private _subscribeToEvents(
|
||||
frigateEventWatcher: FrigateWatcherSubscriptionInterface<FrigateEventChange>,
|
||||
): Promise<void> {
|
||||
): void {
|
||||
const config = this.getConfig();
|
||||
if (
|
||||
!config.triggers.media_events.length ||
|
||||
@@ -473,10 +489,8 @@ export class FrigateCamera extends Camera {
|
||||
event.after.camera === config.frigate.camera_name,
|
||||
};
|
||||
|
||||
await this._setupSubscription(
|
||||
() => frigateEventWatcher.subscribe(hass, request),
|
||||
() => frigateEventWatcher.unsubscribe(request),
|
||||
);
|
||||
frigateEventWatcher.subscribe(request);
|
||||
this._onDestroy(() => frigateEventWatcher.unsubscribe(request));
|
||||
}
|
||||
|
||||
private _frigateEventHandler = (ev: FrigateEventChange): void => {
|
||||
@@ -494,23 +508,33 @@ export class FrigateCamera extends Camera {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(config.frigate.zones?.length &&
|
||||
!config.frigate.zones.some((zone) => ev.after.current_zones.includes(zone))) ||
|
||||
(config.frigate.labels?.length && !config.frigate.labels.includes(ev.after.label))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaEventsToTriggerOn = config.triggers.media_events;
|
||||
if (
|
||||
!(
|
||||
mediaEventsToTriggerOn.includes('events') ||
|
||||
(mediaEventsToTriggerOn.includes('snapshots') && snapshotChange) ||
|
||||
(mediaEventsToTriggerOn.includes('clips') && clipChange)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
||||
// The zone/label/media checks decide when to START a trigger, so they only
|
||||
// apply to 'new'/'update'. An 'end' always passes through: it ends whatever
|
||||
// trigger an earlier event with the same id started, and by 'end' the
|
||||
// object may have left the zone or the media flag may differ -- the trigger
|
||||
// must still clear. (The trigger manager ignores an 'end' for an id that
|
||||
// never triggered, so a pass-through 'end' is harmless.)
|
||||
if (ev.type !== 'end') {
|
||||
if (
|
||||
(config.frigate.zones?.length &&
|
||||
!config.frigate.zones.some((zone) => ev.after.current_zones.includes(zone))) ||
|
||||
(config.frigate.labels?.length &&
|
||||
!config.frigate.labels.includes(ev.after.label))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
mediaEventsToTriggerOn.includes('events') ||
|
||||
(mediaEventsToTriggerOn.includes('snapshots') && snapshotChange) ||
|
||||
(mediaEventsToTriggerOn.includes('clips') && clipChange)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._eventCallback?.({
|
||||
@@ -525,10 +549,9 @@ export class FrigateCamera extends Camera {
|
||||
});
|
||||
};
|
||||
|
||||
private async _subscribeToReviews(
|
||||
hass: HomeAssistant,
|
||||
private _subscribeToReviews(
|
||||
frigateReviewWatcher: FrigateWatcherSubscriptionInterface<FrigateReviewChange>,
|
||||
): Promise<void> {
|
||||
): void {
|
||||
const config = this.getConfig();
|
||||
const reviewConfig = config.triggers.reviews;
|
||||
|
||||
@@ -550,10 +573,8 @@ export class FrigateCamera extends Camera {
|
||||
review.after.camera === config.frigate.camera_name,
|
||||
};
|
||||
|
||||
await this._setupSubscription(
|
||||
() => frigateReviewWatcher.subscribe(hass, request),
|
||||
() => frigateReviewWatcher.unsubscribe(request),
|
||||
);
|
||||
frigateReviewWatcher.subscribe(request);
|
||||
this._onDestroy(() => frigateReviewWatcher.unsubscribe(request));
|
||||
}
|
||||
|
||||
private _frigateReviewHandler = (review: FrigateReviewChange): void => {
|
||||
|
||||
@@ -1,7 +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 { HASSManagerReadonlyInterface } from '../../card-controller/hass/types';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
@@ -136,16 +135,15 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
hassManager: HASSManagerReadonlyInterface,
|
||||
recordingSegmentsCache: RecordingSegmentsCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventWatcher, entityRegistryManager, eventCallback);
|
||||
super(hassManager, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._frigateEventWatcher = new FrigateEventWatcher();
|
||||
this._frigateReviewWatcher = new FrigateReviewWatcher();
|
||||
this._frigateEventWatcher = new FrigateEventWatcher(hassManager);
|
||||
this._frigateReviewWatcher = new FrigateReviewWatcher(hassManager);
|
||||
this._recordingSegmentsCache = recordingSegmentsCache;
|
||||
this._requestCache = requestCache;
|
||||
}
|
||||
@@ -154,18 +152,13 @@ export class FrigateCameraManagerEngine
|
||||
return Engine.Frigate;
|
||||
}
|
||||
|
||||
public async createCamera(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
public async createCamera(cameraConfig: CameraConfig): Promise<Camera> {
|
||||
const camera = new FrigateCamera(cameraConfig, this, {
|
||||
eventCallback: this._eventCallback,
|
||||
});
|
||||
return await camera.initialize({
|
||||
hass,
|
||||
hassManager: this._hassManager,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
frigateEventWatcher: this._frigateEventWatcher,
|
||||
frigateReviewWatcher: this._frigateReviewWatcher,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { KeyedSubscriptionManager } from '../../utils/concurrency/keyed-subscription-manager';
|
||||
import { HASSConnectionSubscriptionManager } from '../../ha/connection/subscription-manager';
|
||||
import { HASSSource } from '../../ha/source';
|
||||
import {
|
||||
FrigateEventChange,
|
||||
FrigateReviewChange,
|
||||
@@ -17,43 +17,48 @@ export interface FrigateWatcherRequest<T> {
|
||||
|
||||
// Generic subscription interface
|
||||
export interface FrigateWatcherSubscriptionInterface<T> {
|
||||
subscribe(hass: HomeAssistant, request: FrigateWatcherRequest<T>): Promise<void>;
|
||||
unsubscribe(request: FrigateWatcherRequest<T>): Promise<void>;
|
||||
subscribe(request: FrigateWatcherRequest<T>): void;
|
||||
unsubscribe(request: FrigateWatcherRequest<T>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for Frigate WebSocket watchers. Counted per `instanceID`: the
|
||||
* first subscriber for an instance opens the WS subscription, the last to
|
||||
* unsubscribe tears it down. Each message is parsed, schema-validated, and
|
||||
* fanned out to every registered request whose `instanceID` matches and whose
|
||||
* `matcher` accepts the payload.
|
||||
* Base class for Frigate WebSocket watchers. Thin wrapper over
|
||||
* `HASSConnectionSubscriptionManager`: keys by `instanceID`, parses and
|
||||
* schema-validates each message, then fans out to every registered request
|
||||
* whose `instanceID` matches and whose optional `matcher` accepts the payload.
|
||||
*/
|
||||
abstract class FrigateWatcher<T> implements FrigateWatcherSubscriptionInterface<T> {
|
||||
protected abstract _type: string;
|
||||
protected abstract _schema: z.ZodType<T>;
|
||||
|
||||
private _subscriptions = new KeyedSubscriptionManager<
|
||||
string,
|
||||
FrigateWatcherRequest<T>
|
||||
>((request) => request.instanceID);
|
||||
private _manager: HASSConnectionSubscriptionManager<string, FrigateWatcherRequest<T>>;
|
||||
|
||||
public async subscribe(
|
||||
hass: HomeAssistant,
|
||||
request: FrigateWatcherRequest<T>,
|
||||
): Promise<void> {
|
||||
await this._subscriptions.subscribe(request, () =>
|
||||
hass.connection.subscribeMessage<string>(
|
||||
(data) => this._receiveHandler(request.instanceID, data),
|
||||
constructor(source: HASSSource) {
|
||||
this._manager = new HASSConnectionSubscriptionManager(
|
||||
(request) => request.instanceID,
|
||||
source,
|
||||
);
|
||||
}
|
||||
|
||||
public subscribe(request: FrigateWatcherRequest<T>): void {
|
||||
this._manager.subscribe(request, (connection, liveness) =>
|
||||
connection.subscribeMessage<string>(
|
||||
(data) => {
|
||||
if (!liveness.isConnected()) {
|
||||
return;
|
||||
}
|
||||
this._receive(request.instanceID, data);
|
||||
},
|
||||
{ type: this._type, instance_id: request.instanceID },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public async unsubscribe(request: FrigateWatcherRequest<T>): Promise<void> {
|
||||
await this._subscriptions.unsubscribe(request);
|
||||
public unsubscribe(request: FrigateWatcherRequest<T>): void {
|
||||
this._manager.unsubscribe(request);
|
||||
}
|
||||
|
||||
protected _receiveHandler(instanceID: string, data: string): void {
|
||||
private _receive(instanceID: string, data: string): void {
|
||||
let json: unknown;
|
||||
try {
|
||||
json = JSON.parse(data);
|
||||
@@ -69,7 +74,7 @@ abstract class FrigateWatcher<T> implements FrigateWatcherSubscriptionInterface<
|
||||
return;
|
||||
}
|
||||
|
||||
for (const request of this._subscriptions.getRequestsForKey(instanceID)) {
|
||||
for (const request of this._manager.getRequestsForKey(instanceID)) {
|
||||
if (!request.matcher || request.matcher(parseResult.data)) {
|
||||
request.callback(parseResult.data);
|
||||
}
|
||||
|
||||
@@ -1,7 +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 { HASSManagerReadonlyInterface } from '../../card-controller/hass/types';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
@@ -41,18 +40,15 @@ import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
|
||||
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
protected _eventCallback?: CameraEventCallback;
|
||||
protected _stateWatcher: StateWatcherSubscriptionInterface;
|
||||
protected _eventWatcher: EventWatcherSubscriptionInterface;
|
||||
protected _hassManager: HASSManagerReadonlyInterface;
|
||||
protected _entityRegistryManager?: EntityRegistryManager;
|
||||
|
||||
constructor(
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
hassManager: HASSManagerReadonlyInterface,
|
||||
entityRegistryManager?: EntityRegistryManager,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
this._stateWatcher = stateWatcher;
|
||||
this._eventWatcher = eventWatcher;
|
||||
this._hassManager = hassManager;
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._eventCallback = eventCallback;
|
||||
}
|
||||
@@ -61,16 +57,11 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
return Engine.Generic;
|
||||
}
|
||||
|
||||
public async createCamera(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
public async createCamera(cameraConfig: CameraConfig): Promise<Camera> {
|
||||
return await new Camera(cameraConfig, this, {
|
||||
eventCallback: this._eventCallback,
|
||||
}).initialize({
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
hassManager: this._hassManager,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
capabilityOptions: {
|
||||
raw: {
|
||||
|
||||
@@ -205,8 +205,7 @@ export class CameraManager {
|
||||
(await this._engineFactory.createEngine(engineType, {
|
||||
eventCallback: (ev) =>
|
||||
this._api.getCameraTriggersManager().handleCameraEvent(ev),
|
||||
stateWatcher: this._api.getHASSManager().getStateWatcher(),
|
||||
eventWatcher: this._api.getHASSManager().getEventWatcher(),
|
||||
hassManager: this._api.getHASSManager(),
|
||||
resolvedMediaCache: this._api.getResolvedMediaCache(),
|
||||
}))
|
||||
: null;
|
||||
@@ -253,7 +252,7 @@ export class CameraManager {
|
||||
// Configuration is initialized in parallel.
|
||||
const cameras = await allPromises(
|
||||
engineByConfig.entries(),
|
||||
async ([cameraConfig, engine]) => await engine.createCamera(hass, cameraConfig),
|
||||
async ([cameraConfig, engine]) => await engine.createCamera(cameraConfig),
|
||||
);
|
||||
|
||||
const destroyCameras = async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { CapabilitiesRaw, Endpoint } from '../../types';
|
||||
import { CameraInitializationOptions } from '../camera';
|
||||
import { EntityCamera } from '../entity-camera';
|
||||
@@ -20,12 +21,13 @@ export class MotionEyeCamera extends EntityCamera {
|
||||
}
|
||||
|
||||
protected async _getRawCapabilities(
|
||||
hass: HomeAssistant,
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<CapabilitiesRaw> {
|
||||
const ptz = getPTZCapabilitiesFromCameraConfig(this.getConfig());
|
||||
|
||||
return {
|
||||
...(await super._getRawCapabilities(options)),
|
||||
...(await super._getRawCapabilities(hass, options)),
|
||||
clips: true,
|
||||
snapshots: true,
|
||||
...(ptz && { ptz }),
|
||||
|
||||
@@ -67,18 +67,13 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
return Engine.MotionEye;
|
||||
}
|
||||
|
||||
public async createCamera(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
public async createCamera(cameraConfig: CameraConfig): Promise<Camera> {
|
||||
const camera = new MotionEyeCamera(cameraConfig, this, {
|
||||
eventCallback: this._eventCallback,
|
||||
});
|
||||
return await camera.initialize({
|
||||
hassManager: this._hassManager,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -172,14 +172,12 @@ export class ReolinkCamera extends EntityCamera {
|
||||
}
|
||||
|
||||
protected async _initialize(
|
||||
hass: HomeAssistant,
|
||||
options: ReolinkCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await super._initialize(options);
|
||||
await this._initializeChannel(options.hass, options.deviceRegistryManager);
|
||||
this._ptzEntities = await this._getPTZEntities(
|
||||
options.hass,
|
||||
options.entityRegistryManager,
|
||||
);
|
||||
await super._initialize(hass, options);
|
||||
await this._initializeChannel(hass, options.deviceRegistryManager);
|
||||
this._ptzEntities = await this._getPTZEntities(hass, options.entityRegistryManager);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -188,18 +186,19 @@ export class ReolinkCamera extends EntityCamera {
|
||||
}
|
||||
|
||||
protected async _getRawCapabilities(
|
||||
hass: HomeAssistant,
|
||||
options: ReolinkCameraInitializationOptions,
|
||||
): Promise<CapabilitiesRaw> {
|
||||
const configPTZ = getPTZCapabilitiesFromCameraConfig(this.getConfig());
|
||||
const reolinkPTZ = this._ptzEntities
|
||||
? this._entitiesToCapabilities(options.hass, this._ptzEntities)
|
||||
? this._entitiesToCapabilities(hass, this._ptzEntities)
|
||||
: null;
|
||||
|
||||
const combinedPTZ: PTZCapabilities | null =
|
||||
configPTZ || reolinkPTZ ? { ...reolinkPTZ, ...configPTZ } : null;
|
||||
|
||||
return {
|
||||
...(await super._getRawCapabilities(options)),
|
||||
...(await super._getRawCapabilities(hass, options)),
|
||||
clips: true,
|
||||
...(combinedPTZ && { ptz: combinedPTZ }),
|
||||
};
|
||||
|
||||
@@ -1,7 +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 { HASSManagerReadonlyInterface } from '../../card-controller/hass/types';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media';
|
||||
import { sortMostRecentFirst } from '../../ha/browse-media/sort';
|
||||
@@ -61,8 +60,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
public constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
deviceRegistryManager: DeviceRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
hassManager: HASSManagerReadonlyInterface,
|
||||
browseMediaManager: BrowseMediaWalker,
|
||||
resolvedMediaCache: ResolvedMediaCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
@@ -70,8 +68,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
) {
|
||||
super(
|
||||
entityRegistryManager,
|
||||
stateWatcher,
|
||||
eventWatcher,
|
||||
hassManager,
|
||||
browseMediaManager,
|
||||
resolvedMediaCache,
|
||||
requestCache,
|
||||
@@ -167,19 +164,14 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
: null;
|
||||
}
|
||||
|
||||
public async createCamera(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
public async createCamera(cameraConfig: CameraConfig): Promise<Camera> {
|
||||
const camera = new ReolinkCamera(cameraConfig, this, {
|
||||
eventCallback: this._eventCallback,
|
||||
});
|
||||
return await camera.initialize({
|
||||
hassManager: this._hassManager,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
deviceRegistryManager: this._deviceRegistryManager,
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,16 +23,15 @@ export class TPLinkCamera extends EntityCamera {
|
||||
private _ptzEntities: PTZEntities | null = null;
|
||||
|
||||
protected async _initialize(
|
||||
hass: HomeAssistant,
|
||||
options: TPLinkCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await super._initialize(options);
|
||||
this._ptzEntities = await this._getPTZEntities(
|
||||
options.hass,
|
||||
options.entityRegistryManager,
|
||||
);
|
||||
await super._initialize(hass, options);
|
||||
this._ptzEntities = await this._getPTZEntities(hass, options.entityRegistryManager);
|
||||
}
|
||||
|
||||
protected async _getRawCapabilities(
|
||||
hass: HomeAssistant,
|
||||
options: TPLinkCameraInitializationOptions,
|
||||
): Promise<CapabilitiesRaw> {
|
||||
const configPTZ = getPTZCapabilitiesFromCameraConfig(this.getConfig());
|
||||
@@ -44,7 +43,7 @@ export class TPLinkCamera extends EntityCamera {
|
||||
configPTZ || tplinkPTZ ? { ...tplinkPTZ, ...configPTZ } : null;
|
||||
|
||||
return {
|
||||
...(await super._getRawCapabilities(options)),
|
||||
...(await super._getRawCapabilities(hass, options)),
|
||||
...(combinedPTZ && { ptz: combinedPTZ }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { EventWatcherSubscriptionInterface } from '../../card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { HASSManagerReadonlyInterface } from '../../card-controller/hass/types';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
@@ -11,11 +10,10 @@ import { TPLinkCamera } from './camera';
|
||||
export class TPLinkCameraManagerEngine extends GenericCameraManagerEngine {
|
||||
constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
hassManager: HASSManagerReadonlyInterface,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventWatcher, entityRegistryManager, eventCallback);
|
||||
super(hassManager, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
}
|
||||
|
||||
@@ -23,18 +21,13 @@ export class TPLinkCameraManagerEngine extends GenericCameraManagerEngine {
|
||||
return Engine.TPLink;
|
||||
}
|
||||
|
||||
public async createCamera(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
public async createCamera(cameraConfig: CameraConfig): Promise<Camera> {
|
||||
const camera = new TPLinkCamera(cameraConfig, this, {
|
||||
eventCallback: this._eventCallback,
|
||||
});
|
||||
return await camera.initialize({
|
||||
hassManager: this._hassManager,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
eventWatcher: this._eventWatcher,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user