Allow camera engines to signal events to the card.
This commit is contained in:
@@ -2,6 +2,7 @@ import { LitElement, ReactiveControllerHost } from 'lit';
|
||||
import { ActionEventTarget } from '../action-handler-directive';
|
||||
import { setOrRemoveAttribute } from '../utils/basic';
|
||||
import { isCardInPanel } from '../utils/ha';
|
||||
import { InitializationAspect } from './initialization-manager';
|
||||
import { CardElementAPI } from './types';
|
||||
|
||||
export type ScrollCallback = () => void;
|
||||
@@ -103,6 +104,16 @@ export class CardElementManager {
|
||||
this._api.getMediaLoadedInfoManager().clear();
|
||||
this._api.getFullscreenManager().disconnect();
|
||||
|
||||
// Reset and uninitialize cameras to cause them to reinitialize on
|
||||
// reconnection, to ensure the state subscription/unsubscription works
|
||||
// correctly for triggers.
|
||||
this._api
|
||||
.getCameraManager()
|
||||
.reset()
|
||||
.then(() =>
|
||||
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS),
|
||||
);
|
||||
|
||||
this._element.removeEventListener(
|
||||
'mousemove',
|
||||
this._api.getInteractionManager().reportInteraction,
|
||||
|
||||
@@ -192,6 +192,10 @@ export class ConditionsManager {
|
||||
};
|
||||
this._triggerChange();
|
||||
}
|
||||
|
||||
public getState(): ConditionState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public hasHAStateConditions(): boolean {
|
||||
return this._hasHAStateConditions;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CameraConfig } from '../config/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { ExtendedHomeAssistant } from '../types';
|
||||
import { hasHAConnectionStateChanged, isHassDifferent } from '../utils/ha';
|
||||
@@ -17,15 +16,6 @@ export class HASSManager {
|
||||
}
|
||||
|
||||
public setHASS(hass?: ExtendedHomeAssistant | null): void {
|
||||
const getSelectedCameraConfig = (): CameraConfig | null => {
|
||||
const view = this._api.getViewManager().getView();
|
||||
const cameraManager = this._api.getCameraManager();
|
||||
|
||||
return view && cameraManager
|
||||
? cameraManager?.getStore().getCameraConfig(view.camera) ?? null
|
||||
: null;
|
||||
};
|
||||
|
||||
if (hasHAConnectionStateChanged(this._hass, hass)) {
|
||||
if (!hass?.connected) {
|
||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||
@@ -54,10 +44,11 @@ export class HASSManager {
|
||||
// Assistant update if there's been recent interaction (e.g. clicks on the
|
||||
// card) or if there is media active playing.
|
||||
this._isAutomatedViewUpdateAllowed() &&
|
||||
isHassDifferent(this._hass, oldHass, [
|
||||
...(this._api.getConfigManager().getConfig()?.view.update_entities ?? []),
|
||||
...(getSelectedCameraConfig()?.triggers.entities ?? []),
|
||||
])
|
||||
isHassDifferent(
|
||||
this._hass,
|
||||
oldHass,
|
||||
this._api.getConfigManager().getConfig()?.view.update_entities ?? [],
|
||||
)
|
||||
) {
|
||||
// If entities being monitored have changed then reset the view to the
|
||||
// default.
|
||||
@@ -74,8 +65,6 @@ export class HASSManager {
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
this._api.getTriggersManager().updateTriggerHAState(oldHass);
|
||||
|
||||
if (this._api.getConditionsManager().hasHAStateConditions()) {
|
||||
this._api.getConditionsManager().setState({ state: this._hass.states });
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import { getHassDifferences, isTriggeredState } from '../utils/ha';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { CameraEvent } from '../camera-manager/types';
|
||||
import { Timer } from '../utils/timer';
|
||||
import { View } from '../view/view';
|
||||
import { CardTriggersAPI } from './types';
|
||||
|
||||
export class TriggersManager {
|
||||
@@ -10,7 +10,10 @@ export class TriggersManager {
|
||||
|
||||
protected _triggeredCameras: Map<string, Date> = new Map();
|
||||
protected _triggeredCameraTimers: Map<string, Timer> = new Map();
|
||||
protected _triggeredState: Set<string> = new Set();
|
||||
|
||||
protected _throttledTriggerAction = throttle(this._triggerAction.bind(this), 1000, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
constructor(api: CardTriggersAPI) {
|
||||
this._api = api;
|
||||
@@ -33,125 +36,112 @@ export class TriggersManager {
|
||||
return sorted.length ? sorted[0][0] : null;
|
||||
}
|
||||
|
||||
public updateTriggerHAState(oldHass?: HomeAssistant | null): void {
|
||||
const scanConfig = this._api.getConfigManager().getConfig()?.view.scan;
|
||||
if (!scanConfig || !scanConfig.enabled) {
|
||||
public handleCameraEvent(ev: CameraEvent): void {
|
||||
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
||||
const selectedCameraID = this._api.getViewManager().getView()?.camera;
|
||||
|
||||
if (!triggersConfig || !selectedCameraID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
let triggerChanges = false;
|
||||
|
||||
const visibleCameraIDs = this._api
|
||||
const dependentCameraIDs = this._api
|
||||
.getCameraManager()
|
||||
.getStore()
|
||||
.getVisibleCameraIDs();
|
||||
for (const [cameraID, config] of this._api
|
||||
.getCameraManager()
|
||||
.getStore()
|
||||
.getCameraConfigEntries(visibleCameraIDs)) {
|
||||
const triggerEntities = config.triggers.entities;
|
||||
const diffs = getHassDifferences(hass, oldHass, triggerEntities, {
|
||||
stateOnly: true,
|
||||
});
|
||||
const shouldTrigger = diffs.some((diff) => isTriggeredState(diff.newState));
|
||||
const shouldUntrigger = triggerEntities.every(
|
||||
(entity) => !isTriggeredState(hass?.states[entity]),
|
||||
);
|
||||
if (shouldTrigger) {
|
||||
this._triggeredState.add(cameraID);
|
||||
triggerChanges = true;
|
||||
} else if (shouldUntrigger && this._triggeredState.has(cameraID)) {
|
||||
this._triggeredState.delete(cameraID);
|
||||
triggerChanges = true;
|
||||
}
|
||||
}
|
||||
.getAllDependentCameras(selectedCameraID);
|
||||
|
||||
if (triggerChanges) {
|
||||
this._evaluateTriggers();
|
||||
}
|
||||
}
|
||||
|
||||
public updateView(oldView?: View | null): void {
|
||||
if (oldView?.camera !== this._api.getViewManager().getView()?.camera) {
|
||||
// If the view changes, a new camera may have been selected, which may
|
||||
// mean a trigger is required (in the case that `filter_selected_camera`
|
||||
// is true).
|
||||
this._evaluateTriggers();
|
||||
}
|
||||
}
|
||||
|
||||
protected _evaluateTriggers(): void {
|
||||
const scanConfig = this._api.getConfigManager().getConfig()?.view.scan;
|
||||
if (!scanConfig) {
|
||||
if (triggersConfig.filter_selected_camera && !dependentCameraIDs.has(ev.cameraID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
for (const cameraID of this._triggeredState.keys()) {
|
||||
if (
|
||||
!this._triggeredCameras.has(cameraID) &&
|
||||
(!scanConfig.filter_selected_camera ||
|
||||
cameraID === this._api.getViewManager().getView()?.camera)
|
||||
) {
|
||||
this._triggeredCameras.set(cameraID, now);
|
||||
this._setConditionState();
|
||||
this._triggerAction(cameraID);
|
||||
}
|
||||
if (ev.type === 'end') {
|
||||
this._startUntriggerTimer(ev.cameraID);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const cameraID of this._triggeredCameras.keys()) {
|
||||
if (!this._triggeredState.has(cameraID)) {
|
||||
this._startUntriggerTimer(cameraID);
|
||||
}
|
||||
}
|
||||
this._triggeredCameras.set(ev.cameraID, new Date());
|
||||
this._setConditionStateIfNecessary();
|
||||
this._throttledTriggerAction(ev);
|
||||
}
|
||||
|
||||
protected _hasAllowableInteractionStateForAction(): boolean {
|
||||
const scanConfig = this._api.getConfigManager().getConfig()?.view.scan;
|
||||
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
||||
const hasInteraction = this._api.getInteractionManager().hasInteraction();
|
||||
|
||||
return (
|
||||
!!scanConfig &&
|
||||
(scanConfig.actions.interaction_mode === 'all' ||
|
||||
(scanConfig.actions.interaction_mode === 'active' && hasInteraction) ||
|
||||
(scanConfig.actions.interaction_mode === 'inactive' && !hasInteraction))
|
||||
!!triggersConfig &&
|
||||
(triggersConfig.actions.interaction_mode === 'all' ||
|
||||
(triggersConfig.actions.interaction_mode === 'active' && hasInteraction) ||
|
||||
(triggersConfig.actions.interaction_mode === 'inactive' && !hasInteraction))
|
||||
);
|
||||
}
|
||||
|
||||
protected _triggerAction(cameraID: string): void {
|
||||
const action = this._api.getConfigManager().getConfig()?.view.scan.actions.trigger;
|
||||
protected _triggerAction(ev: CameraEvent): void {
|
||||
const triggerAction = this._api.getConfigManager().getConfig()?.view.triggers
|
||||
.actions.trigger;
|
||||
const defaultView = this._api.getConfigManager().getConfig()?.view.default;
|
||||
|
||||
if (action === 'live' && this._hasAllowableInteractionStateForAction()) {
|
||||
this._api.getViewManager().setViewByParameters({
|
||||
viewName: 'live',
|
||||
cameraID: cameraID,
|
||||
});
|
||||
// If this is a high-fidelity event where we are certain about new media,
|
||||
// don't take action unless it's to change to live (Frigate engine may pump
|
||||
// out events where there's no new media to show).
|
||||
if (
|
||||
ev.fidelity === 'high' &&
|
||||
!ev.snapshot &&
|
||||
!ev.clip &&
|
||||
!(
|
||||
triggerAction === 'live' ||
|
||||
(triggerAction === 'default' && defaultView === 'live')
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must update master element to add border pulsing.
|
||||
if (this._hasAllowableInteractionStateForAction()) {
|
||||
if (triggerAction === 'live') {
|
||||
this._api.getViewManager().setViewByParameters({
|
||||
viewName: 'live',
|
||||
cameraID: ev.cameraID,
|
||||
});
|
||||
} else if (triggerAction === 'default') {
|
||||
this._api.getViewManager().setViewDefault({
|
||||
cameraID: ev.cameraID,
|
||||
});
|
||||
} else if (ev.fidelity === 'high' && triggerAction === 'media') {
|
||||
this._api.getViewManager().setViewByParameters({
|
||||
viewName: ev.clip ? 'clip' : 'snapshot',
|
||||
cameraID: ev.cameraID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Must update master element to add border pulsing to live view.
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager().setState({
|
||||
triggered: this._triggeredCameras.size
|
||||
? new Set(this._triggeredCameras.keys())
|
||||
: undefined,
|
||||
});
|
||||
protected _setConditionStateIfNecessary(): void {
|
||||
const triggeredCameraIDs = new Set(this._triggeredCameras.keys());
|
||||
const triggeredState = triggeredCameraIDs.size ? triggeredCameraIDs : undefined;
|
||||
|
||||
if (
|
||||
!isEqual(triggeredState, this._api.getConditionsManager().getState().triggered)
|
||||
) {
|
||||
this._api.getConditionsManager().setState({
|
||||
triggered: triggeredState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected _untriggerAction(cameraID: string): void {
|
||||
const action = this._api.getConfigManager().getConfig()?.view.scan.actions.untrigger;
|
||||
const action = this._api.getConfigManager().getConfig()?.view.triggers
|
||||
.actions.untrigger;
|
||||
|
||||
if (action === 'default' && this._hasAllowableInteractionStateForAction()) {
|
||||
this._api.getViewManager().setViewDefault();
|
||||
}
|
||||
this._triggeredCameras.delete(cameraID);
|
||||
this._deleteTimer(cameraID);
|
||||
this._setConditionState();
|
||||
this._setConditionStateIfNecessary();
|
||||
|
||||
// Must update master element to remove border pulsing.
|
||||
// Must update master element to remove border pulsing from live view.
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
@@ -164,7 +154,7 @@ export class TriggersManager {
|
||||
/* istanbul ignore next: the case of config being null here cannot be
|
||||
reached, as there's no way to have the untrigger call happen without
|
||||
a config. -- @preserve */
|
||||
this._api.getConfigManager().getConfig()?.view.scan.untrigger_seconds ?? 0,
|
||||
this._api.getConfigManager().getConfig()?.view.triggers.untrigger_seconds ?? 0,
|
||||
() => {
|
||||
this._untriggerAction(cameraID);
|
||||
},
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { ConditionsManager } from './conditions-manager';
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import { ActionsManager } from './actions-manager';
|
||||
import { AutoUpdateManager } from './auto-update-manager';
|
||||
import { AutomationsManager } from './automations-manager';
|
||||
import { CameraURLManager } from './camera-url-manager';
|
||||
import { CardElementManager } from './card-element-manager';
|
||||
import { ConfigManager } from './config-manager';
|
||||
import { DownloadManager } from './download-manager';
|
||||
import { ExpandManager } from './expand-manager';
|
||||
import { FullscreenManager } from './fullscreen-manager';
|
||||
import { HASSManager } from './hass-manager';
|
||||
import { InitializationManager } from './initialization-manager';
|
||||
import { InteractionManager } from './interaction-manager';
|
||||
import { MediaLoadedInfoManager } from './media-info-manager';
|
||||
import { MediaPlayerManager } from './media-player-manager';
|
||||
import { MessageManager } from './message-manager';
|
||||
import { MicrophoneManager } from './microphone-manager';
|
||||
import { StyleManager } from './style-manager';
|
||||
import { TriggersManager } from './triggers-manager';
|
||||
import { ViewManager } from './view-manager';
|
||||
import { QueryStringManager } from './query-string-manager';
|
||||
import type { CameraManager } from '../camera-manager/manager';
|
||||
import type { ConditionsManager } from './conditions-manager';
|
||||
import type { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import type { ActionsManager } from './actions-manager';
|
||||
import type { AutoUpdateManager } from './auto-update-manager';
|
||||
import type { AutomationsManager } from './automations-manager';
|
||||
import type { CameraURLManager } from './camera-url-manager';
|
||||
import type { CardElementManager } from './card-element-manager';
|
||||
import type { ConfigManager } from './config-manager';
|
||||
import type { DownloadManager } from './download-manager';
|
||||
import type { ExpandManager } from './expand-manager';
|
||||
import type { FullscreenManager } from './fullscreen-manager';
|
||||
import type { HASSManager } from './hass-manager';
|
||||
import type { InitializationManager } from './initialization-manager';
|
||||
import type { InteractionManager } from './interaction-manager';
|
||||
import type { MediaLoadedInfoManager } from './media-info-manager';
|
||||
import type { MediaPlayerManager } from './media-player-manager';
|
||||
import type { MessageManager } from './message-manager';
|
||||
import type { MicrophoneManager } from './microphone-manager';
|
||||
import type { StyleManager } from './style-manager';
|
||||
import type { TriggersManager } from './triggers-manager';
|
||||
import type { ViewManager } from './view-manager';
|
||||
import type { QueryStringManager } from './query-string-manager';
|
||||
|
||||
/**
|
||||
* This defines a series of limited APIs that various manager helpers use to
|
||||
@@ -67,6 +67,7 @@ export interface CardCameraAPI {
|
||||
getHASSManager(): HASSManager;
|
||||
getMessageManager(): MessageManager;
|
||||
getResolvedMediaCache(): ResolvedMediaCache;
|
||||
getTriggersManager(): TriggersManager;
|
||||
}
|
||||
|
||||
export interface CardCameraURLAPI {
|
||||
@@ -100,8 +101,10 @@ export interface CardDownloadAPI {
|
||||
|
||||
export interface CardElementAPI {
|
||||
getActionsManager(): ActionsManager;
|
||||
getCameraManager(): CameraManager;
|
||||
getExpandManager(): ExpandManager;
|
||||
getFullscreenManager(): FullscreenManager;
|
||||
getInitializationManager(): InitializationManager;
|
||||
getInteractionManager(): InteractionManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
getMicrophoneManager(): MicrophoneManager;
|
||||
@@ -205,7 +208,6 @@ export interface CardTriggersAPI {
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getInteractionManager(): InteractionManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
@@ -245,7 +245,6 @@ export class ViewManager {
|
||||
displayMode: view.displayMode ?? undefined,
|
||||
});
|
||||
|
||||
this._api.getTriggersManager().updateView(oldView);
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user