Allow camera engines to signal events to the card.

This commit is contained in:
Dermot Duffy
2024-03-03 15:40:20 -08:00
parent 305a2db573
commit 88bea0ae7d
61 changed files with 7480 additions and 4213 deletions
+65 -5
View File
@@ -1,4 +1,8 @@
import { computeDomain, computeStateDomain, HomeAssistant } from '@dermotduffy/custom-card-helpers';
import {
computeDomain,
computeStateDomain,
HomeAssistant,
} from '@dermotduffy/custom-card-helpers';
import { HassEntity, MessageBase } from 'home-assistant-js-websocket';
import { StyleInfo } from 'lit/directives/style-map.js';
import { ZodSchema } from 'zod';
@@ -13,6 +17,12 @@ import {
} from '../../types.js';
import { domainIcon } from '../icons/domain-icon.js';
import { getParseErrorKeys } from '../zod.js';
import {
HAStateChangeFromTo,
haStateChangeTriggerResponseSchema,
SubscriptionCallback,
SubscriptionUnsubscribe,
} from './types.js';
/**
* Make a HomeAssistant websocket request. May throw.
@@ -103,7 +113,7 @@ interface HassStateDifference {
* strings only, firstOnly: whether or not to get the first difference only.
* @returns An array of HassStateDifference objects.
*/
export function getHassDifferences(
function getHassDifferences(
newHass: HomeAssistant | undefined | null,
oldHass: HomeAssistant | undefined | null,
entities: string[] | null,
@@ -326,11 +336,11 @@ export const sideLoadHomeAssistantElements = async (): Promise<boolean> => {
/**
* Determine if a given state qualifies as 'triggered'.
* @param state The HASSEntity.
* @param state The HA entity state string.
* @returns `true` if triggered, `false` otherwise.
*/
export const isTriggeredState = (state?: HassEntity): boolean => {
return !!state && ['on', 'open'].includes(state.state);
export const isTriggeredState = (state?: string): boolean => {
return !!state && ['on', 'open'].includes(state);
};
/**
@@ -390,3 +400,53 @@ export const hasHAConnectionStateChanged = (
): boolean => {
return oldHass?.connected !== newHass?.connected;
};
/**
* Subscribe to a HA trigger
* @param hass The HA object.
* @param callback The callback to call with the data.
* @param options Parameters to the trigger, see:
* https://www.home-assistant.io/docs/automation/trigger/#state-trigger
* @returns A callback to unsubscribe.
*/
export const subscribeToTrigger = async (
hass: HomeAssistant,
callback: SubscriptionCallback,
options?: {
entityID?: string | string[];
platform?: string;
topic?: string;
payload?: string;
valueTemplate?: string;
stateOnly?: boolean;
},
): Promise<SubscriptionUnsubscribe> => {
return await hass.connection.subscribeMessage(callback, {
type: 'subscribe_trigger',
trigger: {
...(options?.platform && { platform: options.platform }),
...(options?.entityID && { entity_id: options.entityID }),
...(options?.topic && { topic: options.topic }),
...(options?.payload && { payload: options.payload }),
...(options?.valueTemplate && { value_template: options.valueTemplate }),
...(options?.stateOnly && {
from: null,
to: null,
}),
},
});
};
/**
* Parse a state change trigger response.
* @param data The raw data.
* @returns A HAStateChangeFromTo object.
*/
export const parseStateChangeTrigger = (data: unknown): HAStateChangeFromTo | null => {
const parseResult = haStateChangeTriggerResponseSchema.safeParse(data);
if (!parseResult.success) {
console.warn('Ignoring unparseable HA state change', data);
return null;
}
return parseResult.data.variables.trigger;
};
+21
View File
@@ -0,0 +1,21 @@
import { z } from 'zod';
export type SubscriptionUnsubscribe = () => Promise<void>;
export type SubscriptionCallback = (data: unknown) => void;
const haStateChangeSchema = z.object({
entity_id: z.string(),
state: z.string(),
});
const haStateChangeFromToSchema = z.object({
from_state: haStateChangeSchema,
to_state: haStateChangeSchema,
});
export type HAStateChangeFromTo = z.infer<typeof haStateChangeFromToSchema>;
export const haStateChangeTriggerResponseSchema = z.object({
variables: z.object({
trigger: haStateChangeFromToSchema,
}),
});
+1 -1
View File
@@ -22,7 +22,7 @@ export class Initializer {
): Promise<boolean> {
const results = await allPromises(
Object.entries(aspects),
async ([aspect, options]) => this.initializeIfNecessary(aspect, options),
async ([aspect, options]) => await this.initializeIfNecessary(aspect, options),
);
return results.every(Boolean);
}
+10 -1
View File
@@ -27,6 +27,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
eventsMediaType?: ClipsOrSnapshotsOrAll;
targetView?: FrigateCardView;
select?: ResultSelectType;
useCache?: boolean;
},
): Promise<void> => {
const cameraIDs = options?.allCameras
@@ -52,6 +53,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
{
targetView: options?.targetView,
select: options?.select,
useCache: options?.useCache,
},
)
)?.dispatchChangeEvent(element);
@@ -93,6 +95,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
allCameras?: boolean;
targetView?: FrigateCardView;
select?: ResultSelectType;
useCache?: boolean;
},
): Promise<void> => {
const cameraIDs = options?.allCameras
@@ -120,6 +123,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
{
targetView: options?.targetView,
select: options?.select,
useCache: options?.useCache,
},
)
)?.dispatchChangeEvent(element);
@@ -147,6 +151,7 @@ export const executeMediaQueryForView = async (
targetView?: FrigateCardView;
targetTime?: Date;
select?: ResultSelectType;
useCache?: boolean;
},
): Promise<View | null> => {
const queries = query.getQueries();
@@ -154,7 +159,9 @@ export const executeMediaQueryForView = async (
return null;
}
const mediaArray = await cameraManager.executeMediaQueries<MediaQuery>(queries);
const mediaArray = await cameraManager.executeMediaQueries<MediaQuery>(queries, {
useCache: options?.useCache,
});
if (!mediaArray) {
return null;
}
@@ -194,6 +201,7 @@ export const executeMediaQueryForViewWithErrorDispatching = async (
targetView?: FrigateCardView;
targetTime?: Date;
select?: ResultSelectType;
useCache?: boolean;
},
): Promise<View | null> => {
try {
@@ -202,6 +210,7 @@ export const executeMediaQueryForViewWithErrorDispatching = async (
targetView: options?.targetView,
targetTime: options?.targetTime,
select: options?.select,
useCache: options?.useCache,
});
} catch (e: unknown) {
errorToConsole(e as Error);