feat: Support HA event entities as triggers (#2506)
This commit is contained in:
committed by
dermotduffy
parent
f1fad59a89
commit
4b72fcd629
@@ -3,7 +3,7 @@ import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state
|
||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { EnabledProxyConfig, resolveProxyConfig } from '../config/schema/common/proxy';
|
||||
import { isTriggeredState } from '../ha/is-triggered-state';
|
||||
import { getTriggerEventType } from '../ha/get-trigger-event-type';
|
||||
import { HassStateDifference, HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { CapabilitiesRaw, CapabilityKey, Endpoint } from '../types';
|
||||
@@ -262,10 +262,14 @@ export class Camera {
|
||||
}
|
||||
|
||||
protected _stateChangeHandler = (difference: HassStateDifference): void => {
|
||||
const type = getTriggerEventType(difference);
|
||||
if (type === null) {
|
||||
return;
|
||||
}
|
||||
this._eventCallback?.({
|
||||
cameraID: this.getID(),
|
||||
id: difference.entityID,
|
||||
type: isTriggeredState(difference.newState.state) ? 'new' : 'end',
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -157,7 +157,8 @@ export interface CameraEvent {
|
||||
| 'new' // A new event has started.
|
||||
| 'update' // An update for an event is available (except GenAI).
|
||||
| 'end' // An event has ended.
|
||||
| 'genai'; // An AI based update is available.
|
||||
| 'genai' // An AI based update is available.
|
||||
| 'signal'; // A momentary signal (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
|
||||
|
||||
@@ -110,6 +110,17 @@ export class TriggersManager {
|
||||
return this._handleEndEvent(ev);
|
||||
}
|
||||
|
||||
if (ev.type === 'signal') {
|
||||
// A signal is momentary -- handled as a matched new+end so the existing
|
||||
// untrigger_delay_seconds machinery gives it visible duration, and so
|
||||
// concurrent continuous sources still gate untriggering correctly.
|
||||
const handled = await this.handleCameraEvent({ ...ev, type: 'new' }, options);
|
||||
if (handled) {
|
||||
await this.handleCameraEvent({ ...ev, type: 'end' });
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
// Ignore stale updates for force-untriggered IDs before doing any further
|
||||
// processing to avoid re-activating muted IDs.
|
||||
if (this._isIgnoredUpdateEvent(ev)) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { computeDomain } from './compute-domain';
|
||||
import { isTriggeredState } from './is-triggered-state';
|
||||
import { HassStateDifference } from './types';
|
||||
|
||||
// New state must be a real timestamp: `unavailable` means the entity went
|
||||
// offline, `unknown` means it has no recorded fire -- neither is a fresh fire.
|
||||
const isUsableNewState = (state: string): boolean =>
|
||||
state !== 'unavailable' && state !== 'unknown';
|
||||
|
||||
// Old state must be defined and not `unavailable`. `unknown` is allowed -- it
|
||||
// means the entity was alive but had never fired, so the new timestamp is a
|
||||
// real first fire (not a restored last-fire-time after reconnect).
|
||||
const isUsableOldState = (state: string | undefined): boolean =>
|
||||
state !== undefined && state !== 'unavailable';
|
||||
|
||||
/**
|
||||
* Map a watched entity's state change to the `CameraEvent` type to dispatch
|
||||
* for it, or `null` to skip.
|
||||
*
|
||||
* Most entities have an "on"/"off" state (e.g. `binary_sensor`, `switch`) and
|
||||
* 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
|
||||
* 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
|
||||
* state isn't a real timestamp.
|
||||
*/
|
||||
export const getTriggerEventType = (
|
||||
difference: HassStateDifference,
|
||||
): 'new' | 'end' | 'signal' | null => {
|
||||
if (computeDomain(difference.entityID) === 'event') {
|
||||
return isUsableOldState(difference.oldState?.state) &&
|
||||
isUsableNewState(difference.newState.state)
|
||||
? 'signal'
|
||||
: null;
|
||||
}
|
||||
return isTriggeredState(difference.newState.state) ? 'new' : 'end';
|
||||
};
|
||||
Reference in New Issue
Block a user