Add support for configuration profiles.
This commit is contained in:
@@ -0,0 +1,692 @@
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import get from 'lodash-es/get';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import set from 'lodash-es/set';
|
||||
import unset from 'lodash-es/unset';
|
||||
import {
|
||||
FrigateCardCondition,
|
||||
RawFrigateCardConfig,
|
||||
RawFrigateCardConfigArray,
|
||||
} from './types';
|
||||
import {
|
||||
CONF_AUTOMATIONS,
|
||||
CONF_CAMERAS,
|
||||
CONF_CAMERAS_GLOBAL_DIMENSIONS_LAYOUT,
|
||||
CONF_CAMERAS_GLOBAL_IMAGE,
|
||||
CONF_CAMERAS_GLOBAL_JSMPEG,
|
||||
CONF_CAMERAS_GLOBAL_WEBRTC_CARD,
|
||||
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_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';
|
||||
|
||||
// *************************************************************************
|
||||
// General Config Management Functions
|
||||
// *************************************************************************
|
||||
|
||||
/**
|
||||
* Set a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param keys The key to the property to set.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
|
||||
export const setConfigValue = (
|
||||
obj: RawFrigateCardConfig,
|
||||
keys: string | (string | number)[],
|
||||
value: unknown,
|
||||
): void => {
|
||||
set(obj, keys, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param keys The key to the property to retrieve.
|
||||
* @param def Default if key not found.
|
||||
* @returns The property or undefined if not found.
|
||||
*/
|
||||
export const getConfigValue = (
|
||||
obj: RawFrigateCardConfig,
|
||||
keys: string | (string | number)[],
|
||||
def?: unknown,
|
||||
): unknown => {
|
||||
return get(obj, keys, def);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param key The key to the property to delete.
|
||||
*/
|
||||
export const deleteConfigValue = (obj: RawFrigateCardConfig, path: string): void => {
|
||||
unset(obj, path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy a configuration.
|
||||
* @param obj Configuration to copy.
|
||||
* @returns A new deeply-copied configuration.
|
||||
*/
|
||||
export const copyConfig = <T>(obj: T): T => {
|
||||
return cloneDeep(obj);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an array path, return a true path.
|
||||
* @param path The array path (should have a '#').
|
||||
* @param index The numeric array index to use.
|
||||
* @returns The true config path.
|
||||
*/
|
||||
export const getArrayConfigPath = (path: string, index: number): string => {
|
||||
return path.replace('#', `[${index.toString()}]`);
|
||||
};
|
||||
|
||||
// *************************************************************************
|
||||
// Upgrade Related Functions
|
||||
// *************************************************************************
|
||||
|
||||
/**
|
||||
* Upgrade a configuration.
|
||||
* @param obj The configuration to upgrade.
|
||||
* @returns `true` if the configuration is modified.
|
||||
*/
|
||||
export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
|
||||
let upgraded = false;
|
||||
for (let i = 0; i < UPGRADES.length; i++) {
|
||||
upgraded = UPGRADES[i](obj) || upgraded;
|
||||
}
|
||||
return upgraded;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if a configuration is automatically upgradeable.
|
||||
* @param obj The configuration. It is not modified.
|
||||
* @returns `true` if the configuration is upgradeable.
|
||||
*/
|
||||
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
|
||||
return upgradeConfig(copyConfig(obj));
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a property from one location to another.
|
||||
* @param obj The configuration object in which the property resides.
|
||||
* @param oldPath The old property path.
|
||||
* @param newPath The new property path.
|
||||
* @param transform An optional transform for the value.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
export const moveConfigValue = (
|
||||
obj: RawFrigateCardConfig,
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
options?: {
|
||||
transform?: (valueIn: unknown) => unknown;
|
||||
keepOriginal?: boolean;
|
||||
},
|
||||
): boolean => {
|
||||
const inValue = getConfigValue(obj, oldPath);
|
||||
if (inValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
const outValue = options?.transform ? options.transform(inValue) : inValue;
|
||||
if (oldPath === newPath && isEqual(inValue, outValue)) {
|
||||
return false;
|
||||
}
|
||||
if (outValue === null) {
|
||||
if (!options?.keepOriginal) {
|
||||
deleteConfigValue(obj, oldPath);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (outValue !== undefined) {
|
||||
if (!options?.keepOriginal) {
|
||||
deleteConfigValue(obj, oldPath);
|
||||
}
|
||||
setConfigValue(obj, newPath, outValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upgrade by moving a property from one location to another.
|
||||
* @param oldPath The old property path.
|
||||
* @param newPath The new property path.
|
||||
* @param transform An optional transform for the value.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
export const upgradeMoveTo = function (
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
options?: {
|
||||
transform?: (valueIn: unknown) => unknown;
|
||||
keepOriginal?: boolean;
|
||||
},
|
||||
): (obj: RawFrigateCardConfig) => boolean {
|
||||
return function (obj: RawFrigateCardConfig): boolean {
|
||||
return moveConfigValue(obj, oldPath, newPath, options);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Upgrade by moving a property from one location to another, and moving a
|
||||
* property specified in a top-level overrides object.
|
||||
* @param oldPath The old property path.
|
||||
* @param newPath The new property path.
|
||||
* @param transform An optional transform for the value.
|
||||
* @returns A function that returns `true` if the configuration was modified.
|
||||
*/
|
||||
export const upgradeMoveToWithOverrides = function (
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
options?: {
|
||||
transform?: (valueIn: unknown) => unknown;
|
||||
keepOriginal?: boolean;
|
||||
},
|
||||
): (obj: RawFrigateCardConfig) => boolean {
|
||||
return function (obj: RawFrigateCardConfig): boolean {
|
||||
let modified = upgradeMoveTo(oldPath, newPath, options)(obj);
|
||||
modified =
|
||||
upgradeArrayOfObjects(
|
||||
CONF_OVERRIDES,
|
||||
upgradeMoveTo(oldPath, newPath, options),
|
||||
(obj) => obj.overrides as RawFrigateCardConfig | undefined,
|
||||
)(obj) || modified;
|
||||
return modified;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Upgrade a property in place with overrides.
|
||||
* @param path The old property path.
|
||||
* @param transform An optional transform for the value.
|
||||
* @returns A function that returns `true` if the configuration was modified.
|
||||
*/
|
||||
export const upgradeWithOverrides = function (
|
||||
path: string,
|
||||
transform: (valueIn: unknown) => unknown,
|
||||
): (obj: RawFrigateCardConfig) => boolean {
|
||||
return upgradeMoveToWithOverrides(path, path, { transform: transform });
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a property in place with overrides.
|
||||
* @param path The property path.
|
||||
* @returns A function that returns `true` if the configuration was modified.
|
||||
*/
|
||||
export const deleteWithOverrides = function (
|
||||
path: string,
|
||||
): (obj: RawFrigateCardConfig) => boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return upgradeMoveToWithOverrides(path, path, { transform: (_) => null });
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a path to an array, apply an upgrade to each object in the array.
|
||||
* @param arrayPath The path to the array to upgrade.
|
||||
* @param upgrade A function that applies an upgrade to an object.
|
||||
* @param getObject A optional function that takes an item in the array and
|
||||
* returns the object to modify within it.
|
||||
* @returns A function that returns `true` if the configuration was modified.
|
||||
*/
|
||||
export const upgradeArrayOfObjects = function (
|
||||
arrayPath: string,
|
||||
upgrade: (obj: RawFrigateCardConfig) => boolean,
|
||||
getObject?: (obj: RawFrigateCardConfig) => RawFrigateCardConfig | undefined,
|
||||
): (obj: RawFrigateCardConfig) => boolean {
|
||||
return function (obj: RawFrigateCardConfig): boolean {
|
||||
let modified = false;
|
||||
const array = getConfigValue(obj, arrayPath);
|
||||
if (Array.isArray(array)) {
|
||||
array.forEach((item) => {
|
||||
const object = getObject ? getObject(item) : item;
|
||||
if (object && typeof object === 'object') {
|
||||
modified = upgrade(object) || modified;
|
||||
}
|
||||
});
|
||||
}
|
||||
return modified;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively upgrade an object.
|
||||
* @param transform A transform applied to each object recursively.
|
||||
* @param getObject A function to get the object to be upgraded.
|
||||
* @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 => {
|
||||
let result = false;
|
||||
if (data && typeof data === 'object') {
|
||||
const object = getObject ? getObject(data) : data;
|
||||
if (object) {
|
||||
result = transform(object) || result;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((item: RawFrigateCardConfig) => {
|
||||
result = recurse(item) || result;
|
||||
});
|
||||
} else {
|
||||
Object.keys(data).forEach((key) => {
|
||||
result = recurse(data[key] as RawFrigateCardConfig) || result;
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return recurse;
|
||||
};
|
||||
|
||||
// *************************************************************************
|
||||
// Upgrade Related Functions: Generic Transforms
|
||||
// *************************************************************************
|
||||
|
||||
/**
|
||||
* Create a transform that will cap a numeric value.
|
||||
* @param value The value.
|
||||
* @returns A number or null.
|
||||
*/
|
||||
export const createRangedTransform = function (
|
||||
transform: (value: unknown) => unknown,
|
||||
min?: number,
|
||||
max?: number,
|
||||
): (valueIn: unknown) => unknown {
|
||||
return (value: unknown): unknown => {
|
||||
let transformed = transform(value);
|
||||
if (typeof transformed !== 'number') {
|
||||
return transformed;
|
||||
}
|
||||
transformed = min !== undefined ? Math.max(min, transformed as number) : transformed;
|
||||
transformed = max !== undefined ? Math.min(max, transformed as number) : transformed;
|
||||
return transformed;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request a property be deleted.
|
||||
* @param _value Inbound value (not required).
|
||||
* @returns `null` to request the property be deleted.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const deleteTransform = function (_value: unknown): number | null | undefined {
|
||||
return null;
|
||||
};
|
||||
|
||||
// *************************************************************************
|
||||
// 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
|
||||
* @param data Input data.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
const conditionToConditionsTransform = (data: unknown): boolean => {
|
||||
if (
|
||||
typeof data !== 'object' ||
|
||||
!data ||
|
||||
typeof data['conditions'] !== 'object' ||
|
||||
!data['conditions']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldConditions = data['conditions'];
|
||||
const newConditions: FrigateCardCondition[] = [];
|
||||
|
||||
if (oldConditions['view'] !== undefined) {
|
||||
newConditions.push({
|
||||
condition: 'view' as const,
|
||||
views: oldConditions['view'],
|
||||
});
|
||||
}
|
||||
if (oldConditions['fullscreen'] !== undefined) {
|
||||
newConditions.push({
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: oldConditions['fullscreen'],
|
||||
});
|
||||
}
|
||||
if (oldConditions['expand'] !== undefined) {
|
||||
newConditions.push({
|
||||
condition: 'expand' as const,
|
||||
expand: oldConditions['expand'],
|
||||
});
|
||||
}
|
||||
if (oldConditions['camera'] !== undefined) {
|
||||
newConditions.push({
|
||||
condition: 'camera' as const,
|
||||
cameras: oldConditions['camera'],
|
||||
});
|
||||
}
|
||||
if (oldConditions['media_loaded'] !== undefined) {
|
||||
newConditions.push({
|
||||
condition: 'media_loaded' as const,
|
||||
media_loaded: oldConditions['media_loaded'],
|
||||
});
|
||||
}
|
||||
if (oldConditions['state'] !== undefined && Array.isArray(oldConditions['state'])) {
|
||||
for (const stateCondition of oldConditions['state']) {
|
||||
if (
|
||||
typeof stateCondition === 'object' &&
|
||||
stateCondition &&
|
||||
(stateCondition['state'] !== undefined ||
|
||||
stateCondition['state_not'] !== undefined ||
|
||||
stateCondition['entity'] !== undefined)
|
||||
) {
|
||||
newConditions.push({
|
||||
condition: 'state' as const,
|
||||
...(stateCondition['state'] && {
|
||||
state: stateCondition['state'],
|
||||
}),
|
||||
...(stateCondition['state_not'] && {
|
||||
state_not: stateCondition['state_not'],
|
||||
}),
|
||||
...(stateCondition['entity'] && {
|
||||
entity: stateCondition['entity'],
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldConditions['media_query'] !== undefined) {
|
||||
newConditions.push({
|
||||
condition: 'screen' as const,
|
||||
media_query: oldConditions['media_query'],
|
||||
});
|
||||
}
|
||||
|
||||
// These conditions did not exist prior to v6.0.0 and so are not converted:
|
||||
// - display_mode
|
||||
// - triggered
|
||||
// - interaction
|
||||
// - microphone
|
||||
|
||||
if (newConditions.length) {
|
||||
data['conditions'] = newConditions;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform service_data -> data
|
||||
* See: https://github.com/dermotduffy/frigate-hass-card/issues/1103
|
||||
* @param data Input data.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
const serviceDataToDataTransform = (data: unknown): boolean => {
|
||||
if (
|
||||
typeof data === 'object' &&
|
||||
data &&
|
||||
data['action'] === 'call-service' &&
|
||||
data['service'] !== undefined &&
|
||||
data['service_data'] !== undefined &&
|
||||
data['data'] === undefined
|
||||
) {
|
||||
data['data'] = data['service_data'];
|
||||
delete data['service_data'];
|
||||
return true;
|
||||
}
|
||||
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.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
const upgradePTZElementsToLive = function (): (data: unknown) => boolean {
|
||||
return function (data: unknown): boolean {
|
||||
if (
|
||||
typeof data !== 'object' ||
|
||||
!data ||
|
||||
!(CONF_ELEMENTS in data) ||
|
||||
!Array.isArray(data[CONF_ELEMENTS])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let foundPTZ = false;
|
||||
const movePTZ = (element: RawFrigateCardConfig): void => {
|
||||
if (!foundPTZ) {
|
||||
if (!get(data, 'live.controls.ptz')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { type: _, ...newPTZ } = element;
|
||||
set(data, 'live.controls.ptz', newPTZ);
|
||||
}
|
||||
foundPTZ = true;
|
||||
}
|
||||
};
|
||||
|
||||
const processElements = (
|
||||
elements: RawFrigateCardConfigArray,
|
||||
): RawFrigateCardConfigArray => {
|
||||
const newElements: RawFrigateCardConfigArray = [];
|
||||
for (const element of elements) {
|
||||
if (element['type'] === 'custom:frigate-card-ptz') {
|
||||
movePTZ(element);
|
||||
} else if (
|
||||
(element['type'] === 'conditional' ||
|
||||
element['type'] === 'custom:frigate-card-conditional') &&
|
||||
Array.isArray(element['elements'])
|
||||
) {
|
||||
const newConditionalElements = processElements(element['elements']);
|
||||
if (newConditionalElements.length) {
|
||||
element['elements'] = newConditionalElements;
|
||||
newElements.push(element);
|
||||
}
|
||||
} else {
|
||||
newElements.push(element);
|
||||
}
|
||||
}
|
||||
return newElements;
|
||||
};
|
||||
|
||||
const newElements = processElements(data[CONF_ELEMENTS]);
|
||||
|
||||
if (foundPTZ) {
|
||||
if (newElements.length) {
|
||||
data[CONF_ELEMENTS] = newElements;
|
||||
} else {
|
||||
delete data[CONF_ELEMENTS];
|
||||
}
|
||||
}
|
||||
return foundPTZ;
|
||||
};
|
||||
};
|
||||
|
||||
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),
|
||||
}),
|
||||
),
|
||||
|
||||
// v5.2.0 -> v6.0.0
|
||||
(data: unknown): boolean => {
|
||||
return upgradeObjectRecursively(serviceDataToDataTransform)(
|
||||
typeof data === 'object' && data ? <RawFrigateCardConfig>data : {},
|
||||
);
|
||||
},
|
||||
upgradePTZElementsToLive(),
|
||||
upgradeMoveToWithOverrides('view.timeout_seconds', CONF_VIEW_INTERACTION_SECONDS),
|
||||
upgradeWithOverrides('live.lazy_unload', (data) =>
|
||||
data === 'all' ? ['unselected', 'hidden'] : data === 'never' ? null : arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('live.auto_play', (data) =>
|
||||
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('live.auto_pause', (data) =>
|
||||
data === 'all' ? ['unselected', 'hidden'] : data === 'never' ? null : arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('live.auto_mute', (data) =>
|
||||
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('live.auto_unmute', (data) =>
|
||||
data === 'all'
|
||||
? ['selected', 'visible', 'microphone']
|
||||
: data === 'never'
|
||||
? null
|
||||
: arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('media_viewer.auto_play', (data) =>
|
||||
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('media_viewer.auto_pause', (data) =>
|
||||
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('media_viewer.auto_mute', (data) =>
|
||||
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
|
||||
),
|
||||
upgradeWithOverrides('media_viewer.auto_unmute', (data) =>
|
||||
data === 'all' ? ['selected', 'visible'] : data === 'never' ? null : arrayify(data),
|
||||
),
|
||||
upgradeMoveToWithOverrides(
|
||||
'live.controls.thumbnails.media',
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE,
|
||||
),
|
||||
upgradeMoveToWithOverrides('timeline.media', CONF_TIMELINE_EVENTS_MEDIA_TYPE),
|
||||
upgradeMoveToWithOverrides(
|
||||
'live.controls.timeline.media',
|
||||
CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
),
|
||||
upgradeMoveToWithOverrides(
|
||||
'media_viewer.controls.timeline.media',
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
),
|
||||
upgradeMoveToWithOverrides('view.scan', CONF_VIEW_TRIGGERS),
|
||||
upgradeMoveToWithOverrides(
|
||||
'view.triggers.enabled',
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
||||
{
|
||||
transform: (data) => (data === true ? 'live' : null),
|
||||
// Keep it around, for the following transform.
|
||||
keepOriginal: true,
|
||||
},
|
||||
),
|
||||
upgradeMoveToWithOverrides(
|
||||
'view.triggers.enabled',
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
{
|
||||
transform: (data) => (data === true ? false : null),
|
||||
},
|
||||
),
|
||||
upgradeMoveToWithOverrides(
|
||||
'view.triggers.untrigger_reset',
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
{
|
||||
// Delete the value if it's set to the default.
|
||||
transform: (val) => (val ? 'default' : null),
|
||||
},
|
||||
),
|
||||
upgradeMoveToWithOverrides('live.layout', CONF_CAMERAS_GLOBAL_DIMENSIONS_LAYOUT),
|
||||
deleteWithOverrides('media_viewer.layout'),
|
||||
deleteWithOverrides('image.layout'),
|
||||
upgradeArrayOfObjects(CONF_OVERRIDES, conditionToConditionsTransform),
|
||||
(data: unknown): boolean => {
|
||||
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
||||
typeof data === 'object' && data ? data[CONF_ELEMENTS] : {},
|
||||
);
|
||||
},
|
||||
(data: unknown): boolean => {
|
||||
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
||||
typeof data === 'object' && data ? data[CONF_AUTOMATIONS] : {},
|
||||
);
|
||||
},
|
||||
upgradeArrayOfObjects(
|
||||
CONF_CAMERAS,
|
||||
upgradeMoveToWithOverrides('hide', 'capabilities', {
|
||||
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),
|
||||
}),
|
||||
];
|
||||
@@ -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 = <T extends RawFrigateCardConfig>(
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||
CONF_LIVE_CONTROLS_TITLE_MODE,
|
||||
CONF_LIVE_DRAGGABLE,
|
||||
CONF_LIVE_LAZY_UNLOAD,
|
||||
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
|
||||
CONF_LIVE_TRANSITION_EFFECT,
|
||||
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
|
||||
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||
CONF_MEDIA_VIEWER_AUTO_MUTE,
|
||||
CONF_MEDIA_VIEWER_AUTO_PAUSE,
|
||||
CONF_MEDIA_VIEWER_AUTO_PLAY,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
|
||||
CONF_MEDIA_VIEWER_DRAGGABLE,
|
||||
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
|
||||
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||
CONF_MENU_BUTTONS_FRIGATE,
|
||||
CONF_MENU_BUTTONS_MEDIA_PLAYER,
|
||||
CONF_MENU_BUTTONS_TIMELINE,
|
||||
CONF_MENU_STYLE,
|
||||
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
|
||||
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
|
||||
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||
CONF_TIMELINE_SHOW_RECORDINGS,
|
||||
} from '../../const.js';
|
||||
|
||||
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,
|
||||
[CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE]: 'none' as const,
|
||||
|
||||
// Do not show recordings on timelines.
|
||||
[CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS]: false,
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS]: false,
|
||||
[CONF_TIMELINE_SHOW_RECORDINGS]: false,
|
||||
|
||||
// Take no automatic media actions.
|
||||
[CONF_LIVE_AUTO_MUTE]: 'never' as const,
|
||||
[CONF_MEDIA_VIEWER_AUTO_PLAY]: 'never' as const,
|
||||
[CONF_MEDIA_VIEWER_AUTO_PAUSE]: 'never' as const,
|
||||
[CONF_MEDIA_VIEWER_AUTO_MUTE]: 'never' as const,
|
||||
|
||||
// Always unload resources that are lazily loaded.
|
||||
[CONF_LIVE_LAZY_UNLOAD]: 'all' as const,
|
||||
|
||||
// Media carousels do not drag.
|
||||
[CONF_LIVE_DRAGGABLE]: false,
|
||||
[CONF_MEDIA_VIEWER_DRAGGABLE]: false,
|
||||
|
||||
// Media carousels have no effects.
|
||||
[CONF_LIVE_TRANSITION_EFFECT]: 'none' as const,
|
||||
[CONF_MEDIA_VIEWER_TRANSITION_EFFECT]: 'none' as const,
|
||||
|
||||
// Do not show image during load.
|
||||
[CONF_LIVE_SHOW_IMAGE_DURING_LOAD]: false,
|
||||
|
||||
// Media player next/previous are chevrons.
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE]: 'chevrons' as const,
|
||||
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE]: 'none' as const,
|
||||
[CONF_LIVE_CONTROLS_TITLE_MODE]: 'none' as const,
|
||||
|
||||
// Move the menu to outside to remove the need to interact with it with open.
|
||||
[CONF_MENU_STYLE]: 'outside',
|
||||
|
||||
// Hide several buttons that are otherwise visible by default.
|
||||
[`${CONF_MENU_BUTTONS_FRIGATE}.enabled`]: false,
|
||||
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
|
||||
[`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
|
||||
|
||||
// If the media player button is present media player entity fetches are
|
||||
// required on initialization.
|
||||
[`${CONF_MENU_BUTTONS_MEDIA_PLAYER}.enabled`]: false,
|
||||
|
||||
// Disable all options in thumbnails.
|
||||
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
|
||||
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
|
||||
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
|
||||
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
|
||||
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
|
||||
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
|
||||
|
||||
// Disable all optional performance related features.
|
||||
[CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR]: false,
|
||||
|
||||
// Load fewer media items by default.
|
||||
[CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE]: 10,
|
||||
|
||||
// Disable all expensive CSS features.
|
||||
[CONF_PERFORMANCE_STYLE_BORDER_RADIUS]: false,
|
||||
[CONF_PERFORMANCE_STYLE_BOX_SHADOW]: false,
|
||||
|
||||
// Clicking on a snapshot should not play a clip.
|
||||
[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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
+9
-2
@@ -1623,7 +1623,6 @@ export type Automations = z.infer<typeof automationsSchema>;
|
||||
// *************************************************************************
|
||||
|
||||
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,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user