Initial configuration reorganization.

This commit is contained in:
Dermot Duffy
2021-11-01 12:43:33 -07:00
parent 12cee94d49
commit e0daa17834
11 changed files with 638 additions and 477 deletions
+203 -83
View File
@@ -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<typeof pictureElementSchema>;
const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>;
/**
* 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<typeof imageConfigSchema>;
/**
* 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<typeof webrtcConfigSchema>;
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<typeof menuConfigSchema>;
/**
* 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<typeof nextPreviousControlConfigSchema>;
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<typeof viewerConfigSchema>;
/**
* 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<typeof frigateCardConfigSchema>;
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;
}
/**