Files
advanced-camera-card/src/common.ts
T

197 lines
5.2 KiB
TypeScript

import { ZodSchema, z } from 'zod';
import { MessageBase } from 'home-assistant-js-websocket';
import { HomeAssistant } from 'custom-card-helpers';
import { localize } from './localize/localize';
import type {
BrowseMediaQueryParameters,
BrowseMediaSource,
ExtendedHomeAssistant,
MediaLoadInfo,
Message,
} from './types';
import { browseMediaSourceSchema } from './types';
export function getParseErrorKeys<T>(error: z.ZodError<T>): string[] {
const errors = error.format();
return Object.keys(errors).filter((v) => !v.startsWith('_'));
}
export async function homeAssistantWSRequest<T>(
hass: HomeAssistant & ExtendedHomeAssistant,
schema: ZodSchema<T>,
request: MessageBase,
): Promise<T | null> {
const response = await hass.callWS<T>(request);
if (!response) {
const error_message = `${localize('error.empty_response')}: ${JSON.stringify(
request,
)}`;
console.warn(error_message);
throw new Error(error_message);
}
const parseResult = schema.safeParse(response);
if (!parseResult.success) {
const keys = getParseErrorKeys<T>(parseResult.error);
const error_message =
`${localize('error.invalid_response')}: ${JSON.stringify(request)}. ` +
localize('error.invalid_keys') +
`: '${keys}'`;
console.warn(error_message);
throw new Error(error_message);
}
return parseResult.data;
}
// From a BrowseMediaSource item extract the first true media item (i.e. a
// clip/snapshot, not a folder).
export function getFirstTrueMediaChildIndex(
media: BrowseMediaSource | null,
): number | null {
if (!media || !media.children) {
return null;
}
for (let i = 0; i < media.children.length; i++) {
if (!media.children[i].can_expand) {
return i;
}
}
return null;
}
// Browse Frigate media with a media content id.
export async function browseMedia(
hass: (HomeAssistant & ExtendedHomeAssistant) | null,
media_content_id: string,
): Promise<BrowseMediaSource | null> {
if (!hass) {
return null;
}
const request = {
type: 'media_source/browse_media',
media_content_id: media_content_id,
};
return homeAssistantWSRequest(hass, browseMediaSourceSchema, request);
}
// Browse Frigate media with query parameters.
export async function browseMediaQuery(
hass: HomeAssistant & ExtendedHomeAssistant,
params: BrowseMediaQueryParameters,
): Promise<BrowseMediaSource | null> {
return browseMedia(
hass,
// Defined in:
// https://github.com/blakeblackshear/frigate-hass-integration/blob/master/custom_components/frigate/media_source.py
[
'media-source://frigate',
params.clientId,
'event-search',
params.mediaType,
'', // Name/Title to render (not necessary here)
params.after ? String(params.after) : '',
params.before ? String(params.before) : '',
params.cameraName,
params.label,
params.zone,
].join('/'),
);
}
export function dispatchEvent<T>(element: HTMLElement, name: string, detail?: T): void {
element.dispatchEvent(
new CustomEvent<T>(`frigate-card:${name}`, {
bubbles: true,
composed: true,
detail: detail,
}),
);
}
export function dispatchPlayEvent(element: HTMLElement): void {
dispatchEvent(element, 'play');
}
export function dispatchPauseEvent(element: HTMLElement): void {
dispatchEvent(element, 'pause');
}
export function dispatchMediaLoadEvent(
element: HTMLElement,
source: Event | HTMLElement,
): void {
let target: HTMLElement | EventTarget;
if (source instanceof Event) {
target = source.composedPath()[0];
} else {
target = source;
}
if (target instanceof HTMLImageElement) {
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
width: (target as HTMLImageElement).naturalWidth,
height: (target as HTMLImageElement).naturalHeight,
});
} else if (target instanceof HTMLVideoElement) {
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
width: (target as HTMLVideoElement).videoWidth,
height: (target as HTMLVideoElement).videoHeight,
});
} else if (target instanceof HTMLCanvasElement) {
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
width: (target as HTMLCanvasElement).width,
height: (target as HTMLCanvasElement).height,
});
}
}
export function dispatchMessageEvent(
element: HTMLElement,
message: string,
icon?: string,
): void {
dispatchEvent<Message>(element, 'message', {
message: message,
type: 'info',
icon: icon,
});
}
export function dispatchErrorMessageEvent(
element: HTMLElement,
message: string,
): void {
dispatchEvent<Message>(element, 'message', {
message: message,
type: 'error',
});
}
// Determine whether the card should be updated based on Home Assistant changes.
export function shouldUpdateBasedOnHass(
newHass: HomeAssistant | null,
oldHass: HomeAssistant | undefined,
entities: string[] | null,
): boolean {
if (!newHass || !entities) {
return false;
}
if (!entities.length) {
return false;
}
if (oldHass) {
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
if (!entity) {
continue;
}
if (oldHass.states[entity] !== newHass.states[entity]) {
return true;
}
}
return false;
}
return false;
}