From bde4e6cd389ba9aa8ed0241f4b1dce2e4c1eea7d Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 28 Mar 2024 21:41:48 -0700 Subject: [PATCH] Add support for configuration profiles. --- src/card-controller/conditions-manager.ts | 2 +- src/card-controller/config-manager.ts | 9 +-- src/card-controller/style-manager.ts | 21 +++-- src/components/live/live.ts | 6 +- src/{config-mgmt.ts => config/management.ts} | 11 ++- src/config/profiles/index.ts | 51 +++++++++++++ .../profiles/low-performance.ts} | 76 +------------------ src/config/profiles/scrubbing.ts | 18 +++++ src/config/types.ts | 11 ++- src/const.ts | 4 + src/editor.ts | 59 +++++++------- src/localize/languages/ca.json | 12 +-- src/localize/languages/en.json | 12 +-- src/localize/languages/fr.json | 12 +-- src/localize/languages/it.json | 12 +-- src/localize/languages/pt-BR.json | 12 +-- src/localize/languages/pt-PT.json | 12 +-- tests/card-controller/config-manager.test.ts | 33 ++++---- tests/card-controller/style-manager.test.ts | 53 +++++++++---- .../management.test.ts} | 41 +++++++++- tests/config/profiles/index.test.ts | 58 ++++++++++++++ tests/config/profiles/low-performance.test.ts | 54 +++++++++++++ tests/config/profiles/scrubbing.test.ts | 13 ++++ tests/config/types.test.ts | 1 - vite.config.ts | 3 +- 25 files changed, 412 insertions(+), 184 deletions(-) rename src/{config-mgmt.ts => config/management.ts} (98%) create mode 100644 src/config/profiles/index.ts rename src/{performance.ts => config/profiles/low-performance.ts} (71%) create mode 100644 src/config/profiles/scrubbing.ts rename tests/{config-mgmt.test.ts => config/management.test.ts} (98%) create mode 100644 tests/config/profiles/index.test.ts create mode 100644 tests/config/profiles/low-performance.test.ts create mode 100644 tests/config/profiles/scrubbing.test.ts diff --git a/src/card-controller/conditions-manager.ts b/src/card-controller/conditions-manager.ts index d38bf35b..122a44b7 100644 --- a/src/card-controller/conditions-manager.ts +++ b/src/card-controller/conditions-manager.ts @@ -1,7 +1,7 @@ import { CurrentUser } from '@dermotduffy/custom-card-helpers'; import { HassEntities } from 'home-assistant-js-websocket'; import merge from 'lodash-es/merge'; -import { copyConfig } from '../config-mgmt'; +import { copyConfig } from '../config/management'; import { FrigateCardCondition, frigateConditionalSchema, diff --git a/src/card-controller/config-manager.ts b/src/card-controller/config-manager.ts index e597ad3a..a062944d 100644 --- a/src/card-controller/config-manager.ts +++ b/src/card-controller/config-manager.ts @@ -1,5 +1,5 @@ import isEqual from 'lodash-es/isEqual'; -import { isConfigUpgradeable } from '../config-mgmt'; +import { isConfigUpgradeable } from '../config/management'; import { CardWideConfig, FrigateCardConfig, @@ -7,7 +7,7 @@ import { RawFrigateCardConfig } from '../config/types'; import { localize } from '../localize/localize'; -import { setLowPerformanceProfile } from '../performance.js'; +import { setProfiles } from '../config/profiles'; import { getParseErrorPaths } from '../utils/zod.js'; import { getOverriddenConfig } from './conditions-manager'; import { InitializationAspect } from './initialization-manager'; @@ -70,10 +70,7 @@ export class ConfigManager { : localize('error.invalid_configuration_no_hint')), ); } - const config = - parseResult.data.performance.profile !== 'low' - ? parseResult.data - : setLowPerformanceProfile(inputConfig, parseResult.data); + const config = setProfiles(inputConfig, parseResult.data, parseResult.data.profiles); this._rawConfig = inputConfig; if (isEqual(this._config, config)) { diff --git a/src/card-controller/style-manager.ts b/src/card-controller/style-manager.ts index 1a5763de..02a95115 100644 --- a/src/card-controller/style-manager.ts +++ b/src/card-controller/style-manager.ts @@ -1,6 +1,5 @@ import { StyleInfo } from 'lit/directives/style-map'; import { FrigateCardConfig } from '../config/types'; -import { setPerformanceCSSStyles } from '../performance'; import { aspectRatioToStyle, setOrRemoveAttribute } from '../utils/basic'; import { View } from '../view/view'; import { CardStyleAPI } from './types'; @@ -69,10 +68,22 @@ export class StyleManager { } public setPerformance(): void { - setPerformanceCSSStyles( - this._api.getCardElementManager().getElement(), - this._api.getConfigManager().getCardWideConfig()?.performance, - ); + const STYLE_DISABLE_MAP = { + box_shadow: 'none', + border_radius: '0px', + }; + const element = this._api.getCardElementManager().getElement(); + const performance = this._api.getConfigManager().getCardWideConfig()?.performance; + + const styles = performance?.style ?? {}; + for (const configKey of Object.keys(styles)) { + const CSSKey = `--frigate-card-css-${configKey.replaceAll('_', '-')}`; + if (styles[configKey] === false) { + element.style.setProperty(CSSKey, STYLE_DISABLE_MAP[configKey]); + } else { + element.style.removeProperty(CSSKey); + } + } } protected _isAspectRatioEnforced( diff --git a/src/components/live/live.ts b/src/components/live/live.ts index a5f79ad0..4eacabfc 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -855,11 +855,7 @@ export class FrigateCardLiveProvider ) { return 'webrtc-card'; } else if (this.cameraConfig?.camera_entity) { - if (this.cardWideConfig?.performance?.profile === 'low') { - return 'image'; - } else { - return 'ha'; - } + return 'ha'; } else if (this.cameraConfig?.frigate.camera_name) { return 'jsmpeg'; } diff --git a/src/config-mgmt.ts b/src/config/management.ts similarity index 98% rename from src/config-mgmt.ts rename to src/config/management.ts index 28f379ab..146531f2 100644 --- a/src/config-mgmt.ts +++ b/src/config/management.ts @@ -7,7 +7,7 @@ import { FrigateCardCondition, RawFrigateCardConfig, RawFrigateCardConfigArray, -} from './config/types'; +} from './types'; import { CONF_AUTOMATIONS, CONF_CAMERAS, @@ -22,14 +22,15 @@ import { CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE, CONF_MENU_BUTTONS_CAMERA_UI, CONF_OVERRIDES, + CONF_PROFILES, CONF_TIMELINE_EVENTS_MEDIA_TYPE, CONF_VIEW_INTERACTION_SECONDS, CONF_VIEW_TRIGGERS, CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER, CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER, CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA, -} from './const'; -import { arrayify } from './utils/basic'; +} from '../const'; +import { arrayify } from '../utils/basic'; // ************************************************************************* // General Config Management Functions @@ -684,4 +685,8 @@ const UPGRADES = [ transform: (val) => (val === true ? { disable_except: 'substream' } : null), }), ), + upgradeMoveToWithOverrides('performance.profile', CONF_PROFILES, { + // Delete the value if it's set to the default. + transform: (val) => (val === 'low' ? ['low-performance'] : null), + }), ]; diff --git a/src/config/profiles/index.ts b/src/config/profiles/index.ts new file mode 100644 index 00000000..61460daf --- /dev/null +++ b/src/config/profiles/index.ts @@ -0,0 +1,51 @@ +import { getConfigValue, setConfigValue } from '../management.js'; +import { + ProfileType, + RawFrigateCardConfig, + frigateCardConfigSchema, +} from '../types.js'; +import { deepRemoveDefaults } from '../../utils/zod.js'; +import { LOW_PERFORMANCE_PROFILE } from './low-performance.js'; +import { SCRUBBING_PROFILE } from './scrubbing.js'; + +const PROFILES = { + 'low-performance': LOW_PERFORMANCE_PROFILE, + 'scrubbing': SCRUBBING_PROFILE, +}; + +/** + * Set a profile. Sets flags as defined in the relevant profile unless they are + * explicitly overriden in the configuration. + * @param inputConfig The raw unparsed input configuration. + * @param outputConfig The output config to write to. + * @returns A changed (in-place) parsed input configuration. + */ +export const setProfiles = ( + inputConfig: RawFrigateCardConfig, + outputConfig: T, + profiles?: ProfileType[], +): T => { + const defaultLessParseResult = deepRemoveDefaults(frigateCardConfigSchema).safeParse( + inputConfig, + ); + if (!defaultLessParseResult.success) { + return outputConfig; + } + const defaultLessConfig = defaultLessParseResult.data; + + const setIfNotSpecified = (key: string, value: unknown) => { + if (getConfigValue(defaultLessConfig, key) === undefined) { + setConfigValue(outputConfig, key, value); + } + }; + + for (const profile of profiles ?? []) { + if (profile in PROFILES) { + Object.entries(PROFILES[profile]).forEach(([k, v]: [string, unknown]) => + setIfNotSpecified(k, v), + ); + } + } + + return outputConfig; +}; diff --git a/src/performance.ts b/src/config/profiles/low-performance.ts similarity index 71% rename from src/performance.ts rename to src/config/profiles/low-performance.ts index 487de6fc..570c8771 100644 --- a/src/performance.ts +++ b/src/config/profiles/low-performance.ts @@ -1,11 +1,6 @@ -import { getConfigValue, setConfigValue } from './config-mgmt.js'; -import { - PerformanceConfig, - RawFrigateCardConfig, - frigateCardConfigSchema, -} from './config/types.js'; import { CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS, + CONF_CAMERAS_GLOBAL_LIVE_PROVIDER, CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY, CONF_LIVE_AUTO_MUTE, CONF_LIVE_CONTROLS_THUMBNAILS_MODE, @@ -51,13 +46,9 @@ import { CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_TIMELINE_SHOW_RECORDINGS, -} from './const.js'; -import { deepRemoveDefaults } from './utils/zod.js'; +} from '../../const.js'; -// Caution: These values are applied after parsing (since we cannot know the -// performance profile until afterwards), so there is no validation on these -// defaults. -const LOW_PROFILE_DEFAULTS = { +export const LOW_PERFORMANCE_PROFILE = { // Disable thumbnail carousels. [CONF_LIVE_CONTROLS_THUMBNAILS_MODE]: 'none' as const, [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE]: 'none' as const, @@ -138,68 +129,9 @@ const LOW_PROFILE_DEFAULTS = { [CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP]: false, [CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY]: false, + [CONF_CAMERAS_GLOBAL_LIVE_PROVIDER]: 'image', // Refresh the live camera image every 10 seconds (same as stock Home // Assistant Picture Glance). [CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS]: 10, }; - -/** - * Set low performance profile mode. Sets flags as defined in - * LOW_PROFILE_DEFAULTS unless they are explicitly overriden in the - * configuration. - * @param inputConfig The raw unparsed input configuration. - * @param outputConfig The output config to write to. - * @returns A changed (in-place) parsed input configuration. - */ -export const setLowPerformanceProfile = ( - inputConfig: RawFrigateCardConfig, - outputConfig: T, -): T => { - const setIfNotSpecified = ( - defaultLessConfig: RawFrigateCardConfig, - outputConfig: T, - key: string, - value: unknown, - ) => { - if (getConfigValue(defaultLessConfig, key) === undefined) { - setConfigValue(outputConfig, key, value); - } - }; - - const defaultLessParseResult = deepRemoveDefaults(frigateCardConfigSchema).safeParse( - inputConfig, - ); - if (defaultLessParseResult.success) { - const defaultLessConfig = defaultLessParseResult.data; - Object.entries(LOW_PROFILE_DEFAULTS).forEach(([k, v]: [string, unknown]) => - setIfNotSpecified(defaultLessConfig, outputConfig, k, v), - ); - } - return outputConfig; -}; - -const STYLE_DISABLE_MAP = { - box_shadow: 'none', - border_radius: '0px', -}; - -/** - * Set card-wide CSS variables for performance. - * @param element The element to set the variables on. - * @param performance The performance configuration. - */ -export const setPerformanceCSSStyles = ( - element: HTMLElement, - performance?: PerformanceConfig, -): void => { - const styles = performance?.style ?? {}; - for (const configKey of Object.keys(styles)) { - const CSSKey = `--frigate-card-css-${configKey.replaceAll('_', '-')}`; - if (styles[configKey] === false) { - element.style.setProperty(CSSKey, STYLE_DISABLE_MAP[configKey]); - } else { - element.style.removeProperty(CSSKey); - } - } -}; diff --git a/src/config/profiles/scrubbing.ts b/src/config/profiles/scrubbing.ts new file mode 100644 index 00000000..b66fe0f7 --- /dev/null +++ b/src/config/profiles/scrubbing.ts @@ -0,0 +1,18 @@ +import { + CONF_LIVE_CONTROLS_TIMELINE_MODE, + CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE, + CONF_LIVE_CONTROLS_TIMELINE_STYLE, + CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE, + CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE, + CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE, +} from '../../const.js'; + +export const SCRUBBING_PROFILE = { + [CONF_LIVE_CONTROLS_TIMELINE_MODE]: 'below' as const, + [CONF_LIVE_CONTROLS_TIMELINE_STYLE]: 'ribbon' as const, + [CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE]: 'seek' as const, + + [CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE]: 'below' as const, + [CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE]: 'ribbon' as const, + [CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE]: 'seek' as const, +}; diff --git a/src/config/types.ts b/src/config/types.ts index 67508e46..d3ca5191 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1623,7 +1623,6 @@ export type Automations = z.infer; // ************************************************************************* const performanceConfigDefault = { - profile: 'high' as const, features: { animated_progress_indicator: true, media_chunk_size: MEDIA_CHUNK_SIZE_DEFAULT, @@ -1636,7 +1635,6 @@ const performanceConfigDefault = { export const performanceConfigSchema = z .object({ - profile: z.enum(['low', 'high']).default(performanceConfigDefault.profile), features: z .object({ animated_progress_indicator: z @@ -1679,6 +1677,13 @@ export interface CardWideConfig { debug?: DebugConfig; } +// ************************************************************************* +// *** Profile Configuration *** +// ************************************************************************* +const PROFILES = ['low-performance', 'scrubbing'] as const; +export type ProfileType = (typeof PROFILES)[number]; +export const profilesSchema = z.enum(PROFILES).array().optional(); + // ************************************************************************* // *** Card Configuration *** // ************************************************************************* @@ -1705,6 +1710,8 @@ export const frigateCardConfigSchema = z.object({ debug: debugConfigSchema, automations: automationsSchema, + profiles: profilesSchema, + // Configuration overrides. overrides: overridesSchema, diff --git a/src/const.ts b/src/const.ts index 7471da29..6a497438 100644 --- a/src/const.ts +++ b/src/const.ts @@ -72,6 +72,8 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS = const CONF_CAMERAS_GLOBAL = 'cameras_global' as const; export const CONF_CAMERAS_GLOBAL_IMAGE = `${CONF_CAMERAS_GLOBAL}.image` as const; +export const CONF_CAMERAS_GLOBAL_LIVE_PROVIDER = + `${CONF_CAMERAS_GLOBAL}.live_provider` as const; export const CONF_CAMERAS_GLOBAL_JSMPEG = `${CONF_CAMERAS_GLOBAL}.jsmpeg` as const; export const CONF_CAMERAS_GLOBAL_WEBRTC_CARD = `${CONF_CAMERAS_GLOBAL}.webrtc_card` as const; @@ -313,6 +315,8 @@ export const CONF_PERFORMANCE_PROFILE = `${CONF_PERFORMANCE}.profile`; export const CONF_PERFORMANCE_STYLE_BOX_SHADOW = `${CONF_PERFORMANCE}.style.box_shadow`; export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`; +export const CONF_PROFILES = 'profiles' as const; + // Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93 export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072; diff --git a/src/editor.ts b/src/editor.ts index b9865192..9502d69f 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -19,21 +19,24 @@ import { isConfigUpgradeable, setConfigValue, upgradeConfig, -} from './config-mgmt.js'; +} from './config/management.js'; +import { setProfiles } from './config/profiles/index.js'; import { BUTTON_SIZE_MIN, FrigateCardConfig, frigateCardConfigDefaults, FRIGATE_MENU_PRIORITY_MAX, + profilesSchema, RawFrigateCardConfig, RawFrigateCardConfigArray, THUMBNAIL_WIDTH_MAX, THUMBNAIL_WIDTH_MIN, } from './config/types.js'; import { + CONF_CAMERAS, CONF_CAMERAS_ARRAY_CAMERA_ENTITY, - CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT, CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE, + CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT, CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH, CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH, CONF_CAMERAS_ARRAY_CAST_METHOD, @@ -67,9 +70,8 @@ import { CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY, CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, - CONF_CAMERAS, - CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_DIMENSIONS_ASPECT_RATIO, + CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_DIMENSIONS_MAX_HEIGHT, CONF_DIMENSIONS_MIN_HEIGHT, CONF_IMAGE_MODE, @@ -158,8 +160,8 @@ import { CONF_MEDIA_VIEWER_TRANSITION_EFFECT, CONF_MEDIA_VIEWER_ZOOMABLE, CONF_MENU_ALIGNMENT, - CONF_MENU_BUTTON_SIZE, CONF_MENU_BUTTONS, + CONF_MENU_BUTTON_SIZE, CONF_MENU_POSITION, CONF_MENU_STYLE, CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR, @@ -167,6 +169,7 @@ import { CONF_PERFORMANCE_PROFILE, CONF_PERFORMANCE_STYLE_BORDER_RADIUS, CONF_PERFORMANCE_STYLE_BOX_SHADOW, + CONF_PROFILES, CONF_TIMELINE_CLUSTERING_THRESHOLD, CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS, @@ -183,21 +186,20 @@ import { CONF_VIEW_DEFAULT, CONF_VIEW_INTERACTION_SECONDS, CONF_VIEW_RESET_AFTER_INTERACTION, + CONF_VIEW_TRIGGERS, + CONF_VIEW_TRIGGERS_ACTIONS, CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE, CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER, CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER, - CONF_VIEW_TRIGGERS_ACTIONS, CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA, CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS, CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS, - CONF_VIEW_TRIGGERS, CONF_VIEW_UPDATE_CYCLE_CAMERA, CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, MEDIA_CHUNK_SIZE_MAX, } from './const.js'; import { localize } from './localize/localize.js'; -import { setLowPerformanceProfile } from './performance.js'; import frigate_card_editor_style from './scss/editor.scss'; import { arrayMove, prettifyTitle } from './utils/basic.js'; import { getCameraID } from './utils/camera.js'; @@ -315,6 +317,11 @@ const options: EditorOptions = { name: localize('editor.performance'), secondary: localize('editor.performance_secondary'), }, + profiles: { + icon: 'folder-wrench-outline', + name: localize('editor.profiles'), + secondary: localize('editor.profiles_secondary'), + }, overrides: { icon: 'file-replace', name: localize('editor.overrides'), @@ -589,10 +596,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { value: 'below', label: localize('config.common.controls.timeline.modes.below') }, ]; - protected _performanceProfiles: EditorSelectOption[] = [ + protected _profiles: EditorSelectOption[] = [ { value: '', label: '' }, - { value: 'low', label: localize('config.performance.profiles.low') }, - { value: 'high', label: localize('config.performance.profiles.high') }, + { value: 'low-performance', label: localize('config.profiles.low-performance') }, + { value: 'scrubbing', label: localize('config.profiles.scrubbing') }, ]; protected _go2rtcModes: EditorSelectOption[] = [ @@ -788,25 +795,20 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ]; public setConfig(config: RawFrigateCardConfig): void { - // Note: This does not use Zod to parse the configuration, so it may be + // Note: This does not use Zod to parse the full configuration, so it may be // partially or completely invalid. It's more useful to have a partially // valid configuration here, to allow the user to fix the broken parts. As // such, RawFrigateCardConfig is used as the type. this._config = config; this._configUpgradeable = isConfigUpgradeable(config); - let unvalidatedProfile: string | null = null; - try { - // this._config may not be a valid FrigateCardConfig as it has not been - // parsed. Attempt to pull out the performance profile. - unvalidatedProfile = (this._config as FrigateCardConfig).performance?.profile; - } catch (_) {} + const profiles = profilesSchema.safeParse( + (this._config as FrigateCardConfig).profiles, + ); - if (unvalidatedProfile === 'high' || unvalidatedProfile === 'low') { + if (profiles.success) { const defaults = copyConfig(frigateCardConfigDefaults); - if (unvalidatedProfile === 'low') { - setLowPerformanceProfile(this._config, defaults); - } + setProfiles(this._config, defaults, profiles.data); this._defaults = defaults; } } @@ -2130,6 +2132,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ` : ''} + ${this._renderOptionSetHeader('profiles')} + ${this._expandedMenus[MENU_OPTIONS] === 'profiles' + ? html`
+ ${this._renderOptionSelector(CONF_PROFILES, this._profiles, { + multiple: true, + label: localize('config.profiles.editor_label'), + })} +
` + : ''} ${this._renderOptionSetHeader('view')} ${this._expandedMenus[MENU_OPTIONS] === 'view' ? html` @@ -2556,10 +2567,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${getConfigValue(this._config, CONF_PERFORMANCE_PROFILE) === 'low' ? this._renderInfo(localize('config.performance.warning')) : html``} - ${this._renderOptionSelector( - CONF_PERFORMANCE_PROFILE, - this._performanceProfiles, - )} ${this._putInSubmenu( MENU_PERFORMANCE_FEATURES, true, diff --git a/src/localize/languages/ca.json b/src/localize/languages/ca.json index 20f72390..a822bf33 100644 --- a/src/localize/languages/ca.json +++ b/src/localize/languages/ca.json @@ -401,11 +401,6 @@ "editor_label": "Opcions de característiques", "media_chunk_size": "Mida del fragment multimèdia" }, - "profile": "Perfil de rendiment", - "profiles": { - "high": "Alt rendiment", - "low": "Baix rendiment" - }, "style": { "border_radius": "Corbes", "box_shadow": "Ombres", @@ -413,6 +408,11 @@ }, "warning": "Aquesta targeta està en mode de perfil baix, de manera que els valors predeterminats han canviat per optimitzar el rendiment" }, + "profiles": { + "editor_label": "", + "low-performance": "", + "scrubbing": "" + }, "view": { "camera_select": "Visualitza les càmeres seleccionades recentment", "dark_mode": "Mode fosc", @@ -497,6 +497,8 @@ "overrides_secondary": "S'han detectat substitucions de configuració dinàmica", "performance": "Rendiment", "performance_secondary": "Opcions de rendiment de la targeta", + "profiles": "", + "profiles_secondary": "", "timeline": "Cronologia", "timeline_secondary": "Opcions de la cronologia d'esdeveniments", "upgrade": "Upgrade", diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index d3bb0d23..f0c5df92 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -401,11 +401,6 @@ "editor_label": "Feature Options", "media_chunk_size": "Media chunk size" }, - "profile": "Performance profile", - "profiles": { - "high": "High/full performance", - "low": "Low performance" - }, "style": { "border_radius": "Curves", "box_shadow": "Shadows", @@ -413,6 +408,11 @@ }, "warning": "This card is in low profile mode so defaults have changed to optimize performance" }, + "profiles": { + "editor_label": "Configuration profiles", + "low-performance": "Low performance", + "scrubbing": "Video scrubbing" + }, "view": { "camera_select": "View for newly selected cameras", "dark_mode": "Dark mode", @@ -497,6 +497,8 @@ "overrides_secondary": "Dynamic configuration overrides detected", "performance": "Performance", "performance_secondary": "Card performance options", + "profiles": "Configuration profiles", + "profiles_secondary": "Choose pre-configured sets of defaults", "timeline": "Timeline", "timeline_secondary": "Event timeline options", "upgrade": "Upgrade", diff --git a/src/localize/languages/fr.json b/src/localize/languages/fr.json index 041ed7eb..fdc1107b 100644 --- a/src/localize/languages/fr.json +++ b/src/localize/languages/fr.json @@ -401,11 +401,6 @@ "editor_label": "Options de fonctionnalités", "media_chunk_size": "Taille du morceau de média" }, - "profile": "Profil de performances", - "profiles": { - "high": "Performances élevées", - "low": "Performances faibles" - }, "style": { "border_radius": "Courbes", "box_shadow": "Ombres", @@ -413,6 +408,11 @@ }, "warning": "Cette carte est en mode profil bas, les paramètres par défaut ont donc été modifiés pour optimiser les performances." }, + "profiles": { + "editor_label": "", + "low-performance": "", + "scrubbing": "" + }, "view": { "camera_select": "Afficher les caméras nouvellement sélectionnées", "dark_mode": "Mode sombre", @@ -497,6 +497,8 @@ "overrides_secondary": "Remplacements de configuration dynamique détectés", "performance": "Performance", "performance_secondary": "Options de performances de la carte", + "profiles": "", + "profiles_secondary": "", "timeline": "Chronologie", "timeline_secondary": "Options de chronologie des événements", "upgrade": "Mise à niveau", diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index 7acdea38..e515dc57 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -401,11 +401,6 @@ "editor_label": "Opzioni funzionalità", "media_chunk_size": "Dimensione del blocco multimediale" }, - "profile": "Profilo delle prestazioni", - "profiles": { - "high": "Prestazioni alte", - "low": "Prestazioni basse" - }, "style": { "border_radius": "Curve", "box_shadow": "Ombre", @@ -413,6 +408,11 @@ }, "warning": "Questa scheda è in modalità basso profilo, quindi le impostazioni predefinite sono state modificate per ottimizzare le prestazioni" }, + "profiles": { + "editor_label": "", + "low-performance": "", + "scrubbing": "" + }, "view": { "camera_select": "Visualizza per le telecamere appena selezionate", "dark_mode": "Tema scuro", @@ -497,6 +497,8 @@ "overrides_secondary": "Rilevate sovrascritture della configurazione dinamica", "performance": "", "performance_secondary": "", + "profiles": "", + "profiles_secondary": "", "timeline": "Timeline", "timeline_secondary": "Opzioni della timeline degli eventi", "upgrade": "Aggiornamento", diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index 8e566333..ad8fe495 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -401,11 +401,6 @@ "editor_label": "Opções de recursos", "media_chunk_size": "Tamanho do bloco de mídia" }, - "profile": "Perfil de desempenho", - "profiles": { - "high": "Alto desempenho/completo", - "low": "Baixo desempenho" - }, "style": { "border_radius": "Curvas", "box_shadow": "Sombras", @@ -413,6 +408,11 @@ }, "warning": "Este cartão está no modo de baixo desempenho, então os padrões foram alterados para otimizar o desempenho" }, + "profiles": { + "editor_label": "", + "low-performance": "", + "scrubbing": "" + }, "view": { "camera_select": "Visualização de câmeras recém-selecionadas", "dark_mode": "Modo escuro", @@ -497,6 +497,8 @@ "overrides_secondary": "Substituições de configuração dinâmica detectadas", "performance": "Desempenho", "performance_secondary": "Opções de desempenho do cartão", + "profiles": "", + "profiles_secondary": "", "timeline": "Linha do tempo", "timeline_secondary": "Opções do evento da linha do tempo", "upgrade": "Upgrade", diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json index e153b660..fb8f531c 100644 --- a/src/localize/languages/pt-PT.json +++ b/src/localize/languages/pt-PT.json @@ -401,11 +401,6 @@ "editor_label": "Editor de etiquetas", "media_chunk_size": "Tamanho do ficheiro" }, - "profile": "Perfil", - "profiles": { - "high": "Alto", - "low": "Baixo" - }, "style": { "border_radius": "Tamanho do bordo", "box_shadow": "Caixa de Fundo", @@ -413,6 +408,11 @@ }, "warning": "Avisos" }, + "profiles": { + "editor_label": "", + "low-performance": "", + "scrubbing": "" + }, "view": { "camera_select": "Visualização de câmeras recém-selecionadas", "dark_mode": "Modo escuro", @@ -497,6 +497,8 @@ "overrides_secondary": "Substituições de configuração dinâmica detectadas", "performance": "", "performance_secondary": "", + "profiles": "", + "profiles_secondary": "", "timeline": "Linha do tempo", "timeline_secondary": "Opções do evento da linha do tempo", "upgrade": "Actualização", diff --git a/tests/card-controller/config-manager.test.ts b/tests/card-controller/config-manager.test.ts index 6d063b27..7bbb2f2b 100644 --- a/tests/card-controller/config-manager.test.ts +++ b/tests/card-controller/config-manager.test.ts @@ -78,7 +78,7 @@ describe('ConfigManager', () => { manager.setConfig(config); - expect(manager.hasConfig()).toBeTruthy() + expect(manager.hasConfig()).toBeTruthy(); expect(manager.getRawConfig()).toBe(config); // Verify at least the camera is set. @@ -102,12 +102,12 @@ describe('ConfigManager', () => { expect(api.getCardElementManager().update).toBeCalled(); }); - it('should apply low performance defaults', () => { + it('should apply profiles', () => { const manager = new ConfigManager(createCardAPI()); const config = { type: 'custom:frigate-card', cameras: [{ camera_entity: 'camera.office' }], - performance: { profile: 'low' }, + profiles: ['low-performance'], }; manager.setConfig(config); @@ -143,7 +143,9 @@ describe('ConfigManager', () => { logging: true, }, performance: { - profile: 'low', + style: { + box_shadow: false, + }, }, }; @@ -155,12 +157,11 @@ describe('ConfigManager', () => { }, performance: { features: { - animated_progress_indicator: false, - media_chunk_size: 10, + animated_progress_indicator: true, + media_chunk_size: 50, }, - profile: 'low', style: { - border_radius: false, + border_radius: true, box_shadow: false, }, }, @@ -257,8 +258,8 @@ describe('ConfigManager', () => { const config_2 = { ...config_1, cameras_global: { - live_provider: 'jsmpeg' - } + live_provider: 'jsmpeg', + }, }; vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2)); manager.computeOverrideConfig(); @@ -276,9 +277,9 @@ describe('ConfigManager', () => { cameras: [{ camera_entity: 'camera.office' }], live: { microphone: { - always_connected: false - } - } + always_connected: false, + }, + }, }; vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1)); @@ -289,9 +290,9 @@ describe('ConfigManager', () => { ...config_1, live: { microphone: { - always_connected: true - } - } + always_connected: true, + }, + }, }; vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2)); manager.computeOverrideConfig(); diff --git a/tests/card-controller/style-manager.test.ts b/tests/card-controller/style-manager.test.ts index 23a58bae..c2920b70 100644 --- a/tests/card-controller/style-manager.test.ts +++ b/tests/card-controller/style-manager.test.ts @@ -1,11 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FrigateCardView } from '../../src/config/types'; -import { setPerformanceCSSStyles } from '../../src/performance'; import { StyleManager } from '../../src/card-controller/style-manager'; import { createCardAPI, createConfig, createHASS, createView } from '../test-utils'; -vi.mock('../../src/performance'); - // @vitest-environment jsdom describe('StyleManager', () => { beforeEach(() => { @@ -226,19 +223,49 @@ describe('StyleManager', () => { }); }); - it('setPerformance', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const config = createConfig(); - vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue({ - performance: config.performance, + describe('setPerformance', () => { + it('no styles set', () => { + const api = createCardAPI(); + const element = document.createElement('div'); + vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); + vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue({}); + const manager = new StyleManager(api); + + manager.setPerformance(); + + expect( + element.style.getPropertyValue('--frigate-card-css-box-shadow'), + ).toBeFalsy(); + expect( + element.style.getPropertyValue('--frigate-card-css-border-radius'), + ).toBeFalsy(); }); - const manager = new StyleManager(api); - manager.setPerformance(); + it('valid styles set', () => { + const api = createCardAPI(); + const element = document.createElement('div'); + vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); + vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue( + createConfig({ + performance: { + style: { + box_shadow: false, + border_radius: true, + }, + }, + }), + ); + const manager = new StyleManager(api); - expect(setPerformanceCSSStyles).toBeCalledWith(element, config.performance); + manager.setPerformance(); + + expect(element.style.getPropertyValue('--frigate-card-css-box-shadow')).toEqual( + 'none', + ); + expect( + element.style.getPropertyValue('--frigate-card-css-border-radius'), + ).toBeFalsy(); + }); }); describe('getAspectRatioStyle', () => { diff --git a/tests/config-mgmt.test.ts b/tests/config/management.test.ts similarity index 98% rename from tests/config-mgmt.test.ts rename to tests/config/management.test.ts index 3961f0e0..e12e60e4 100644 --- a/tests/config-mgmt.test.ts +++ b/tests/config/management.test.ts @@ -16,8 +16,8 @@ import { upgradeMoveToWithOverrides, upgradeObjectRecursively, upgradeWithOverrides, -} from '../src/config-mgmt'; -import { RawFrigateCardConfig } from '../src/config/types'; +} from '../../src/config/management'; +import { RawFrigateCardConfig } from '../../src/config/types'; describe('general functions', () => { it('should set value', () => { @@ -2459,6 +2459,43 @@ describe('should handle version specific upgrades', () => { ], }); }); + + describe('from performance profile to generic profile', () => { + it('low performance', () => { + const config = { + type: 'custom:frigate-card', + cameras: [], + performance: { + profile: 'low', + }, + }; + + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [], + profiles: ['low-performance'], + performance: {}, + }); + }); + + it('high performance', () => { + const config = { + type: 'custom:frigate-card', + cameras: [], + performance: { + profile: 'high', + }, + }; + + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [], + performance: {}, + }); + }); + }); }); }); }); diff --git a/tests/config/profiles/index.test.ts b/tests/config/profiles/index.test.ts new file mode 100644 index 00000000..20fd2ad5 --- /dev/null +++ b/tests/config/profiles/index.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; +import { ProfileType } from '../../../src/config/types'; +import { setProfiles } from '../../../src/config/profiles'; +import { createConfig } from '../../test-utils'; + +describe('setProfiles', () => { + it('should handle failed parse', () => { + expect(setProfiles({ cameras: 'not_an_array' }, {})).toEqual({}); + }); + + it('should handle no profiles', () => { + const input = createConfig(); + const output = createConfig(); + + expect(setProfiles(input, output)).toEqual(input); + }); + + it('should handle invalid profiles', () => { + const input = createConfig(); + const output = createConfig(); + + expect(setProfiles(input, output, ['bogus' as ProfileType])).toEqual(input); + }); + + it('should handle profiles', () => { + const input = { + type: 'frigate-hass-card', + cameras: [], + live: { + controls: { + timeline: { + // This will not be overridden. + style: 'stack', + }, + }, + }, + }; + expect(setProfiles(input, {}, ['scrubbing'])).toEqual({ + live: { + controls: { + timeline: { + mode: 'below', + pan_mode: 'seek', + }, + }, + }, + media_viewer: { + controls: { + timeline: { + mode: 'below', + style: 'ribbon', + pan_mode: 'seek', + }, + }, + }, + }); + }); +}); diff --git a/tests/config/profiles/low-performance.test.ts b/tests/config/profiles/low-performance.test.ts new file mode 100644 index 00000000..c048d020 --- /dev/null +++ b/tests/config/profiles/low-performance.test.ts @@ -0,0 +1,54 @@ +import { expect, it } from 'vitest'; +import { LOW_PERFORMANCE_PROFILE } from '../../../src/config/profiles/low-performance'; + +it('low performance profile', () => { + expect(LOW_PERFORMANCE_PROFILE).toEqual({ + 'cameras_global.image.refresh_seconds': 10, + 'cameras_global.live_provider': 'image', + 'cameras_global.triggers.occupancy': false, + 'live.auto_mute': 'never', + 'live.controls.thumbnails.mode': 'none', + 'live.controls.thumbnails.show_details': false, + 'live.controls.thumbnails.show_download_control': false, + 'live.controls.thumbnails.show_favorite_control': false, + 'live.controls.thumbnails.show_timeline_control': false, + 'live.controls.timeline.show_recordings': false, + 'live.controls.title.mode': 'none', + 'live.draggable': false, + 'live.lazy_unload': 'all', + 'live.show_image_during_load': false, + 'live.transition_effect': 'none', + 'media_gallery.controls.thumbnails.show_details': false, + 'media_gallery.controls.thumbnails.show_download_control': false, + 'media_gallery.controls.thumbnails.show_favorite_control': false, + 'media_gallery.controls.thumbnails.show_timeline_control': false, + 'media_viewer.auto_mute': 'never', + 'media_viewer.auto_pause': 'never', + 'media_viewer.auto_play': 'never', + 'media_viewer.controls.next_previous.style': 'chevrons', + 'media_viewer.controls.thumbnails.mode': 'none', + 'media_viewer.controls.thumbnails.show_details': false, + 'media_viewer.controls.thumbnails.show_download_control': false, + 'media_viewer.controls.thumbnails.show_favorite_control': false, + 'media_viewer.controls.thumbnails.show_timeline_control': false, + 'media_viewer.controls.timeline.show_recordings': false, + 'media_viewer.controls.title.mode': 'none', + 'media_viewer.draggable': false, + 'media_viewer.snapshot_click_plays_clip': false, + 'media_viewer.transition_effect': 'none', + 'menu.buttons.frigate.enabled': false, + 'menu.buttons.media_player.enabled': false, + 'menu.buttons.timeline.enabled': false, + 'menu.style': 'outside', + 'performance.features.animated_progress_indicator': false, + 'performance.features.media_chunk_size': 10, + 'performance.style.border_radius': false, + 'performance.style.box_shadow': false, + 'timeline.controls.thumbnails.mode': 'none', + 'timeline.controls.thumbnails.show_details': false, + 'timeline.controls.thumbnails.show_download_control': false, + 'timeline.controls.thumbnails.show_favorite_control': false, + 'timeline.controls.thumbnails.show_timeline_control': false, + 'timeline.show_recordings': false, + }); +}); diff --git a/tests/config/profiles/scrubbing.test.ts b/tests/config/profiles/scrubbing.test.ts new file mode 100644 index 00000000..b043dc0e --- /dev/null +++ b/tests/config/profiles/scrubbing.test.ts @@ -0,0 +1,13 @@ +import { expect, it } from 'vitest'; +import { SCRUBBING_PROFILE } from '../../../src/config/profiles/scrubbing'; + +it('scrubbing profile', () => { + expect(SCRUBBING_PROFILE).toEqual({ + 'live.controls.timeline.mode': 'below', + 'live.controls.timeline.style': 'ribbon', + 'live.controls.timeline.pan_mode': 'seek', + 'media_viewer.controls.timeline.mode': 'below', + 'media_viewer.controls.timeline.style': 'ribbon', + 'media_viewer.controls.timeline.pan_mode': 'seek', + }); +}); diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts index 9ed3a817..76ec3350 100644 --- a/tests/config/types.test.ts +++ b/tests/config/types.test.ts @@ -253,7 +253,6 @@ describe('config defaults', () => { animated_progress_indicator: true, media_chunk_size: 50, }, - profile: 'high', style: { border_radius: true, box_shadow: true, diff --git a/vite.config.ts b/vite.config.ts index afc7ade6..0aaea85e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,8 +18,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [ 'components-lib/menu-controller.ts', 'components-lib/ptz-controller.ts', 'components-lib/zoom-controller.ts', - 'config-mgmt.ts', - 'config/types.ts', + 'config/**/*.ts', 'const.ts', 'types.ts', 'utils/action.ts',