diff --git a/package.json b/package.json index 10b14726..4c00c7ed 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "license": "MIT", "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.4", + "@dermotduffy/custom-card-helpers": "^1.9.0", "@dermotduffy/panzoom": "^4.5.1", "@egjs/hammerjs": "^2.0.17", "@graphiteds/core": "^1.9.11", @@ -24,7 +25,6 @@ "@types/bluebird": "^3.5.36", "component-emitter": "^1.3.0", "crypto": "^1.0.1", - "custom-card-helpers": "^1.9.0", "date-fns": "^2.29.2", "date-fns-tz": "^1.3.7", "embla-carousel": "8.0.0-rc14", diff --git a/src/action-handler-directive.ts b/src/action-handler-directive.ts index fdfd0472..73fe78ab 100644 --- a/src/action-handler-directive.ts +++ b/src/action-handler-directive.ts @@ -1,8 +1,8 @@ -import { fireEvent } from 'custom-card-helpers'; +import { fireEvent } from '@dermotduffy/custom-card-helpers'; import type { ActionHandlerDetail, ActionHandlerOptions, -} from 'custom-card-helpers/dist/types.d.js'; +} from '@dermotduffy/custom-card-helpers'; import { noChange } from 'lit'; import { AttributePart, diff --git a/src/camera-manager/browse-media/engine-browse-media.ts b/src/camera-manager/browse-media/engine-browse-media.ts index 55c3fbb8..f6eea202 100644 --- a/src/camera-manager/browse-media/engine-browse-media.ts +++ b/src/camera-manager/browse-media/engine-browse-media.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CameraConfig } from '../../config/types'; import { localize } from '../../localize/localize'; import { ExtendedHomeAssistant } from '../../types'; diff --git a/src/camera-manager/engine-factory.ts b/src/camera-manager/engine-factory.ts index 4f1e8de4..38a694ca 100644 --- a/src/camera-manager/engine-factory.ts +++ b/src/camera-manager/engine-factory.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CameraConfig } from '../config/types'; import { localize } from '../localize/localize'; import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager'; diff --git a/src/camera-manager/engine.ts b/src/camera-manager/engine.ts index 89313507..c2d8710c 100644 --- a/src/camera-manager/engine.ts +++ b/src/camera-manager/engine.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CameraConfig } from '../config/types'; import { ExtendedHomeAssistant } from '../types'; import { EntityRegistryManager } from '../utils/ha/entity-registry'; diff --git a/src/camera-manager/frigate/engine-frigate.ts b/src/camera-manager/frigate/engine-frigate.ts index adea7998..0a4dd6ea 100644 --- a/src/camera-manager/frigate/engine-frigate.ts +++ b/src/camera-manager/frigate/engine-frigate.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import add from 'date-fns/add'; import endOfHour from 'date-fns/endOfHour'; import format from 'date-fns/format'; diff --git a/src/camera-manager/frigate/requests.ts b/src/camera-manager/frigate/requests.ts index 3cf751ba..6bb5fc8b 100644 --- a/src/camera-manager/frigate/requests.ts +++ b/src/camera-manager/frigate/requests.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { localize } from '../../localize/localize'; import { FrigateCardError } from '../../types'; import { homeAssistantWSRequest } from '../../utils/ha'; diff --git a/src/camera-manager/generic/engine-generic.ts b/src/camera-manager/generic/engine-generic.ts index 7332fd92..3a5fe04e 100644 --- a/src/camera-manager/generic/engine-generic.ts +++ b/src/camera-manager/generic/engine-generic.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CameraConfig } from '../../config/types'; import { ExtendedHomeAssistant } from '../../types'; import { getEntityIcon, getEntityTitle } from '../../utils/ha'; diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index 347eb346..5ca659e8 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import add from 'date-fns/add'; import cloneDeep from 'lodash-es/cloneDeep'; import merge from 'lodash-es/merge.js'; diff --git a/src/camera-manager/motioneye/engine-motioneye.ts b/src/camera-manager/motioneye/engine-motioneye.ts index a220a948..c7116239 100644 --- a/src/camera-manager/motioneye/engine-motioneye.ts +++ b/src/camera-manager/motioneye/engine-motioneye.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import add from 'date-fns/add'; import endOfDay from 'date-fns/endOfDay'; import parse from 'date-fns/parse'; diff --git a/src/card-controller/controller.ts b/src/card-controller/controller.ts index 50a0ca47..e44e6d8a 100644 --- a/src/card-controller/controller.ts +++ b/src/card-controller/controller.ts @@ -1,4 +1,4 @@ -import { LovelaceCardEditor } from 'custom-card-helpers'; +import { LovelaceCardEditor } from '@dermotduffy/custom-card-helpers'; import { ReactiveController } from 'lit'; import { CameraManager } from '../camera-manager/manager'; import { FrigateCardConfig } from '../config/types'; diff --git a/src/card-controller/triggers-manager.ts b/src/card-controller/triggers-manager.ts index 95605faf..4850b01e 100644 --- a/src/card-controller/triggers-manager.ts +++ b/src/card-controller/triggers-manager.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import orderBy from 'lodash-es/orderBy'; import { getHassDifferences, isTriggeredState } from '../utils/ha'; import { Timer } from '../utils/timer'; diff --git a/src/card.ts b/src/card.ts index 52ee0d1b..a759833d 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1,4 +1,4 @@ -import { HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers'; +import { HomeAssistant, LovelaceCardEditor } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { customElement } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; diff --git a/src/components-lib/menu-controller.ts b/src/components-lib/menu-controller.ts index 58beb41b..ac561641 100644 --- a/src/components-lib/menu-controller.ts +++ b/src/components-lib/menu-controller.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { StyleInfo } from 'lit/directives/style-map'; import { CameraManager } from '../camera-manager/manager'; import { diff --git a/src/components/diagnostics.ts b/src/components/diagnostics.ts index 6d908a1c..44d962ec 100644 --- a/src/components/diagnostics.ts +++ b/src/components/diagnostics.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { RawFrigateCardConfig } from '../config/types'; diff --git a/src/components/elements.ts b/src/components/elements.ts index e2c50e72..06d63dc7 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -1,4 +1,4 @@ -import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers'; +import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, LitElement, diff --git a/src/components/image.ts b/src/components/image.ts index a222972e..5bdff165 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { HassEntity } from 'home-assistant-js-websocket'; import { CSSResultGroup, diff --git a/src/components/live/live-ha.ts b/src/components/live/live-ha.ts index 603cc22c..eb090928 100644 --- a/src/components/live/live-ha.ts +++ b/src/components/live/live-ha.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js'; diff --git a/src/components/live/live-image.ts b/src/components/live/live-image.ts index 7e2c8859..eb6a24d4 100644 --- a/src/components/live/live-image.ts +++ b/src/components/live/live-image.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js'; diff --git a/src/components/live/live-webrtc-card.ts b/src/components/live/live-webrtc-card.ts index 00b43bbf..6a8f139d 100644 --- a/src/components/live/live-webrtc-card.ts +++ b/src/components/live/live-webrtc-card.ts @@ -1,5 +1,5 @@ import { Task } from '@lit-labs/task'; -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { CameraEndpoints } from '../../camera-manager/types.js'; diff --git a/src/components/media-filter.ts b/src/components/media-filter.ts index f5d71206..68447f7f 100644 --- a/src/components/media-filter.ts +++ b/src/components/media-filter.ts @@ -1,5 +1,5 @@ import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import endOfDay from 'date-fns/endOfDay'; import endOfMonth from 'date-fns/endOfMonth'; import endOfYesterday from 'date-fns/endOfYesterday'; diff --git a/src/components/menu.ts b/src/components/menu.ts index 255fd893..c791fd07 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -1,4 +1,4 @@ -import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers'; +import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, LitElement, diff --git a/src/components/next-prev-control.ts b/src/components/next-prev-control.ts index ad4ff24d..8d989f2a 100644 --- a/src/components/next-prev-control.ts +++ b/src/components/next-prev-control.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; diff --git a/src/components/submenu.ts b/src/components/submenu.ts index ef945698..f0e9ad8d 100644 --- a/src/components/submenu.ts +++ b/src/components/submenu.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, html, @@ -230,7 +230,7 @@ export class FrigateCardSubmenuSelect extends LitElement { service: entityID.startsWith('select.') ? 'select.select_option' : 'input_select.select_option', - service_data: { + data: { entity_id: entityID, option: option, }, diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts index cc3dfdfe..7d484646 100644 --- a/src/config-mgmt.ts +++ b/src/config-mgmt.ts @@ -2,40 +2,24 @@ 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 { - BUTTON_SIZE_MIN, - RawFrigateCardConfig, - THUMBNAIL_WIDTH_MAX, - THUMBNAIL_WIDTH_MIN, -} from './config/types'; +import unset from 'lodash-es/unset'; +import { RawFrigateCardConfig } from './config/types'; import { CONF_CAMERAS, CONF_CAMERAS_GLOBAL_IMAGE, CONF_CAMERAS_GLOBAL_JSMPEG, CONF_CAMERAS_GLOBAL_WEBRTC_CARD, CONF_ELEMENTS, - CONF_LIVE_AUTO_UNMUTE, - CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, - CONF_LIVE_CONTROLS_THUMBNAILS_SIZE, - CONF_LIVE_LAZY_UNLOAD, CONF_MEDIA_GALLERY, - CONF_MEDIA_VIEWER, - CONF_MENU_BUTTONS_CAMERAS, CONF_MENU_BUTTONS_CAMERA_UI, - CONF_MENU_BUTTONS_CLIPS, - CONF_MENU_BUTTONS_DOWNLOAD, - CONF_MENU_BUTTONS_FRIGATE, - CONF_MENU_BUTTONS_FULLSCREEN, - CONF_MENU_BUTTONS_IMAGE, - CONF_MENU_BUTTONS_LIVE, - CONF_MENU_BUTTONS_SNAPSHOTS, - CONF_MENU_BUTTON_SIZE, - CONF_MENU_POSITION, - CONF_MENU_STYLE, CONF_OVERRIDES, } from './const'; import { arrayify } from './utils/basic'; +// ************************************************************************* +// General Config Management Functions +// ************************************************************************* + /** * Set a configuration value. * @param obj The configuration. @@ -71,19 +55,33 @@ export const getConfigValue = ( * @param obj The configuration. * @param key The key to the property to delete. */ -export const deleteConfigValue = (obj: RawFrigateCardConfig, key: string): void => { - let id = key; - let targetObj: unknown = obj; - if (key && key.split && key.includes('.')) { - const keys = key.split('.'); - id = keys[keys.length - 1]; - targetObj = getConfigValue(obj, keys.slice(0, -1).join('.')); - } - if (targetObj && typeof targetObj === 'object') { - delete targetObj[id]; - } +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 = (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. @@ -106,67 +104,6 @@ export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean return upgradeConfig(copyConfig(obj)); }; -/** - * Copy a configuration. - * @param obj Configuration to copy. - * @returns A new deeply-copied configuration. - */ -export const copyConfig = (obj: T): T => { - return cloneDeep(obj); -}; - -/** - * Create a transform that will cap a numeric value. - * @param value The value. - * @returns A number or null. - */ -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 ? Math.max(min, transformed as number) : transformed; - transformed = max ? Math.min(max, transformed as number) : transformed; - return transformed; - }; -}; - -/** - * Convert a value from 'XXpx' to XX (as a number). - * @param value Incoming value. - * @returns A number, null if the property should be deleted or undefined if it - * should be ignored. - */ -const toPixelsOrDelete = function (value: unknown): number | null | undefined { - // Ignore the value if it's a number. - if (typeof value === 'number') { - return undefined; - } - // Delete the value if it's not a string. - if (typeof value !== 'string') { - return null; - } - // Remove 'px' and return the number, unless it's an invalid number -- then - // delete it. - value = value.replace(/px$/i, ''); - return isNaN(value as number) ? null : Number(value); -}; - -/** - * 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 -const deleteProperty = function (_value: unknown): number | null | undefined { - return null; -}; - /** * Move a property from one location to another. * @param obj The configuration object in which the property resides. @@ -175,7 +112,7 @@ const deleteProperty = function (_value: unknown): number | null | undefined { * @param transform An optional transform for the value. * @returns `true` if the configuration was modified. */ -const moveConfigValue = ( +export const moveConfigValue = ( obj: RawFrigateCardConfig, oldPath: string, newPath: string, @@ -209,16 +146,6 @@ const moveConfigValue = ( return false; }; -/** - * 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 by moving a property from one location to another. * @param oldPath The old property path. @@ -226,7 +153,7 @@ export const getArrayConfigPath = (path: string, index: number): string => { * @param transform An optional transform for the value. * @returns `true` if the configuration was modified. */ -const upgradeMoveTo = function ( +export const upgradeMoveTo = function ( oldPath: string, newPath: string, options?: { @@ -247,7 +174,7 @@ const upgradeMoveTo = function ( * @param transform An optional transform for the value. * @returns A function that returns `true` if the configuration was modified. */ -const upgradeMoveToWithOverrides = function ( +export const upgradeMoveToWithOverrides = function ( oldPath: string, newPath: string, options?: { @@ -258,7 +185,7 @@ const upgradeMoveToWithOverrides = function ( return function (obj: RawFrigateCardConfig): boolean { let modified = upgradeMoveTo(oldPath, newPath, options)(obj); modified = - upgradeArrayValue( + upgradeArrayOfObjects( CONF_OVERRIDES, upgradeMoveTo(oldPath, newPath, options), (obj) => obj.overrides as RawFrigateCardConfig | undefined, @@ -273,26 +200,13 @@ const upgradeMoveToWithOverrides = function ( * @param transform An optional transform for the value. * @returns A function that returns `true` if the configuration was modified. */ -const upgradeWithOverrides = function ( +export const upgradeWithOverrides = function ( path: string, transform: (valueIn: unknown) => unknown, ): (obj: RawFrigateCardConfig) => boolean { return upgradeMoveToWithOverrides(path, path, { transform: transform }); }; -/** - * Upgrade a property in place without 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. - */ -const upgrade = function ( - path: string, - transform: (valueIn: unknown) => unknown, -): (obj: RawFrigateCardConfig) => boolean { - return upgradeMoveTo(path, path, { transform: transform }); -}; - /** * Given a path to an array, apply an upgrade to each object in the array. * @param arrayPath The path to the array to upgrade. @@ -301,7 +215,7 @@ const upgrade = function ( * returns the object to modify within it. * @returns A function that returns `true` if the configuration was modified. */ -const upgradeArrayValue = function ( +export const upgradeArrayOfObjects = function ( arrayPath: string, upgrade: (obj: RawFrigateCardConfig) => boolean, getObject?: (obj: RawFrigateCardConfig) => RawFrigateCardConfig | undefined, @@ -321,127 +235,13 @@ const upgradeArrayValue = function ( }; }; -/** - * Upgrade from a menu-mode to a style & position. - * @returns An upgrade function. - */ -const upgradeMenuModeToStyleAndPosition = (): (( - obj: RawFrigateCardConfig, -) => boolean) => { - return function (obj: RawFrigateCardConfig): boolean { - let modified = false; - - // Change the 'start' of the mode into a style. - modified = - upgradeMoveToWithOverrides('menu.mode', CONF_MENU_STYLE, { - transform: (mode: unknown): string | undefined => { - if (typeof mode === 'string') { - const result = mode.match(/^(hover|hidden|overlay|above|below|none)/); - if (result) { - switch (result[1]) { - case 'hover': - case 'hidden': - case 'overlay': - case 'none': - return result[1]; - case 'above': - case 'below': - return 'outside'; - } - } - } - return undefined; - }, - keepOriginal: true, - })(obj) || modified; - - // Change the 'end' of the mode into a position. - modified = - upgradeMoveToWithOverrides('menu.mode', CONF_MENU_POSITION, { - transform: (mode: unknown): string | undefined => { - if (typeof mode === 'string') { - const result = mode.match(/(above|below|left|right|top|bottom)$/); - if (result) { - switch (result[1]) { - case 'left': - case 'right': - case 'top': - case 'bottom': - return result[1]; - case 'above': - return 'top'; - case 'below': - return 'bottom'; - } - } - } - return undefined; - }, - keepOriginal: true, - })(obj) || modified; - - // Delete the old `menu.mode` . - return upgradeWithOverrides('menu.mode', deleteProperty)(obj) || modified; - }; -}; - -/** - * Transform a menu button from a boolean to a priority. - * @param value The boolean true/false for show/hide the switch. - * @returns A priority value. - */ -const menuButtonBooleanToObject = function ( - value: unknown, -): { enabled: boolean } | null | undefined { - if (typeof value === 'object') { - return undefined; - } - // If it's not a boolean remove it. - if (typeof value !== 'boolean') { - return null; - } - return { enabled: value }; -}; - -/** - * Upgrade from a show_controls key to individual favorite/timeline keys. - * @returns An upgrade function. - */ -const upgradeThumbnailShowControlsToIndividualControls = ( - thumbnailsBasePath: string, -): ((obj: RawFrigateCardConfig) => boolean) => { - const thumbnailsShowControlsPath = `${thumbnailsBasePath}.show_controls`; - - return function (obj: RawFrigateCardConfig): boolean { - let modified = false; - modified = - upgradeMoveToWithOverrides( - thumbnailsShowControlsPath, - `${thumbnailsBasePath}.show_favorite_control`, - { keepOriginal: true }, - )(obj) || modified; - - modified = - upgradeMoveToWithOverrides( - thumbnailsShowControlsPath, - `${thumbnailsBasePath}.show_timeline_control`, - { keepOriginal: true }, - )(obj) || modified; - - // Delete the old `show_controls`. - return ( - upgradeWithOverrides(thumbnailsShowControlsPath, deleteProperty)(obj) || 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. */ -const recursiveUpgradeObject = ( +export const upgradeObjectRecursively = ( transform: (data: RawFrigateCardConfig) => boolean, getObject?: (data: RawFrigateCardConfig) => RawFrigateCardConfig | undefined | null, ): ((data: RawFrigateCardConfig) => boolean) => { @@ -453,17 +253,13 @@ const recursiveUpgradeObject = ( result = transform(object) || result; } if (Array.isArray(data)) { - data - .filter((item) => typeof item === 'object') - .forEach((item: RawFrigateCardConfig) => { - result = recurse(item) || result; - }); + data.forEach((item: RawFrigateCardConfig) => { + result = recurse(item) || result; + }); } else { - Object.keys(data) - .filter((key) => typeof data[key] === 'object') - .forEach((key) => { - result = recurse(data[key] as RawFrigateCardConfig) || result; - }); + Object.keys(data).forEach((key) => { + result = recurse(data[key] as RawFrigateCardConfig) || result; + }); } } return result; @@ -471,12 +267,51 @@ const recursiveUpgradeObject = ( 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 +// ************************************************************************* + /** * Transform mediaLoaded -> media_loaded * @param data Input data. * @returns `true` if the configuration was modified. */ -const transformConditionMediaLoaded = (data: unknown): boolean => { +const conditionMediaLoadedTransform = (data: unknown): boolean => { if (typeof data === 'object' && data && data['mediaLoaded'] !== undefined) { data['media_loaded'] = data['mediaLoaded']; delete data['mediaLoaded']; @@ -485,12 +320,34 @@ const transformConditionMediaLoaded = (data: unknown): boolean => { 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 transformFrigateUIAction = (data: unknown): boolean => { +const frigateUIActionTransform = (data: unknown): boolean => { if ( typeof data === 'object' && data && @@ -504,101 +361,51 @@ const transformFrigateUIAction = (data: unknown): boolean => { }; const UPGRADES = [ - // v3.0.0 -> v4.0.0-rc.1 - upgradeWithOverrides( - CONF_LIVE_CONTROLS_THUMBNAILS_SIZE, - createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX), - ), - upgradeWithOverrides( - 'event_viewer.controls.thumbnails.size', - createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX), - ), - upgradeWithOverrides( - CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, - createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN), - ), - upgradeWithOverrides( - 'event_viewer.controls.next_previous.size', - createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN), - ), - upgradeWithOverrides( - CONF_MENU_BUTTON_SIZE, - createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN), - ), - upgradeWithOverrides('event_gallery.min_columns', deleteProperty), - upgradeMenuModeToStyleAndPosition(), - upgradeWithOverrides(CONF_MENU_BUTTONS_FRIGATE, menuButtonBooleanToObject), - upgradeWithOverrides(CONF_MENU_BUTTONS_CAMERAS, menuButtonBooleanToObject), - upgradeWithOverrides(CONF_MENU_BUTTONS_LIVE, menuButtonBooleanToObject), - upgradeWithOverrides(CONF_MENU_BUTTONS_CLIPS, menuButtonBooleanToObject), - upgradeWithOverrides(CONF_MENU_BUTTONS_SNAPSHOTS, menuButtonBooleanToObject), - upgradeWithOverrides(CONF_MENU_BUTTONS_IMAGE, menuButtonBooleanToObject), - upgradeWithOverrides(CONF_MENU_BUTTONS_DOWNLOAD, menuButtonBooleanToObject), - upgradeWithOverrides('menu.buttons.frigate_ui', menuButtonBooleanToObject), - upgradeWithOverrides(CONF_MENU_BUTTONS_FULLSCREEN, menuButtonBooleanToObject), - upgrade(CONF_LIVE_LAZY_UNLOAD, (val) => - typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined, - ), - upgrade(CONF_LIVE_AUTO_UNMUTE, (val) => - typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined, - ), - upgrade('event_viewer.auto_play', (val) => - typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined, - ), - upgrade('event_viewer.auto_unmute', (val) => - typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined, - ), - upgradeMoveToWithOverrides('event_viewer', CONF_MEDIA_VIEWER), - upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('camera_name', 'frigate.camera_name')), - upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('client_id', 'frigate.client_id')), - upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('label', 'frigate.label')), - upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('frigate_url', 'frigate.url')), - upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('zone', 'frigate.zone')), - - // v4.0.0-rc.1 -> v4.0.0-rc.3 - upgradeThumbnailShowControlsToIndividualControls('event_gallery.controls.thumbnails'), - upgradeThumbnailShowControlsToIndividualControls('media_viewer.controls.thumbnails'), - upgradeThumbnailShowControlsToIndividualControls('live.controls.thumbnails'), - upgradeThumbnailShowControlsToIndividualControls('timeline.controls.thumbnails'), - // v4.0.0 -> v4.1.0 - upgradeArrayValue( + upgradeArrayOfObjects( CONF_OVERRIDES, - transformConditionMediaLoaded, + conditionMediaLoadedTransform, (data) => data.conditions as RawFrigateCardConfig | undefined, ), (data: unknown): boolean => { - return recursiveUpgradeObject( - transformConditionMediaLoaded, + 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 recursiveUpgradeObject(transformFrigateUIAction)( + return upgradeObjectRecursively(frigateUIActionTransform)( typeof data === 'object' && data ? data : {}, ); }, - upgradeArrayValue( + upgradeArrayOfObjects( CONF_CAMERAS, upgradeWithOverrides('live_provider', (val) => val === 'frigate-jsmpeg' ? 'jsmpeg' : val, ), ), - upgradeMoveToWithOverrides('live.image', CONF_CAMERAS_GLOBAL_IMAGE), upgradeMoveToWithOverrides('live.jsmpeg', CONF_CAMERAS_GLOBAL_JSMPEG), + upgradeMoveToWithOverrides('live.image', CONF_CAMERAS_GLOBAL_IMAGE), upgradeMoveToWithOverrides('live.webrtc_card', CONF_CAMERAS_GLOBAL_WEBRTC_CARD), - upgradeArrayValue( + upgradeArrayOfObjects( CONF_CAMERAS, upgradeMoveToWithOverrides('frigate.zone', 'frigate.zones', { transform: (zone) => arrayify(zone), }), ), - upgradeArrayValue( + 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 ? data : {}, + ); + }, ]; diff --git a/src/config/types.ts b/src/config/types.ts index 5618ebb6..e5d44251 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -6,7 +6,7 @@ import { NoActionConfig, ToggleActionConfig, UrlActionConfig, -} from 'custom-card-helpers'; +} from '@dermotduffy/custom-card-helpers'; import { z } from 'zod'; import { MEDIA_CHUNK_SIZE_DEFAULT, MEDIA_CHUNK_SIZE_MAX } from '../const.js'; import { deepRemoveDefaults } from '../utils/zod.js'; @@ -133,7 +133,7 @@ const callServiceActionSchema = schemaForType< actionBaseSchema.extend({ action: z.literal('call-service'), service: z.string(), - service_data: z.object({}).passthrough().optional(), + data: z.object({}).passthrough().optional(), }), ); @@ -498,7 +498,7 @@ export const frigateCardPTZSchema = z.preprocess( tap_action: { action: 'call-service', service: data['service'], - service_data: data[`data_${name}`], + data: data[`data_${name}`], }, }; delete out[`data_${name}`]; diff --git a/src/editor.ts b/src/editor.ts index fac18b5c..ba410643 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,4 +1,4 @@ -import { fireEvent, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers'; +import { fireEvent, HomeAssistant, LovelaceCardEditor } from '@dermotduffy/custom-card-helpers'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; diff --git a/src/localize/localize.ts b/src/localize/localize.ts index 75d10059..2ecac5dd 100644 --- a/src/localize/localize.ts +++ b/src/localize/localize.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import * as en from './languages/en.json'; const DEFAULT_LANG = 'en' as const; diff --git a/src/types.ts b/src/types.ts index 8ac565c7..80527d0c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { HomeAssistant, LovelaceCardConfig, Themes } from 'custom-card-helpers'; +import { HomeAssistant, LovelaceCardConfig, Themes } from '@dermotduffy/custom-card-helpers'; import { StyleInfo } from 'lit/directives/style-map.js'; import { z } from 'zod'; diff --git a/src/utils/action.ts b/src/utils/action.ts index fcf055ec..8b0e60d4 100644 --- a/src/utils/action.ts +++ b/src/utils/action.ts @@ -3,7 +3,7 @@ import { handleActionConfig, hasAction, HomeAssistant, -} from 'custom-card-helpers'; +} from '@dermotduffy/custom-card-helpers'; import { Actions, ActionType, diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts index 32c97bf7..b64447ca 100644 --- a/src/utils/diagnostics.ts +++ b/src/utils/diagnostics.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import pkg from '../../package.json'; import { RawFrigateCardConfig } from '../config/types'; import { getLanguage } from '../localize/localize'; diff --git a/src/utils/ha/browse-media/browse-media-manager.ts b/src/utils/ha/browse-media/browse-media-manager.ts index d46d988a..c5a66800 100644 --- a/src/utils/ha/browse-media/browse-media-manager.ts +++ b/src/utils/ha/browse-media/browse-media-manager.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import add from 'date-fns/add'; import { homeAssistantWSRequest } from '..'; import { MemoryRequestCache } from '../../../camera-manager/cache'; diff --git a/src/utils/ha/device-registry.ts b/src/utils/ha/device-registry.ts index 45694418..43e5b109 100644 --- a/src/utils/ha/device-registry.ts +++ b/src/utils/ha/device-registry.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { z } from 'zod'; import { homeAssistantWSRequest } from '.'; diff --git a/src/utils/ha/entity-registry/index.ts b/src/utils/ha/entity-registry/index.ts index 79b1dfe5..69057849 100644 --- a/src/utils/ha/entity-registry/index.ts +++ b/src/utils/ha/entity-registry/index.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { homeAssistantWSRequest } from '..'; import { EntityCache } from './cache'; import { Entity, EntityList, entitySchema, entityListSchema } from './types.js'; diff --git a/src/utils/ha/entity-state-translation.ts b/src/utils/ha/entity-state-translation.ts index 2a4e26fe..f09a4fac 100644 --- a/src/utils/ha/entity-state-translation.ts +++ b/src/utils/ha/entity-state-translation.ts @@ -1,4 +1,4 @@ -import { computeDomain, HomeAssistant } from 'custom-card-helpers'; +import { computeDomain, HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { HassEntity } from 'home-assistant-js-websocket'; import { Entity } from './entity-registry/types'; diff --git a/src/utils/ha/index.ts b/src/utils/ha/index.ts index 2b8d4ff4..b8d46426 100644 --- a/src/utils/ha/index.ts +++ b/src/utils/ha/index.ts @@ -1,4 +1,4 @@ -import { computeDomain, computeStateDomain, HomeAssistant } from 'custom-card-helpers'; +import { computeDomain, computeStateDomain, HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { HassEntity, MessageBase } from 'home-assistant-js-websocket'; import { StyleInfo } from 'lit/directives/style-map.js'; import { ZodSchema } from 'zod'; diff --git a/src/utils/ha/resolved-media.ts b/src/utils/ha/resolved-media.ts index d948a69c..2824f9bb 100644 --- a/src/utils/ha/resolved-media.ts +++ b/src/utils/ha/resolved-media.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import QuickLRU from 'quick-lru'; import { homeAssistantWSRequest } from '.'; import { ResolvedMedia, resolvedMediaSchema } from '../../types.js'; diff --git a/src/utils/icons/domain-icon.ts b/src/utils/icons/domain-icon.ts index 12dbdc99..b995aa76 100644 --- a/src/utils/icons/domain-icon.ts +++ b/src/utils/icons/domain-icon.ts @@ -5,7 +5,7 @@ import { binarySensorIcon } from './binary-sensor-icon'; import { coverIcon } from './cover-icon'; import { sensorIcon } from './sensor-icon'; -export const DEFAULT_DOMAIN_ICON = 'mdi:bookmark'; +const DEFAULT_DOMAIN_ICON = 'mdi:bookmark'; const FIXED_DOMAIN_ICONS = { alert: 'mdi:alert', diff --git a/src/utils/icons/sensor-icon.ts b/src/utils/icons/sensor-icon.ts index 8331a9aa..e9000472 100644 --- a/src/utils/icons/sensor-icon.ts +++ b/src/utils/icons/sensor-icon.ts @@ -1,4 +1,4 @@ -import { UNIT_C, UNIT_F } from 'custom-card-helpers'; +import { UNIT_C, UNIT_F } from '@dermotduffy/custom-card-helpers'; import { HassEntity } from 'home-assistant-js-websocket'; const FIXED_DEVICE_CLASS_ICONS = { diff --git a/src/utils/thumbnail.ts b/src/utils/thumbnail.ts index 1c370cb7..6efdb8c9 100644 --- a/src/utils/thumbnail.ts +++ b/src/utils/thumbnail.ts @@ -1,6 +1,6 @@ import { Task } from '@lit-labs/task'; import { ReactiveControllerHost } from '@lit/reactive-element'; -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; // See: https://github.com/sindresorhus/is-absolute-url // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 diff --git a/tests/components-lib/menu-controller.test.ts b/tests/components-lib/menu-controller.test.ts index 1711d4ec..590e27ac 100644 --- a/tests/components-lib/menu-controller.test.ts +++ b/tests/components-lib/menu-controller.test.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import isEqual from 'lodash-es/isEqual'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; diff --git a/tests/config-mgmt.test.ts b/tests/config-mgmt.test.ts new file mode 100644 index 00000000..4ca5eb32 --- /dev/null +++ b/tests/config-mgmt.test.ts @@ -0,0 +1,742 @@ +import { describe, expect, it } from 'vitest'; +import { + copyConfig, + createRangedTransform, + deleteConfigValue, + deleteTransform, + getArrayConfigPath, + getConfigValue, + isConfigUpgradeable, + moveConfigValue, + setConfigValue, + upgradeArrayOfObjects, + upgradeConfig, + upgradeMoveTo, + upgradeMoveToWithOverrides, + upgradeObjectRecursively, + upgradeWithOverrides, +} from '../src/config-mgmt'; +import { RawFrigateCardConfig } from '../src/config/types'; + +describe('general functions', () => { + it('should set value', () => { + const target = {}; + setConfigValue(target, 'a', 10); + expect(target).toEqual({ + a: 10, + }); + }); + + describe('should get value', () => { + it('present', () => { + expect(getConfigValue({ b: 11 }, 'b')).toEqual(11); + }); + it('absent', () => { + expect(getConfigValue({ b: 11 }, 'c')).toBeUndefined(); + }); + it('absent with default', () => { + expect(getConfigValue({ b: 11 }, 'c', 12)).toBe(12); + }); + }); + + describe('should unset value', () => { + it('nested', () => { + const target = { + moo: { + foo: { + a: 10, + }, + bar: { + b: 11, + }, + }, + }; + deleteConfigValue(target, 'moo.foo'); + expect(target).toEqual({ moo: { bar: { b: 11 } } }); + }); + + it('top-level', () => { + const target = { + a: 10, + b: 11, + }; + deleteConfigValue(target, 'a'); + expect(target).toEqual({ b: 11 }); + }); + }); + + it('should copy config', () => { + const target = { + a: { + b: { + c: 10, + }, + }, + }; + const copy = copyConfig(target); + + expect(copy).toEqual(target); + expect(copy).not.toBe(target); + }); + + it('should get array config path', () => { + expect(getArrayConfigPath('a.#.b', 10)).toBe('a.[10].b'); + }); +}); + +describe('upgrade functions', () => { + it('should determine if config is upgradeable', () => { + expect( + // Upgrade example: rename of service_data to data. + isConfigUpgradeable({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + elements: [ + { + type: 'icon', + icon: 'mdi:cow', + style: { + right: '20px', + top: '20px', + color: 'white', + }, + tap_action: { + action: 'call-service', + service: 'notify.persistent_notification', + service_data: { + message: 'Hello 1', + }, + }, + }, + ], + }), + ).toBeTruthy(); + }); + + describe('should create ranged transform', () => { + describe('with numbers', () => { + it('inside range', () => { + expect(createRangedTransform((val) => val, 10, 20)(11)).toBe(11); + }); + it('outside range', () => { + expect(createRangedTransform((val) => val, 10, 20)(1)).toBe(10); + }); + it('with a range', () => { + expect(createRangedTransform((val) => val)(100)).toBe(100); + }); + }); + it('with non-number', () => { + expect(createRangedTransform((_val) => 'foo')(1)).toBe('foo'); + }); + }); + + it('should return null from delete property transform', () => { + expect(deleteTransform(10)).toBeNull(); + }); + + describe('should move config value', () => { + it('simple', () => { + const config = { + foo: { + c: 10, + }, + }; + expect(moveConfigValue(config, 'foo', 'bar')).toBeTruthy(); + expect(config).toEqual({ + bar: { + c: 10, + }, + }); + }); + + describe('in place', () => { + it('non-transformed', () => { + const config = { + foo: { + c: 10, + }, + }; + expect(moveConfigValue(config, 'foo', 'foo')).toBeFalsy(); + expect(config).toEqual({ + foo: { + c: 10, + }, + }); + }); + + it('transformed', () => { + const config = { + foo: { + c: 10, + }, + }; + expect( + moveConfigValue(config, 'foo.c', 'foo.c', { transform: (val) => String(val) }), + ).toBeTruthy(); + expect(config).toEqual({ + foo: { + c: '10', + }, + }); + }); + }); + + describe('with transform result', () => { + it('move', () => { + const config = { + c: 10, + }; + expect( + moveConfigValue(config, 'c', 'd', { transform: (val) => String(val) }), + ).toBeTruthy(); + expect(config).toEqual({ d: '10' }); + }); + + it('keep original', () => { + const config = { + c: 10, + }; + expect( + moveConfigValue(config, 'c', 'd', { + transform: (val) => String(val), + keepOriginal: true, + }), + ).toBeTruthy(); + expect(config).toEqual({ c: 10, d: '10' }); + }); + }); + + describe('with transform null result', () => { + it('remove', () => { + const config = { + c: 10, + }; + expect( + moveConfigValue(config, 'c', 'd', { transform: (_val) => null }), + ).toBeTruthy(); + expect(config).toEqual({}); + }); + + it('keep', () => { + const config = { + c: 10, + }; + expect( + moveConfigValue(config, 'c', 'd', { + transform: (_val) => null, + keepOriginal: true, + }), + ).toBeFalsy(); + expect(config).toEqual({ c: 10 }); + }); + }); + + it('with transform undefined result', () => { + const config = { + c: 10, + }; + expect( + moveConfigValue(config, 'c', 'd', { transform: (_val) => undefined }), + ).toBeFalsy(); + expect(config).toEqual({ c: 10 }); + }); + }); + + it('should upgrade with a move', () => { + const config = { + c: 10, + }; + + expect(upgradeMoveTo('c', 'd')(config)).toBeTruthy(); + expect(config).toEqual({ d: 10 }); + }); + + it('should upgrade config and overrides with a move', () => { + const config = { + c: 10, + overrides: [ + { + overrides: { + c: 10, + }, + }, + ], + }; + + expect(upgradeMoveToWithOverrides('c', 'd')(config)).toBeTruthy(); + expect(config).toEqual({ d: 10, overrides: [{ overrides: { d: 10 } }] }); + }); + + it('should upgrade config and overrides in-place', () => { + const config = { + c: 10, + overrides: [ + { + overrides: { + c: 10, + }, + }, + ], + }; + + expect(upgradeWithOverrides('c', (val) => String(val))(config)).toBeTruthy(); + expect(config).toEqual({ c: '10', overrides: [{ overrides: { c: '10' } }] }); + }); + + describe('should upgrade array', () => { + it('in case of non-array', () => { + const config = { c: 10 }; + expect(upgradeArrayOfObjects('c', (_val) => false)(config)).toBeFalsy(); + }); + + it('in case of non-object items', () => { + const config = { c: [10, 11] }; + expect(upgradeArrayOfObjects('c', (_val) => false)(config)).toBeFalsy(); + }); + + it('in case of array', () => { + const config = { c: [{ d: 10 }, { d: 11 }] }; + expect( + upgradeArrayOfObjects('c', (val) => { + val['e'] = 12; + return true; + })(config), + ).toBeTruthy(); + expect(config).toEqual({ + c: [ + { + d: 10, + e: 12, + }, + { + d: 11, + e: 12, + }, + ], + }); + }); + }); + + describe('should recursively upgrade', () => { + it('ignoring simple objects', () => { + const config = { c: 10, d: 10 }; + expect(upgradeObjectRecursively((_val) => false)(config)).toBeFalsy(); + expect(config).toEqual({ c: 10, d: 10 }); + }); + it('iterating into arrays', () => { + const config = { values: [{ c: 10 }, { d: 10 }, 'random'] }; + expect( + upgradeObjectRecursively((val) => { + if (!Array.isArray(val)) { + val['e'] = 11; + } + return true; + })(config), + ).toBeTruthy(); + + expect(config).toEqual({ + e: 11, + values: [{ c: 10, e: 11 }, { d: 10, e: 11 }, 'random'], + }); + }); + }); + + it('should have upgrades with bad input data', () => { + expect(upgradeConfig(3 as unknown as RawFrigateCardConfig)).toBeFalsy(); + }); +}); + +describe('should handle version specific upgrades', () => { + describe('v4.1.0', () => { + describe('should rename mediaLoaded to media_loaded', () => { + it('elements', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + elements: [ + { + conditions: { + mediaLoaded: true, + }, + }, + { + conditions: 'not an object', + }, + ], + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + elements: [ + { + conditions: { + media_loaded: true, + }, + }, + { + conditions: 'not an object', + }, + ], + }); + }); + + it('overrides', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + overrides: [ + { + conditions: { + mediaLoaded: true, + }, + overrides: { + view: { + default: 'clips', + }, + }, + }, + ], + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + overrides: [ + { + conditions: { + media_loaded: true, + }, + overrides: { + view: { + default: 'clips', + }, + }, + }, + ], + }); + }); + }); + + it('should rename event_gallery to media_gallery', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + event_gallery: { + foo: 'bar', + }, + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + media_gallery: { + foo: 'bar', + }, + }); + }); + + it('should rename menu.buttons.frigate_ui to menu.buttons.camera_ui', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + menu: { + buttons: { + frigate_ui: { + foo: 'bar', + }, + }, + }, + overrides: [ + { + conditions: {}, + overrides: { + menu: { + buttons: { + frigate_ui: { + foo: 'bar', + }, + }, + }, + }, + }, + ], + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + menu: { + buttons: { + camera_ui: { + foo: 'bar', + }, + }, + }, + overrides: [ + { + conditions: {}, + overrides: { + menu: { + buttons: { + camera_ui: { + foo: 'bar', + }, + }, + }, + }, + }, + ], + }); + }); + + it('should rename frigate ui actions', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + elements: [ + { + type: 'icon', + icon: 'mdi:cow', + tap_action: { + action: 'custom:frigate-card-action', + frigate_card_action: 'frigate_ui', + }, + }, + ], + view: { + actions: { + double_tap_action: { + action: 'custom:frigate-card-action', + frigate_card_action: 'frigate_ui', + }, + }, + }, + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + elements: [ + { + type: 'icon', + icon: 'mdi:cow', + tap_action: { + action: 'custom:frigate-card-action', + frigate_card_action: 'camera_ui', + }, + }, + ], + view: { + actions: { + double_tap_action: { + action: 'custom:frigate-card-action', + frigate_card_action: 'camera_ui', + }, + }, + }, + }); + }); + + describe('should rename frigate-jsmpeg provider to jsmpeg', () => { + it('in cameras', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ live_provider: 'ha' }, { live_provider: 'frigate-jsmpeg' }], + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ live_provider: 'ha' }, { live_provider: 'jsmpeg' }], + }); + }); + }); + + describe('should move live object into cameras_global', () => { + it.each([['image' as const], ['jsmpeg' as const], ['webrtc_card' as const]])( + '%s', + (objName: string) => { + const config = { + type: 'custom:frigate-card', + live: { + [objName]: { + foo: 'bar', + }, + }, + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras_global: { + [objName]: { + foo: 'bar', + }, + }, + live: {}, + }); + }, + ); + }); + + describe('should convert to array and rename', () => { + it.each([['zone' as const], ['label' as const]])(`%s`, (objName: string) => { + const config = { + type: 'custom:frigate-card', + cameras: [ + { live_provider: 'ha', frigate: { [objName]: 'foo' } }, + { live_provider: 'jsmpeg' }, + ], + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [ + { live_provider: 'ha', frigate: { [objName + 's']: ['foo'] } }, + { live_provider: 'jsmpeg' }, + ], + }); + }); + }); + + it('should convert and rename frigate.label to frigate.labels', () => { + const config = { + type: 'custom:frigate-card', + cameras: [ + { live_provider: 'ha', frigate: { zone: 'foo' } }, + { live_provider: 'jsmpeg' }, + ], + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [ + { live_provider: 'ha', frigate: { zones: ['foo'] } }, + { live_provider: 'jsmpeg' }, + ], + }); + }); + }); + + describe('v5.2.0', () => { + describe('should rename service_data to data', () => { + it('positive case', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + elements: [ + { + type: 'icon', + icon: 'mdi:cow', + style: { + right: '20px', + top: '20px', + color: 'white', + }, + tap_action: { + action: 'call-service', + service: 'notify.persistent_notification', + service_data: { + message: 'Hello 1', + }, + }, + }, + { + type: 'service-button', + title: 'title', + service: 'service', + service_data: { + message: "It's a trick", + }, + }, + ], + view: { + actions: { + double_tap_action: { + action: 'call-service', + service: 'notify.persistent_notification', + service_data: { + message: 'Hello 2', + }, + }, + hold_action: { + action: 'call-service', + service: 'notify.persistent_notification', + data: { + message: 'Hello 3', + }, + }, + }, + }, + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + elements: [ + { + type: 'icon', + icon: 'mdi:cow', + style: { + right: '20px', + top: '20px', + color: 'white', + }, + tap_action: { + action: 'call-service', + service: 'notify.persistent_notification', + data: { + message: 'Hello 1', + }, + }, + }, + { + type: 'service-button', + title: 'title', + service: 'service', + // Trick: This *is* still called service_data in HA, so should not + // be modified. + service_data: { + message: "It's a trick", + }, + }, + ], + view: { + actions: { + double_tap_action: { + action: 'call-service', + service: 'notify.persistent_notification', + data: { + message: 'Hello 2', + }, + }, + hold_action: { + action: 'call-service', + service: 'notify.persistent_notification', + data: { + message: 'Hello 3', + }, + }, + }, + }, + }); + }); + it('negative case', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + view: { + default: 'live', + }, + }; + expect(upgradeConfig(config)).toBeFalsy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + view: { + default: 'live', + }, + }); + }); + }); + }); +}); diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts index 6793101b..6d762c68 100644 --- a/tests/config/types.test.ts +++ b/tests/config/types.test.ts @@ -335,7 +335,7 @@ describe('should convert webrtc card PTZ to Frigate card PTZ', () => { tap_action: { action: 'call-service', service: 'foo', - service_data: { + data: { tap_action: { action: 'none', }, diff --git a/tests/utils/action.test.ts b/tests/utils/action.test.ts index e03bfa7c..1a8c3e69 100644 --- a/tests/utils/action.test.ts +++ b/tests/utils/action.test.ts @@ -1,4 +1,4 @@ -import { handleActionConfig, hasAction } from 'custom-card-helpers'; +import { handleActionConfig, hasAction } from '@dermotduffy/custom-card-helpers'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { actionSchema } from '../../src/config/types'; @@ -13,7 +13,7 @@ import { } from '../../src/utils/action'; import { createHASS } from '../test-utils'; -vi.mock('custom-card-helpers'); +vi.mock('@dermotduffy/custom-card-helpers'); describe('convertActionToFrigateCardCustomAction', () => { it('should skip null action', () => { diff --git a/tests/utils/ha/index.test.ts b/tests/utils/ha/index.test.ts index 02903b8f..823f7ead 100644 --- a/tests/utils/ha/index.test.ts +++ b/tests/utils/ha/index.test.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { describe, expect, it } from 'vitest'; import { getEntityIcon, diff --git a/yarn.lock b/yarn.lock index 88775fdf..e298d324 100644 --- a/yarn.lock +++ b/yarn.lock @@ -603,6 +603,21 @@ __metadata: languageName: node linkType: hard +"@dermotduffy/custom-card-helpers@npm:^1.9.0": + version: 1.9.0 + resolution: "@dermotduffy/custom-card-helpers@npm:1.9.0" + dependencies: + "@formatjs/intl-utils": ^3.8.4 + home-assistant-js-websocket: ^6.0.1 + intl-messageformat: ^9.11.1 + lit: ^2.1.1 + rollup: ^2.63.0 + superstruct: ^0.15.3 + typescript: ^4.5.4 + checksum: 356b6ee436a9c07ce09f8ce435d39eae0448f3d9b4eaef1d32bd6ce3565d5a84c57624b3a28506d265bf55c86bb444e8211054bc9cdd0eb6cf08e1a5ca1e7e4d + languageName: node + linkType: hard + "@dermotduffy/panzoom@npm:^4.5.1": version: 4.5.1 resolution: "@dermotduffy/panzoom@npm:4.5.1" @@ -2379,21 +2394,6 @@ __metadata: languageName: node linkType: hard -"custom-card-helpers@npm:^1.9.0": - version: 1.9.0 - resolution: "custom-card-helpers@npm:1.9.0" - dependencies: - "@formatjs/intl-utils": ^3.8.4 - home-assistant-js-websocket: ^6.0.1 - intl-messageformat: ^9.11.1 - lit: ^2.1.1 - rollup: ^2.63.0 - superstruct: ^0.15.3 - typescript: ^4.5.4 - checksum: 5cefdad2487c1d9e6a190e1837637817c8f7ac86c970c2117fac9b57493f2b235d99af301a7bafcf0a53d1c42b7e75b2accda63de6381607d4beb6d175aa07f4 - languageName: node - linkType: hard - "data-urls@npm:^4.0.0": version: 4.0.0 resolution: "data-urls@npm:4.0.0" @@ -3324,6 +3324,7 @@ __metadata: "@babel/plugin-proposal-class-properties": ^7.18.6 "@babel/plugin-proposal-decorators": ^7.19.0 "@cycjimmy/jsmpeg-player": ^6.0.4 + "@dermotduffy/custom-card-helpers": ^1.9.0 "@dermotduffy/panzoom": ^4.5.1 "@egjs/hammerjs": ^2.0.17 "@graphiteds/core": ^1.9.11 @@ -3343,7 +3344,6 @@ __metadata: "@vitest/coverage-istanbul": ^0.34.3 component-emitter: ^1.3.0 crypto: ^1.0.1 - custom-card-helpers: ^1.9.0 date-fns: ^2.29.2 date-fns-tz: ^1.3.7 embla-carousel: 8.0.0-rc14