@@ -158,7 +170,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html`
@@ -174,29 +186,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
-
- ${Object.keys(viewModes).map((key) => {
- return html`
- ${viewModes[key]}
- `;
- })}
-
-
+
+ ${Object.keys(viewModes).map((key) => {
+ return html`
+ ${viewModes[key]}
+ `;
+ })}
+
+
`
: ''}
@@ -210,62 +222,64 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.optional.show
? html`
- ${Object.keys(liveProvider).map((key) => {
+ ${Object.keys(liveProviders).map((key) => {
return html`
- ${liveProvider[key]}
+ ${liveProviders[key]}
`;
})}
-
+
-
+
-
+
@@ -281,14 +295,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.appearance.show
? html`
${Object.keys(menuModes).map((key) => {
@@ -300,26 +314,28 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
- ${Object.keys(controlsNextPrev).map((key) => {
+ ${Object.keys(eventViewerNextPreviousControlStyles).map((key) => {
return html`
- ${controlsNextPrev[key]}
+
+ ${eventViewerNextPreviousControlStyles[key]}
+
`;
})}
@@ -338,7 +354,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
-
-
-
-
-
-
-
-
@@ -390,11 +387,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@@ -402,11 +400,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@@ -414,11 +413,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@@ -426,11 +426,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@@ -438,11 +439,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
+
+
+
+
+
+
+
+
@@ -458,17 +486,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.advanced.show
? html`
`
@@ -483,9 +511,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.webrtc.show
? html`
`
: ''}
@@ -549,13 +577,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
let objectTarget = newConfig;
if (key.includes('.')) {
- const parts = key.split('.', 2);
- const configName = parts[0];
- if (!(configName in newConfig)) {
- newConfig[configName] = {};
- }
- objectTarget = newConfig[configName];
- key = parts[1];
+ const parts = key.split('.');
+ objectTarget = parts.slice(0, -1).reduce((obj, key) => {
+ if (!(key in obj)) {
+ obj[key] = {};
+ }
+ return obj[key];
+ }, newConfig);
+ key = parts[parts.length-1];
}
if (value !== undefined && objectTarget[key] === value) {
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index f2b46c1b..7a45a692 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -1,15 +1,103 @@
{
"common": {
"version": "Version",
- "show_warning": "Show Warning",
- "show_error": "Show Error",
- "frigate_card": "Frigate Card",
+ "show_warning": "Show warning",
+ "show_error": "Show error",
+ "frigate_card": "Frigate card",
"frigate_card_description": "A Lovelace card for use with Frigate",
"no_snapshots": "No snapshots",
"no_clips": "No clips",
"no_snapshot": "No recent snapshot",
"no_clip": "No recent clip"
},
+ "config": {
+ "camera_entity": "Camera Entity",
+ "frigate": {
+ "camera_name": "Frigate camera name (Optional, autodetected from entity)",
+ "client_id": "Frigate client id (For >1 Frigate server)",
+ "label": "Label/object filter",
+ "url": "Frigate server URL",
+ "zone": "Zone"
+ },
+ "view": {
+ "default": "Default view",
+ "views": {
+ "live": "Live view",
+ "clip": "Most recent clip",
+ "clips": "Clips gallery",
+ "snapshot": "Most recent snapshot",
+ "snapshots": "Snapshots gallery",
+ "image": "Static image"
+ },
+ "timeout": "View timeout (seconds)"
+ },
+ "event_viewer": {
+ "autoplay_clip": "Autoplay most recent clip",
+ "lazy_load": "Lazily load event media",
+ "controls": {
+ "next_previous": {
+ "style": "Event Viewer Next & Previous Control style",
+ "size": "Event Viewer Next & Previous Control size",
+ "styles": {
+ "thumbnails": "Thumbnails",
+ "chevrons": "Chevrons",
+ "none": "None"
+ }
+ }
+ }
+ },
+ "live": {
+ "preload": "Preload live view",
+ "provider": "Live view provider",
+ "providers": {
+ "frigate": "Frigate",
+ "frigate-jsmpeg": "Frigate JSMpeg",
+ "webrtc": "WebRTC"
+ },
+ "webrtc": {
+ "entity": "WebRTC Camera Entity (Not a Frigate camera)",
+ "url": "WebRTC Camera URL"
+ }
+ },
+ "image": {
+ "src": "Static image URL/data for image view"
+ },
+ "menu": {
+ "buttons": {
+ "frigate": "Frigate menu / Default view",
+ "frigate_ui": "Frigate user Interface",
+ "fullscreen": "Fullscreen",
+ "download": "Download event media"
+ },
+ "mode": "Menu mode",
+ "modes": {
+ "none": "No menu",
+ "hidden-top": "Hidden top",
+ "hidden-left": "Hidden left",
+ "hidden-bottom": "Hidden bottom",
+ "hidden-right": "Hidden right",
+ "overlay-top": "Overlay top",
+ "overlay-left": "Overlay left",
+ "overlay-bottom": "Overlay bottom",
+ "overlay-right": "Overlay right",
+ "hover-top": "Hover top",
+ "hover-left": "Hover left",
+ "hover-bottom": "Hover bottom",
+ "hover-right": "Hover right",
+ "above": "Above",
+ "below": "Below"
+ }
+ },
+ "dimensions": {
+ "aspect_ratio": "Default aspect ratio",
+ "aspect_ratio_mode": "Aspect ratio mode",
+ "aspect_ratio_modes": {
+ "unconstrained": "Unconstrained aspect ratio",
+ "dynamic": "Aspect ratio adjusts to media",
+ "static": "Static aspect ratio"
+ }
+ }
+ },
"editor": {
"basic": "Basic",
"basic_secondary": "Basic options for most users",
@@ -21,74 +109,7 @@
"webrtc_secondary": "Optional WebRTC parameters",
"advanced": "Advanced",
"advanced_secondary": "Advanced options",
- "camera_entity": "Camera Entity (Optional)",
- "frigate_camera_name": "Frigate camera name (Optional, autodetected from entity)",
- "default_view": "Default view (Optional)",
- "frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)",
- "view_timeout": "View timeout (seconds)",
- "frigate_url": "Frigate URL (Optional, for Frigate UI button)",
- "autoplay_clip": "Autoplay latest clip",
- "menu_mode": "Menu mode (Optional)",
- "show_button": "Show button",
- "zone": "Zone",
- "label": "Frigate label/object filter (Optional)",
- "live_provider": "Live view provider (Optional)",
- "live_preload": "Preload live view",
- "image": "Static image URL for image view (Optional)",
- "lazy_load": "Lazily load event media"
- },
- "menu": {
- "frigate": "Frigate Menu / Default View",
- "live": "Live View",
- "clips": "Clips Gallery",
- "snapshots": "Snapshots Gallery",
- "clip": "Latest Clip",
- "snapshot": "Latest Snapshot",
- "frigate_ui": "Frigate User Interface",
- "fullscreen": "Fullscreen",
- "image": "Static Image",
- "download": "Download event media"
- },
- "control": {
- "nextprev": "Media Next & Previous Controls",
- "thumbnails": "Thumbnails",
- "chevrons": "Chevrons",
- "none": "None"
- },
- "dimensions": {
- "aspect_ratio_mode": "Aspect Ratio Mode",
- "aspect_ratio": "Default aspect ratio (Optional)"
- },
- "aspect_ratio_mode": {
- "unconstrained": "Unconstrained aspect ratio",
- "dynamic": "Aspect ratio adjusts to media",
- "static": "Static aspect ratio"
- },
- "menu_mode": {
- "none": "No menu",
- "hidden-top": "Hidden Top",
- "hidden-left": "Hidden Left",
- "hidden-bottom": "Hidden Bottom",
- "hidden-right": "Hidden Right",
- "overlay-top": "Overlay Top",
- "overlay-left": "Overlay Left",
- "overlay-bottom": "Overlay Bottom",
- "overlay-right": "Overlay Right",
- "hover-top": "Hover Top",
- "hover-left": "Hover Left",
- "hover-bottom": "Hover Bottom",
- "hover-right": "Hover Right",
- "above": "Above",
- "below": "Below"
- },
- "live_provider": {
- "frigate": "Frigate",
- "frigate-jsmpeg": "Frigate JSMpeg",
- "webrtc": "WebRTC"
- },
- "webrtc": {
- "entity": "Optional WebRTC Camera Entity (Not a Frigate camera)",
- "url": "Optional WebRTC Camera URL"
+ "show_button": "Show button"
},
"error": {
"empty_response": "Received empty response from Home Assistant for request",
@@ -101,7 +122,7 @@
"invalid_configuration": "Invalid configuration",
"invalid_configuration_no_hint": "No location hint available (bad or missing type?)",
"missing_webrtc": "WebRTC component not found",
- "no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate_camera_name",
+ "no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate.camera_name",
"could_not_render_elements": "Could not render picture elements",
"invalid_elements_config": "Invalid picture elements configuration",
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
diff --git a/src/scss/viewer.scss b/src/scss/viewer.scss
index a0596030..9fec4d30 100644
--- a/src/scss/viewer.scss
+++ b/src/scss/viewer.scss
@@ -1,6 +1,11 @@
+:host {
+ --video-max-height: none;
+}
+
ha-hls-player {
transform-style: preserve-3d;
}
+
img,video {
width: 100%;
height: 100%;
diff --git a/src/types.ts b/src/types.ts
index 42c5db02..6420751d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -38,7 +38,7 @@ export type FrigateCardView =
| typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number]
| typeof FRIGATE_CARD_VIEWS_INTERNAL[number];
-export const FRIGATE_MENU_MODES = [
+const FRIGATE_MENU_MODES = [
'none',
'hidden-top',
'hidden-left',
@@ -55,13 +55,8 @@ export const FRIGATE_MENU_MODES = [
'above',
'below',
] as const;
-export type FrigateMenuMode = typeof FRIGATE_MENU_MODES[number];
-
export const NEXT_PREVIOUS_CONTROL_STYLES = ['none', 'thumbnails', 'chevrons'] as const;
-export type NextPreviousControlStyle = typeof NEXT_PREVIOUS_CONTROL_STYLES[number];
-
export const LIVE_PROVIDERS = ['frigate', 'frigate-jsmpeg', 'webrtc'] as const;
-export type LiveProvider = typeof LIVE_PROVIDERS[number];
/**
* Action Types (for "Picture Elements" / Menu)
@@ -122,7 +117,7 @@ const elementsBaseSchema = z.object({
});
/**
- * Picture Element Types
+ * Picture Element Configuration.
*
* All picture element types are validated (not just the Frigate card custom
* ones) as a convenience to present the user with a consistent error display
@@ -214,7 +209,7 @@ const customSchema = z
if (!val.match(/^custom:(?!frigate-card).+/)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
- message: "Frigate-card custom elements must match specific schemas",
+ message: 'Frigate-card custom elements must match specific schemas',
fatal: true,
});
}
@@ -223,7 +218,7 @@ const customSchema = z
.passthrough();
/**
- * Custom Element Types
+ * Custom Element Types.
*/
export const menuIconSchema = iconSchema.merge(
@@ -269,82 +264,199 @@ export type PictureElement = z.infer
;
const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer;
+/**
+ * Frigate configuration section.
+ */
+const frigateConfigDefault = {
+ client_id: 'frigate' as const,
+};
+const frigateConfigDefaultSchema = z
+ .object({
+ // No URL validation to allow relative URLs within HA (e.g. addons).
+ url: z.string().optional(),
+ client_id: z.string().optional().default(frigateConfigDefault.client_id),
+ camera_name: z.string().optional(),
+ label: z.string().optional(),
+ zone: z.string().optional(),
+ })
+ .default(frigateConfigDefault);
+
+/**
+ * View configuration section.
+ */
+const viewConfigDefault = {
+ default: 'live' as const,
+ timeout: 180,
+};
+const viewConfigSchema = z
+ .object({
+ default: z
+ .enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED)
+ .optional()
+ .default(viewConfigDefault.default),
+ timeout: z
+ .number()
+ .or(
+ z
+ .string()
+ .regex(/^\d+$/)
+ .transform((val) => Number(val)),
+ )
+ .optional()
+ .default(viewConfigDefault.timeout),
+ })
+ .default(viewConfigDefault);
+
+/**
+ * Image view configuration section.
+ */
+const imageConfigSchema = z
+ .object({
+ src: z.string().optional(),
+ })
+ .optional();
+export type ImageViewConfig = z.infer;
+
+/**
+ * Live view configuration section.
+ */
+const liveConfigDefault = {
+ provider: 'frigate' as const,
+ preload: false,
+};
+const webrtcConfigSchema = z
+ .object({
+ entity: z.string().optional(),
+ url: z.string().optional(),
+ })
+ .passthrough()
+ .optional();
+export type WebRTCConfig = z.infer;
+
+const liveConfigSchema = z
+ .object({
+ provider: z.enum(LIVE_PROVIDERS).default(liveConfigDefault.provider),
+ preload: z.boolean().default(liveConfigDefault.preload),
+ webrtc: webrtcConfigSchema,
+ })
+ .default(liveConfigDefault);
+
+/**
+ * Menu configuration section.
+ */
+const menuConfigDefault = {
+ mode: 'hidden-top' as const,
+ buttons: {
+ frigate: true,
+ live: true,
+ clips: true,
+ snapshots: true,
+ image: false,
+ download: true,
+ frigate_ui: true,
+ fullscreen: true,
+ },
+ button_size: '40px',
+};
+const menuConfigSchema = z
+ .object({
+ mode: z.enum(FRIGATE_MENU_MODES).optional().default(menuConfigDefault.mode),
+ buttons: z
+ .object({
+ frigate: z.boolean().default(menuConfigDefault.buttons.frigate),
+ live: z.boolean().default(menuConfigDefault.buttons.live),
+ clips: z.boolean().default(menuConfigDefault.buttons.clips),
+ snapshots: z.boolean().default(menuConfigDefault.buttons.snapshots),
+ image: z.boolean().default(menuConfigDefault.buttons.image),
+ download: z.boolean().default(menuConfigDefault.buttons.download),
+ frigate_ui: z.boolean().default(menuConfigDefault.buttons.frigate_ui),
+ fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen),
+ })
+ .default(menuConfigDefault.buttons),
+ button_size: z.string().default(menuConfigDefault.button_size),
+ })
+ .default(menuConfigDefault);
+export type MenuConfig = z.infer;
+
+/**
+ * Event viewer configuration section (clip, snapshot, clip-specific, snapshot-specific).
+ */
+const viewerConfigDefault = {
+ autoplay_clip: false,
+ lazy_load: true,
+ controls: {
+ next_previous: {
+ size: '48px',
+ style: 'thumbnails' as const,
+ },
+ },
+};
+const nextPreviousControlConfigSchema = z
+ .object({
+ style: z
+ .enum(NEXT_PREVIOUS_CONTROL_STYLES)
+ .default(viewerConfigDefault.controls.next_previous.style),
+ size: z.string().default(viewerConfigDefault.controls.next_previous.size),
+ })
+ .default(viewerConfigDefault.controls.next_previous);
+export type NextPreviousControlConfig = z.infer;
+
+const viewerConfigSchema = z
+ .object({
+ autoplay_clip: z.boolean().default(viewerConfigDefault.autoplay_clip),
+ lazy_load: z.boolean().default(viewerConfigDefault.lazy_load),
+ controls: z
+ .object({
+ next_previous: nextPreviousControlConfigSchema,
+ })
+ .default(viewerConfigDefault.controls),
+ })
+ .default(viewerConfigDefault);
+export type ViewerConfig = z.infer;
+
+/**
+ * Dimensions configuration section.
+ */
+const dimensionsConfigDefault = {
+ aspect_ratio_mode: 'dynamic' as const,
+ aspect_ratio: [16, 9],
+};
+const dimensionsConfigSchema = z
+ .object({
+ aspect_ratio_mode: z
+ .enum(['dynamic', 'static', 'unconstrained'])
+ .default(dimensionsConfigDefault.aspect_ratio_mode),
+ aspect_ratio: z
+ .number()
+ .array()
+ .length(2)
+ .or(
+ z
+ .string()
+ .regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/)
+ .transform((input) => input.split(/[:\/]/).map((d) => Number(d))),
+ )
+ .default(dimensionsConfigDefault.aspect_ratio),
+ })
+ .default(dimensionsConfigDefault);
+
+/**
+ * Main card config.
+ */
export const frigateCardConfigSchema = z.object({
camera_entity: z.string().optional(),
- // No URL validation to allow relative URLs within HA (e.g. addons).
- frigate_url: z.string().optional(),
- frigate_client_id: z.string().optional().default('frigate'),
- frigate_camera_name: z.string().optional(),
- view_default: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED).optional().default('live'),
- view_timeout: z
- .number()
- .or(
- z
- .string()
- .regex(/^\d+$/)
- .transform((val) => Number(val)),
- )
- .optional()
- .default(180),
- live_provider: z.enum(LIVE_PROVIDERS).default('frigate'),
- live_preload: z.boolean().default(false),
- image: z.string().optional(),
- webrtc: z
- .object({
- entity: z.string().optional(),
- url: z.string().optional(),
- })
- .passthrough()
- .optional(),
- label: z.string().optional(),
- zone: z.string().optional(),
- autoplay_clip: z.boolean().default(false),
- event_viewer: z
- .object({
- lazy_load: z.boolean().default(true),
- })
- .optional(),
- menu_mode: z.enum(FRIGATE_MENU_MODES).optional().default('hidden-top'),
- menu_buttons: z
- .object({
- frigate: z.boolean().default(true),
- live: z.boolean().default(true),
- clips: z.boolean().default(true),
- snapshots: z.boolean().default(true),
- image: z.boolean().default(false),
- download: z.boolean().default(true),
- frigate_ui: z.boolean().default(true),
- fullscreen: z.boolean().default(true),
- })
- .optional(),
- menu_button_size: z.string().default('40px'),
- update_entities: z.string().array().optional(),
+ frigate: frigateConfigDefaultSchema,
+ view: viewConfigSchema,
+ menu: menuConfigSchema,
+ live: liveConfigSchema,
+ event_viewer: viewerConfigSchema,
+ image: imageConfigSchema,
elements: pictureElementsSchema,
- controls: z
- .object({
- nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
- nextprev_size: z.string().default('48px'),
- })
- .optional(),
- dimensions: z
- .object({
- aspect_ratio_mode: z
- .enum(['dynamic', 'static', 'unconstrained'])
- .default('dynamic'),
- aspect_ratio: z
- .number()
- .array()
- .length(2)
- .or(
- z
- .string()
- .regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/)
- .transform((input) => input.split(/[:\/]/).map((d) => Number(d))),
- )
- .default([16, 9]),
- })
- .optional(),
+ dimensions: dimensionsConfigSchema,
+
+ // Entities that should trigger a card update.
+ update_entities: z.string().array().optional(),
// Stock lovelace card config.
type: z.string(),
@@ -354,6 +466,14 @@ export const frigateCardConfigSchema = z.object({
});
export type FrigateCardConfig = z.infer;
+export const frigateCardConfigDefaults = {
+ frigate: frigateConfigDefault,
+ view: viewConfigDefault,
+ menu: menuConfigDefault,
+ live: liveConfigDefault,
+ event_viewer: viewerConfigDefault,
+};
+
// Schema for card (non-user configured) menu icons.
const internalMenuIconSchema = z.object({
type: z.literal('internal-menu-icon'),
@@ -404,8 +524,8 @@ export interface Message {
}
export interface MenuInteraction {
- interaction: "hold" | "tap" | "double_tap",
- button: MenuButton,
+ interaction: 'hold' | 'tap' | 'double_tap';
+ button: MenuButton;
}
/**