diff --git a/package.json b/package.json index eef11833..102173fc 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "custom-card-helpers": "^1.7.1", "home-assistant-js-websocket": "^5.11.1", "lit-element": "^2.5.1", - "lit-html": "^1.4.1" + "lit-html": "^1.4.1", + "zod": "^3.7.1" }, "devDependencies": { "@babel/core": "^7.15.0", diff --git a/src/frigate-card.ts b/src/frigate-card.ts index ea166fdd..37099112 100644 --- a/src/frigate-card.ts +++ b/src/frigate-card.ts @@ -1,12 +1,12 @@ -// TODO: Can I use ajv (https://ajv.js.org/guide/typescript.html) to verify -// event return matches the TS interface? - -// TODO Does each event contain thumbnail? - // TODO Check for HA state presence and validity before using it, otherwise warn. // TODO Add material tooltips +// TODO Action handlers. + +// TODO _getEvents may throw errors, catch them when called. + +// TODO Can I use Zod for FrigateCardConfig validation? /* eslint-disable @typescript-eslint/no-explicit-any */ import { @@ -34,7 +34,8 @@ import './editor'; import style from './frigate-card.scss' -import type { FrigateCardConfig, FrigateEvent, GetEventsParameters, ControlVideosParameters } from './types'; +import { frigateEventSchema, frigateGetEventsResponseSchema } from './types'; +import type { FrigateCardConfig, FrigateEvent, FrigateGetEventsResponse, GetEventsParameters, ControlVideosParameters } from './types'; import { actionHandler } from './action-handler-directive'; import { CARD_VERSION } from './const'; import { localize } from './localize/localize'; @@ -205,7 +206,7 @@ export class FrigateCard extends LitElement { has_clip = false, has_snapshot = false, limit = 100, - }: GetEventsParameters): Promise { + }: GetEventsParameters): Promise { let url = `${this.config.frigate_url}/api/events?camera=${this.config.frigate_camera_name}`; if (has_clip) { url += `&has_clip=1` @@ -223,7 +224,17 @@ export class FrigateCard extends LitElement { const response = await fetch(url); if (response.ok) { - return await response.json(); + let raw_json; + try { + raw_json = await response.json(); + } catch(e) { + throw new Error(`Could not JSON decode Frigate API response: ${e}`); + } + try { + return frigateGetEventsResponseSchema.parse(raw_json); + } catch(e) { + throw new Error(`Frigate events were malformed: ${e}`); + } } else { // TODO: Catch when json decoding fails. throw new Error(`Frigate API request failed with status: ${response.status}`); diff --git a/src/types.ts b/src/types.ts index 1bf688c6..40e2ab74 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import { ActionConfig, LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from 'custom-card-helpers'; +import { z } from "zod"; declare global { interface HTMLElementTagNameMap { 'frigate-card-editor': LovelaceCardEditor; @@ -6,6 +7,9 @@ declare global { } } +/** + * Internal types. + */ export interface FrigateCardConfig extends LovelaceCardConfig { type: string; name?: string; @@ -25,20 +29,6 @@ export interface FrigateCardConfig extends LovelaceCardConfig { double_tap_action?: ActionConfig; } -export interface FrigateEvent { - camera: string; - end_time: number; - false_positive: boolean; - has_clip: boolean; - has_snapshot: boolean; - id: string; - label: string; - start_time: number; - thumbnail: string; - top_score: number; - zones: string[]; -} - export interface GetEventsParameters { has_clip?: boolean; has_snapshot?: boolean; @@ -49,4 +39,26 @@ export interface ControlVideosParameters { stop: boolean; control_live?: boolean; control_clip?: boolean; -} \ No newline at end of file +} + +/** + * Frigate API types. + */ + +export const frigateEventSchema = z.object({ + camera: z.string(), + end_time: z.number(), + false_positive: z.boolean(), + has_clip: z.boolean(), + has_snapshot: z.boolean(), + id: z.string(), + label: z.string(), + start_time: z.number(), + thumbnail: z.string(), + top_score: z.number(), + zones: z.string().array(), +}) +export type FrigateEvent = z.infer; + +export const frigateGetEventsResponseSchema = z.array(frigateEventSchema); +export type FrigateGetEventsResponse = z.infer; \ No newline at end of file