feat: Trigger cameras on HA bus events (#2512)
This commit is contained in:
committed by
dermotduffy
parent
406176eebc
commit
0a36358394
@@ -381,14 +381,40 @@ cameras:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `doorbell` | `false` | Whether to trigger the camera by automatically detecting an [HA `event.*` entity](https://www.home-assistant.io/integrations/event/#device-class) with `device_class: doorbell` on the same HA device as the camera entity. Requires `camera_entity` to be set. |
|
||||
| `entities` | | Whether to not to trigger the camera when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). |
|
||||
| `events` | `[]` | Whether to trigger the camera when `events` occur (whether or not media is available) or whenever updated `clips` or `snapshots` are detected. Detection support varies by camera [engine](engine.md). |
|
||||
| `motion` | `false` | Whether to not to trigger the camera by automatically detecting and using the motion `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the motion `binary_sensor` entity has been enabled in Home Assistant. |
|
||||
| `occupancy` | `false` | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera and its configured zones and labels. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant. If this camera has configured zones, only occupancy sensors for those zones are used -- if the overall _camera_ occupancy sensor is also required, it can be manually added to `entities`. If this camera has configured labels, only occupancy sensors for those labels are used. |
|
||||
| `reviews` | | Configuration for triggering on review items. Currently only supported by Frigate. See [`reviews`](#reviews). |
|
||||
| Option | Default | Description |
|
||||
| -------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `doorbell` | `false` | Whether to trigger the camera by automatically detecting an [HA `event.*` entity](https://www.home-assistant.io/integrations/event/#device-class) with `device_class: doorbell` on the same HA device as the camera entity. Requires `camera_entity` to be set. |
|
||||
| `entities` | | Whether to not to trigger the camera when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). |
|
||||
| `events` | `[]` | A list of Home Assistant bus events to trigger on, with optional payload filtering. Each entry subscribes to one HA event type and fires the camera trigger every time that event is dispatched (and the optional `event_data` filter matches). See [`events`](#events). |
|
||||
| `media_events` | `[]` | Whether to trigger the camera when `events` occur (whether or not media is available) or whenever updated `clips` or `snapshots` are detected. Detection support varies by camera [engine](engine.md). |
|
||||
| `motion` | `false` | Whether to not to trigger the camera by automatically detecting and using the motion `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the motion `binary_sensor` entity has been enabled in Home Assistant. |
|
||||
| `occupancy` | `false` | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera and its configured zones and labels. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant. If this camera has configured zones, only occupancy sensors for those zones are used -- if the overall _camera_ occupancy sensor is also required, it can be manually added to `entities`. If this camera has configured labels, only occupancy sensors for those labels are used. |
|
||||
| `reviews` | | Configuration for triggering on review items. Currently only supported by Frigate. See [`reviews`](#reviews). |
|
||||
|
||||
### `events`
|
||||
|
||||
The `events` list subscribes the card to Home Assistant bus events. Each entry maps to a single `event_type` (e.g. `zha_event`, `deconz_event`, or any custom event your automations fire). Because events are momentary (HA fires them once, with no on/off), the card treats each fire as an instantaneous trigger; the visible "active" window is then controlled by [`view.triggers.event_hold_seconds`](../view.md?id=triggers).
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
- camera_entity: camera.front_door
|
||||
triggers:
|
||||
events:
|
||||
# Trigger only on a specific ZHA device firing its 'press' command.
|
||||
- event_type: zha_event
|
||||
event_data:
|
||||
device_ieee: '00:11:22:33:44:55:66:77'
|
||||
command: press
|
||||
# No `event_data` filter -- triggers every time this event is fired.
|
||||
- event_type: home_doorbell_pressed
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `event_type` | | The Home Assistant event type to subscribe to (e.g. `zha_event`, `deconz_event`, or a custom event name fired by one of your automations). Same field name and meaning as in HA automation YAML. |
|
||||
| `event_data` | | Optional dictionary of key/value pairs the event's payload must contain for this entry to trigger. Matching is a deep subset (every key listed must be present in the event payload and match; extra keys in the event are ignored). Same field name and semantics as in HA automation YAML. Omit entirely to trigger on every fire of this `event_type`. |
|
||||
|
||||
> [!TIP] Shared `event_type` values like `zha_event` and `deconz_event` fire for **every** device on that integration. Without an `event_data` filter the camera would trigger on any Zigbee/deCONZ device press in your home. Use `event_data` to narrow down to the specific device you care about; you can copy values straight out of **Developer tools → Events** in Home Assistant.
|
||||
|
||||
### `reviews`
|
||||
|
||||
@@ -429,6 +455,11 @@ cameras:
|
||||
entities:
|
||||
- binary_sensor.front_door_sensor
|
||||
events:
|
||||
- event_type: zha_event
|
||||
event_data:
|
||||
device_ieee: '00:11:22:33:44:55:66:77'
|
||||
command: press
|
||||
media_events:
|
||||
- events
|
||||
- clips
|
||||
- snapshots
|
||||
|
||||
@@ -153,7 +153,7 @@ human interaction with the card; this behavior can be configured via the
|
||||
| `actions` | | The actions to take when a camera is triggered. See [Trigger action configuration](#trigger-action-configuration). |
|
||||
| `filter_selected_camera` | `true` | If set to `true` will only trigger on the currently selected camera. |
|
||||
| `show_trigger_status` | `false` | Whether or not the `live` view should show a visual indication that it is triggered (a pulsing border around the camera edge). |
|
||||
| `signal_hold_seconds` | `30` | The synthesized on-period for momentary trigger sources that have no native on/off state (e.g. HA `event.*` entities or anything that fires as a single signal). For a doorbell press paired with `trigger: call`, this is effectively the ring window during which the call can be answered. Added _on top of_ `untrigger_delay_seconds`. Ignored for stateful sources (`binary_sensor`, `switch`, etc.). |
|
||||
| `event_hold_seconds` | `30` | The synthesized on-period for momentary trigger sources that have no native on/off state (e.g. HA `event.*` entities or anything that fires as a single signal). For a doorbell press paired with `trigger: call`, this is effectively the ring window during which the call can be answered. Added _on top of_ `untrigger_delay_seconds`. Ignored for stateful sources (`binary_sensor`, `switch`, etc.). |
|
||||
| `untrigger_delay_seconds` | `0` | The number of seconds to continue to consider the camera triggered after the source ends, before taking the configured `untrigger` action. |
|
||||
| `untrigger_force_seconds` | `0` | The number of seconds after a camera first triggers before force untriggering that camera. Set to `0` to disable. |
|
||||
|
||||
@@ -165,7 +165,8 @@ human interaction with the card; this behavior can be configured via the
|
||||
> "ring-then-end-if-unanswered" pattern), the ring lasts until the source ends
|
||||
> plus `untrigger_delay_seconds`. For momentary sources (HA `event.*` entities,
|
||||
> a doorbell press), the source ends instantly so the ring window is
|
||||
> `signal_hold_seconds` (default `30`s) plus `untrigger_delay_seconds`.
|
||||
> `event_hold_seconds` (default `30`s) plus `untrigger_delay_seconds` (default
|
||||
> `0`s).
|
||||
|
||||
### Trigger action configuration
|
||||
|
||||
|
||||
+31
-2
@@ -363,7 +363,9 @@ elements:
|
||||
|
||||
The [`doorbell` profile](configuration/profiles.md?id=doorbell) turns a dashboard into a phone-like ringer when somebody presses the doorbell, by setting [`view.triggers.actions.trigger: call`](configuration/view.md?id=triggers) and auto-discovering [HA `event.*` entities](https://www.home-assistant.io/integrations/event/#device-class) with `device_class: doorbell` on the camera's device (Ring, UniFi Protect, Nest, DoorBird, Reolink, etc.). The intended deployment is a wall-mounted tablet sitting on the dashboard.
|
||||
|
||||
A doorbell press is instantaneous, so the card synthesises a ring window from [`view.triggers.signal_hold_seconds`](configuration/view.md?id=triggers) (default `30`s) — long enough for a typical phone-style answer window. `untrigger_delay_seconds` then lingers past that, same as for any stateful trigger.
|
||||
A doorbell press is instantaneous, so the card synthesises a ring window from [`view.triggers.event_hold_seconds`](configuration/view.md?id=triggers) (default `30`s) — long enough for a typical phone-style answer window. `untrigger_delay_seconds` then lingers past that, same as for any stateful trigger.
|
||||
|
||||
`triggers.motion`, `triggers.occupancy`, and `triggers.media_events` are off by default — only the explicit doorbell press triggers the call, so casual motion won't make the card ring.
|
||||
|
||||
```yaml
|
||||
type: custom:advanced-camera-card
|
||||
@@ -381,6 +383,33 @@ profiles:
|
||||
> [!TIP]
|
||||
> If your doorbell exposes a `binary_sensor.*` or `switch.*` instead of an `event.*` entity, list it under [`triggers.entities`](configuration/cameras/README.md?id=triggers) on the camera manually. Auto-discovery only covers `event.*` based doorbell entities.
|
||||
|
||||
#### With a Zigbee (ZHA / deCONZ) doorbell button
|
||||
|
||||
Zigbee buttons connected via ZHA or deCONZ typically don't expose a per-device entity — they fire raw HA bus events (`zha_event`, `deconz_event`) shared across every Zigbee device on the integration. The `doorbell` profile's auto-discovery doesn't cover this case; reuse the profile (it still wires up `trigger: call` / `untrigger: call` and the ring window) but opt out of auto-discovery per camera and add [`triggers.events`](configuration/cameras/README.md?id=events) with an `event_data` filter to pick out the right device.
|
||||
|
||||
You can copy the exact `device_ieee` (ZHA) or `id` (deCONZ) and command values straight out of **Developer tools → Events** in Home Assistant — listen to the event type, press the doorbell, and use whatever appears in the `data` payload.
|
||||
|
||||
```yaml
|
||||
type: custom:advanced-camera-card
|
||||
cameras:
|
||||
- camera_entity: camera.front_door
|
||||
live_provider: go2rtc
|
||||
go2rtc:
|
||||
modes:
|
||||
- webrtc
|
||||
triggers:
|
||||
# Opt out of the profile's `event.*` auto-discovery -- this camera
|
||||
# uses an HA bus event instead.
|
||||
doorbell: false
|
||||
events:
|
||||
- event_type: zha_event
|
||||
event_data:
|
||||
device_ieee: '00:11:22:33:44:55:66:77'
|
||||
command: press
|
||||
profiles:
|
||||
- doorbell
|
||||
```
|
||||
|
||||
## Events from other cameras
|
||||
|
||||
`dependencies.cameras` allows events/recordings for other cameras to be shown
|
||||
@@ -1412,7 +1441,7 @@ cameras:
|
||||
motion: true
|
||||
entities:
|
||||
- binary_sensor.kitchen_door_opened
|
||||
events: []
|
||||
media_events: []
|
||||
view:
|
||||
triggers:
|
||||
show_trigger_status: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { HassEvent } from 'home-assistant-js-websocket';
|
||||
import { HomeAssistant, SubscriptionUnsubscribe } from '../../ha/types';
|
||||
|
||||
export interface EventSubscriptionRequest {
|
||||
event_type: string;
|
||||
callback: (data: unknown) => void;
|
||||
|
||||
// Optional payload filter. Receives the event's `data`; if it returns false
|
||||
// the event is dropped for this request.
|
||||
matcher?: (data: unknown) => boolean;
|
||||
}
|
||||
|
||||
export interface EventWatcherSubscriptionInterface {
|
||||
subscribe(hass: HomeAssistant, request: EventSubscriptionRequest): Promise<void>;
|
||||
unsubscribe(request: EventSubscriptionRequest): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to HA bus events via the WebSocket connection. Refcounted per
|
||||
* `event_type`: the first subscriber for a type opens the WS subscription, the
|
||||
* last to unsubscribe tears it down. Each fired event is fanned out to every
|
||||
* registered request whose `event_type` matches and whose `matcher` accepts the
|
||||
* payload.
|
||||
*/
|
||||
export class EventWatcher implements EventWatcherSubscriptionInterface {
|
||||
private _requests: EventSubscriptionRequest[] = [];
|
||||
|
||||
// Stored as a promise so an unsubscribe that races against an in-flight
|
||||
// subscribe can await completion before tearing down -- otherwise the unsub
|
||||
// func is unavailable and the subscription would leak (via hass.connection's
|
||||
// internal subscription map).
|
||||
private _unsubscribers = new Map<string, Promise<SubscriptionUnsubscribe>>();
|
||||
|
||||
public async subscribe(
|
||||
hass: HomeAssistant,
|
||||
request: EventSubscriptionRequest,
|
||||
): Promise<void> {
|
||||
const isFirst = !this._hasSubscribers(request.event_type);
|
||||
this._requests.push(request);
|
||||
if (isFirst) {
|
||||
const pendingSubscription = hass.connection.subscribeEvents<HassEvent>(
|
||||
(event) => this._receiveEvent(event),
|
||||
request.event_type,
|
||||
);
|
||||
this._unsubscribers.set(request.event_type, pendingSubscription);
|
||||
await pendingSubscription;
|
||||
}
|
||||
}
|
||||
|
||||
public async unsubscribe(request: EventSubscriptionRequest): Promise<void> {
|
||||
this._requests = this._requests.filter((r) => r !== request);
|
||||
if (!this._hasSubscribers(request.event_type)) {
|
||||
const pendingSubscription = this._unsubscribers.get(request.event_type);
|
||||
this._unsubscribers.delete(request.event_type);
|
||||
const unsubscribeCallback = await pendingSubscription;
|
||||
await unsubscribeCallback?.();
|
||||
}
|
||||
}
|
||||
|
||||
private _hasSubscribers(eventType: string): boolean {
|
||||
return this._requests.some((r) => r.event_type === eventType);
|
||||
}
|
||||
|
||||
private _receiveEvent(event: HassEvent): void {
|
||||
for (const request of this._requests) {
|
||||
if (
|
||||
request.event_type === event.event_type &&
|
||||
(!request.matcher || request.matcher(event.data))
|
||||
) {
|
||||
request.callback(event.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ import { HomeAssistant } from '../../ha/types';
|
||||
import { log } from '../../utils/debug';
|
||||
import { InitializationAspect } from '../initialization-manager';
|
||||
import { CardHASSAPI } from '../types';
|
||||
import { EventWatcher, EventWatcherSubscriptionInterface } from './event-watcher';
|
||||
import { StateWatcher, StateWatcherSubscriptionInterface } from './state-watcher';
|
||||
|
||||
export class HASSManager {
|
||||
private _hass: HomeAssistant | null = null;
|
||||
private _api: CardHASSAPI;
|
||||
private _stateWatcher: StateWatcher = new StateWatcher();
|
||||
private _eventWatcher: EventWatcher = new EventWatcher();
|
||||
|
||||
constructor(api: CardHASSAPI) {
|
||||
this._api = api;
|
||||
@@ -26,6 +28,10 @@ export class HASSManager {
|
||||
return this._stateWatcher;
|
||||
}
|
||||
|
||||
public getEventWatcher(): EventWatcherSubscriptionInterface {
|
||||
return this._eventWatcher;
|
||||
}
|
||||
|
||||
public setHASS(hass?: HomeAssistant | null): void {
|
||||
// When HA transitions from "not ready" to "ready" (WebSocket reconnected
|
||||
// AND all integrations finished loading), reinitialize cameras and the
|
||||
|
||||
@@ -110,15 +110,15 @@ export class TriggersManager {
|
||||
return this._handleEndEvent(ev);
|
||||
}
|
||||
|
||||
if (ev.type === 'signal') {
|
||||
if (ev.type === 'momentary') {
|
||||
const handled = await this.handleCameraEvent({ ...ev, type: 'new' }, options);
|
||||
if (handled) {
|
||||
// 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
|
||||
// A momentary event has no start/end -- handled as a matched new+end so
|
||||
// concurrent continuous sources still gate untriggering correctly. The
|
||||
// end leg is tagged `{ momentary: true }` so `_startUntrigger` adds the
|
||||
// synthesized on-period (`event_hold_seconds`) on top of the usual
|
||||
// post-source-end linger (`untrigger_delay_seconds`).
|
||||
await this._handleEndEvent(ev, { signal: true });
|
||||
await this._handleEndEvent(ev, { momentary: true });
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export class TriggersManager {
|
||||
|
||||
private async _handleEndEvent(
|
||||
ev: CameraEvent,
|
||||
options?: { signal?: boolean },
|
||||
options?: { momentary?: boolean },
|
||||
): Promise<boolean> {
|
||||
this._deleteIgnoredEventID(ev.cameraID, ev.id);
|
||||
|
||||
@@ -313,7 +313,7 @@ export class TriggersManager {
|
||||
|
||||
private async _startUntrigger(
|
||||
cameraID: string,
|
||||
options?: { signal?: boolean },
|
||||
options?: { momentary?: boolean },
|
||||
): Promise<void> {
|
||||
this._deleteUntriggerDelayTimer(cameraID);
|
||||
this._deleteForceUntriggerTimer(cameraID);
|
||||
@@ -325,12 +325,14 @@ export class TriggersManager {
|
||||
|
||||
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;
|
||||
// For momentary events, add the synthesized on-period (they have no
|
||||
// native on/off, so hold them visible before the usual post-source-end
|
||||
// linger kicks in). The user-facing field is `event_hold_seconds` because
|
||||
// HA events are the common case; internally this is the hold for any
|
||||
// momentary source.
|
||||
const momentaryHoldSeconds = triggersConfig?.event_hold_seconds ?? 0;
|
||||
const effectiveDelaySeconds =
|
||||
untriggerDelaySeconds + (options?.signal ? signalHoldSeconds : 0);
|
||||
untriggerDelaySeconds + (options?.momentary ? momentaryHoldSeconds : 0);
|
||||
|
||||
if (effectiveDelaySeconds > 0) {
|
||||
state.untriggerDelayTimer = new Timer();
|
||||
|
||||
@@ -877,6 +877,29 @@ const frigateCardToAdvancedCameraCardStyleTransform = (data: unknown): unknown =
|
||||
return newStyleOverrides;
|
||||
};
|
||||
|
||||
// Legacy `triggers.events: string[]` (Frigate engine media-availability filter)
|
||||
// was renamed to `triggers.media_events` to free up `triggers.events` for the
|
||||
// new HA-bus-event trigger list (object shape). Distinguish old from new by
|
||||
// element type: an all-string array is legacy; any non-string element marks
|
||||
// the new shape and must not be touched. If `media_events` already exists we
|
||||
// refuse to overwrite it -- but we still drop the legacy `events` (otherwise
|
||||
// it would fail the new schema, which expects objects).
|
||||
const triggersEventsToMediaEventsTransform = (triggers: unknown): unknown => {
|
||||
if (typeof triggers !== 'object' || !triggers) {
|
||||
return undefined;
|
||||
}
|
||||
const events = triggers['events'];
|
||||
if (!Array.isArray(events) || events.some((x) => typeof x !== 'string')) {
|
||||
return undefined;
|
||||
}
|
||||
const result = { ...triggers };
|
||||
delete result['events'];
|
||||
if (!('media_events' in result)) {
|
||||
result['media_events'] = events;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const UPGRADES = [
|
||||
// v5.2.0 -> v6.0.0
|
||||
(data: unknown): boolean => {
|
||||
@@ -1090,4 +1113,13 @@ const UPGRADES = [
|
||||
typeof data === 'object' && data ? (data as RawAdvancedCameraCardConfig) : {},
|
||||
);
|
||||
},
|
||||
|
||||
// Legacy `triggers.events: string[]` → `triggers.media_events`. Targets the
|
||||
// two known places a camera config lives: `cameras_global` and `cameras[]`.
|
||||
// Mirrors the PTZ rename migration above.
|
||||
upgradeWithOverrides('cameras_global.triggers', triggersEventsToMediaEventsTransform),
|
||||
upgradeArrayOfObjects(
|
||||
CONF_CAMERAS,
|
||||
upgradeWithOverrides('triggers', triggersEventsToMediaEventsTransform),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { imageBaseConfigDefault, imageBaseConfigSchema } from './common/image';
|
||||
import { proxyBaseConfigDefault, proxyBaseConfigSchema } from './common/proxy';
|
||||
import { severitySchema } from './common/severity';
|
||||
|
||||
const CAMERA_TRIGGER_EVENT_TYPES = [
|
||||
const CAMERA_TRIGGER_MEDIA_EVENT_TYPES = [
|
||||
// An event whether or not it has any media yet associated with it.
|
||||
'events',
|
||||
|
||||
@@ -16,7 +16,8 @@ const CAMERA_TRIGGER_EVENT_TYPES = [
|
||||
'clips',
|
||||
'snapshots',
|
||||
] as const;
|
||||
export type CameraTriggerEventType = (typeof CAMERA_TRIGGER_EVENT_TYPES)[number];
|
||||
export type CameraTriggerMediaEventType =
|
||||
(typeof CAMERA_TRIGGER_MEDIA_EVENT_TYPES)[number];
|
||||
|
||||
// *************************************************************************
|
||||
// Live Provider Configuration
|
||||
@@ -149,8 +150,9 @@ export const cameraConfigDefault = {
|
||||
motion: false,
|
||||
occupancy: false,
|
||||
doorbell: false,
|
||||
events: [],
|
||||
media_events: [],
|
||||
entities: [],
|
||||
events: [],
|
||||
reviews: {
|
||||
severities: ['high' as const],
|
||||
description: true,
|
||||
@@ -217,6 +219,12 @@ const cameraMediaConfigSchema = z.object({
|
||||
.default(cameraMediaConfigDefault.reviewed),
|
||||
});
|
||||
|
||||
const triggerEventSchema = z.object({
|
||||
event_type: z.string().min(1),
|
||||
event_data: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
export type TriggerEvent = z.infer<typeof triggerEventSchema>;
|
||||
|
||||
export const cameraConfigSchema = z
|
||||
.looseObject({
|
||||
camera_entity: z.string().optional(),
|
||||
@@ -251,10 +259,11 @@ export const cameraConfigSchema = z
|
||||
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)
|
||||
events: triggerEventSchema.array().default(cameraConfigDefault.triggers.events),
|
||||
media_events: z
|
||||
.enum(CAMERA_TRIGGER_MEDIA_EVENT_TYPES)
|
||||
.array()
|
||||
.default(cameraConfigDefault.triggers.events),
|
||||
.default(cameraConfigDefault.triggers.media_events),
|
||||
reviews: z
|
||||
.object({
|
||||
severities: severitySchema
|
||||
|
||||
@@ -72,7 +72,7 @@ export const viewConfigDefault = {
|
||||
},
|
||||
untrigger_delay_seconds: 0,
|
||||
untrigger_force_seconds: 0,
|
||||
signal_hold_seconds: 30,
|
||||
event_hold_seconds: 30,
|
||||
},
|
||||
keyboard_shortcuts: keyboardShortcutsDefault,
|
||||
issues: {
|
||||
@@ -110,9 +110,7 @@ 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),
|
||||
event_hold_seconds: z.number().default(viewConfigDefault.triggers.event_hold_seconds),
|
||||
});
|
||||
export type TriggersOptions = z.infer<typeof triggersSchema>;
|
||||
|
||||
|
||||
+4
-2
@@ -117,6 +117,8 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES =
|
||||
`${CONF_CAMERAS}.#.triggers.entities` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS =
|
||||
`${CONF_CAMERAS}.#.triggers.events` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_MEDIA_EVENTS =
|
||||
`${CONF_CAMERAS}.#.triggers.media_events` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_REVIEWS_SEVERITIES =
|
||||
`${CONF_CAMERAS}.#.triggers.reviews.severities` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_REVIEWS_DESCRIPTION =
|
||||
@@ -197,8 +199,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_EVENT_HOLD_SECONDS =
|
||||
`${CONF_VIEW_TRIGGERS}.event_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;
|
||||
|
||||
+132
-13
@@ -93,6 +93,7 @@ import {
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_DOORBELL,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_MEDIA_EVENTS,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_MOTION,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_REVIEWS_DESCRIPTION,
|
||||
@@ -279,7 +280,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_EVENT_HOLD_SECONDS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS,
|
||||
DOCS_URL,
|
||||
@@ -313,6 +314,8 @@ const MENU_CAMERAS_MOTIONEYE = 'cameras.motioneye';
|
||||
const MENU_CAMERAS_PROXY = 'cameras.proxy';
|
||||
const MENU_CAMERAS_REOLINK = 'cameras.reolink';
|
||||
const MENU_CAMERAS_TRIGGERS = 'cameras.triggers';
|
||||
const MENU_CAMERAS_TRIGGERS_EVENT = 'cameras.triggers.event';
|
||||
const MENU_CAMERAS_TRIGGERS_EVENTS = 'cameras.triggers.events';
|
||||
const MENU_CAMERAS_TRIGGERS_REVIEWS = 'cameras.triggers.reviews';
|
||||
const MENU_CAMERAS_WEBRTC_CARD = 'cameras.webrtc_card';
|
||||
const MENU_CAMERAS_MEDIA = 'cameras.media';
|
||||
@@ -1040,19 +1043,19 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
},
|
||||
];
|
||||
|
||||
private _triggersEvents: EditorSelectOption[] = [
|
||||
private _triggersMediaEvents: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{
|
||||
value: 'events',
|
||||
label: localize('config.cameras.triggers.events.events'),
|
||||
label: localize('config.cameras.triggers.media_events.events'),
|
||||
},
|
||||
{
|
||||
value: 'clips',
|
||||
label: localize('config.cameras.triggers.events.clips'),
|
||||
label: localize('config.cameras.triggers.media_events.clips'),
|
||||
},
|
||||
{
|
||||
value: 'snapshots',
|
||||
label: localize('config.cameras.triggers.events.snapshots'),
|
||||
label: localize('config.cameras.triggers.media_events.snapshots'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1523,6 +1526,16 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
);
|
||||
}
|
||||
|
||||
private _getEditorTriggerEventTitle(
|
||||
eventIndex: number,
|
||||
eventConfig: RawAdvancedCameraCardConfig,
|
||||
): string {
|
||||
return (
|
||||
(typeof eventConfig?.event_type === 'string' && eventConfig.event_type) ||
|
||||
localize('common.event') + ' #' + eventIndex
|
||||
);
|
||||
}
|
||||
|
||||
private _renderViewDefaultResetMenu(): TemplateResult {
|
||||
return this._putInSubmenu(
|
||||
MENU_VIEW_DEFAULT_RESET,
|
||||
@@ -1599,8 +1612,8 @@ 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._renderNumberInput(CONF_VIEW_TRIGGERS_EVENT_HOLD_SECONDS, {
|
||||
default: this._defaults.view.triggers.event_hold_seconds,
|
||||
})}
|
||||
${this._putInSubmenu(
|
||||
MENU_VIEW_TRIGGERS_ACTIONS,
|
||||
@@ -2340,8 +2353,8 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
.label=${localize('editor.move_down')}
|
||||
.disabled=${add ||
|
||||
!this._config ||
|
||||
!Array.isArray(this._config.cameras) ||
|
||||
index >= this._config.cameras.length - 1}
|
||||
!Array.isArray(array) ||
|
||||
index >= array.length - 1}
|
||||
@click=${() =>
|
||||
!add &&
|
||||
this._modifyConfig((config: RawAdvancedCameraCardConfig): boolean => {
|
||||
@@ -2360,7 +2373,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${localize('editor.delete')}
|
||||
.disabled=${add}
|
||||
.disabled=${!!add}
|
||||
@click=${() => {
|
||||
this._modifyConfig((config: RawAdvancedCameraCardConfig): boolean => {
|
||||
const array = getConfigValue(config, configPathArray);
|
||||
@@ -2463,6 +2476,83 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderTriggerEvent(
|
||||
cameraIndex: number,
|
||||
events: RawAdvancedCameraCardConfigArray,
|
||||
eventIndex: number,
|
||||
addNewEvent?: boolean,
|
||||
): TemplateResult | void {
|
||||
const eventsPath = getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS,
|
||||
cameraIndex,
|
||||
);
|
||||
|
||||
const submenuClasses = {
|
||||
submenu: true,
|
||||
selected: this._expandedMenus[MENU_CAMERAS_TRIGGERS_EVENT] === eventIndex,
|
||||
};
|
||||
const title = this._getEditorTriggerEventTitle(eventIndex, events[eventIndex] ?? {});
|
||||
const eventTypePath = `${eventsPath}.[${eventIndex}].event_type`;
|
||||
|
||||
return html` <div class="${classMap(submenuClasses)}">
|
||||
<div
|
||||
class="submenu-header"
|
||||
@click=${this._toggleMenu}
|
||||
.domain=${MENU_CAMERAS_TRIGGERS_EVENT}
|
||||
.key=${eventIndex}
|
||||
>
|
||||
<advanced-camera-card-icon
|
||||
.icon=${{ icon: addNewEvent ? 'mdi:plus' : 'mdi:flash' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span>
|
||||
${addNewEvent
|
||||
? html` <span class="new">
|
||||
[${localize('config.cameras.triggers.events.add_new_event')}...]
|
||||
</span>`
|
||||
: html`<span>${title}</span>`}
|
||||
</span>
|
||||
</div>
|
||||
${this._expandedMenus[MENU_CAMERAS_TRIGGERS_EVENT] === eventIndex
|
||||
? html` <div class="values">
|
||||
${this._renderArrayManagementControls(
|
||||
eventsPath,
|
||||
eventIndex,
|
||||
MENU_CAMERAS_TRIGGERS_EVENT,
|
||||
addNewEvent,
|
||||
)}
|
||||
${this._renderStringInput(eventTypePath, {
|
||||
label: localize('config.cameras.triggers.events.event_type'),
|
||||
})}
|
||||
${this._renderObjectSelector(`${eventsPath}.[${eventIndex}].event_data`, {
|
||||
label: localize('config.cameras.triggers.events.event_data'),
|
||||
})}
|
||||
</div>`
|
||||
: ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderTriggerEvents(cameraIndex: number): TemplateResult | void {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const events =
|
||||
(getConfigValue(
|
||||
this._config,
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS, cameraIndex),
|
||||
) as RawAdvancedCameraCardConfigArray | undefined) ?? [];
|
||||
|
||||
return this._putInSubmenu(
|
||||
MENU_CAMERAS_TRIGGERS_EVENTS,
|
||||
cameraIndex,
|
||||
'config.cameras.triggers.events.editor_label',
|
||||
'mdi:home-assistant',
|
||||
html`
|
||||
${events.map((_, index) => this._renderTriggerEvent(cameraIndex, events, index))}
|
||||
${this._renderTriggerEvent(cameraIndex, events, events.length, true)}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a camera section.
|
||||
* @param cameras The full array of cameras.
|
||||
@@ -2810,11 +2900,16 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
},
|
||||
)}
|
||||
${this._renderOptionSelector(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS, cameraIndex),
|
||||
this._triggersEvents,
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_MEDIA_EVENTS,
|
||||
cameraIndex,
|
||||
),
|
||||
this._triggersMediaEvents,
|
||||
{
|
||||
multiple: true,
|
||||
label: localize('config.cameras.triggers.events.editor_label'),
|
||||
label: localize(
|
||||
'config.cameras.triggers.media_events.editor_label',
|
||||
),
|
||||
},
|
||||
)}
|
||||
${this._putInSubmenu(
|
||||
@@ -2843,6 +2938,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
${this._renderTriggerEvents(cameraIndex)}
|
||||
`,
|
||||
)}
|
||||
${this._putInSubmenu(
|
||||
@@ -3083,6 +3179,29 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderObjectSelector(
|
||||
configPath: string,
|
||||
params?: {
|
||||
label?: string;
|
||||
},
|
||||
): TemplateResult | void {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${{ object: {} }}
|
||||
.label=${params?.label ?? this._getLabel(configPath)}
|
||||
.value=${getConfigValue(this._config, configPath)}
|
||||
.required=${false}
|
||||
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
|
||||
>
|
||||
</ha-selector>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a boolean selector.
|
||||
* @param configPath The configuration path to set/read.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isMatch } from 'lodash-es';
|
||||
|
||||
// Deep subset match between an HA bus event's payload `data` and a user-
|
||||
// configured filter. Mirrors HA automation `event_data` semantics: every key in
|
||||
// `filter` must exist in `data` and recursively match; extra keys in `data` are
|
||||
// ignored. The `unknown` guard lives here (not at the caller) because HA event
|
||||
// payloads arrive untyped from the WebSocket bus.
|
||||
export const matchesEventData = (
|
||||
filter: Record<string, unknown>,
|
||||
data: unknown,
|
||||
): boolean => typeof data === 'object' && data !== null && isMatch(data, filter);
|
||||
@@ -21,7 +21,7 @@ const isUsableOldState = (state: string | undefined): boolean =>
|
||||
* produce a single `'new'` or `'end'`.
|
||||
*
|
||||
* HA `event.*` entities are different: each fire just updates `state` to a new
|
||||
* ISO timestamp, with no continuous on/off. Those map to `signal` -- the
|
||||
* ISO timestamp, with no continuous on/off. Those map to `momentary` -- the
|
||||
* instantaneous-event discriminator. Transitions are skipped when the old state
|
||||
* is undefined (entity not previously observed) or `unavailable` (entity
|
||||
* reconnecting -- new state could be restored, not fresh), or when the new
|
||||
@@ -29,11 +29,11 @@ const isUsableOldState = (state: string | undefined): boolean =>
|
||||
*/
|
||||
export const getTriggerEventType = (
|
||||
difference: HassStateDifference,
|
||||
): 'new' | 'end' | 'signal' | null => {
|
||||
): 'new' | 'end' | 'momentary' | null => {
|
||||
if (computeDomain(difference.entityID) === 'event') {
|
||||
return isUsableOldState(difference.oldState?.state) &&
|
||||
isUsableNewState(difference.newState.state)
|
||||
? 'signal'
|
||||
? 'momentary'
|
||||
: null;
|
||||
}
|
||||
return isTriggeredState(difference.newState.state) ? 'new' : 'end';
|
||||
|
||||
@@ -243,6 +243,8 @@ export interface HassStateDifference {
|
||||
newState: HassEntity;
|
||||
}
|
||||
|
||||
export type SubscriptionUnsubscribe = () => Promise<void>;
|
||||
|
||||
// *************************************************************************
|
||||
// Home Assistant API types.
|
||||
// *************************************************************************
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"common": {
|
||||
"advanced_camera_card": "Advanced Camera Card",
|
||||
"advanced_camera_card_description": "An Advanced Camera Card",
|
||||
"event": "Event",
|
||||
"folder": "Folder",
|
||||
"in_progress": "In progress...",
|
||||
"no_media": "No media to display",
|
||||
@@ -188,12 +189,18 @@
|
||||
"editor_label": "Trigger options",
|
||||
"entities": "Trigger from other entities",
|
||||
"events": {
|
||||
"add_new_event": "Add new event trigger",
|
||||
"editor_label": "Home Assistant Events",
|
||||
"event_data": "Event data filter",
|
||||
"event_type": "Event type"
|
||||
},
|
||||
"doorbell": "Trigger by auto-detecting doorbell event entities",
|
||||
"media_events": {
|
||||
"clips": "Events with new clips",
|
||||
"editor_label": "Trigger Events",
|
||||
"editor_label": "Trigger Media Events",
|
||||
"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": {
|
||||
@@ -680,7 +687,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",
|
||||
"event_hold_seconds": "Seconds to hold a momentary trigger (e.g. a doorbell press, an HA bus event) visible before the post-end untrigger delay",
|
||||
"untrigger_delay_seconds": "Seconds delay after trigger state change before untrigger",
|
||||
"untrigger_force_seconds": "Seconds before forced untrigger"
|
||||
},
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { BrowseMediaCameraManagerEngine } from '../../../src/camera-manager/browse-media/engine-browse-media';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../../../src/camera-manager/store';
|
||||
import {
|
||||
CameraManagerRequestCache,
|
||||
CameraQuery,
|
||||
QueryType,
|
||||
} from '../../../src/camera-manager/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../../src/ha/browse-media/types';
|
||||
import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
|
||||
import { ResolvedMediaCache } from '../../../src/ha/resolved-media';
|
||||
import { QuerySource } from '../../../src/query-source';
|
||||
import { ViewMedia } from '../../../src/view/item';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../../../src/camera-manager/store';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
import { createCameraConfig, createHASS } from '../../test-utils';
|
||||
|
||||
const createEngine = (): BrowseMediaCameraManagerEngine => {
|
||||
return new BrowseMediaCameraManagerEngine(
|
||||
new EntityRegistryManagerMock(),
|
||||
mock<StateWatcher>(),
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
new BrowseMediaWalker(),
|
||||
new ResolvedMediaCache(),
|
||||
new CameraManagerRequestCache(),
|
||||
|
||||
@@ -3,10 +3,12 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { Camera } from '../../src/camera-manager/camera.js';
|
||||
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic.js';
|
||||
import { CameraProxyConfig } from '../../src/camera-manager/types.js';
|
||||
import { EventWatcherSubscriptionInterface } from '../../src/card-controller/hass/event-watcher.js';
|
||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||
import { liveProviderSupports2WayAudio } from '../../src/utils/live-provider.js';
|
||||
import { EntityRegistryManagerMock } from '../ha/registry/entity/mock.js';
|
||||
import {
|
||||
callEventWatcherCallback,
|
||||
callStateWatcherCallback,
|
||||
createCameraConfig,
|
||||
createCapabilities,
|
||||
@@ -23,7 +25,10 @@ describe('Camera', () => {
|
||||
const config = createCameraConfig();
|
||||
const camera = new Camera(
|
||||
config,
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getConfig()).toBe(config);
|
||||
});
|
||||
@@ -33,7 +38,10 @@ describe('Camera', () => {
|
||||
const capabilities = createCapabilities();
|
||||
const camera = await createInitializedCamera(
|
||||
createCameraConfig(),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
capabilities,
|
||||
);
|
||||
expect(camera.getCapabilities()).toBe(capabilities);
|
||||
@@ -42,7 +50,10 @@ describe('Camera', () => {
|
||||
it('when unpopulated', async () => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig(),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getCapabilities()).toBeNull();
|
||||
});
|
||||
@@ -51,6 +62,7 @@ describe('Camera', () => {
|
||||
it('should get engine', async () => {
|
||||
const engine = new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
);
|
||||
const camera = new Camera(createCameraConfig(), engine);
|
||||
expect(camera.getEngine()).toBe(engine);
|
||||
@@ -59,7 +71,10 @@ describe('Camera', () => {
|
||||
it('should set and get id', async () => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig(),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
camera.setID('foo');
|
||||
expect(camera.getID()).toBe('foo');
|
||||
@@ -69,7 +84,10 @@ describe('Camera', () => {
|
||||
it('should throw without id', async () => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig(),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(() => camera.getID()).toThrowError(
|
||||
'Could not determine camera id for the following ' +
|
||||
@@ -89,13 +107,17 @@ describe('Camera', () => {
|
||||
entities: ['camera.foo'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: stateWatcher,
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
|
||||
});
|
||||
|
||||
@@ -116,7 +138,10 @@ describe('Camera', () => {
|
||||
stream: 'stream',
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
|
||||
@@ -124,6 +149,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
|
||||
@@ -154,7 +180,10 @@ describe('Camera', () => {
|
||||
stream: 'stream',
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(false);
|
||||
@@ -162,6 +191,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false);
|
||||
@@ -176,7 +206,10 @@ describe('Camera', () => {
|
||||
metadata_fetch_timeout_seconds: 20,
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
|
||||
@@ -184,6 +217,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
|
||||
@@ -206,7 +240,10 @@ describe('Camera', () => {
|
||||
live: true,
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
|
||||
@@ -217,6 +254,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass,
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
|
||||
@@ -243,7 +281,10 @@ describe('Camera', () => {
|
||||
createCameraConfig({
|
||||
proxy: { live: true },
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getLiveProxyConfig()).toEqual(
|
||||
expect.objectContaining({ enabled: true, enforce: true }),
|
||||
@@ -255,7 +296,10 @@ describe('Camera', () => {
|
||||
createCameraConfig({
|
||||
proxy: { media: true },
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getMediaProxyConfig()).toEqual(
|
||||
expect.objectContaining({ enabled: true, enforce: true }),
|
||||
@@ -268,7 +312,10 @@ describe('Camera', () => {
|
||||
live_provider: 'go2rtc',
|
||||
go2rtc: { url: 'http://go2rtc' },
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getLiveProxyConfig()).toEqual(
|
||||
expect.objectContaining({ enabled: true, enforce: false }),
|
||||
@@ -280,7 +327,10 @@ describe('Camera', () => {
|
||||
createCameraConfig({
|
||||
proxy: { media: 'auto' },
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getMediaProxyConfig()).toEqual(
|
||||
expect.objectContaining({ enabled: false, enforce: false }),
|
||||
@@ -294,12 +344,16 @@ describe('Camera', () => {
|
||||
force: ['2-way-audio'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
|
||||
@@ -314,12 +368,16 @@ describe('Camera', () => {
|
||||
force: ['2-way-audio'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
|
||||
@@ -334,12 +392,16 @@ describe('Camera', () => {
|
||||
force: ['2-way-audio'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
|
||||
@@ -353,12 +415,16 @@ describe('Camera', () => {
|
||||
disable: ['2-way-audio'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
|
||||
@@ -372,12 +438,16 @@ describe('Camera', () => {
|
||||
disable_except: ['substream'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
|
||||
@@ -391,13 +461,17 @@ describe('Camera', () => {
|
||||
disable_except: ['substream', '2-way-audio'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).toHaveBeenCalled();
|
||||
@@ -411,13 +485,17 @@ describe('Camera', () => {
|
||||
disable_except: [],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(liveProviderSupports2WayAudio).toHaveBeenCalled();
|
||||
@@ -432,12 +510,16 @@ describe('Camera', () => {
|
||||
});
|
||||
const camera = new Camera(
|
||||
createCameraConfig({ camera_entity: 'camera.front_door' }),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
});
|
||||
|
||||
@@ -447,12 +529,16 @@ describe('Camera', () => {
|
||||
it('should leave entity null when camera_entity is unset', async () => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig(),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
});
|
||||
|
||||
@@ -462,12 +548,16 @@ describe('Camera', () => {
|
||||
it('should leave entity null when entityRegistryManager is not provided', async () => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig({ camera_entity: 'camera.front_door' }),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getEntity()).toBeNull();
|
||||
@@ -504,7 +594,10 @@ describe('Camera', () => {
|
||||
...(options?.userEntities && { entities: options.userEntities }),
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
||||
const hass = createHASS(
|
||||
@@ -518,6 +611,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass,
|
||||
stateWatcher,
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
...(!options?.omitRegistryManager && {
|
||||
entityRegistryManager: new EntityRegistryManagerMock(
|
||||
options?.registryEntities ?? [cameraEntity, doorbellEntity],
|
||||
@@ -643,7 +737,10 @@ describe('Camera', () => {
|
||||
entities: ['binary_sensor.foo'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
{
|
||||
eventCallback: eventCallback,
|
||||
},
|
||||
@@ -653,6 +750,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: stateWatcher,
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
|
||||
});
|
||||
|
||||
@@ -673,6 +771,100 @@ describe('Camera', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('should subscribe to configured triggers.events and dispatch momentary events', async () => {
|
||||
const eventCallback = vi.fn();
|
||||
const camera = new Camera(
|
||||
createCameraConfig({
|
||||
id: 'camera_1',
|
||||
triggers: {
|
||||
events: [{ event_type: 'zha_event' }],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
{ eventCallback },
|
||||
);
|
||||
|
||||
const eventWatcher = mock<EventWatcherSubscriptionInterface>();
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher,
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
|
||||
});
|
||||
|
||||
expect(eventWatcher.subscribe).toBeCalledTimes(1);
|
||||
const request = vi.mocked(eventWatcher.subscribe).mock.calls[0][1];
|
||||
expect(request.event_type).toBe('zha_event');
|
||||
expect(request.matcher).toBeUndefined();
|
||||
|
||||
callEventWatcherCallback(eventWatcher, { command: 'press' });
|
||||
|
||||
expect(eventCallback).toBeCalledWith({
|
||||
cameraID: 'camera_1',
|
||||
id: 'event:zha_event',
|
||||
type: 'momentary',
|
||||
});
|
||||
|
||||
await camera.destroy();
|
||||
expect(eventWatcher.unsubscribe).toBeCalled();
|
||||
});
|
||||
|
||||
it('should attach a matcher when triggers.events entry has data filter', async () => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig({
|
||||
id: 'camera_1',
|
||||
triggers: {
|
||||
events: [{ event_type: 'zha_event', event_data: { command: 'press' } }],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
const eventWatcher = mock<EventWatcherSubscriptionInterface>();
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher,
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
|
||||
});
|
||||
|
||||
const matcher = vi.mocked(eventWatcher.subscribe).mock.calls[0][1].matcher;
|
||||
expect(matcher).toBeDefined();
|
||||
expect(matcher?.({ command: 'press', extra: 1 })).toBe(true);
|
||||
expect(matcher?.({ command: 'release' })).toBe(false);
|
||||
});
|
||||
|
||||
it('should not subscribe to events when trigger capability is disabled', async () => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig({
|
||||
id: 'camera_1',
|
||||
triggers: {
|
||||
events: [{ event_type: 'zha_event' }],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
const eventWatcher = mock<EventWatcherSubscriptionInterface>();
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher,
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: false }) },
|
||||
});
|
||||
|
||||
expect(eventWatcher.subscribe).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not dispatch when the helper returns null', async () => {
|
||||
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||
|
||||
@@ -684,7 +876,10 @@ describe('Camera', () => {
|
||||
entities: ['event.front_door_doorbell'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
{
|
||||
eventCallback: eventCallback,
|
||||
},
|
||||
@@ -694,6 +889,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: stateWatcher,
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
|
||||
});
|
||||
|
||||
@@ -706,7 +902,7 @@ describe('Camera', () => {
|
||||
expect(eventCallback).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch a signal for an event entity fire', async () => {
|
||||
it('should dispatch a momentary event for an event entity fire', async () => {
|
||||
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||
|
||||
const eventCallback = vi.fn();
|
||||
@@ -717,7 +913,10 @@ describe('Camera', () => {
|
||||
entities: ['event.front_door_doorbell'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
{
|
||||
eventCallback: eventCallback,
|
||||
},
|
||||
@@ -727,6 +926,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: stateWatcher,
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
|
||||
});
|
||||
|
||||
@@ -740,7 +940,7 @@ describe('Camera', () => {
|
||||
expect(eventCallback).toBeCalledWith({
|
||||
cameraID: 'camera_1',
|
||||
id: 'event.front_door_doorbell',
|
||||
type: 'signal',
|
||||
type: 'momentary',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -753,7 +953,10 @@ describe('Camera', () => {
|
||||
entities: ['binary_sensor.foo'],
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
{
|
||||
eventCallback: eventCallback,
|
||||
},
|
||||
@@ -763,6 +966,7 @@ describe('Camera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: stateWatcher,
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
capabilityOptions: { capabilities: createCapabilities({ trigger: false }) },
|
||||
});
|
||||
|
||||
@@ -924,7 +1128,10 @@ describe('Camera', () => {
|
||||
(_name: string, cameraConfig: unknown, expectedResult: CameraProxyConfig) => {
|
||||
const camera = new Camera(
|
||||
createCameraConfig(cameraConfig),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getProxyConfig()).toEqual(expectedResult);
|
||||
},
|
||||
@@ -938,7 +1145,10 @@ describe('Camera', () => {
|
||||
go2rtc: { stream: '' },
|
||||
camera_entity: '',
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
expect(camera.getEndpoints()).toBeNull();
|
||||
});
|
||||
@@ -952,7 +1162,10 @@ describe('Camera', () => {
|
||||
},
|
||||
camera_entity: 'camera.foo',
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(camera.getEndpoints()).toEqual({
|
||||
|
||||
@@ -7,10 +7,11 @@ import { MotionEyeCameraManagerEngine } from '../../src/camera-manager/motioneye
|
||||
import { ReolinkCameraManagerEngine } from '../../src/camera-manager/reolink/engine-reolink.js';
|
||||
import { TPLinkCameraManagerEngine } from '../../src/camera-manager/tplink/engine-tplink.js';
|
||||
import { Engine } from '../../src/camera-manager/types.js';
|
||||
import { EventWatcherSubscriptionInterface } from '../../src/card-controller/hass/event-watcher.js';
|
||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||
import { CardWideConfig } from '../../src/config/schema/types.js';
|
||||
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
|
||||
import { DeviceRegistryManager } from '../../src/ha/registry/device';
|
||||
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
|
||||
import { ResolvedMediaCache } from '../../src/ha/resolved-media.js';
|
||||
import { EntityRegistryManagerMock } from '../ha/registry/entity/mock.js';
|
||||
import {
|
||||
@@ -232,6 +233,7 @@ describe('createEngine()', () => {
|
||||
expect(
|
||||
await createFactory().createEngine(Engine.Generic, {
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
resolvedMediaCache: mock<ResolvedMediaCache>(),
|
||||
}),
|
||||
).toBeInstanceOf(GenericCameraManagerEngine);
|
||||
@@ -240,6 +242,7 @@ describe('createEngine()', () => {
|
||||
expect(
|
||||
await createFactory().createEngine(Engine.Frigate, {
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
resolvedMediaCache: mock<ResolvedMediaCache>(),
|
||||
}),
|
||||
).toBeInstanceOf(FrigateCameraManagerEngine);
|
||||
@@ -248,6 +251,7 @@ describe('createEngine()', () => {
|
||||
expect(
|
||||
await createFactory().createEngine(Engine.MotionEye, {
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
resolvedMediaCache: mock<ResolvedMediaCache>(),
|
||||
}),
|
||||
).toBeInstanceOf(MotionEyeCameraManagerEngine);
|
||||
@@ -256,6 +260,7 @@ describe('createEngine()', () => {
|
||||
expect(
|
||||
await createFactory().createEngine(Engine.Reolink, {
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
resolvedMediaCache: mock<ResolvedMediaCache>(),
|
||||
}),
|
||||
).toBeInstanceOf(ReolinkCameraManagerEngine);
|
||||
@@ -264,6 +269,7 @@ describe('createEngine()', () => {
|
||||
expect(
|
||||
await createFactory().createEngine(Engine.TPLink, {
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
resolvedMediaCache: mock<ResolvedMediaCache>(),
|
||||
}),
|
||||
).toBeInstanceOf(TPLinkCameraManagerEngine);
|
||||
|
||||
@@ -18,9 +18,10 @@ import {
|
||||
FrigateReviewWatcher,
|
||||
} from '../../../src/camera-manager/frigate/watcher';
|
||||
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
|
||||
import { CameraTriggerEventType } from '../../../src/config/schema/cameras';
|
||||
import { CameraTriggerMediaEventType } from '../../../src/config/schema/cameras';
|
||||
import { Entity, EntityRegistryManager } from '../../../src/ha/registry/entity/types';
|
||||
import { ViewMediaType } from '../../../src/view/item';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
@@ -69,7 +70,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -91,7 +93,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
}),
|
||||
@@ -116,7 +119,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -141,7 +145,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -166,7 +171,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -191,7 +197,8 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
}),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -214,7 +221,8 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
}),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -231,7 +239,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -254,7 +263,8 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
}),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -278,7 +288,8 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
}),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -301,7 +312,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -329,7 +341,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -361,7 +374,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -391,7 +405,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -428,7 +443,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -460,7 +476,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -496,7 +513,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -814,7 +832,7 @@ describe('FrigateCamera', () => {
|
||||
camera_name: 'CAMERA',
|
||||
},
|
||||
triggers: {
|
||||
events: ['events'],
|
||||
media_events: ['events'],
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -825,7 +843,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -845,7 +864,7 @@ describe('FrigateCamera', () => {
|
||||
camera_name: 'CAMERA',
|
||||
},
|
||||
triggers: {
|
||||
events: [],
|
||||
media_events: [],
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -856,7 +875,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -882,7 +902,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -896,7 +917,7 @@ describe('FrigateCamera', () => {
|
||||
client_id: 'CLIENT_ID',
|
||||
},
|
||||
triggers: {
|
||||
events: ['events'],
|
||||
media_events: ['events'],
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -907,7 +928,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -919,7 +941,7 @@ describe('FrigateCamera', () => {
|
||||
createCameraConfig({
|
||||
frigate: { camera_name: 'front_door' },
|
||||
triggers: {
|
||||
events: ['events'],
|
||||
media_events: ['events'],
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -932,7 +954,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -1002,7 +1025,7 @@ describe('FrigateCamera', () => {
|
||||
])(
|
||||
'with events %s when snapshot %s and clip %s',
|
||||
async (
|
||||
events: CameraTriggerEventType[],
|
||||
events: CameraTriggerMediaEventType[],
|
||||
hasSnapshot: boolean,
|
||||
hasClip: boolean,
|
||||
call: boolean,
|
||||
@@ -1015,7 +1038,7 @@ describe('FrigateCamera', () => {
|
||||
camera_name: 'camera.front_door',
|
||||
},
|
||||
triggers: {
|
||||
events: events,
|
||||
media_events: events,
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -1029,7 +1052,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -1087,7 +1111,7 @@ describe('FrigateCamera', () => {
|
||||
zones: ['front_steps'],
|
||||
},
|
||||
triggers: {
|
||||
events: ['events'],
|
||||
media_events: ['events'],
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -1101,7 +1125,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -1146,7 +1171,7 @@ describe('FrigateCamera', () => {
|
||||
labels: ['person'],
|
||||
},
|
||||
triggers: {
|
||||
events: ['events'],
|
||||
media_events: ['events'],
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -1160,7 +1185,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -1202,7 +1228,7 @@ describe('FrigateCamera', () => {
|
||||
camera_name: 'camera.front_door',
|
||||
},
|
||||
triggers: {
|
||||
events: ['events'],
|
||||
media_events: ['events'],
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
@@ -1216,7 +1242,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: eventWatcher,
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -1269,7 +1296,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1302,7 +1330,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1331,7 +1360,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1365,7 +1395,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1429,7 +1460,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1501,7 +1533,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1573,7 +1606,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1631,7 +1665,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1688,7 +1723,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1749,7 +1785,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1807,7 +1844,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1873,7 +1911,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: reviewWatcher,
|
||||
});
|
||||
@@ -1951,7 +1990,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -1981,7 +2021,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2005,7 +2046,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2030,7 +2072,8 @@ describe('FrigateCamera', () => {
|
||||
camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
}),
|
||||
@@ -2056,7 +2099,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2081,7 +2125,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2105,7 +2150,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2138,7 +2184,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2173,7 +2220,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2193,7 +2241,8 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2226,7 +2275,8 @@ describe('FrigateCamera', () => {
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2264,7 +2314,8 @@ describe('FrigateCamera', () => {
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2317,7 +2368,8 @@ describe('FrigateCamera', () => {
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
@@ -2353,7 +2405,8 @@ describe('FrigateCamera', () => {
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { RecordingSegmentsCache } from '../../../src/camera-manager/cache';
|
||||
import { Camera } from '../../../src/camera-manager/camera';
|
||||
import {
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
QueryResultsType,
|
||||
QueryType,
|
||||
} from '../../../src/camera-manager/types';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../../src/config/schema/cameras';
|
||||
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
||||
@@ -58,6 +60,7 @@ const createEngine = (options?: {
|
||||
return new FrigateCameraManagerEngine(
|
||||
new EntityRegistryManagerMock(),
|
||||
new StateWatcher(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
options?.cache ?? new RecordingSegmentsCache(),
|
||||
options?.requestCache ?? new CameraManagerRequestCache(),
|
||||
);
|
||||
|
||||
@@ -2,10 +2,11 @@ import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { GenericCameraManagerEngine } from '../../../src/camera-manager/generic/engine-generic';
|
||||
import { Engine, QueryResultsType, QueryType } from '../../../src/camera-manager/types';
|
||||
import { QuerySource } from '../../../src/query-source';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../../src/config/schema/cameras';
|
||||
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
||||
import { QuerySource } from '../../../src/query-source';
|
||||
import {
|
||||
TestViewMedia,
|
||||
createCameraConfig,
|
||||
@@ -15,7 +16,10 @@ import {
|
||||
} from '../../test-utils';
|
||||
|
||||
const createEngine = (): GenericCameraManagerEngine => {
|
||||
return new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>());
|
||||
return new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
);
|
||||
};
|
||||
|
||||
const createGenericCameraConfig = (
|
||||
|
||||
@@ -574,15 +574,15 @@ describe('CameraManager', () => {
|
||||
config: createCameraConfig({
|
||||
...baseCameraConfig,
|
||||
triggers: {
|
||||
events: ['snapshots'],
|
||||
media_events: ['snapshots'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
await manager.initializeCamerasFromConfig();
|
||||
expect(manager.getStore().getCamera('id')?.getConfig().triggers.events).toEqual([
|
||||
'snapshots',
|
||||
]);
|
||||
expect(
|
||||
manager.getStore().getCamera('id')?.getConfig().triggers.media_events,
|
||||
).toEqual(['snapshots']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||
import { MotionEyeCamera } from '../../../src/camera-manager/motioneye/camera';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||
|
||||
@@ -48,7 +49,8 @@ describe('MotionEyeCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
const endpoints = camera.getEndpoints();
|
||||
@@ -65,7 +67,8 @@ describe('MotionEyeCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
const endpoints = camera.getEndpoints();
|
||||
@@ -82,7 +85,8 @@ describe('MotionEyeCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
const capabilities = camera.getCapabilities();
|
||||
@@ -107,7 +111,8 @@ describe('MotionEyeCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
const capabilities = camera.getCapabilities();
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
QueryResultsType,
|
||||
QueryType,
|
||||
} from '../../../src/camera-manager/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { BrowseMediaMetadata } from '../../../src/ha/browse-media/types';
|
||||
import { BrowseMediaStep, BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
|
||||
import { Entity } from '../../../src/ha/registry/entity/types';
|
||||
@@ -50,7 +51,8 @@ const createEngine = (options?: {
|
||||
}): MotionEyeCameraManagerEngine => {
|
||||
return new MotionEyeCameraManagerEngine(
|
||||
new EntityRegistryManagerMock(options?.entities ?? [createEntity()]),
|
||||
mock<StateWatcher>(),
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
options?.walker ?? new BrowseMediaWalker(),
|
||||
new ResolvedMediaCache(),
|
||||
options?.requestCache ?? new CameraManagerRequestCache(),
|
||||
@@ -73,7 +75,8 @@ const createMotionEyeStore = async (
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([entity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
camera.setID(options?.cameraID ?? 'camera-1');
|
||||
const store = new CameraManagerStore();
|
||||
@@ -497,7 +500,8 @@ describe('MotionEyeCameraManagerEngine', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([createEntity()]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
camera.setID('camera-1');
|
||||
const store = new CameraManagerStore();
|
||||
@@ -545,7 +549,8 @@ describe('MotionEyeCameraManagerEngine', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([createEntity()]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
camera.setID('camera-1');
|
||||
const store = new CameraManagerStore();
|
||||
@@ -599,7 +604,8 @@ describe('MotionEyeCameraManagerEngine', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([createEntity()]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
camera.setID('camera-1');
|
||||
const store = new CameraManagerStore();
|
||||
|
||||
@@ -4,7 +4,8 @@ import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||
import { ReolinkCamera } from '../../../src/camera-manager/reolink/camera';
|
||||
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 { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { DeviceRegistryManager } from '../../../src/ha/registry/device';
|
||||
import { EntityRegistryManagerLive } from '../../../src/ha/registry/entity';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
@@ -105,7 +106,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManagerLive>(),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not find camera entity');
|
||||
});
|
||||
@@ -129,7 +131,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not initialize Reolink camera');
|
||||
});
|
||||
@@ -153,7 +156,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not initialize Reolink camera');
|
||||
});
|
||||
@@ -169,7 +173,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(0);
|
||||
@@ -201,7 +206,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(3);
|
||||
@@ -224,7 +230,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(7);
|
||||
@@ -247,7 +254,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(0);
|
||||
@@ -270,7 +278,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(7);
|
||||
@@ -295,7 +304,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(0);
|
||||
@@ -327,7 +337,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(0);
|
||||
@@ -359,7 +370,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(0);
|
||||
@@ -391,7 +403,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
deviceRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(0);
|
||||
@@ -410,7 +423,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -449,7 +463,8 @@ describe('ReolinkCamera', () => {
|
||||
}),
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -475,7 +490,8 @@ describe('ReolinkCamera', () => {
|
||||
}),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -508,7 +524,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -695,7 +712,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -727,7 +745,8 @@ describe('ReolinkCamera', () => {
|
||||
buttonEntityPTZLeft,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||
@@ -754,7 +773,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -795,7 +815,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -821,7 +842,8 @@ describe('ReolinkCamera', () => {
|
||||
}),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -852,7 +874,8 @@ describe('ReolinkCamera', () => {
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -875,7 +898,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -902,7 +926,8 @@ describe('ReolinkCamera', () => {
|
||||
}),
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toBeNull();
|
||||
@@ -924,7 +949,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -946,7 +972,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -984,7 +1011,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1022,7 +1050,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1060,7 +1089,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1098,7 +1128,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1127,7 +1158,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1155,7 +1187,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1180,7 +1213,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1211,7 +1245,8 @@ describe('ReolinkCamera', () => {
|
||||
buttonEntityPTZStop,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -1233,7 +1268,8 @@ describe('ReolinkCamera', () => {
|
||||
numberEntityZoom,
|
||||
]),
|
||||
deviceRegistryManager: mock<DeviceRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
|
||||
@@ -23,19 +23,20 @@ import {
|
||||
QueryReturnType,
|
||||
QueryType,
|
||||
} from '../../../src/camera-manager/types';
|
||||
import { QuerySource } from '../../../src/query-source';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { BrowseMedia, browseMediaSchema } from '../../../src/ha/browse-media/types';
|
||||
import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
|
||||
import { DeviceRegistryManager } from '../../../src/ha/registry/device';
|
||||
import { EntityRegistryManager } from '../../../src/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../../../src/ha/resolved-media';
|
||||
import { homeAssistantWSRequest } from '../../../src/ha/ws-request';
|
||||
import { QuerySource } from '../../../src/query-source';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
import {
|
||||
createInitializedCamera,
|
||||
createCameraConfig,
|
||||
createHASS,
|
||||
createInitializedCamera,
|
||||
createRegistryEntity,
|
||||
createStore,
|
||||
} from '../../test-utils';
|
||||
@@ -186,7 +187,8 @@ const createEngine = (options?: {
|
||||
return new ReolinkCameraManagerEngine(
|
||||
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
|
||||
mock<DeviceRegistryManager>(),
|
||||
mock<StateWatcher>(),
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
options?.browseMediaManager ?? new BrowseMediaWalker(),
|
||||
new ResolvedMediaCache(),
|
||||
new CameraManagerRequestCache(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Capabilities } from '../../src/camera-manager/capabilities.js';
|
||||
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
|
||||
import { CameraManagerStore } from '../../src/camera-manager/store.js';
|
||||
import { Engine } from '../../src/camera-manager/types.js';
|
||||
import { EventWatcherSubscriptionInterface } from '../../src/card-controller/hass/event-watcher.js';
|
||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||
import { DeviceRegistryManager } from '../../src/ha/registry/device/index.js';
|
||||
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
|
||||
@@ -31,10 +32,12 @@ describe('CameraManagerStore', async () => {
|
||||
|
||||
const engineGeneric = await engineFactory.createEngine(Engine.Generic, {
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
resolvedMediaCache: mock<ResolvedMediaCache>(),
|
||||
});
|
||||
const engineFrigate = await engineFactory.createEngine(Engine.Frigate, {
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
resolvedMediaCache: mock<ResolvedMediaCache>(),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||
import { TPLinkCamera } from '../../../src/camera-manager/tplink/camera';
|
||||
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||
|
||||
@@ -78,7 +79,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not find camera entity');
|
||||
});
|
||||
@@ -94,7 +96,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not find camera entity');
|
||||
});
|
||||
@@ -112,7 +115,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getEntity()).toBe(cameraEntity);
|
||||
@@ -129,7 +133,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getEntity()).toBe(cameraEntity);
|
||||
@@ -146,7 +151,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -175,7 +181,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
@@ -198,7 +205,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -219,7 +227,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -242,7 +251,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -262,7 +272,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -290,7 +301,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -318,7 +330,8 @@ describe('TPLinkCamera', () => {
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
@@ -357,7 +370,8 @@ describe('TPLinkCamera', () => {
|
||||
cameraEntity,
|
||||
buttonEntityPanLeft,
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||
@@ -395,7 +409,8 @@ describe('TPLinkCamera', () => {
|
||||
cameraWithDifferentUniqueId,
|
||||
buttonEntityPanLeft,
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
// Should not find PTZ entities since unique_id doesn't end with _live_view
|
||||
@@ -420,7 +435,8 @@ describe('TPLinkCamera', () => {
|
||||
cameraWithoutUniqueId,
|
||||
buttonEntityPanLeft,
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
|
||||
// Should not find PTZ entities since camera has no unique_id
|
||||
@@ -442,7 +458,8 @@ describe('TPLinkCamera', () => {
|
||||
cameraEntity,
|
||||
buttonEntityPanLeft, // Only left button available
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { TPLinkCameraManagerEngine } from '../../../src/camera-manager/tplink/engine-tplink';
|
||||
import { Engine } from '../../../src/camera-manager/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||
|
||||
@@ -11,7 +12,8 @@ const createEngine = (options?: {
|
||||
}): TPLinkCameraManagerEngine => {
|
||||
return new TPLinkCameraManagerEngine(
|
||||
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
|
||||
mock<StateWatcher>(),
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { HassEvent } from 'home-assistant-js-websocket';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { EventWatcher } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { HomeAssistant } from '../../../src/ha/types';
|
||||
import { createHASS } from '../../test-utils';
|
||||
|
||||
const fireEvent = (hass: HomeAssistant, event: HassEvent, n = 0): void => {
|
||||
const mock = vi.mocked(hass.connection.subscribeEvents).mock;
|
||||
expect(mock.calls.length).greaterThan(n);
|
||||
|
||||
// subscribeEvents(callback, event_type) -- callback is the first argument.
|
||||
mock.calls[n][0]?.(event);
|
||||
};
|
||||
|
||||
const createHassEvent = (event_type: string, data: object = {}): HassEvent => ({
|
||||
event_type,
|
||||
data: data as { [key: string]: string },
|
||||
origin: 'LOCAL',
|
||||
time_fired: '2026-05-25T00:00:00Z',
|
||||
context: { id: 'ctx', user_id: null, parent_id: null },
|
||||
});
|
||||
|
||||
describe('EventWatcher', () => {
|
||||
it('opens a single WS subscription per event_type regardless of subscribers', async () => {
|
||||
const watcher = new EventWatcher();
|
||||
const hass = createHASS();
|
||||
|
||||
await watcher.subscribe(hass, { event_type: 'zha_event', callback: vi.fn() });
|
||||
await watcher.subscribe(hass, { event_type: 'zha_event', callback: vi.fn() });
|
||||
|
||||
expect(hass.connection.subscribeEvents).toBeCalledTimes(1);
|
||||
expect(vi.mocked(hass.connection.subscribeEvents).mock.calls[0][1]).toBe(
|
||||
'zha_event',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens separate WS subscriptions for distinct event_types', async () => {
|
||||
const watcher = new EventWatcher();
|
||||
const hass = createHASS();
|
||||
|
||||
await watcher.subscribe(hass, { event_type: 'zha_event', callback: vi.fn() });
|
||||
await watcher.subscribe(hass, { event_type: 'deconz_event', callback: vi.fn() });
|
||||
|
||||
expect(hass.connection.subscribeEvents).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('only tears down the WS subscription when the last subscriber unsubscribes', async () => {
|
||||
const watcher = new EventWatcher();
|
||||
const hass = createHASS();
|
||||
const unsub = vi.fn();
|
||||
vi.mocked(hass.connection.subscribeEvents).mockResolvedValue(unsub);
|
||||
|
||||
const req1 = { event_type: 'zha_event', callback: vi.fn() };
|
||||
const req2 = { event_type: 'zha_event', callback: vi.fn() };
|
||||
await watcher.subscribe(hass, req1);
|
||||
await watcher.subscribe(hass, req2);
|
||||
|
||||
await watcher.unsubscribe(req1);
|
||||
expect(unsub).not.toBeCalled();
|
||||
|
||||
await watcher.unsubscribe(req2);
|
||||
expect(unsub).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('dispatches to all subscribers whose event_type matches', async () => {
|
||||
const watcher = new EventWatcher();
|
||||
const hass = createHASS();
|
||||
const cb1 = vi.fn();
|
||||
const cb2 = vi.fn();
|
||||
|
||||
await watcher.subscribe(hass, { event_type: 'zha_event', callback: cb1 });
|
||||
await watcher.subscribe(hass, { event_type: 'zha_event', callback: cb2 });
|
||||
|
||||
fireEvent(hass, createHassEvent('zha_event', { command: 'press' }));
|
||||
|
||||
expect(cb1).toBeCalledWith({ command: 'press' });
|
||||
expect(cb2).toBeCalledWith({ command: 'press' });
|
||||
});
|
||||
|
||||
it('drops events whose event_type does not match the request', async () => {
|
||||
const watcher = new EventWatcher();
|
||||
const hass = createHASS();
|
||||
const cb = vi.fn();
|
||||
|
||||
await watcher.subscribe(hass, { event_type: 'zha_event', callback: cb });
|
||||
// Inject an unrelated event into the shared dispatcher.
|
||||
fireEvent(hass, createHassEvent('other_event', { x: 1 }));
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('gates dispatch on the request matcher when provided', async () => {
|
||||
const watcher = new EventWatcher();
|
||||
const hass = createHASS();
|
||||
const cb = vi.fn();
|
||||
const matcher = vi.fn((data: unknown) => (data as { x?: number }).x === 1);
|
||||
|
||||
await watcher.subscribe(hass, { event_type: 'zha_event', matcher, callback: cb });
|
||||
|
||||
fireEvent(hass, createHassEvent('zha_event', { x: 1 }));
|
||||
fireEvent(hass, createHassEvent('zha_event', { x: 2 }));
|
||||
|
||||
expect(matcher).toBeCalledTimes(2);
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith({ x: 1 });
|
||||
});
|
||||
|
||||
it('handles unsubscribe during a still-pending subscribe without leaking', async () => {
|
||||
const watcher = new EventWatcher();
|
||||
const hass = createHASS();
|
||||
const unsub = vi.fn();
|
||||
|
||||
let resolveSubscription: ((cb: () => Promise<void>) => void) | undefined;
|
||||
const subscriptionPromise = new Promise<() => Promise<void>>((resolve) => {
|
||||
resolveSubscription = resolve;
|
||||
});
|
||||
vi.mocked(hass.connection.subscribeEvents).mockReturnValue(subscriptionPromise);
|
||||
|
||||
const req = { event_type: 'zha_event', callback: vi.fn() };
|
||||
const subscribePromise = watcher.subscribe(hass, req);
|
||||
|
||||
// Unsubscribe before the underlying connection has resolved.
|
||||
const unsubscribePromise = watcher.unsubscribe(req);
|
||||
|
||||
// Resolve the connection -- the watcher should now have the unsub fn and
|
||||
// call it as part of completing the unsubscribe.
|
||||
resolveSubscription?.(unsub);
|
||||
await subscribePromise;
|
||||
await unsubscribePromise;
|
||||
|
||||
expect(unsub).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { STATE_RUNNING, STATE_STARTING } from 'home-assistant-js-websocket';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { EventWatcher } from '../../../src/card-controller/hass/event-watcher';
|
||||
import { HASSManager } from '../../../src/card-controller/hass/hass-manager';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import {
|
||||
@@ -29,6 +30,11 @@ describe('HASSManager', () => {
|
||||
expect(manager.getStateWatcher()).toEqual(expect.any(StateWatcher));
|
||||
});
|
||||
|
||||
it('should get event watcher', () => {
|
||||
const manager = new HASSManager(createCardAPI());
|
||||
expect(manager.getEventWatcher()).toEqual(expect.any(EventWatcher));
|
||||
});
|
||||
|
||||
it('should get hass after set', () => {
|
||||
const manager = new HASSManager(createCardAPI());
|
||||
const hass = createHASS();
|
||||
|
||||
@@ -25,7 +25,7 @@ vi.mock('lodash-es', async () => ({
|
||||
const baseTriggersConfig: TriggersOptions = {
|
||||
untrigger_delay_seconds: 10,
|
||||
untrigger_force_seconds: 0,
|
||||
signal_hold_seconds: 0,
|
||||
event_hold_seconds: 0,
|
||||
filter_selected_camera: false,
|
||||
show_trigger_status: false,
|
||||
actions: {
|
||||
@@ -810,7 +810,7 @@ describe('TriggersManager', () => {
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should auto-untrigger after the delay on a signal event', async () => {
|
||||
it('should auto-untrigger after the delay on a momentary event', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
actions: { trigger: 'none', untrigger: 'default' },
|
||||
@@ -821,7 +821,7 @@ describe('TriggersManager', () => {
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
id: 'event.doorbell',
|
||||
type: 'signal',
|
||||
type: 'momentary',
|
||||
});
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
@@ -835,7 +835,7 @@ describe('TriggersManager', () => {
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||
});
|
||||
|
||||
it('should auto-untrigger immediately on a signal when delay is 0', async () => {
|
||||
it('should auto-untrigger immediately on a momentary event when delay is 0', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
untrigger_delay_seconds: 0,
|
||||
@@ -847,7 +847,7 @@ describe('TriggersManager', () => {
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
id: 'event.doorbell',
|
||||
type: 'signal',
|
||||
type: 'momentary',
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
@@ -855,7 +855,7 @@ describe('TriggersManager', () => {
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not auto-untrigger from a signal while a continuous source remains active', async () => {
|
||||
it('should not auto-untrigger from a momentary event while a continuous source remains active', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
actions: { trigger: 'none', untrigger: 'default' },
|
||||
@@ -874,7 +874,7 @@ describe('TriggersManager', () => {
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
id: 'event.doorbell',
|
||||
type: 'signal',
|
||||
type: 'momentary',
|
||||
});
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
@@ -886,11 +886,11 @@ describe('TriggersManager', () => {
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should add signal_hold_seconds on top of untrigger_delay_seconds for signals', async () => {
|
||||
it('should add event_hold_seconds on top of untrigger_delay_seconds for momentary events', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
untrigger_delay_seconds: 5,
|
||||
signal_hold_seconds: 30,
|
||||
event_hold_seconds: 30,
|
||||
actions: { trigger: 'none', untrigger: 'default' },
|
||||
},
|
||||
});
|
||||
@@ -899,7 +899,7 @@ describe('TriggersManager', () => {
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
id: 'event.doorbell',
|
||||
type: 'signal',
|
||||
type: 'momentary',
|
||||
});
|
||||
|
||||
// At 34s the additive 35s window is still active.
|
||||
@@ -916,11 +916,11 @@ describe('TriggersManager', () => {
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not apply signal_hold_seconds to non-signal events', async () => {
|
||||
it('should not apply event_hold_seconds to non-momentary events', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
untrigger_delay_seconds: 5,
|
||||
signal_hold_seconds: 30,
|
||||
event_hold_seconds: 30,
|
||||
actions: { trigger: 'none', untrigger: 'default' },
|
||||
},
|
||||
});
|
||||
@@ -1305,9 +1305,9 @@ describe('TriggersManager', () => {
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not reset the untrigger timer when a filtered-out signal arrives', async () => {
|
||||
// Guards the `if (handled)` branch on the signal synthesis: a
|
||||
// filter-rejected signal must not call the internal 'end', which would
|
||||
it('should not reset the untrigger timer when a filtered-out momentary event arrives', async () => {
|
||||
// Guards the `if (handled)` branch on the momentary-event synthesis: a
|
||||
// filter-rejected momentary event must not call the internal 'end', which would
|
||||
// otherwise reset an already-running untrigger-delay timer.
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
@@ -1341,7 +1341,7 @@ describe('TriggersManager', () => {
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
id: 'event.doorbell',
|
||||
type: 'signal',
|
||||
type: 'momentary',
|
||||
});
|
||||
|
||||
// At t=10 (the original delay's deadline), the timer should fire.
|
||||
|
||||
@@ -4052,6 +4052,154 @@ describe('should handle version specific upgrades', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cameras[].triggers.events string[] → triggers.media_events', () => {
|
||||
it('migrates a legacy string array to media_events', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [
|
||||
{
|
||||
camera_entity: 'camera.office',
|
||||
triggers: { events: ['events', 'clips'] },
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config.cameras[0].triggers).toEqual({
|
||||
media_events: ['events', 'clips'],
|
||||
});
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
|
||||
it('migrates an empty legacy array', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [{ camera_entity: 'camera.office', triggers: { events: [] } }],
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config.cameras[0].triggers).toEqual({ media_events: [] });
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
|
||||
it('leaves the new object-array shape untouched', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [
|
||||
{
|
||||
camera_entity: 'camera.office',
|
||||
triggers: { events: [{ event_type: 'zha_event' }] },
|
||||
},
|
||||
],
|
||||
};
|
||||
// The new shape might still trigger OTHER upgrades to fire on the
|
||||
// config, so we don't assert the overall upgrade result -- only that
|
||||
// this specific field is not touched.
|
||||
upgradeConfig(config);
|
||||
expect(config.cameras[0].triggers).toEqual({
|
||||
events: [{ event_type: 'zha_event' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('drops legacy events but keeps an existing media_events untouched', () => {
|
||||
// Both fields present is implausible in real user configs, but if it
|
||||
// happens we still must remove the legacy `events: string[]` because
|
||||
// the new schema would reject it; the explicit `media_events` wins.
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [
|
||||
{
|
||||
camera_entity: 'camera.office',
|
||||
triggers: {
|
||||
events: ['snapshots'],
|
||||
media_events: ['clips'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config.cameras[0].triggers).toEqual({
|
||||
media_events: ['clips'],
|
||||
});
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
|
||||
it('is idempotent across repeated runs', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [
|
||||
{
|
||||
camera_entity: 'camera.office',
|
||||
triggers: { events: ['events'] },
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(upgradeConfig(config)).toBeFalsy();
|
||||
expect(config.cameras[0].triggers).toEqual({ media_events: ['events'] });
|
||||
});
|
||||
|
||||
it('migrates per camera independently', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [
|
||||
{ camera_entity: 'camera.a', triggers: { events: ['clips'] } },
|
||||
{
|
||||
camera_entity: 'camera.b',
|
||||
triggers: { events: [{ event_type: 'zha_event' }] },
|
||||
},
|
||||
{ camera_entity: 'camera.c' },
|
||||
],
|
||||
};
|
||||
upgradeConfig(config);
|
||||
expect(config.cameras[0].triggers).toEqual({ media_events: ['clips'] });
|
||||
expect(config.cameras[1].triggers).toEqual({
|
||||
events: [{ event_type: 'zha_event' }],
|
||||
});
|
||||
expect(config.cameras[2].triggers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('migrates a legacy cameras_global.triggers.events string array', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
cameras_global: {
|
||||
triggers: {
|
||||
events: ['clips', 'snapshots'],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config.cameras_global.triggers).toEqual({
|
||||
media_events: ['clips', 'snapshots'],
|
||||
});
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
|
||||
it('leaves cameras_global new-shape events untouched', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
cameras_global: {
|
||||
triggers: {
|
||||
events: [{ event_type: 'zha_event' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
upgradeConfig(config);
|
||||
expect(config.cameras_global.triggers).toEqual({
|
||||
events: [{ event_type: 'zha_event' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('is a no-op when triggers is not an object (malformed user config)', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [{ camera_entity: 'camera.office', triggers: 'not-an-object' }],
|
||||
};
|
||||
upgradeConfig(config);
|
||||
expect(config.cameras[0].triggers).toBe('not-an-object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('live_substream_{on,off,select} → substream_{on,off}', () => {
|
||||
it('rewrites live_substream_on to substream_on in an automation', () => {
|
||||
const config = {
|
||||
|
||||
@@ -57,6 +57,7 @@ describe('config defaults', () => {
|
||||
doorbell: false,
|
||||
entities: [],
|
||||
events: [],
|
||||
media_events: [],
|
||||
motion: false,
|
||||
occupancy: false,
|
||||
reviews: {
|
||||
@@ -550,7 +551,7 @@ describe('config defaults', () => {
|
||||
},
|
||||
filter_selected_camera: true,
|
||||
show_trigger_status: false,
|
||||
signal_hold_seconds: 30,
|
||||
event_hold_seconds: 30,
|
||||
untrigger_delay_seconds: 0,
|
||||
untrigger_force_seconds: 0,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { matchesEventData } from '../../src/ha/event-data-match';
|
||||
|
||||
describe('matchesEventData', () => {
|
||||
describe('non-object data', () => {
|
||||
it.each([['string'], [42], [true], [null], [undefined]])(
|
||||
'rejects non-object data (%s) with a non-empty filter',
|
||||
(data) => {
|
||||
expect(matchesEventData({ a: 1 }, data)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('matches when every filter key matches', () => {
|
||||
expect(matchesEventData({ command: 'press' }, { command: 'press', extra: 1 })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects when a filter value differs', () => {
|
||||
expect(matchesEventData({ command: 'press' }, { command: 'release' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects when a filter key is missing from data', () => {
|
||||
expect(matchesEventData({ command: 'press' }, { other: 'press' })).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores extra keys in data', () => {
|
||||
expect(matchesEventData({ a: 1 }, { a: 1, b: 2, c: 3 })).toBe(true);
|
||||
});
|
||||
|
||||
it('matches nested objects as a subset', () => {
|
||||
expect(
|
||||
matchesEventData(
|
||||
{ device: { id: 'abc' } },
|
||||
{ device: { id: 'abc', name: 'Front Door' } },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects nested objects when a nested key differs', () => {
|
||||
expect(matchesEventData({ device: { id: 'abc' } }, { device: { id: 'xyz' } })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('matches arrays element-wise', () => {
|
||||
expect(matchesEventData({ tags: ['a', 'b'] }, { tags: ['a', 'b'] })).toBe(true);
|
||||
});
|
||||
|
||||
it('matches arrays as a subset by index (filter shorter than data)', () => {
|
||||
// Same partial-match semantics lodash uses for objects: a shorter filter
|
||||
// array matches if every index it specifies matches in data. Useful when
|
||||
// the user only cares about the first N values.
|
||||
expect(matchesEventData({ tags: ['a'] }, { tags: ['a', 'b'] })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects arrays with mismatched elements at the same index', () => {
|
||||
expect(matchesEventData({ tags: ['a', 'c'] }, { tags: ['a', 'b'] })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects when filter array is longer than data array', () => {
|
||||
expect(matchesEventData({ tags: ['a', 'b'] }, { tags: ['a'] })).toBe(false);
|
||||
});
|
||||
|
||||
it('distinguishes null from undefined', () => {
|
||||
expect(matchesEventData({ a: null }, { a: null })).toBe(true);
|
||||
expect(matchesEventData({ a: null }, { a: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts an empty filter against any object', () => {
|
||||
expect(matchesEventData({}, {})).toBe(true);
|
||||
expect(matchesEventData({}, { anything: 1, nested: { x: 2 } })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -36,14 +36,14 @@ describe('getTriggerEventType', () => {
|
||||
});
|
||||
|
||||
describe('for event entities', () => {
|
||||
it('returns "signal" for a transition between two real timestamps', () => {
|
||||
it('returns "momentary" for a transition between two real timestamps', () => {
|
||||
expect(
|
||||
getTriggerEventType({
|
||||
entityID: 'event.front_door_doorbell',
|
||||
oldState: createStateEntity({ state: '2026-05-24T12:00:00.000+00:00' }),
|
||||
newState: createStateEntity({ state: '2026-05-24T12:00:05.123+00:00' }),
|
||||
}),
|
||||
).toBe('signal');
|
||||
).toBe('momentary');
|
||||
});
|
||||
|
||||
it('returns null when there is no old state', () => {
|
||||
@@ -65,14 +65,14 @@ describe('getTriggerEventType', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns "signal" when the old state is unknown (first fire after startup)', () => {
|
||||
it('returns "momentary" when the old state is unknown (first fire after startup)', () => {
|
||||
expect(
|
||||
getTriggerEventType({
|
||||
entityID: 'event.front_door_doorbell',
|
||||
oldState: createStateEntity({ state: 'unknown' }),
|
||||
newState: createStateEntity({ state: '2026-05-24T12:00:05.123+00:00' }),
|
||||
}),
|
||||
).toBe('signal');
|
||||
).toBe('momentary');
|
||||
});
|
||||
|
||||
it.each([['unavailable'], ['unknown']])(
|
||||
|
||||
@@ -38,6 +38,7 @@ import { FoldersManager } from '../src/card-controller/folders/manager';
|
||||
import { FolderQuery } from '../src/card-controller/folders/types';
|
||||
import { FullscreenManager } from '../src/card-controller/fullscreen/fullscreen-manager';
|
||||
import { HASSManager } from '../src/card-controller/hass/hass-manager';
|
||||
import { EventWatcherSubscriptionInterface } from '../src/card-controller/hass/event-watcher';
|
||||
import { StateWatcherSubscriptionInterface } from '../src/card-controller/hass/state-watcher';
|
||||
import { InitializationManager } from '../src/card-controller/initialization-manager';
|
||||
import { InteractionManager } from '../src/card-controller/interaction-manager';
|
||||
@@ -124,6 +125,7 @@ export const createInitializedCamera = async (
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
|
||||
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
|
||||
...(capabilities ? { capabilityOptions: { capabilities } } : {}),
|
||||
});
|
||||
return camera;
|
||||
@@ -143,6 +145,7 @@ export const createHASS = (states?: HassEntities, user?: CurrentUser): HomeAssis
|
||||
// startup state still represent a "ready" instance.
|
||||
hass.config.state = STATE_RUNNING;
|
||||
hass.connection.subscribeMessage = vi.fn();
|
||||
hass.connection.subscribeEvents = vi.fn();
|
||||
|
||||
// ha-nunjucks calls sendMessagePromise to fetch label registry; return empty array to prevent crash.
|
||||
hass.connection.sendMessagePromise = vi.fn().mockResolvedValue([]);
|
||||
@@ -282,6 +285,7 @@ export const createStore = (
|
||||
cameraProps.engine ??
|
||||
new GenericCameraManagerEngine(
|
||||
mock<StateWatcherSubscriptionInterface>(),
|
||||
mock<EventWatcherSubscriptionInterface>(),
|
||||
mock<EntityRegistryManager>(),
|
||||
eventCallback,
|
||||
),
|
||||
@@ -737,6 +741,16 @@ export const callStateWatcherCallback = (
|
||||
mock.calls[n][0](diff);
|
||||
};
|
||||
|
||||
export const callEventWatcherCallback = (
|
||||
eventWatcher: EventWatcherSubscriptionInterface,
|
||||
data: unknown,
|
||||
n = 0,
|
||||
): void => {
|
||||
const mock = vi.mocked(eventWatcher.subscribe).mock;
|
||||
expect(mock.calls.length).greaterThan(n);
|
||||
mock.calls[n][1].callback(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Flush resolved promises.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user