feat: Add automatic doorbell detection (#2507)
This commit is contained in:
committed by
dermotduffy
parent
4b72fcd629
commit
773cee95a5
@@ -42,7 +42,7 @@ export class BrowseMediaCameraManagerEngine
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventCallback);
|
||||
super(stateWatcher, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._browseMediaWalker = browseMediaManager;
|
||||
this._resolvedMediaCache = resolvedMediaCache;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { uniq } from 'lodash-es';
|
||||
import { ActionsExecutor } from '../card-controller/actions/types';
|
||||
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { EnabledProxyConfig, resolveProxyConfig } from '../config/schema/common/proxy';
|
||||
import { computeDomain } from '../ha/compute-domain';
|
||||
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';
|
||||
@@ -17,6 +20,7 @@ import {
|
||||
CameraEventCallback,
|
||||
CameraProxyConfig,
|
||||
} from './types';
|
||||
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
|
||||
import {
|
||||
getGo2RTCMetadataEndpoint,
|
||||
getGo2RTCStreamEndpoint,
|
||||
@@ -37,6 +41,7 @@ export interface CameraInitializationOptions {
|
||||
hass: HomeAssistant;
|
||||
stateWatcher: StateWatcherSubscriptionInterface;
|
||||
capabilityOptions?: CapabilityOptions;
|
||||
entityRegistryManager?: EntityRegistryManager;
|
||||
}
|
||||
|
||||
type DestroyCallback = () => void | Promise<void>;
|
||||
@@ -47,6 +52,7 @@ export class Camera {
|
||||
protected _capabilities?: Capabilities;
|
||||
protected _eventCallback?: CameraEventCallback;
|
||||
protected _destroyCallbacks: DestroyCallback[] = [];
|
||||
protected _entity: Entity | null = null;
|
||||
|
||||
constructor(
|
||||
config: CameraConfig,
|
||||
@@ -62,17 +68,86 @@ export class Camera {
|
||||
this._capabilities = options?.capabilities;
|
||||
}
|
||||
|
||||
public getEntity(): Entity | null {
|
||||
return this._entity;
|
||||
}
|
||||
|
||||
async initialize(options: CameraInitializationOptions): Promise<Camera> {
|
||||
this._entity = await this._resolveEntity(options);
|
||||
await this._initialize(options);
|
||||
|
||||
this._capabilities =
|
||||
options.capabilityOptions?.capabilities ??
|
||||
this._capabilities ??
|
||||
(await this._buildCapabilities(options));
|
||||
this._subscribeBasedOnCapabilities(options.stateWatcher);
|
||||
this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler));
|
||||
|
||||
if (this._capabilities.has('trigger')) {
|
||||
await this._getTriggerEntities(options);
|
||||
this._config.triggers.entities = uniq(this._config.triggers.entities);
|
||||
|
||||
options.stateWatcher.subscribe(
|
||||
this._stateChangeHandler,
|
||||
this._config.triggers.entities,
|
||||
);
|
||||
this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private async _resolveEntity(
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<Entity | null> {
|
||||
const cameraEntityID = getCameraEntityFromConfig(this._config);
|
||||
if (!cameraEntityID || !options.entityRegistryManager) {
|
||||
return null;
|
||||
}
|
||||
return await options.entityRegistryManager.getEntity(options.hass, cameraEntityID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trigger entities (specified or auto-detected). Subclasses may override
|
||||
* to add engine-specific discovery; call `super` to keep the base discoveries.
|
||||
*/
|
||||
protected async _getTriggerEntities(
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await this._getDoorbellEntities(options);
|
||||
}
|
||||
|
||||
private async _getDoorbellEntities(
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
if (
|
||||
!this._config.triggers.doorbell ||
|
||||
!this._entity?.device_id ||
|
||||
!options.entityRegistryManager
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const deviceID = this._entity.device_id;
|
||||
|
||||
// `device_class` lives on state attributes (not the registry entry), so
|
||||
// narrow by `device_id` + domain first and filter by device_class against
|
||||
// `hass.states` second.
|
||||
const candidates = await options.entityRegistryManager.getMatchingEntities(
|
||||
options.hass,
|
||||
(ent) =>
|
||||
ent.device_id === deviceID &&
|
||||
!ent.disabled_by &&
|
||||
computeDomain(ent.entity_id) === 'event',
|
||||
);
|
||||
|
||||
const doorbells = candidates
|
||||
.filter(
|
||||
(ent) =>
|
||||
options.hass.states[ent.entity_id]?.attributes?.device_class === 'doorbell',
|
||||
)
|
||||
.map((ent) => ent.entity_id);
|
||||
|
||||
this._config.triggers.entities.push(...doorbells);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclass initialization hook. Override for async initialization work.
|
||||
*/
|
||||
@@ -276,12 +351,4 @@ export class Camera {
|
||||
protected _onDestroy(callback: DestroyCallback): void {
|
||||
this._destroyCallbacks.push(callback);
|
||||
}
|
||||
|
||||
private _subscribeBasedOnCapabilities(
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
): void {
|
||||
if (this._capabilities?.has('trigger')) {
|
||||
stateWatcher.subscribe(this._stateChangeHandler, this._config.triggers.entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export class CameraManagerEngineFactory {
|
||||
const { GenericCameraManagerEngine } = await import('./generic/engine-generic');
|
||||
cameraManagerEngine = new GenericCameraManagerEngine(
|
||||
options.stateWatcher,
|
||||
this._entityRegistryManager,
|
||||
options.eventCallback,
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
import { Entity, EntityRegistryManager } from '../ha/registry/entity/types';
|
||||
import { Camera, CameraInitializationOptions } from './camera';
|
||||
import { CameraNoEntityError } from './error';
|
||||
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
|
||||
|
||||
export interface EntityCameraInitializationOptions extends CameraInitializationOptions {
|
||||
entityRegistryManager: EntityRegistryManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera variant that requires a `camera_entity` to be present in the HA
|
||||
* entity registry. Base `Camera` resolves `_entity` opportunistically; this
|
||||
* subclass turns absence into an error for engines that cannot function
|
||||
* without it (motionEye, Reolink, TPLink).
|
||||
*/
|
||||
export class EntityCamera extends Camera {
|
||||
protected _entity: Entity | null = null;
|
||||
|
||||
public async initialize(options: EntityCameraInitializationOptions): Promise<Camera> {
|
||||
const config = this.getConfig();
|
||||
const cameraEntityID = getCameraEntityFromConfig(config);
|
||||
const entity = cameraEntityID
|
||||
? await options.entityRegistryManager.getEntity(options.hass, cameraEntityID)
|
||||
: null;
|
||||
|
||||
if (!entity || !cameraEntityID) {
|
||||
throw new CameraNoEntityError(config);
|
||||
protected override async _initialize(
|
||||
options: CameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
if (!this._entity) {
|
||||
throw new CameraNoEntityError(this.getConfig());
|
||||
}
|
||||
this._entity = entity;
|
||||
return await super.initialize(options);
|
||||
}
|
||||
|
||||
public getEntity(): Entity | null {
|
||||
return this._entity;
|
||||
await super._initialize(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { format } from 'date-fns';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { ActionsExecutor } from '../../card-controller/actions/types';
|
||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
@@ -45,7 +44,6 @@ export const isBirdseye = (cameraConfig: CameraConfig): boolean => {
|
||||
|
||||
export class FrigateCamera extends Camera {
|
||||
public async initialize(options: FrigateCameraInitializationOptions): Promise<Camera> {
|
||||
await this._initializeConfig(options.hass, options.entityRegistryManager);
|
||||
await super.initialize(options);
|
||||
|
||||
if (this._capabilities?.has('trigger')) {
|
||||
@@ -106,35 +104,29 @@ export class FrigateCamera extends Camera {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _initializeConfig(
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
protected override async _initialize(
|
||||
options: FrigateCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
const config = this.getConfig();
|
||||
const hasCameraName = !!config.frigate?.camera_name;
|
||||
const hasAutoTriggers = config.triggers.motion || config.triggers.occupancy;
|
||||
|
||||
let entity: Entity | null = null;
|
||||
const cameraEntity = getCameraEntityFromConfig(config);
|
||||
|
||||
// Entity information is required if the Frigate camera name is missing, or
|
||||
// if the entity requires automatic resolution of motion/occupancy sensors.
|
||||
if (cameraEntity && (!hasCameraName || hasAutoTriggers)) {
|
||||
entity = await entityRegistryManager.getEntity(hass, cameraEntity);
|
||||
if (!entity) {
|
||||
throw new CameraNoEntityError(config);
|
||||
}
|
||||
// Frigate needs the entity to derive `camera_name` when one isn't set. The
|
||||
// entity is resolved by base Camera; throw here only when its absence
|
||||
// breaks Frigate setup.
|
||||
if (cameraEntity && !hasCameraName && !this._entity) {
|
||||
throw new CameraNoEntityError(config);
|
||||
}
|
||||
|
||||
if (entity && !hasCameraName) {
|
||||
const resolvedName = this._getFrigateCameraNameFromEntity(entity);
|
||||
if (this._entity && !hasCameraName) {
|
||||
const resolvedName = this._getFrigateCameraNameFromEntity(this._entity);
|
||||
if (resolvedName) {
|
||||
this._config.frigate.camera_name = resolvedName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._config.frigate.client_id) {
|
||||
const stateEntity = cameraEntity ? hass.states[cameraEntity] : undefined;
|
||||
const stateEntity = cameraEntity ? options.hass.states[cameraEntity] : undefined;
|
||||
const clientID = stateEntity?.attributes?.client_id;
|
||||
if (typeof clientID === 'string' && clientID) {
|
||||
this._config.frigate.client_id = clientID;
|
||||
@@ -142,40 +134,57 @@ export class FrigateCamera extends Camera {
|
||||
this._config.frigate.client_id = 'frigate';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAutoTriggers) {
|
||||
// Try to find the correct entities for the motion & occupancy sensors.
|
||||
// We know they are binary_sensors, and that they'll have the same
|
||||
// config entry ID as the camera. Searching via unique_id ensures this
|
||||
// search still works if the user renames the entity_id.
|
||||
const binarySensorEntities = await entityRegistryManager.getMatchingEntities(
|
||||
hass,
|
||||
(ent) =>
|
||||
ent.config_entry_id === entity?.config_entry_id &&
|
||||
!ent.disabled_by &&
|
||||
ent.entity_id.startsWith('binary_sensor.'),
|
||||
);
|
||||
protected override async _getTriggerEntities(
|
||||
options: FrigateCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await this._getFrigateMotionAndOccupancyEntities(options);
|
||||
await super._getTriggerEntities(options);
|
||||
}
|
||||
|
||||
if (config.triggers.motion) {
|
||||
const motionEntity = this._getMotionSensor(config, [
|
||||
...binarySensorEntities.values(),
|
||||
]);
|
||||
if (motionEntity) {
|
||||
config.triggers.entities.push(motionEntity);
|
||||
}
|
||||
private async _getFrigateMotionAndOccupancyEntities(
|
||||
options: FrigateCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
const config = this.getConfig();
|
||||
if (!config.triggers.motion && !config.triggers.occupancy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Motion/occupancy auto-discovery requires the camera entity to derive
|
||||
// the matching binary_sensor unique_ids.
|
||||
if (getCameraEntityFromConfig(config) && !this._entity) {
|
||||
throw new CameraNoEntityError(config);
|
||||
}
|
||||
|
||||
// Find the correct entities for the motion & occupancy sensors. They
|
||||
// are binary_sensors with the same config entry ID as the camera;
|
||||
// searching via unique_id ensures this still works if the user renames
|
||||
// the entity_id.
|
||||
const binarySensorEntities = await options.entityRegistryManager.getMatchingEntities(
|
||||
options.hass,
|
||||
(ent) =>
|
||||
ent.config_entry_id === this._entity?.config_entry_id &&
|
||||
!ent.disabled_by &&
|
||||
ent.entity_id.startsWith('binary_sensor.'),
|
||||
);
|
||||
|
||||
if (config.triggers.motion) {
|
||||
const motionEntity = this._getMotionSensor(config, [
|
||||
...binarySensorEntities.values(),
|
||||
]);
|
||||
if (motionEntity) {
|
||||
config.triggers.entities.push(motionEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.triggers.occupancy) {
|
||||
const occupancyEntities = this._getOccupancySensor(config, [
|
||||
...binarySensorEntities.values(),
|
||||
]);
|
||||
if (occupancyEntities) {
|
||||
config.triggers.entities.push(...occupancyEntities);
|
||||
}
|
||||
if (config.triggers.occupancy) {
|
||||
const occupancyEntities = this._getOccupancySensor(config, [
|
||||
...binarySensorEntities.values(),
|
||||
]);
|
||||
if (occupancyEntities) {
|
||||
config.triggers.entities.push(...occupancyEntities);
|
||||
}
|
||||
|
||||
// De-duplicate triggering entities.
|
||||
config.triggers.entities = uniq(config.triggers.entities);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export class FrigateCameraManagerEngine
|
||||
extends GenericCameraManagerEngine
|
||||
implements CameraManagerEngine
|
||||
{
|
||||
private _entityRegistryManager: EntityRegistryManager;
|
||||
protected override _entityRegistryManager: EntityRegistryManager;
|
||||
private _frigateEventWatcher: FrigateEventWatcher;
|
||||
private _frigateReviewWatcher: FrigateReviewWatcher;
|
||||
private _recordingSegmentsCache: RecordingSegmentsCache;
|
||||
@@ -140,7 +140,7 @@ export class FrigateCameraManagerEngine
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventCallback);
|
||||
super(stateWatcher, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._frigateEventWatcher = new FrigateEventWatcher();
|
||||
this._frigateReviewWatcher = new FrigateReviewWatcher();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Endpoint } from '../../types';
|
||||
import { ViewMedia } from '../../view/item';
|
||||
@@ -40,12 +41,15 @@ import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
protected _eventCallback?: CameraEventCallback;
|
||||
protected _stateWatcher: StateWatcherSubscriptionInterface;
|
||||
protected _entityRegistryManager?: EntityRegistryManager;
|
||||
|
||||
constructor(
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
entityRegistryManager?: EntityRegistryManager,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
this._stateWatcher = stateWatcher;
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._eventCallback = eventCallback;
|
||||
}
|
||||
|
||||
@@ -62,6 +66,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
}).initialize({
|
||||
hass,
|
||||
stateWatcher: this._stateWatcher,
|
||||
entityRegistryManager: this._entityRegistryManager,
|
||||
capabilityOptions: {
|
||||
raw: {
|
||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||
|
||||
@@ -174,6 +174,7 @@ export class ReolinkCamera extends EntityCamera {
|
||||
protected async _initialize(
|
||||
options: ReolinkCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await super._initialize(options);
|
||||
await this._initializeChannel(options.hass, options.deviceRegistryManager);
|
||||
this._ptzEntities = await this._getPTZEntities(
|
||||
options.hass,
|
||||
|
||||
@@ -3,10 +3,13 @@ import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/pt
|
||||
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { CapabilitiesRaw, PTZCapabilities, PTZMovementType } from '../../types';
|
||||
import { EntityCamera, EntityCameraInitializationOptions } from '../entity-camera';
|
||||
import { CameraInitializationOptions } from '../camera';
|
||||
import { EntityCamera } from '../entity-camera';
|
||||
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
|
||||
type TPLinkCameraInitializationOptions = EntityCameraInitializationOptions;
|
||||
interface TPLinkCameraInitializationOptions extends CameraInitializationOptions {
|
||||
entityRegistryManager: EntityRegistryManager;
|
||||
}
|
||||
|
||||
interface PTZEntities {
|
||||
left?: string;
|
||||
@@ -22,6 +25,7 @@ export class TPLinkCamera extends EntityCamera {
|
||||
protected async _initialize(
|
||||
options: TPLinkCameraInitializationOptions,
|
||||
): Promise<void> {
|
||||
await super._initialize(options);
|
||||
this._ptzEntities = await this._getPTZEntities(
|
||||
options.hass,
|
||||
options.entityRegistryManager,
|
||||
|
||||
@@ -8,14 +8,12 @@ import { CameraEventCallback, CameraManagerCameraMetadata, Engine } from '../typ
|
||||
import { TPLinkCamera } from './camera';
|
||||
|
||||
export class TPLinkCameraManagerEngine extends GenericCameraManagerEngine {
|
||||
private _entityRegistryManager: EntityRegistryManager;
|
||||
|
||||
constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventCallback);
|
||||
super(stateWatcher, entityRegistryManager, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -111,12 +111,14 @@ export class TriggersManager {
|
||||
}
|
||||
|
||||
if (ev.type === 'signal') {
|
||||
// A signal is momentary -- handled as a matched new+end so the existing
|
||||
// untrigger_delay_seconds machinery gives it visible duration, and so
|
||||
// concurrent continuous sources still gate untriggering correctly.
|
||||
const handled = await this.handleCameraEvent({ ...ev, type: 'new' }, options);
|
||||
if (handled) {
|
||||
await this.handleCameraEvent({ ...ev, type: 'end' });
|
||||
// A signal is momentary -- handled as a matched new+end so concurrent
|
||||
// continuous sources still gate untriggering correctly. The end leg is
|
||||
// tagged `{ signal: true }` so `_startUntrigger` adds the synthesized
|
||||
// signal on-period (`signal_hold_seconds`) on top of the usual
|
||||
// post-source-end linger (`untrigger_delay_seconds`).
|
||||
await this._handleEndEvent(ev, { signal: true });
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
@@ -160,13 +162,16 @@ export class TriggersManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _handleEndEvent(ev: CameraEvent): Promise<boolean> {
|
||||
private async _handleEndEvent(
|
||||
ev: CameraEvent,
|
||||
options?: { signal?: boolean },
|
||||
): Promise<boolean> {
|
||||
this._deleteIgnoredEventID(ev.cameraID, ev.id);
|
||||
|
||||
const state = this._states.get(ev.cameraID);
|
||||
state?.sources.delete(ev.id);
|
||||
if (!state?.sources.size) {
|
||||
await this._startUntrigger(ev.cameraID);
|
||||
await this._startUntrigger(ev.cameraID, options);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -306,7 +311,10 @@ export class TriggersManager {
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
private async _startUntrigger(cameraID: string): Promise<void> {
|
||||
private async _startUntrigger(
|
||||
cameraID: string,
|
||||
options?: { signal?: boolean },
|
||||
): Promise<void> {
|
||||
this._deleteUntriggerDelayTimer(cameraID);
|
||||
this._deleteForceUntriggerTimer(cameraID);
|
||||
|
||||
@@ -315,12 +323,18 @@ export class TriggersManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
const untriggerDelaySeconds = config?.view?.triggers.untrigger_delay_seconds ?? 0;
|
||||
const triggersConfig = this._api.getConfigManager().getConfig()?.view?.triggers;
|
||||
const untriggerDelaySeconds = triggersConfig?.untrigger_delay_seconds ?? 0;
|
||||
// For signals, add the synthesized on-period (signals have no native
|
||||
// on/off, so hold them visible for `signal_hold_seconds` before the usual
|
||||
// post-source-end linger kicks in).
|
||||
const signalHoldSeconds = triggersConfig?.signal_hold_seconds ?? 0;
|
||||
const effectiveDelaySeconds =
|
||||
untriggerDelaySeconds + (options?.signal ? signalHoldSeconds : 0);
|
||||
|
||||
if (untriggerDelaySeconds > 0) {
|
||||
if (effectiveDelaySeconds > 0) {
|
||||
state.untriggerDelayTimer = new Timer();
|
||||
state.untriggerDelayTimer.start(untriggerDelaySeconds, async () => {
|
||||
state.untriggerDelayTimer.start(effectiveDelaySeconds, async () => {
|
||||
await this._untriggerAction(cameraID);
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -148,6 +148,7 @@ export const cameraConfigDefault = {
|
||||
triggers: {
|
||||
motion: false,
|
||||
occupancy: false,
|
||||
doorbell: false,
|
||||
events: [],
|
||||
entities: [],
|
||||
reviews: {
|
||||
@@ -248,6 +249,7 @@ export const cameraConfigSchema = z
|
||||
.object({
|
||||
motion: z.boolean().default(cameraConfigDefault.triggers.motion),
|
||||
occupancy: z.boolean().default(cameraConfigDefault.triggers.occupancy),
|
||||
doorbell: z.boolean().default(cameraConfigDefault.triggers.doorbell),
|
||||
entities: z.string().array().default(cameraConfigDefault.triggers.entities),
|
||||
events: z
|
||||
.enum(CAMERA_TRIGGER_EVENT_TYPES)
|
||||
|
||||
@@ -72,6 +72,7 @@ export const viewConfigDefault = {
|
||||
},
|
||||
untrigger_delay_seconds: 0,
|
||||
untrigger_force_seconds: 0,
|
||||
signal_hold_seconds: 30,
|
||||
},
|
||||
keyboard_shortcuts: keyboardShortcutsDefault,
|
||||
issues: {
|
||||
@@ -109,6 +110,9 @@ export const triggersSchema = z.object({
|
||||
untrigger_force_seconds: z
|
||||
.number()
|
||||
.default(viewConfigDefault.triggers.untrigger_force_seconds),
|
||||
signal_hold_seconds: z
|
||||
.number()
|
||||
.default(viewConfigDefault.triggers.signal_hold_seconds),
|
||||
});
|
||||
export type TriggersOptions = z.infer<typeof triggersSchema>;
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_MOTION =
|
||||
`${CONF_CAMERAS}.#.triggers.motion` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY =
|
||||
`${CONF_CAMERAS}.#.triggers.occupancy` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_DOORBELL =
|
||||
`${CONF_CAMERAS}.#.triggers.doorbell` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES =
|
||||
`${CONF_CAMERAS}.#.triggers.entities` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS =
|
||||
@@ -195,6 +197,8 @@ export const CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS =
|
||||
`${CONF_VIEW_TRIGGERS}.untrigger_delay_seconds` as const;
|
||||
export const CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS =
|
||||
`${CONF_VIEW_TRIGGERS}.untrigger_force_seconds` as const;
|
||||
export const CONF_VIEW_TRIGGERS_SIGNAL_HOLD_SECONDS =
|
||||
`${CONF_VIEW_TRIGGERS}.signal_hold_seconds` as const;
|
||||
export const CONF_VIEW_TRIGGERS_ACTIONS = `${CONF_VIEW_TRIGGERS}.actions` as const;
|
||||
export const CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE =
|
||||
`${CONF_VIEW_TRIGGERS_ACTIONS}.interaction_mode` as const;
|
||||
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
CONF_CAMERAS_ARRAY_REOLINK_MEDIA_RESOLUTION,
|
||||
CONF_CAMERAS_ARRAY_REOLINK_URL,
|
||||
CONF_CAMERAS_ARRAY_TITLE,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_DOORBELL,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_MOTION,
|
||||
@@ -278,6 +279,7 @@ import {
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
|
||||
CONF_VIEW_TRIGGERS_SIGNAL_HOLD_SECONDS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS,
|
||||
DOCS_URL,
|
||||
@@ -1596,6 +1598,9 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS, {
|
||||
default: this._defaults.view.triggers.untrigger_force_seconds,
|
||||
})}
|
||||
${this._renderNumberInput(CONF_VIEW_TRIGGERS_SIGNAL_HOLD_SECONDS, {
|
||||
default: this._defaults.view.triggers.signal_hold_seconds,
|
||||
})}
|
||||
${this._putInSubmenu(
|
||||
MENU_VIEW_TRIGGERS_ACTIONS,
|
||||
true,
|
||||
@@ -2786,6 +2791,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_MOTION, cameraIndex),
|
||||
this._defaults.cameras.triggers.motion,
|
||||
)}
|
||||
${this._renderSwitch(
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_DOORBELL,
|
||||
cameraIndex,
|
||||
),
|
||||
this._defaults.cameras.triggers.doorbell,
|
||||
)}
|
||||
${this._renderOptionSelector(
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES,
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"events": "All events",
|
||||
"snapshots": "Events with new snapshots"
|
||||
},
|
||||
"doorbell": "Trigger by auto-detecting doorbell event entities",
|
||||
"motion": "Trigger by auto-detecting the motion sensor",
|
||||
"occupancy": "Trigger by auto-detecting the occupancy sensor",
|
||||
"reviews": {
|
||||
@@ -678,6 +679,7 @@
|
||||
"editor_label": "Trigger behavior",
|
||||
"filter_selected_camera": "Only trigger on selected camera",
|
||||
"show_trigger_status": "Show pulsing border when triggered",
|
||||
"signal_hold_seconds": "Seconds to hold a momentary (signal) trigger visible (e.g. a doorbell press) before the post-end untrigger delay",
|
||||
"untrigger_delay_seconds": "Seconds delay after trigger state change before untrigger",
|
||||
"untrigger_force_seconds": "Seconds before forced untrigger"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user