feat: Add experimental reolink media support (#1694)
* feat: Add experimental rich reolink support. * Formatting fix
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import frigateSVG from '../camera-manager/frigate/assets/frigate.svg';
|
||||
import motioneyeSVG from '../camera-manager/motioneye/assets/motioneye.svg';
|
||||
import reolinkSVG from '../camera-manager/reolink/assets/reolink.svg';
|
||||
|
||||
export const getCustomIconURL = (icon?: string): string | null => {
|
||||
switch (icon) {
|
||||
@@ -7,6 +8,8 @@ export const getCustomIconURL = (icon?: string): string | null => {
|
||||
return frigateSVG;
|
||||
case 'motioneye':
|
||||
return motioneyeSVG;
|
||||
case 'reolink':
|
||||
return reolinkSVG;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import pkg from '../../package.json';
|
||||
import { RawFrigateCardConfig } from '../config/types';
|
||||
import { getLanguage } from '../localize/localize';
|
||||
import { getIntegrationManifest } from './ha/integration';
|
||||
import { IntegrationManifest } from './ha/integration/types';
|
||||
import { DeviceRegistryManager } from './ha/registry/device';
|
||||
import { HASS_WEB_PROXY_DOMAIN } from './ha/web-proxy';
|
||||
|
||||
type FrigateVersions = Record<string, string>;
|
||||
type FrigateDevices = Record<string, string>;
|
||||
|
||||
interface GitDiagnostics {
|
||||
build_version?: string;
|
||||
@@ -12,6 +15,11 @@ interface GitDiagnostics {
|
||||
commit_date?: string;
|
||||
}
|
||||
|
||||
interface IntegrationDiagnostics {
|
||||
detected: boolean;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const getReleaseVersion = (): string => {
|
||||
const releaseVersion = '__FRIGATE_CARD_RELEASE_VERSION__';
|
||||
|
||||
@@ -36,11 +44,39 @@ export interface Diagnostics {
|
||||
timezone: string;
|
||||
git: GitDiagnostics;
|
||||
|
||||
frigate_versions?: FrigateVersions;
|
||||
ha_version?: string;
|
||||
config?: RawFrigateCardConfig;
|
||||
|
||||
integrations: {
|
||||
frigate: IntegrationDiagnostics & {
|
||||
devices?: FrigateDevices;
|
||||
};
|
||||
hass_web_proxy: IntegrationDiagnostics;
|
||||
reolink: IntegrationDiagnostics;
|
||||
motioneye: IntegrationDiagnostics;
|
||||
};
|
||||
}
|
||||
|
||||
export const getIntegrationDiagnostics = async (
|
||||
integration: string,
|
||||
hass?: HomeAssistant,
|
||||
): Promise<IntegrationDiagnostics> => {
|
||||
let manifest: IntegrationManifest | null = null;
|
||||
|
||||
if (hass) {
|
||||
try {
|
||||
manifest = await getIntegrationManifest(hass, integration);
|
||||
} catch (e) {
|
||||
// Silently ignore integrations not being found.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
detected: !!manifest,
|
||||
...(manifest?.version && { version: manifest.version }),
|
||||
};
|
||||
};
|
||||
|
||||
export const getDiagnostics = async (
|
||||
hass?: HomeAssistant,
|
||||
deviceRegistryManager?: DeviceRegistryManager,
|
||||
@@ -69,9 +105,6 @@ export const getDiagnostics = async (
|
||||
card_version: getReleaseVersion(),
|
||||
browser: navigator.userAgent,
|
||||
date: new Date(),
|
||||
...(frigateVersionMap.size && {
|
||||
frigate_versions: Object.fromEntries(frigateVersionMap),
|
||||
}),
|
||||
lang: getLanguage(),
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
git: {
|
||||
@@ -80,6 +113,17 @@ export const getDiagnostics = async (
|
||||
...(pkg['gitDate'] && { commit_date: pkg['gitDate'] }),
|
||||
},
|
||||
...(hass && { ha_version: hass.config.version }),
|
||||
integrations: {
|
||||
reolink: await getIntegrationDiagnostics('reolink', hass),
|
||||
frigate: {
|
||||
...(await getIntegrationDiagnostics('frigate', hass)),
|
||||
...(frigateVersionMap.size && {
|
||||
devices: Object.fromEntries(frigateVersionMap),
|
||||
}),
|
||||
},
|
||||
hass_web_proxy: await getIntegrationDiagnostics(HASS_WEB_PROXY_DOMAIN, hass),
|
||||
motioneye: await getIntegrationDiagnostics('motioneye', hass),
|
||||
},
|
||||
...(rawConfig && { config: rawConfig }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { add } from 'date-fns';
|
||||
import { homeAssistantWSRequest } from '..';
|
||||
import chunk from 'lodash-es/chunk';
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import { BrowseMediaMetadata } from '../../../camera-manager/browse-media/types';
|
||||
import { MemoryRequestCache } from '../../../camera-manager/cache';
|
||||
import { allPromises } from '../../basic';
|
||||
import { homeAssistantWSRequest } from '../ws-request';
|
||||
import {
|
||||
BROWSE_MEDIA_CACHE_SECONDS,
|
||||
BrowseMedia,
|
||||
browseMediaSchema,
|
||||
BROWSE_MEDIA_CACHE_SECONDS,
|
||||
RichBrowseMedia,
|
||||
} from './types';
|
||||
|
||||
@@ -19,10 +22,19 @@ type RichMetadataGenerator<M> = (
|
||||
export type BrowseMediaTarget<M> = string | RichBrowseMedia<M>;
|
||||
type RichBrowseMediaPredicate<M> = (media: RichBrowseMedia<M>) => boolean;
|
||||
|
||||
export const sortMediaByStartDate = (
|
||||
media: RichBrowseMedia<BrowseMediaMetadata>[],
|
||||
): RichBrowseMedia<BrowseMediaMetadata>[] => {
|
||||
return orderBy(media, (media) => media._metadata?.startDate, 'desc');
|
||||
};
|
||||
|
||||
export interface BrowseMediaStep<M> {
|
||||
// The targets to start the media walk from.
|
||||
targets: BrowseMediaTarget<M>[];
|
||||
|
||||
// How many children to process concurrently. Default is infinite.
|
||||
concurrency?: number;
|
||||
|
||||
// All children of the target have the metadata generator applied to them
|
||||
// first.
|
||||
metadataGenerator?: RichMetadataGenerator<M>;
|
||||
@@ -31,6 +43,12 @@ export interface BrowseMediaStep<M> {
|
||||
// output.
|
||||
matcher: RichBrowseMediaPredicate<M>;
|
||||
|
||||
// Children (once past the matcher) will be sorted before the next step.
|
||||
sorter?: (media: RichBrowseMedia<M>[]) => RichBrowseMedia<M>[];
|
||||
|
||||
// Whether to exit the walk early with the given output.
|
||||
earlyExit?: (media: RichBrowseMedia<M>[]) => boolean;
|
||||
|
||||
// advance will be called to generate a next step (or null if the child should
|
||||
// just be included straight through to the output with no further steps).
|
||||
advance?: BrowseMediaStepAdvancer<M>;
|
||||
@@ -39,23 +57,18 @@ export interface BrowseMediaStep<M> {
|
||||
type BrowseMediaStepAdvancer<M> = (media: RichBrowseMedia<M>[]) => BrowseMediaStep<M>[];
|
||||
|
||||
export class BrowseMediaManager<M> {
|
||||
protected _cache: BrowseMediaCache<M>;
|
||||
|
||||
constructor(cache: BrowseMediaCache<M>) {
|
||||
this._cache = cache;
|
||||
}
|
||||
|
||||
// Walk down a browse media tree according to instructions included in `steps`.
|
||||
public async walkBrowseMedias(
|
||||
hass: HomeAssistant,
|
||||
steps: BrowseMediaStep<M>[] | null,
|
||||
options?: {
|
||||
useCache?: boolean;
|
||||
cache?: BrowseMediaCache<M>;
|
||||
},
|
||||
): Promise<RichBrowseMedia<M>[]> {
|
||||
if (!steps || !steps.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
await allPromises(
|
||||
steps,
|
||||
@@ -68,60 +81,57 @@ export class BrowseMediaManager<M> {
|
||||
hass: HomeAssistant,
|
||||
step: BrowseMediaStep<M>,
|
||||
options?: {
|
||||
useCache?: boolean;
|
||||
cache?: BrowseMediaCache<M>;
|
||||
},
|
||||
): Promise<RichBrowseMedia<M>[]> {
|
||||
const media = await allPromises(
|
||||
step.targets,
|
||||
async (target) =>
|
||||
await this._browseMedia(hass, target, {
|
||||
useCache: options?.useCache,
|
||||
metadataGenerator: step.metadataGenerator,
|
||||
}),
|
||||
);
|
||||
let output: RichBrowseMedia<M>[] = [];
|
||||
|
||||
const newTargets: RichBrowseMedia<M>[] = [];
|
||||
for (const parent of media) {
|
||||
for (const child of parent.children ?? []) {
|
||||
if (step.matcher(child)) {
|
||||
newTargets.push(child);
|
||||
for (const targetChunk of chunk(step.targets, step.concurrency ?? Infinity)) {
|
||||
const mediaChunk = await allPromises(
|
||||
targetChunk,
|
||||
async (target) =>
|
||||
await this._browseMedia(hass, target, {
|
||||
cache: options?.cache,
|
||||
matcher: step.matcher,
|
||||
metadataGenerator: step.metadataGenerator,
|
||||
}),
|
||||
);
|
||||
|
||||
for (const parent of mediaChunk) {
|
||||
for (const child of parent.children ?? []) {
|
||||
if (step.matcher(child)) {
|
||||
output.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextSteps = step.advance ? step.advance(newTargets) : null;
|
||||
if (!nextSteps || !nextSteps.length) {
|
||||
return newTargets;
|
||||
}
|
||||
if (step.sorter) {
|
||||
output = step.sorter(output);
|
||||
}
|
||||
|
||||
const targetsIncludedInNextSteps = new Set(
|
||||
nextSteps.map((nextStep) => nextStep.targets).flat(),
|
||||
);
|
||||
const finished: RichBrowseMedia<M>[] = [];
|
||||
|
||||
// Any new target that doesn't have a proposed 'next step' is assumed to be
|
||||
// ready to return.
|
||||
for (const target of newTargets) {
|
||||
if (!targetsIncludedInNextSteps.has(target)) {
|
||||
finished.push(target);
|
||||
if (step.earlyExit && step.earlyExit(output)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const downstream = await this.walkBrowseMedias(hass, nextSteps, options);
|
||||
return finished.concat(downstream);
|
||||
const nextSteps = step.advance ? step.advance(output) : null;
|
||||
if (!nextSteps?.length) {
|
||||
return output;
|
||||
}
|
||||
return await this.walkBrowseMedias(hass, nextSteps, options);
|
||||
}
|
||||
|
||||
protected async _browseMedia(
|
||||
hass: HomeAssistant,
|
||||
target: string | RichBrowseMedia<M>,
|
||||
options?: {
|
||||
useCache?: boolean;
|
||||
cache?: BrowseMediaCache<M>;
|
||||
matcher?: RichBrowseMediaPredicate<M>;
|
||||
metadataGenerator?: RichMetadataGenerator<M>;
|
||||
},
|
||||
): Promise<RichBrowseMedia<M>> {
|
||||
const mediaContentID = typeof target === 'object' ? target.media_content_id : target;
|
||||
const cachedResult =
|
||||
options?.useCache ?? true ? this._cache.get(mediaContentID) : null;
|
||||
const cachedResult = options?.cache ? options.cache.get(mediaContentID) : null;
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
@@ -130,11 +140,11 @@ export class BrowseMediaManager<M> {
|
||||
type: 'media_source/browse_media',
|
||||
media_content_id: mediaContentID,
|
||||
};
|
||||
const browseMedia = (await homeAssistantWSRequest(
|
||||
const browseMedia = await homeAssistantWSRequest<RichBrowseMedia<M>>(
|
||||
hass,
|
||||
browseMediaSchema,
|
||||
request,
|
||||
)) as RichBrowseMedia<M>;
|
||||
);
|
||||
|
||||
if (options?.metadataGenerator) {
|
||||
for (const child of browseMedia.children ?? []) {
|
||||
@@ -146,8 +156,8 @@ export class BrowseMediaManager<M> {
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.useCache ?? true) {
|
||||
this._cache.set(
|
||||
if (options?.cache) {
|
||||
options.cache.set(
|
||||
mediaContentID,
|
||||
browseMedia,
|
||||
add(new Date(), { seconds: BROWSE_MEDIA_CACHE_SECONDS }),
|
||||
|
||||
+13
-53
@@ -3,73 +3,25 @@ import {
|
||||
computeStateDomain,
|
||||
HomeAssistant,
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import { HassEntity, MessageBase } from 'home-assistant-js-websocket';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { StyleInfo } from 'lit/directives/style-map.js';
|
||||
import { ZodSchema } from 'zod';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import {
|
||||
CardHelpers,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardError,
|
||||
LovelaceCardWithEditor,
|
||||
SignedPath,
|
||||
signedPathSchema,
|
||||
StateParameters,
|
||||
} from '../../types.js';
|
||||
import { domainIcon } from '../icons/domain-icon.js';
|
||||
import { getParseErrorKeys } from '../zod.js';
|
||||
|
||||
/**
|
||||
* Make a HomeAssistant websocket request. May throw.
|
||||
* @param hass The HomeAssistant object to send the request with.
|
||||
* @param schema The expected Zod schema of the response.
|
||||
* @param request The request to make.
|
||||
* @returns The parsed valid response or null on malformed.
|
||||
*/
|
||||
export async function homeAssistantWSRequest<T>(
|
||||
hass: HomeAssistant,
|
||||
schema: ZodSchema<T>,
|
||||
request: MessageBase,
|
||||
passthrough = false,
|
||||
): Promise<T> {
|
||||
let response;
|
||||
try {
|
||||
response = await hass.callWS<T>(request);
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) {
|
||||
throw new FrigateCardError(localize('error.failed_response'), {
|
||||
request: request,
|
||||
response: e,
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new FrigateCardError(localize('error.empty_response'), {
|
||||
request: request,
|
||||
});
|
||||
}
|
||||
// Some endpoints on the integration pass through JSON directly from Frigate
|
||||
// These end up wrapped in a string and must be unwrapped first
|
||||
const parseResult = passthrough
|
||||
? schema.safeParse(JSON.parse(response))
|
||||
: schema.safeParse(response);
|
||||
if (!parseResult.success) {
|
||||
throw new FrigateCardError(localize('error.invalid_response'), {
|
||||
request: request,
|
||||
response: response,
|
||||
invalid_keys: getParseErrorKeys<T>(parseResult.error),
|
||||
});
|
||||
}
|
||||
return parseResult.data;
|
||||
}
|
||||
import { homeAssistantWSRequest } from './ws-request.js';
|
||||
|
||||
/**
|
||||
* Request that HA sign a path. May throw.
|
||||
* @param hass The HomeAssistant object used to request the signature.
|
||||
* @param path The path to sign.
|
||||
* @param expires An optional number of seconds to sign the path for.
|
||||
* @param expires An optional number of seconds to sign the path for (by default
|
||||
* HA will sign for 30 seconds).
|
||||
* @returns The signed URL, or null if the response was malformed.
|
||||
*/
|
||||
export async function homeAssistantSignPath(
|
||||
@@ -379,17 +331,25 @@ export const isCardInPanel = (card: HTMLElement): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
export function isHARelativeURL(url?: string): boolean {
|
||||
return !!url?.startsWith('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure URLs use the correct HA URL (relevant for Chromecast where the default
|
||||
* location will be the Chromecast receiver, not HA).
|
||||
* @param url The media URL
|
||||
*/
|
||||
export function canonicalizeHAURL(hass: ExtendedHomeAssistant, url: string): string;
|
||||
export function canonicalizeHAURL(
|
||||
hass: ExtendedHomeAssistant,
|
||||
url?: string,
|
||||
): string | null;
|
||||
export function canonicalizeHAURL(
|
||||
hass: ExtendedHomeAssistant,
|
||||
url?: string,
|
||||
): string | null {
|
||||
if (hass && url && url.startsWith('/')) {
|
||||
if (isHARelativeURL(url)) {
|
||||
return hass.hassUrl(url);
|
||||
}
|
||||
return url ?? null;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { homeAssistantWSRequest } from '../ws-request';
|
||||
import { IntegrationManifest, integrationManifestSchema } from './types';
|
||||
|
||||
export const getIntegrationManifest = async (
|
||||
hass: HomeAssistant,
|
||||
integration: string,
|
||||
): Promise<IntegrationManifest> => {
|
||||
return await homeAssistantWSRequest(hass, integrationManifestSchema, {
|
||||
type: 'manifest/get',
|
||||
integration: integration,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const integrationManifestSchema = z
|
||||
.object({
|
||||
domain: z.string(),
|
||||
version: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
export type IntegrationManifest = z.infer<typeof integrationManifestSchema>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { homeAssistantWSRequest } from '../..';
|
||||
import { homeAssistantWSRequest } from '../../ws-request';
|
||||
import { errorToConsole } from '../../../basic';
|
||||
import { RegistryCache } from '../cache';
|
||||
import { Device, DeviceList, deviceListSchema } from './types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { homeAssistantWSRequest } from '../..';
|
||||
import { homeAssistantWSRequest } from '../../ws-request';
|
||||
import { errorToConsole } from '../../../basic';
|
||||
import { RegistryCache } from '../cache';
|
||||
import { Entity, EntityList, entityListSchema, entitySchema } from './types.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import QuickLRU from 'quick-lru';
|
||||
import { homeAssistantWSRequest } from '.';
|
||||
import { homeAssistantWSRequest } from './ws-request';
|
||||
import { ResolvedMedia, resolvedMediaSchema } from '../../types.js';
|
||||
import { errorToConsole } from '../basic';
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CameraProxyConfig } from '../../camera-manager/types';
|
||||
import { ExtendedHomeAssistant } from '../../types';
|
||||
|
||||
export const HASS_WEB_PROXY_DOMAIN = 'hass_web_proxy';
|
||||
|
||||
const hasWebProxyAvailable = (hass: HomeAssistant): boolean => {
|
||||
return hass.config.components.includes(HASS_WEB_PROXY_DOMAIN);
|
||||
};
|
||||
|
||||
export const getWebProxiedURL = (url: string, v?: number): string => {
|
||||
return `/api/${HASS_WEB_PROXY_DOMAIN}/v${v ?? 0}/?url=${encodeURIComponent(url)}`;
|
||||
};
|
||||
|
||||
export const shouldUseWebProxy = (
|
||||
hass: HomeAssistant,
|
||||
proxyConfig: CameraProxyConfig,
|
||||
context: 'media' = 'media',
|
||||
): boolean => {
|
||||
return hasWebProxyAvailable(hass) && !!proxyConfig[context];
|
||||
};
|
||||
|
||||
/**
|
||||
* Request that HA sign a path. May throw.
|
||||
* @param hass The HomeAssistant object used to request the signature.
|
||||
* @param path The path to sign.
|
||||
* @param expires An optional number of seconds to sign the path for (by default
|
||||
* HA will sign for 30 seconds).
|
||||
* @returns The signed URL, or null if the response was malformed.
|
||||
*/
|
||||
export async function addDynamicProxyURL(
|
||||
hass: ExtendedHomeAssistant,
|
||||
url_pattern: string,
|
||||
options?: {
|
||||
urlID?: string;
|
||||
sslVerification?: boolean;
|
||||
sslCiphers?: string;
|
||||
openLimit?: number;
|
||||
ttl?: number;
|
||||
allowUnauthenticated?: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
await hass.callService(HASS_WEB_PROXY_DOMAIN, 'create_proxied_url', {
|
||||
url_pattern: url_pattern,
|
||||
...(options && {
|
||||
url_id: options.urlID,
|
||||
ssl_verification: options.sslVerification,
|
||||
ssl_ciphers: options.sslCiphers,
|
||||
open_limit: options.openLimit,
|
||||
ttl: options.ttl,
|
||||
allow_unauthenticated: options.allowUnauthenticated,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { MessageBase } from 'home-assistant-js-websocket';
|
||||
import { ZodSchema } from 'zod';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { FrigateCardError } from '../../types';
|
||||
import { getParseErrorKeys } from '../zod';
|
||||
|
||||
/**
|
||||
* Make a HomeAssistant websocket request. May throw.
|
||||
* @param hass The HomeAssistant object to send the request with.
|
||||
* @param schema The expected Zod schema of the response.
|
||||
* @param request The request to make.
|
||||
* @returns The parsed valid response or null on malformed.
|
||||
*/
|
||||
|
||||
export async function homeAssistantWSRequest<T>(
|
||||
hass: HomeAssistant,
|
||||
schema: ZodSchema<T>,
|
||||
request: MessageBase,
|
||||
passthrough = false,
|
||||
): Promise<T> {
|
||||
let response;
|
||||
try {
|
||||
response = await hass.callWS<T>(request);
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) {
|
||||
throw new FrigateCardError(localize('error.failed_response'), {
|
||||
request: request,
|
||||
response: e,
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new FrigateCardError(localize('error.empty_response'), {
|
||||
request: request,
|
||||
});
|
||||
}
|
||||
// Some endpoints on the integration pass through JSON directly from Frigate
|
||||
// These end up wrapped in a string and must be unwrapped first
|
||||
const parseResult = passthrough
|
||||
? schema.safeParse(JSON.parse(response))
|
||||
: schema.safeParse(response);
|
||||
if (!parseResult.success) {
|
||||
throw new FrigateCardError(localize('error.invalid_response'), {
|
||||
request: request,
|
||||
response: response,
|
||||
invalid_keys: getParseErrorKeys<T>(parseResult.error),
|
||||
});
|
||||
}
|
||||
return parseResult.data;
|
||||
}
|
||||
Reference in New Issue
Block a user