refactor: Break apart monolithic configuration schema (#1977)

[skip ci]
This commit is contained in:
Dermot Duffy
2025-03-22 15:06:11 -07:00
committed by GitHub
parent 92fc8dd844
commit d6225fefda
266 changed files with 2789 additions and 2562 deletions
+25
View File
@@ -0,0 +1,25 @@
import { z } from 'zod';
import { cardIDRegex } from '../common/const';
// https://www.home-assistant.io/dashboards/actions/#options-for-confirmation
export const actionBaseSchema = z.object({
confirmation: z
.boolean()
.or(
z.object({
text: z.string().optional(),
exemptions: z
.object({
user: z.string(),
})
.array()
.optional(),
}),
)
.optional(),
card_id: z
.string()
.regex(cardIDRegex, 'card_id parameter can only contain [a-z][A-Z][0-9_]-')
.optional(),
});
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
export const advancedCameraCardCustomActionsBaseSchema = actionBaseSchema.extend({
action: z
.literal('fire-dom-event')
.or(
z
.literal('custom:advanced-camera-card-action')
.transform((): 'fire-dom-event' => 'fire-dom-event'),
),
});
@@ -0,0 +1,10 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export 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>;
@@ -0,0 +1,10 @@
import { z } from 'zod';
import { viewDisplayModeSchema } from '../../common/display';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const viewDisplayModeActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('display_mode_select'),
display_mode: viewDisplayModeSchema,
});
export type DisplayModeActionConfig = z.infer<typeof viewDisplayModeActionConfigSchema>;
@@ -0,0 +1,29 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
const GENERAL_ACTIONS = [
'camera_ui',
'default',
'download',
'expand',
'fullscreen',
'live_substream_off',
'live_substream_on',
'menu_toggle',
'microphone_connect',
'microphone_disconnect',
'microphone_mute',
'microphone_unmute',
'mute',
'pause',
'play',
'screenshot',
'unmute',
] as const;
export type AdvancedCameraCardGeneralAction = (typeof GENERAL_ACTIONS)[number];
export const generalActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.enum(GENERAL_ACTIONS),
});
export type GeneralActionConfig = z.infer<typeof generalActionConfigSchema>;
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
// An action that can be used internally to call a callback (it is not possible
// for the user to pass this through via the configuration).
export const INTERNAL_CALLBACK_ACTION = '__INTERNAL_CALLBACK_ACTION__';
export const internalCallbackActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal(INTERNAL_CALLBACK_ACTION),
// The callback is expected to be called with a CardController API object.
callback: z.function().args(z.any()).returns(z.promise(z.void())),
});
export type InternalCallbackActionConfig = z.infer<
typeof internalCallbackActionConfigSchema
>;
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
const LOG_ACTIONS_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
export type LogActionLevel = (typeof LOG_ACTIONS_LEVELS)[number];
export 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>;
@@ -0,0 +1,10 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export 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>;
@@ -0,0 +1,9 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const ptzControlsActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('ptz_controls'),
enabled: z.boolean(),
});
export type PTZControlsActionConfig = z.infer<typeof ptzControlsActionConfigSchema>;
@@ -0,0 +1,20 @@
import { z } from 'zod';
import { panSchema } from '../../common/pan';
import { zoomSchema } from '../../common/zoom';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
import { PTZ_ACTION_PHASES, PTZ_ACTIONS } from './ptz';
export const ptzDigitalActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('ptz_digital'),
target_id: z.string().optional(),
absolute: z
.object({
zoom: zoomSchema.optional(),
pan: panSchema.optional(),
})
.optional(),
ptz_action: z.enum(PTZ_ACTIONS).optional(),
ptz_phase: z.enum(PTZ_ACTION_PHASES).optional(),
});
export type PTZDigitialActionConfig = z.infer<typeof ptzDigitalActionConfigSchema>;
@@ -0,0 +1,13 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
import { PTZ_ACTION_PHASES, PTZ_ACTIONS } from './ptz';
export const ptzMultiActionSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('ptz_multi'),
target_id: z.string().optional(),
ptz_action: z.enum(PTZ_ACTIONS).optional(),
ptz_phase: z.enum(PTZ_ACTION_PHASES).optional(),
ptz_preset: z.string().optional(),
});
export type PTZMultiActionConfig = z.infer<typeof ptzMultiActionSchema>;
+27
View File
@@ -0,0 +1,27 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
const PTZ_PAN_TILT_ACTIONS = ['left', 'right', 'up', 'down'] as const;
const PTZ_ZOOM_ACTIONS = ['zoom_in', 'zoom_out'] as const;
const PTZ_BASE_ACTIONS = [...PTZ_PAN_TILT_ACTIONS, ...PTZ_ZOOM_ACTIONS] as const;
export type PTZBaseAction = (typeof PTZ_BASE_ACTIONS)[number];
// PTZ actions as used by the PTZ control (includes a 'home' button).
const PTZ_CONTROL_ACTIONS = [...PTZ_BASE_ACTIONS, 'home'] as const;
export type PTZControlAction = (typeof PTZ_CONTROL_ACTIONS)[number];
// PTZ actions as used by the camera manager (includes generic presets).
export const PTZ_ACTIONS = [...PTZ_BASE_ACTIONS, 'preset'] as const;
export type PTZAction = (typeof PTZ_ACTIONS)[number];
export const PTZ_ACTION_PHASES = ['start', 'stop'] as const;
export type PTZActionPhase = (typeof PTZ_ACTION_PHASES)[number];
export 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(PTZ_ACTION_PHASES).optional(),
ptz_preset: z.string().optional(),
});
export type PTZActionConfig = z.infer<typeof ptzActionConfigSchema>;
+16
View File
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
const timeDeltaSchema = z.object({
ms: z.number().optional(),
s: z.number().optional(),
m: z.number().optional(),
h: z.number().optional(),
});
export type TimeDelta = z.infer<typeof timeDeltaSchema>;
export const sleepActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('sleep'),
duration: timeDeltaSchema.optional().default({ s: 1 }),
});
export type SleepActionConfig = z.infer<typeof sleepActionConfigSchema>;
@@ -0,0 +1,11 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const substreamSelectActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('live_substream_select'),
camera: z.string(),
});
export type SubstreamSelectActionConfig = z.infer<
typeof substreamSelectActionConfigSchema
>;
+8
View File
@@ -0,0 +1,8 @@
import { z } from 'zod';
import { VIEWS_USER_SPECIFIED } from '../../common/const';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const viewActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.enum(VIEWS_USER_SPECIFIED),
});
export type ViewActionConfig = z.infer<typeof viewActionConfigSchema>;
@@ -0,0 +1,15 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
import { targetSchema } from './target';
// Note: call-service is deprecated and will eventually go away. Please use
// perform-action instead.
// See: https://www.home-assistant.io/blog/2024/08/07/release-20248/#goodbye-service-calls-hello-actions-
export const callServiceActionSchema = actionBaseSchema.extend({
action: z.literal('call-service'),
service: z.string(),
data: z.object({}).passthrough().optional(),
target: targetSchema.optional(),
});
export type CallServiceActionConfig = z.infer<typeof callServiceActionSchema>;
@@ -0,0 +1,9 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
export const customActionSchema = actionBaseSchema
.extend({
action: z.literal('fire-dom-event'),
})
.passthrough();
export type CustomActionConfig = z.infer<typeof customActionSchema>;
@@ -0,0 +1,8 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
export const moreInfoActionSchema = actionBaseSchema.extend({
action: z.literal('more-info'),
entity: z.string().optional(),
});
export type MoreInfoActionConfig = z.infer<typeof moreInfoActionSchema>;
@@ -0,0 +1,9 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
export const navigateActionSchema = actionBaseSchema.extend({
action: z.literal('navigate'),
navigation_path: z.string(),
navigation_replace: z.boolean().optional(),
});
export type NavigateActionConfig = z.infer<typeof navigateActionSchema>;
+7
View File
@@ -0,0 +1,7 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
export const noneActionSchema = actionBaseSchema.extend({
action: z.literal('none'),
});
export type NoneActionConfig = z.infer<typeof noneActionSchema>;
@@ -0,0 +1,11 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
import { targetSchema } from './target';
export const performActionActionSchema = actionBaseSchema.extend({
action: z.literal('perform-action'),
perform_action: z.string(),
data: z.object({}).passthrough().optional(),
target: targetSchema.optional(),
});
export type PerformActionActionConfig = z.infer<typeof performActionActionSchema>;
+10
View File
@@ -0,0 +1,10 @@
import { HassServiceTarget } from 'home-assistant-js-websocket';
import { z } from 'zod';
export const targetSchema: z.ZodSchema<HassServiceTarget, z.ZodTypeDef> = z.object({
entity_id: z.string().or(z.string().array()).optional(),
device_id: z.string().or(z.string().array()).optional(),
area_id: z.string().or(z.string().array()).optional(),
floor_id: z.string().or(z.string().array()).optional(),
label_id: z.string().or(z.string().array()).optional(),
});
@@ -0,0 +1,7 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
export const toggleActionSchema = actionBaseSchema.extend({
action: z.literal('toggle'),
});
export type ToggleActionConfig = z.infer<typeof toggleActionSchema>;
+20
View File
@@ -0,0 +1,20 @@
import { z } from 'zod';
import { callServiceActionSchema } from './call-service';
import { customActionSchema } from './custom';
import { moreInfoActionSchema } from './more-info';
import { navigateActionSchema } from './navigate';
import { noneActionSchema } from './none';
import { performActionActionSchema } from './perform-action';
import { toggleActionSchema } from './toggle';
import { urlActionSchema } from './url';
export const stockActionSchema = z.union([
callServiceActionSchema,
customActionSchema,
moreInfoActionSchema,
navigateActionSchema,
noneActionSchema,
performActionActionSchema,
toggleActionSchema,
urlActionSchema,
]);
+8
View File
@@ -0,0 +1,8 @@
import { z } from 'zod';
import { actionBaseSchema } from '../base';
export const urlActionSchema = actionBaseSchema.extend({
action: z.literal('url'),
url_path: z.string(),
});
export type URLActionConfig = z.infer<typeof urlActionSchema>;
+133
View File
@@ -0,0 +1,133 @@
import { z } from 'zod';
import { statusBarItemBaseSchema } from '../common/status-bar';
import { advancedCameraCardCustomActionsBaseSchema } from './custom/base';
import { cameraSelectActionConfigSchema } from './custom/camera-select';
import { viewDisplayModeActionConfigSchema } from './custom/display-mode';
import { generalActionConfigSchema } from './custom/general';
import { internalCallbackActionConfigSchema } from './custom/internal';
import { logActionConfigSchema } from './custom/log';
import { mediaPlayerActionConfigSchema } from './custom/media-player';
import { ptzActionConfigSchema } from './custom/ptz';
import { ptzControlsActionConfigSchema } from './custom/ptz-controls';
import { ptzDigitalActionConfigSchema } from './custom/ptz-digital';
import { ptzMultiActionSchema } from './custom/ptz-multi';
import { sleepActionConfigSchema } from './custom/sleep';
import { substreamSelectActionConfigSchema } from './custom/substream-select';
import { viewActionConfigSchema } from './custom/view';
import { stockActionSchema } from './stock/types';
// Provide a manual type definition to avoid the `any` that would be created by
// the lazy() evaluation below.
// See: https://zod.dev/?id=recursive-types
export type StatusBarActionConfig = z.infer<
typeof advancedCameraCardCustomActionsBaseSchema
> & {
advanced_camera_card_action: 'status_bar';
status_bar_action: 'add' | 'remove' | 'reset';
items?: StatusBarItem[];
};
export const statusBarActionConfigSchema: z.ZodSchema<
StatusBarActionConfig,
z.ZodTypeDef,
unknown
> = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('status_bar'),
status_bar_action: z.enum(['add', 'remove', 'reset']),
items: z
.lazy(() => statusBarItemSchema)
.array()
.optional(),
});
const advancedCameraCardCustomActionSchema = z.union([
cameraSelectActionConfigSchema,
generalActionConfigSchema,
internalCallbackActionConfigSchema,
logActionConfigSchema,
mediaPlayerActionConfigSchema,
ptzActionConfigSchema,
ptzControlsActionConfigSchema,
ptzDigitalActionConfigSchema,
ptzMultiActionSchema,
sleepActionConfigSchema,
statusBarActionConfigSchema,
substreamSelectActionConfigSchema,
viewActionConfigSchema,
viewDisplayModeActionConfigSchema,
]);
export type AdvancedCameraCardCustomActionConfig = z.infer<
typeof advancedCameraCardCustomActionSchema
>;
export const actionConfigSchema = z.union([
stockActionSchema,
advancedCameraCardCustomActionSchema,
]);
export type ActionConfig = z.infer<typeof actionConfigSchema>;
export const actionsBaseSchema = z
.object({
tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
hold_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
double_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
start_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
end_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
})
// Passthrough to allow (at least) entity/camera_image to go through. This
// card doesn't need these attributes, but handleAction() in
// custom_card_helpers may depending on how the action is configured.
.passthrough();
export type Actions = z.infer<typeof actionsBaseSchema>;
export interface AuxillaryActionConfig {
entity?: string;
}
export type ActionsConfig = Actions & AuxillaryActionConfig;
export const actionsSchema = z.object({
actions: actionsBaseSchema.optional(),
});
// ============================================================================
// Status Bar Elements
//
// Note: Status Bar action & elements are included in this file, since this is
// the only action that may include content that refers to *other* actions (e.g.
// a status bar action, compromises of status bar items, which themselves may
// have actions). This circular relationship means the components must be
// together in a file, or they will generate typescript circular dependency
// errors.
// ============================================================================
const statusBarItemElementsBaseSchema = statusBarItemBaseSchema.extend({
sufficient: z.boolean().default(false).optional(),
exclusive: z.boolean().default(false).optional(),
expand: z.boolean().default(false).optional(),
actions: actionsBaseSchema.optional(),
});
export const statusBarIconItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:advanced-camera-card-status-bar-icon'),
icon: z.string(),
});
export type StatusBarIcon = z.infer<typeof statusBarIconItemSchema>;
export const statusBarImageItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:advanced-camera-card-status-bar-image'),
image: z.string(),
});
export type StatusBarImage = z.infer<typeof statusBarImageItemSchema>;
export const statusBarStringItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:advanced-camera-card-status-bar-string'),
string: z.string(),
});
export type StatusBarString = z.infer<typeof statusBarStringItemSchema>;
const statusBarItemSchema = z.union([
statusBarIconItemSchema,
statusBarImageItemSchema,
statusBarStringItemSchema,
]);
export type StatusBarItem = z.infer<typeof statusBarItemSchema>;