feat: Rename the card to advanced-camera-card (#1873)

Whilst the Frigate support in this card is the best among camera
engines, the name incorrectly suggests that Frigate is a requirement.
Instead, to broaden the appeal, change to more camera agnostic name.
This does not suggest any change in priority, role or support for
Frigate.

This change is likely to be bug prone, due to the size of the rename --
the code contains 1500+ references to "Frigate" most of which make sense
to rename, some which do not, all of which needed human assessment.

- Closes #1298


BREAKING CHANGE: References to `frigate-card` in all kinds of
configuration need to be updated to `advanced-camera-card`. An automated
config upgrade should take care of the majority of usecases (click `Edit
-> Upgrade -> Save`), though may not be perfect.
This commit is contained in:
Dermot Duffy
2025-02-06 19:32:32 -08:00
committed by GitHub
parent 43947b49dc
commit cc376a8be1
305 changed files with 4270 additions and 3686 deletions
+178 -114
View File
@@ -7,17 +7,12 @@ import {
CONF_AUTOMATIONS,
CONF_CAMERAS,
CONF_CAMERAS_GLOBAL_DIMENSIONS_LAYOUT,
CONF_CAMERAS_GLOBAL_IMAGE,
CONF_CAMERAS_GLOBAL_JSMPEG,
CONF_CAMERAS_GLOBAL_PTZ,
CONF_CAMERAS_GLOBAL_WEBRTC_CARD,
CONF_DIMENSIONS_HEIGHT,
CONF_ELEMENTS,
CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE,
CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
CONF_MEDIA_GALLERY,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
CONF_MENU_BUTTONS_CAMERA_UI,
CONF_OVERRIDES,
CONF_PROFILES,
CONF_STATUS_BAR,
@@ -35,9 +30,9 @@ import {
} from '../const';
import { arrayify } from '../utils/basic';
import {
FrigateCardCondition,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
AdvancedCameraCardCondition,
RawAdvancedCameraCardConfig,
RawAdvancedCameraCardConfigArray,
} from './types';
// *************************************************************************
@@ -51,7 +46,7 @@ import {
* @param value The value to set.
*/
export const setConfigValue = (
obj: RawFrigateCardConfig,
obj: RawAdvancedCameraCardConfig,
keys: string | (string | number)[],
value: unknown,
): void => {
@@ -66,7 +61,7 @@ export const setConfigValue = (
* @returns The property or undefined if not found.
*/
export const getConfigValue = (
obj: RawFrigateCardConfig,
obj: RawAdvancedCameraCardConfig,
keys: string | (string | number)[],
def?: unknown,
): unknown => {
@@ -78,7 +73,10 @@ export const getConfigValue = (
* @param obj The configuration.
* @param key The key to the property to delete.
*/
export const deleteConfigValue = (obj: RawFrigateCardConfig, path: string): void => {
export const deleteConfigValue = (
obj: RawAdvancedCameraCardConfig,
path: string,
): void => {
unset(obj, path);
};
@@ -110,7 +108,7 @@ export const getArrayConfigPath = (path: string, index: number): string => {
* @param obj The configuration to upgrade.
* @returns `true` if the configuration is modified.
*/
export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
export const upgradeConfig = function (obj: RawAdvancedCameraCardConfig): boolean {
let upgraded = false;
for (let i = 0; i < UPGRADES.length; i++) {
upgraded = UPGRADES[i](obj) || upgraded;
@@ -123,7 +121,7 @@ export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
* @param obj The configuration. It is not modified.
* @returns `true` if the configuration is upgradeable.
*/
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
export const isConfigUpgradeable = function (obj: RawAdvancedCameraCardConfig): boolean {
return upgradeConfig(copyConfig(obj));
};
@@ -136,7 +134,7 @@ export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean
* @returns `true` if the configuration was modified.
*/
export const moveConfigValue = (
obj: RawFrigateCardConfig,
obj: RawAdvancedCameraCardConfig,
oldPath: string,
newPath: string,
options?: {
@@ -183,8 +181,8 @@ export const upgradeMoveTo = function (
transform?: (valueIn: unknown) => unknown;
keepOriginal?: boolean;
},
): (obj: RawFrigateCardConfig) => boolean {
return function (obj: RawFrigateCardConfig): boolean {
): (obj: RawAdvancedCameraCardConfig) => boolean {
return function (obj: RawAdvancedCameraCardConfig): boolean {
return moveConfigValue(obj, oldPath, newPath, options);
};
};
@@ -204,14 +202,26 @@ export const upgradeMoveToWithOverrides = function (
transform?: (valueIn: unknown) => unknown;
keepOriginal?: boolean;
},
): (obj: RawFrigateCardConfig) => boolean {
return function (obj: RawFrigateCardConfig): boolean {
): (obj: RawAdvancedCameraCardConfig) => boolean {
return function (obj: RawAdvancedCameraCardConfig): boolean {
let modified = upgradeMoveTo(oldPath, newPath, options)(obj);
modified =
upgradeArrayOfObjects(
CONF_OVERRIDES,
upgradeMoveTo(oldPath, newPath, options),
(obj) => obj.overrides as RawFrigateCardConfig | undefined,
(obj) =>
obj.merge && typeof obj.merge === 'object'
? (obj.merge as RawAdvancedCameraCardConfig | undefined)
: undefined,
)(obj) || modified;
modified =
upgradeArrayOfObjects(
CONF_OVERRIDES,
upgradeMoveTo(oldPath, newPath, options),
(obj) =>
obj.set && typeof obj.set === 'object'
? (obj.set as RawAdvancedCameraCardConfig | undefined)
: undefined,
)(obj) || modified;
return modified;
};
@@ -226,7 +236,7 @@ export const upgradeMoveToWithOverrides = function (
export const upgradeWithOverrides = function (
path: string,
transform: (valueIn: unknown) => unknown,
): (obj: RawFrigateCardConfig) => boolean {
): (obj: RawAdvancedCameraCardConfig) => boolean {
return upgradeMoveToWithOverrides(path, path, { transform: transform });
};
@@ -237,7 +247,7 @@ export const upgradeWithOverrides = function (
*/
export const deleteWithOverrides = function (
path: string,
): (obj: RawFrigateCardConfig) => boolean {
): (obj: RawAdvancedCameraCardConfig) => boolean {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return upgradeMoveToWithOverrides(path, path, { transform: (_) => null });
};
@@ -252,10 +262,12 @@ export const deleteWithOverrides = function (
*/
export const upgradeArrayOfObjects = function (
arrayPath: string,
upgrade: (obj: RawFrigateCardConfig) => boolean,
getObject?: (obj: RawFrigateCardConfig) => RawFrigateCardConfig | undefined,
): (obj: RawFrigateCardConfig) => boolean {
return function (obj: RawFrigateCardConfig): boolean {
upgrade: (obj: RawAdvancedCameraCardConfig) => boolean,
getObject?: (
obj: RawAdvancedCameraCardConfig,
) => RawAdvancedCameraCardConfig | undefined,
): (obj: RawAdvancedCameraCardConfig) => boolean {
return function (obj: RawAdvancedCameraCardConfig): boolean {
let modified = false;
const array = getConfigValue(obj, arrayPath);
if (Array.isArray(array)) {
@@ -277,10 +289,12 @@ export const upgradeArrayOfObjects = function (
* @returns An upgrade function.
*/
export const upgradeObjectRecursively = (
transform: (data: RawFrigateCardConfig) => boolean,
getObject?: (data: RawFrigateCardConfig) => RawFrigateCardConfig | undefined | null,
): ((data: RawFrigateCardConfig) => boolean) => {
const recurse = (data: RawFrigateCardConfig): boolean => {
transform: (data: RawAdvancedCameraCardConfig) => boolean,
getObject?: (
data: RawAdvancedCameraCardConfig,
) => RawAdvancedCameraCardConfig | undefined | null,
): ((data: RawAdvancedCameraCardConfig) => boolean) => {
const recurse = (data: RawAdvancedCameraCardConfig): boolean => {
let result = false;
if (data && typeof data === 'object') {
const object = getObject ? getObject(data) : data;
@@ -288,12 +302,12 @@ export const upgradeObjectRecursively = (
result = transform(object) || result;
}
if (Array.isArray(data)) {
data.forEach((item: RawFrigateCardConfig) => {
data.forEach((item: RawAdvancedCameraCardConfig) => {
result = recurse(item) || result;
});
} else {
Object.keys(data).forEach((key) => {
result = recurse(data[key] as RawFrigateCardConfig) || result;
result = recurse(data[key] as RawAdvancedCameraCardConfig) || result;
});
}
}
@@ -341,20 +355,6 @@ export const deleteTransform = function (_value: unknown): number | null | undef
// Upgrade Related Functions: Specific Transforms / Upgraders
// *************************************************************************
/**
* Transform mediaLoaded -> media_loaded
* @param data Input data.
* @returns `true` if the configuration was modified.
*/
const conditionMediaLoadedTransform = (data: unknown): boolean => {
if (typeof data === 'object' && data && data['mediaLoaded'] !== undefined) {
data['media_loaded'] = data['mediaLoaded'];
delete data['mediaLoaded'];
return true;
}
return false;
};
/**
* Transform a single object with multiple conditions to multiple objects with
* single conditions
@@ -372,7 +372,7 @@ const conditionToConditionsTransform = (data: unknown): boolean => {
}
const oldConditions = data['conditions'];
const newConditions: FrigateCardCondition[] = [];
const newConditions: AdvancedCameraCardCondition[] = [];
if (oldConditions['view'] !== undefined) {
newConditions.push({
@@ -465,7 +465,7 @@ const callServiceToPerformActionTransform = (data: unknown): boolean => {
/**
* Transform service_data -> data
* See: https://github.com/dermotduffy/frigate-hass-card/issues/1103
* See: https://github.com/dermotduffy/advanced-camera-card/issues/1103
* @param data Input data.
* @returns `true` if the configuration was modified.
*/
@@ -485,24 +485,6 @@ const serviceDataToDataTransform = (data: unknown): boolean => {
return false;
};
/**
* Transform action frigate_ui -> camera_ui
* @param data Input data.
* @returns `true` if the configuration was modified.
*/
const frigateUIActionTransform = (data: unknown): boolean => {
if (
typeof data === 'object' &&
data &&
data['action'] === 'custom:frigate-card-action' &&
data['frigate_card_action'] === 'frigate_ui'
) {
data['frigate_card_action'] = 'camera_ui';
return true;
}
return false;
};
/**
* Transform element PTZ to native live PTZ.
* @param data Input data.
@@ -520,7 +502,7 @@ const upgradePTZElementsToLive = function (): (data: unknown) => boolean {
}
let foundPTZ = false;
const movePTZ = (element: RawFrigateCardConfig): void => {
const movePTZ = (element: RawAdvancedCameraCardConfig): void => {
if (!foundPTZ) {
if (!get(data, 'live.controls.ptz')) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -532,15 +514,15 @@ const upgradePTZElementsToLive = function (): (data: unknown) => boolean {
};
const processElements = (
elements: RawFrigateCardConfigArray,
): RawFrigateCardConfigArray => {
const newElements: RawFrigateCardConfigArray = [];
elements: RawAdvancedCameraCardConfigArray,
): RawAdvancedCameraCardConfigArray => {
const newElements: RawAdvancedCameraCardConfigArray = [];
for (const element of elements) {
if (element['type'] === 'custom:frigate-card-ptz') {
if (element['type'] === 'custom:advanced-camera-card-ptz') {
movePTZ(element);
} else if (
(element['type'] === 'conditional' ||
element['type'] === 'custom:frigate-card-conditional') &&
element['type'] === 'custom:advanced-camera-card-conditional') &&
Array.isArray(element['elements'])
) {
const newConditionalElements = processElements(element['elements']);
@@ -699,52 +681,122 @@ const titleControlTransform = (data: unknown): unknown => {
return null;
};
const UPGRADES = [
// v4.0.0 -> v4.1.0
upgradeArrayOfObjects(
CONF_OVERRIDES,
conditionMediaLoadedTransform,
(data) => data.conditions as RawFrigateCardConfig | undefined,
),
(data: unknown): boolean => {
return upgradeObjectRecursively(
conditionMediaLoadedTransform,
(data) => data.conditions as RawFrigateCardConfig | undefined,
)(typeof data === 'object' && data ? data[CONF_ELEMENTS] : {});
},
upgradeMoveToWithOverrides('event_gallery', CONF_MEDIA_GALLERY),
upgradeMoveToWithOverrides('menu.buttons.frigate_ui', CONF_MENU_BUTTONS_CAMERA_UI),
(data: unknown): boolean => {
return upgradeObjectRecursively(frigateUIActionTransform)(
typeof data === 'object' && data ? <RawFrigateCardConfig>data : {},
);
},
upgradeArrayOfObjects(
CONF_CAMERAS,
upgradeWithOverrides('live_provider', (val) =>
val === 'frigate-jsmpeg' ? 'jsmpeg' : val,
),
),
upgradeMoveToWithOverrides('live.jsmpeg', CONF_CAMERAS_GLOBAL_JSMPEG),
upgradeMoveToWithOverrides('live.image', CONF_CAMERAS_GLOBAL_IMAGE),
upgradeMoveToWithOverrides('live.webrtc_card', CONF_CAMERAS_GLOBAL_WEBRTC_CARD),
upgradeArrayOfObjects(
CONF_CAMERAS,
upgradeMoveToWithOverrides('frigate.zone', 'frigate.zones', {
transform: (zone) => arrayify(zone),
}),
),
upgradeArrayOfObjects(
CONF_CAMERAS,
upgradeMoveToWithOverrides('frigate.label', 'frigate.labels', {
transform: (label) => arrayify(label),
}),
),
const frigateCardToAdvancedCameraCardTransform = (
data: RawAdvancedCameraCardConfig,
): boolean => {
interface Substitution {
oldKey: string;
oldValue?: string;
newKey?: string;
newValue?: string;
}
const substitutions: Substitution[] = [
{
oldKey: 'type',
oldValue: 'custom:frigate-card',
newValue: 'custom:advanced-camera-card',
},
{
oldKey: 'action',
oldValue: 'custom:frigate-card-action',
newValue: 'custom:advanced-camera-card-action',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-menu-icon',
newValue: 'custom:advanced-camera-card-menu-icon',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-menu-state-icon',
newValue: 'custom:advanced-camera-card-menu-state-icon',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-menu-submenu',
newValue: 'custom:advanced-camera-card-menu-submenu',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-menu-submenu-select',
newValue: 'custom:advanced-camera-card-menu-submenu-select',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-status-bar-icon',
newValue: 'custom:advanced-camera-card-status-bar-icon',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-status-bar-image',
newValue: 'custom:advanced-camera-card-status-bar-image',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-status-bar-string',
newValue: 'custom:advanced-camera-card-status-bar-string',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-conditional',
newValue: 'custom:advanced-camera-card-conditional',
},
{
oldKey: 'type',
oldValue: 'custom:frigate-card-conditional',
newValue: 'custom:advanced-camera-card-conditional',
},
{
oldKey: 'frigate_card_action',
newKey: 'advanced_camera_card_action',
},
];
let modified = false;
for (const substitution of substitutions) {
if (
substitution.oldValue &&
substitution.newValue &&
data[substitution.oldKey] === substitution.oldValue
) {
data[substitution.newKey ?? substitution.oldKey] = substitution.newValue;
modified = true;
}
if (substitution.oldKey in data && substitution.newKey) {
data[substitution.newKey] = data[substitution.oldKey];
delete data[substitution.oldKey];
modified = true;
}
}
return modified;
};
const frigateCardToAdvancedCameraCardStyleTransform = (data: unknown): unknown => {
if (typeof data !== 'object' || !data || Array.isArray(data)) {
return data;
}
const newStyleOverrides = { ...data };
const frigateCardStyleRegexp = new RegExp(/^--frigate-card-/);
for (const key of Object.keys(data)) {
if (key.match(frigateCardStyleRegexp)) {
const newKey = key.replace(frigateCardStyleRegexp, '--advanced-camera-card-');
newStyleOverrides[newKey] = data[key];
delete newStyleOverrides[key];
}
}
return newStyleOverrides;
};
const UPGRADES = [
// v5.2.0 -> v6.0.0
(data: unknown): boolean => {
return upgradeObjectRecursively(serviceDataToDataTransform)(
typeof data === 'object' && data ? <RawFrigateCardConfig>data : {},
typeof data === 'object' && data ? <RawAdvancedCameraCardConfig>data : {},
);
},
upgradePTZElementsToLive(),
@@ -877,7 +929,7 @@ const UPGRADES = [
// different.
(data: unknown): boolean => {
return upgradeObjectRecursively(callServiceToPerformActionTransform)(
typeof data === 'object' && data ? (data as RawFrigateCardConfig) : {},
typeof data === 'object' && data ? (data as RawAdvancedCameraCardConfig) : {},
);
},
upgradeMoveToWithOverrides('dimensions.max_height', CONF_DIMENSIONS_HEIGHT),
@@ -887,4 +939,16 @@ const UPGRADES = [
upgradeMoveToWithOverrides('view.dark_mode', CONF_VIEW_DIM, {
transform: (val) => val === 'on',
}),
// v7.0.0+
(data: unknown): boolean => {
return upgradeObjectRecursively(frigateCardToAdvancedCameraCardTransform)(
typeof data === 'object' && data ? (data as RawAdvancedCameraCardConfig) : {},
);
},
upgradeWithOverrides(
'view.theme.overrides',
frigateCardToAdvancedCameraCardStyleTransform,
),
upgradeMoveToWithOverrides('menu.buttons.frigate', 'menu.buttons.iris'),
];
+10 -6
View File
@@ -1,6 +1,10 @@
import { deepRemoveDefaults } from '../../utils/zod.js';
import { getConfigValue, setConfigValue } from '../management.js';
import { ProfileType, RawFrigateCardConfig, frigateCardConfigSchema } from '../types.js';
import {
ProfileType,
RawAdvancedCameraCardConfig,
advancedCameraCardConfigSchema,
} from '../types.js';
import { CASTING_PROFILE } from './casting.js';
import { LOW_PERFORMANCE_PROFILE } from './low-performance.js';
import { SCRUBBING_PROFILE } from './scrubbing.js';
@@ -18,14 +22,14 @@ const PROFILES = {
* @param outputConfig The output config to write to.
* @returns A changed (in-place) parsed input configuration.
*/
export const setProfiles = <T extends RawFrigateCardConfig>(
inputConfig: RawFrigateCardConfig,
export const setProfiles = <T extends RawAdvancedCameraCardConfig>(
inputConfig: RawAdvancedCameraCardConfig,
outputConfig: T,
profiles?: ProfileType[],
): T => {
const defaultLessParseResult = deepRemoveDefaults(frigateCardConfigSchema).safeParse(
inputConfig,
);
const defaultLessParseResult = deepRemoveDefaults(
advancedCameraCardConfigSchema,
).safeParse(inputConfig);
if (!defaultLessParseResult.success) {
return outputConfig;
}
+3 -3
View File
@@ -30,12 +30,12 @@ import {
CONF_MEDIA_VIEWER_DRAGGABLE,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MENU_BUTTONS_FRIGATE,
CONF_MENU_BUTTONS_IRIS,
CONF_MENU_BUTTONS_MEDIA_PLAYER,
CONF_MENU_BUTTONS_TIMELINE,
CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_CARD_LOADING_INDICATOR,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
CONF_PERFORMANCE_FEATURES_CARD_LOADING_INDICATOR,
CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
@@ -91,7 +91,7 @@ export const LOW_PERFORMANCE_PROFILE = {
[CONF_MENU_STYLE]: 'outside',
// Hide several buttons that are otherwise visible by default.
[`${CONF_MENU_BUTTONS_FRIGATE}.enabled`]: false,
[`${CONF_MENU_BUTTONS_IRIS}.enabled`]: false,
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
+104 -99
View File
@@ -26,13 +26,13 @@ import { PTZ_ACTIONS } from './ptz';
export const BUTTON_SIZE_MIN = 20;
export const STATUS_BAR_HEIGHT_MIN = BUTTON_SIZE_MIN;
const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
export const FRIGATE_MENU_PRIORITY_MAX = 100;
const MENU_PRIORITY_DEFAULT = 50;
export const MENU_PRIORITY_MAX = 100;
export const FRIGATE_STATUS_BAR_PRIORITY_DEFAULT = FRIGATE_MENU_PRIORITY_DEFAULT;
export const FRIGATE_STATUS_BAR_PRIORITY_MAX = FRIGATE_MENU_PRIORITY_MAX;
export const STATUS_BAR_PRIORITY_DEFAULT = MENU_PRIORITY_DEFAULT;
export const STATUS_BAR_PRIORITY_MAX = MENU_PRIORITY_MAX;
export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
export const VIEWS_USER_SPECIFIED = [
'diagnostics',
'live',
'clip',
@@ -44,21 +44,20 @@ export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
'image',
'timeline',
] as const;
export type FrigateCardUserSpecifiedView =
(typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED)[number];
export type AdvancedCameraCardUserSpecifiedView = (typeof VIEWS_USER_SPECIFIED)[number];
const FRIGATE_CARD_VIEWS = [
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
const VIEWS = [
...VIEWS_USER_SPECIFIED,
'diagnostics',
// Media: A generic piece of media (could be clip, snapshot, recording).
'media',
] as const;
export type FrigateCardView = (typeof FRIGATE_CARD_VIEWS)[number];
export type AdvancedCameraCardView = (typeof VIEWS)[number];
// The default view (may not be supported on all cameras).
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
export const VIEW_DEFAULT = 'live' as const;
export const MEDIA_ACTION_NEGATIVE_CONDITIONS = ['unselected', 'hidden'] as const;
export type LazyUnloadCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
@@ -243,9 +242,9 @@ const noActionSchema = schemaForType<
}),
);
export const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
export const advancedCameraCardCustomActionsBaseSchema = customActionSchema.extend({
action: z
.literal('custom:frigate-card-action')
.literal('custom:advanced-camera-card-action')
// Syntactic sugar to avoid 'fire-dom-event' as part of an external API.
.transform((): 'fire-dom-event' => 'fire-dom-event')
.or(z.literal('fire-dom-event')),
@@ -261,7 +260,7 @@ export const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
// Custom Actions
// *************************************************************************
const FRIGATE_CARD_GENERAL_ACTIONS = [
const GENERAL_ACTIONS = [
'camera_ui',
'default',
'download',
@@ -280,48 +279,50 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
'screenshot',
'unmute',
] as const;
export type FrigateCardGeneralAction = (typeof FRIGATE_CARD_GENERAL_ACTIONS)[number];
export type AdvancedCameraCardGeneralAction = (typeof GENERAL_ACTIONS)[number];
const viewActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED),
const viewActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.enum(VIEWS_USER_SPECIFIED),
});
export type ViewActionConfig = z.infer<typeof viewActionConfigSchema>;
const generalActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
const generalActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.enum(GENERAL_ACTIONS),
});
export type GeneralActionConfig = z.infer<typeof generalActionConfigSchema>;
const cameraSelectActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('camera_select'),
const cameraSelectActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('camera_select'),
camera: z.string().optional(),
triggered: z.boolean().optional(),
});
export type CameraSelectActionConfig = z.infer<typeof cameraSelectActionConfigSchema>;
const substreamSelectActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('live_substream_select'),
camera: z.string(),
});
const substreamSelectActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('live_substream_select'),
camera: z.string(),
});
export type SubstreamSelectActionConfig = z.infer<
typeof substreamSelectActionConfigSchema
>;
const mediaPlayerActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('media_player'),
const mediaPlayerActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('media_player'),
media_player: z.string(),
media_player_action: z.enum(['play', 'stop']),
});
export type MediaPlayerActionConfig = z.infer<typeof mediaPlayerActionConfigSchema>;
const viewDisplayModeActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('display_mode_select'),
display_mode: viewDisplayModeSchema,
});
const viewDisplayModeActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('display_mode_select'),
display_mode: viewDisplayModeSchema,
});
export type DisplayModeActionConfig = z.infer<typeof viewDisplayModeActionConfigSchema>;
const ptzActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('ptz'),
const ptzActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('ptz'),
camera: z.string().optional(),
ptz_action: z.enum(PTZ_ACTIONS).optional(),
ptz_phase: z.enum(ACTION_PHASES).optional(),
@@ -329,8 +330,8 @@ const ptzActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
});
export type PTZActionConfig = z.infer<typeof ptzActionConfigSchema>;
const ptzDigitalActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('ptz_digital'),
const ptzDigitalActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('ptz_digital'),
target_id: z.string().optional(),
absolute: z
.object({
@@ -343,8 +344,8 @@ const ptzDigitalActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
});
export type PTZDigitialActionConfig = z.infer<typeof ptzDigitalActionConfigSchema>;
const ptzMultiActionSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('ptz_multi'),
const ptzMultiActionSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('ptz_multi'),
target_id: z.string().optional(),
ptz_action: z.enum(PTZ_ACTIONS).optional(),
@@ -353,8 +354,8 @@ const ptzMultiActionSchema = frigateCardCustomActionsBaseSchema.extend({
});
export type PTZMultiActionConfig = z.infer<typeof ptzMultiActionSchema>;
const ptzControlsActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('ptz_controls'),
const ptzControlsActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('ptz_controls'),
enabled: z.boolean(),
});
export type PTZControlsActionConfig = z.infer<typeof ptzControlsActionConfigSchema>;
@@ -367,14 +368,14 @@ const timeDeltaSchema = z.object({
});
export type TimeDelta = z.infer<typeof timeDeltaSchema>;
const sleepActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('sleep'),
const sleepActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('sleep'),
duration: timeDeltaSchema.optional().default({ s: 1 }),
});
export type SleepActionConfig = z.infer<typeof sleepActionConfigSchema>;
const statusBarActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('status_bar'),
const statusBarActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('status_bar'),
status_bar_action: z.enum(['add', 'remove', 'reset']),
// This needs to be lazily evaluated since statusBarItemSchema may itself
@@ -389,14 +390,14 @@ export type StatusBarActionConfig = z.infer<typeof statusBarActionConfigSchema>;
const LOG_ACTIONS_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
export type LogActionLevel = (typeof LOG_ACTIONS_LEVELS)[number];
const logActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('log'),
const logActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('log'),
message: z.string(),
level: z.enum(LOG_ACTIONS_LEVELS).default('info'),
});
export type LogActionConfig = z.infer<typeof logActionConfigSchema>;
export const frigateCardCustomActionSchema = z.union([
export const advancedCameraCardCustomActionSchema = z.union([
cameraSelectActionConfigSchema,
generalActionConfigSchema,
substreamSelectActionConfigSchema,
@@ -411,10 +412,12 @@ export const frigateCardCustomActionSchema = z.union([
sleepActionConfigSchema,
statusBarActionConfigSchema,
]);
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
export type AdvancedCameraCardCustomAction = z.infer<
typeof advancedCameraCardCustomActionSchema
>;
// Cannot use discriminatedUnion since frigateCardCustomActionSchema uses a
// transform on the discriminated union key.
// Cannot use discriminatedUnion since advancedCameraCardCustomActionSchema uses
// a transform on the discriminated union key.
export const actionSchema = z.union([
toggleActionSchema,
callServiceActionSchema,
@@ -424,7 +427,7 @@ export const actionSchema = z.union([
moreInfoActionSchema,
noActionSchema,
customActionSchema,
frigateCardCustomActionSchema,
advancedCameraCardCustomActionSchema,
]);
export type ActionType = z.infer<typeof actionSchema>;
@@ -459,9 +462,9 @@ const elementsBaseSchema = actionsBaseSchema.extend({
// *************************************************************************
// Picture Elements
//
// 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
// up-front regardless of where they made their error.
// All picture element types are validated (not just the Advanced Camera Card
// custom ones) as a convenience to present the user with a consistent error
// display up-front regardless of where they made their error.
// *************************************************************************
// https://www.home-assistant.io/lovelace/picture-elements/#state-badge
@@ -564,12 +567,12 @@ export const conditionalSchema = z.object({
// https://www.home-assistant.io/lovelace/picture-elements/#custom-elements
export const customSchema = z
.object({
// Insist that Frigate card custom elements are handled by other schemas.
// Insist that Advanced Camera Card custom elements are handled by other schemas.
type: z.string().superRefine((val, ctx) => {
if (!val.match(/^custom:(?!frigate-card).+/)) {
if (!val.match(/^custom:(?!advanced-camera-card).+/)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Frigate-card custom elements must match specific schemas',
message: 'advanced-camera-card custom elements must match specific schemas',
fatal: true,
});
}
@@ -586,8 +589,8 @@ const menuBaseSchema = z.object({
priority: z
.number()
.min(0)
.max(FRIGATE_MENU_PRIORITY_MAX)
.default(FRIGATE_MENU_PRIORITY_DEFAULT)
.max(MENU_PRIORITY_MAX)
.default(MENU_PRIORITY_DEFAULT)
.optional(),
alignment: z.enum(['matching', 'opposing']).default('matching').optional(),
icon: z.string().optional(),
@@ -595,14 +598,14 @@ const menuBaseSchema = z.object({
});
const menuIconSchema = menuBaseSchema.merge(iconSchema).extend({
type: z.literal('custom:frigate-card-menu-icon'),
type: z.literal('custom:advanced-camera-card-menu-icon'),
});
export type MenuIcon = z.infer<typeof menuIconSchema>;
const menuStateIconSchema = menuBaseSchema
.merge(stateIconSchema)
.extend({
type: z.literal('custom:frigate-card-menu-state-icon'),
type: z.literal('custom:advanced-camera-card-menu-state-icon'),
})
.merge(menuBaseSchema);
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
@@ -618,13 +621,13 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({
type: z.literal('custom:frigate-card-menu-submenu'),
type: z.literal('custom:advanced-camera-card-menu-submenu'),
items: menuSubmenuItemSchema.array(),
});
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
const menuSubmenuSelectSchema = menuBaseSchema.merge(stateIconSchema).extend({
type: z.literal('custom:frigate-card-menu-submenu-select'),
type: z.literal('custom:advanced-camera-card-menu-submenu-select'),
options: z.record(menuSubmenuItemSchema.deepPartial()).optional(),
});
export type MenuSubmenuSelect = z.infer<typeof menuSubmenuSelectSchema>;
@@ -639,8 +642,8 @@ const statusBarItemBaseSchema = z.object({
priority: z
.number()
.min(0)
.max(FRIGATE_STATUS_BAR_PRIORITY_MAX)
.default(FRIGATE_STATUS_BAR_PRIORITY_DEFAULT)
.max(STATUS_BAR_PRIORITY_MAX)
.default(STATUS_BAR_PRIORITY_DEFAULT)
.optional(),
});
@@ -652,19 +655,19 @@ const statusBarItemElementsBaseSchema = statusBarItemBaseSchema.extend({
});
const statusBarIconItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:frigate-card-status-bar-icon'),
type: z.literal('custom:advanced-camera-card-status-bar-icon'),
icon: z.string(),
});
export type StatusBarIcon = z.infer<typeof statusBarIconItemSchema>;
const statusBarImageItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:frigate-card-status-bar-image'),
type: z.literal('custom:advanced-camera-card-status-bar-image'),
image: z.string(),
});
export type StatusBarImage = z.infer<typeof statusBarImageItemSchema>;
const statusBarStringItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:frigate-card-status-bar-string'),
type: z.literal('custom:advanced-camera-card-status-bar-string'),
string: z.string(),
});
export type StatusBarString = z.infer<typeof statusBarStringItemSchema>;
@@ -746,7 +749,7 @@ const userAgentConditionSchema = z.object({
companion: z.boolean().optional(),
});
export const frigateCardConditionSchema = z.discriminatedUnion('condition', [
export const advancedCameraCardConditionSchema = z.discriminatedUnion('condition', [
// Stock conditions:
stateConditionSchema,
numericStateConditionSchema,
@@ -766,14 +769,18 @@ export const frigateCardConditionSchema = z.discriminatedUnion('condition', [
keyConditionSchema,
userAgentConditionSchema,
]);
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
export type AdvancedCameraCardCondition = z.infer<
typeof advancedCameraCardConditionSchema
>;
export const frigateConditionalSchema = z.object({
type: z.literal('custom:frigate-card-conditional'),
conditions: frigateCardConditionSchema.array(),
export const advancedCameraCardConditionalSchema = z.object({
type: z.literal('custom:advanced-camera-card-conditional'),
conditions: advancedCameraCardConditionSchema.array(),
elements: z.lazy(() => pictureElementsSchema),
});
export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
export type AdvancedCameraCardConditional = z.infer<
typeof advancedCameraCardConditionalSchema
>;
// *************************************************************************
// Custom Element Configuration: Stock Picture Elements + Custom
@@ -784,7 +791,7 @@ export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
const pictureElementSchema = z.union([
conditionalSchema,
customSchema,
frigateConditionalSchema,
advancedCameraCardConditionalSchema,
iconSchema,
imageSchema,
menuIconSchema,
@@ -1324,7 +1331,7 @@ export type LiveConfig = z.infer<typeof liveConfigSchema>;
// This schema is used when the live config needs to be overridden (see
// `live.ts`). Overrides will always be "relative" to the config root, so this
// schema maintains that 'depth' from the root but without the other
// requirements that frigateCardConfigSchema has. Without this, overrides
// requirements that advancedCameraCardConfigSchema has. Without this, overrides
// calculated in `live.ts` would fail since cameras/type are not provided (as
// these are mandatory parameters in the full config).
export const liveConfigAbsoluteRootSchema = z.object({
@@ -1519,7 +1526,7 @@ export const cameraConfigSchema = z
proxy: proxyConfigSchema.default(cameraConfigDefault.proxy),
// See: https://github.com/dermotduffy/frigate-hass-card/issues/1650
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1650
always_error_if_entity_unavailable: z
.boolean()
.default(cameraConfigDefault.always_error_if_entity_unavailable),
@@ -1537,7 +1544,7 @@ export type CamerasConfig = z.infer<typeof camerasConfigSchema>;
// *************************************************************************
const viewConfigDefault = {
default: FRIGATE_CARD_VIEW_DEFAULT,
default: VIEW_DEFAULT,
camera_select: 'current' as const,
interaction_seconds: 300,
default_reset: {
@@ -1599,11 +1606,9 @@ export type ThemeConfig = z.infer<typeof themeConfigSchema>;
const viewConfigSchema = z
.object({
default: z
.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED)
.default(viewConfigDefault.default),
default: z.enum(VIEWS_USER_SPECIFIED).default(viewConfigDefault.default),
camera_select: z
.enum([...FRIGATE_CARD_VIEWS_USER_SPECIFIED, 'current'])
.enum([...VIEWS_USER_SPECIFIED, 'current'])
.default(viewConfigDefault.camera_select),
interaction_seconds: z.number().default(viewConfigDefault.interaction_seconds),
default_cycle_camera: z.boolean().default(viewConfigDefault.default_cycle_camera),
@@ -1638,7 +1643,7 @@ const viewConfigSchema = z
// Menu Configuration
// *************************************************************************
const FRIGATE_MENU_STYLES = [
const MENU_STYLES = [
'none',
'hidden',
'overlay',
@@ -1646,16 +1651,16 @@ const FRIGATE_MENU_STYLES = [
'hover-card',
'outside',
] as const;
const FRIGATE_MENU_POSITIONS = ['left', 'right', 'top', 'bottom'] as const;
const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS;
const MENU_POSITIONS = ['left', 'right', 'top', 'bottom'] as const;
const MENU_ALIGNMENTS = MENU_POSITIONS;
const visibleButtonDefault = {
priority: FRIGATE_MENU_PRIORITY_DEFAULT,
priority: MENU_PRIORITY_DEFAULT,
enabled: true,
};
const hiddenButtonDefault = {
priority: FRIGATE_MENU_PRIORITY_DEFAULT,
priority: MENU_PRIORITY_DEFAULT,
enabled: false,
};
@@ -1670,7 +1675,7 @@ const menuConfigDefault = {
display_mode: visibleButtonDefault,
download: visibleButtonDefault,
expand: hiddenButtonDefault,
frigate: visibleButtonDefault,
iris: visibleButtonDefault,
fullscreen: visibleButtonDefault,
image: hiddenButtonDefault,
live: visibleButtonDefault,
@@ -1704,9 +1709,9 @@ const hiddenButtonSchema = menuBaseSchema.extend({
export const menuConfigSchema = z
.object({
style: z.enum(FRIGATE_MENU_STYLES).default(menuConfigDefault.style),
position: z.enum(FRIGATE_MENU_POSITIONS).default(menuConfigDefault.position),
alignment: z.enum(FRIGATE_MENU_ALIGNMENTS).default(menuConfigDefault.alignment),
style: z.enum(MENU_STYLES).default(menuConfigDefault.style),
position: z.enum(MENU_POSITIONS).default(menuConfigDefault.position),
alignment: z.enum(MENU_ALIGNMENTS).default(menuConfigDefault.alignment),
buttons: z
.object({
camera_ui: visibleButtonSchema.default(menuConfigDefault.buttons.camera_ui),
@@ -1718,7 +1723,7 @@ export const menuConfigSchema = z
),
download: visibleButtonSchema.default(menuConfigDefault.buttons.download),
expand: hiddenButtonSchema.default(menuConfigDefault.buttons.expand),
frigate: visibleButtonSchema.default(menuConfigDefault.buttons.frigate),
iris: visibleButtonSchema.default(menuConfigDefault.buttons.iris),
fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen),
image: hiddenButtonSchema.default(menuConfigDefault.buttons.image),
live: visibleButtonSchema.default(menuConfigDefault.buttons.live),
@@ -1763,7 +1768,7 @@ const STATUS_BAR_STYLES = [
const STATUS_BAR_POSITIONS = ['top', 'bottom'] as const;
const statusBarItemDefault = {
priority: FRIGATE_STATUS_BAR_PRIORITY_DEFAULT,
priority: STATUS_BAR_PRIORITY_DEFAULT,
enabled: true,
};
@@ -1970,7 +1975,7 @@ export const dimensionsConfigSchema = z
const overridesSchema = z
.object({
conditions: frigateCardConditionSchema.array(),
conditions: advancedCameraCardConditionSchema.array(),
merge: z.object({}).passthrough().optional(),
set: z.object({}).passthrough().optional(),
delete: z.string().array().optional(),
@@ -1988,7 +1993,7 @@ export type AutomationActions = z.infer<typeof automationActionSchema>;
const automationSchema = z
.object({
conditions: frigateCardConditionSchema.array(),
conditions: advancedCameraCardConditionSchema.array(),
actions: automationActionSchema.optional(),
actions_not: automationActionSchema.optional(),
})
@@ -2075,7 +2080,7 @@ export const profilesSchema = z.enum(PROFILES).array().optional();
// *** Card Configuration ***
// *************************************************************************
export const frigateCardConfigSchema = z.object({
export const advancedCameraCardConfigSchema = z.object({
// Defaults are stripped out of the individual cameras, since each camera will
// be merged with `cameras_global` which *does* have defaults. If we didn't do
// this, the default values of each individual camera would override the
@@ -2114,11 +2119,11 @@ export const frigateCardConfigSchema = z.object({
type: z.string(),
});
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
export type RawFrigateCardConfig = Record<string, unknown>;
export type RawFrigateCardConfigArray = RawFrigateCardConfig[];
export type AdvancedCameraCardConfig = z.infer<typeof advancedCameraCardConfigSchema>;
export type RawAdvancedCameraCardConfig = Record<string, unknown>;
export type RawAdvancedCameraCardConfigArray = RawAdvancedCameraCardConfig[];
export const frigateCardConfigDefaults = {
export const configDefaults = {
cameras: cameraConfigDefault,
view: viewConfigDefault,
menu: menuConfigDefault,