From 02fb6e1d4c06b5db4f11de28c91c443a081299ac Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 29 Sep 2024 15:55:28 -0700 Subject: [PATCH] refactor: Refactor entity register manager (#1597) --- src/camera-manager/browse-media/camera.ts | 4 +- .../browse-media/engine-browse-media.ts | 2 +- src/camera-manager/engine-factory.ts | 9 +- src/camera-manager/frigate/camera.ts | 9 +- src/camera-manager/frigate/engine-frigate.ts | 2 +- src/card-controller/controller.ts | 14 ++- src/card-controller/media-player-manager.ts | 2 +- src/card-controller/types.ts | 2 +- src/components/menu.ts | 2 +- src/components/submenu.ts | 2 +- src/utils/diagnostics.ts | 3 +- src/utils/ha/entity-state-translation.ts | 2 +- .../ha/{entity-registry => registry}/cache.ts | 23 ++-- .../device/index.ts} | 12 +- src/utils/ha/registry/device/types.ts | 12 ++ .../entity}/index.ts | 41 ++++--- .../entity}/types.ts | 0 .../browse-media/camera.test.ts | 6 +- tests/camera-manager/engine-factory.test.ts | 47 ++++++-- tests/camera-manager/frigate/camera.test.ts | 6 +- tests/camera-manager/store.test.ts | 4 +- tests/card-controller/controller.test.ts | 4 +- .../media-player-manager.test.ts | 2 +- tests/test-utils.ts | 4 +- tests/utils/diagnostics.test.ts | 6 +- tests/utils/ha/registry/cache.test.ts | 49 ++++++++ tests/utils/ha/registry/entity/index.test.ts | 110 ++++++++++++++++++ vite.config.ts | 2 +- 28 files changed, 290 insertions(+), 91 deletions(-) rename src/utils/ha/{entity-registry => registry}/cache.ts (59%) rename src/utils/ha/{device-registry.ts => registry/device/index.ts} (57%) create mode 100644 src/utils/ha/registry/device/types.ts rename src/utils/ha/{entity-registry => registry/entity}/index.ts (68%) rename src/utils/ha/{entity-registry => registry/entity}/types.ts (100%) create mode 100644 tests/utils/ha/registry/cache.test.ts create mode 100644 tests/utils/ha/registry/entity/index.test.ts diff --git a/src/camera-manager/browse-media/camera.ts b/src/camera-manager/browse-media/camera.ts index 031ef5c2..eace0a73 100644 --- a/src/camera-manager/browse-media/camera.ts +++ b/src/camera-manager/browse-media/camera.ts @@ -1,7 +1,7 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { localize } from '../../localize/localize'; -import { EntityRegistryManager } from '../../utils/ha/entity-registry'; -import { Entity } from '../../utils/ha/entity-registry/types'; +import { EntityRegistryManager } from '../../utils/ha/registry/entity'; +import { Entity } from '../../utils/ha/registry/entity/types'; import { Camera, CameraInitializationOptions } from '../camera'; import { CameraInitializationError } from '../error'; diff --git a/src/camera-manager/browse-media/engine-browse-media.ts b/src/camera-manager/browse-media/engine-browse-media.ts index 997cf9d3..da1a7fe4 100644 --- a/src/camera-manager/browse-media/engine-browse-media.ts +++ b/src/camera-manager/browse-media/engine-browse-media.ts @@ -10,7 +10,7 @@ import { MEDIA_CLASS_VIDEO, RichBrowseMedia, } from '../../utils/ha/browse-media/types'; -import { EntityRegistryManager } from '../../utils/ha/entity-registry'; +import { EntityRegistryManager } from '../../utils/ha/registry/entity'; import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media'; import { ViewMedia } from '../../view/media'; import { RequestCache } from '../cache'; diff --git a/src/camera-manager/engine-factory.ts b/src/camera-manager/engine-factory.ts index 00ac031b..47438c75 100644 --- a/src/camera-manager/engine-factory.ts +++ b/src/camera-manager/engine-factory.ts @@ -4,8 +4,7 @@ import { CameraConfig } from '../config/types'; import { localize } from '../localize/localize'; import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager'; import { BrowseMedia } from '../utils/ha/browse-media/types'; -import { EntityRegistryManager } from '../utils/ha/entity-registry'; -import { Entity } from '../utils/ha/entity-registry/types'; +import { EntityRegistryManager } from '../utils/ha/registry/entity'; import { ResolvedMediaCache } from '../utils/ha/resolved-media'; import { MemoryRequestCache, RecordingSegmentsCache, RequestCache } from './cache'; import { CameraManagerEngine } from './engine'; @@ -81,10 +80,8 @@ export class CameraManagerEngineFactory { const cameraEntity = getCameraEntityFromConfig(cameraConfig); if (cameraEntity) { - let entity: Entity | null; - try { - entity = await this._entityRegistryManager.getEntity(hass, cameraEntity); - } catch (e) { + const entity = await this._entityRegistryManager.getEntity(hass, cameraEntity); + if (!entity) { // If the camera is not in the registry, but is in the HA states it is // assumed to be a generic camera. if (hass.states[cameraEntity]) { diff --git a/src/camera-manager/frigate/camera.ts b/src/camera-manager/frigate/camera.ts index 34012192..28707947 100644 --- a/src/camera-manager/frigate/camera.ts +++ b/src/camera-manager/frigate/camera.ts @@ -5,8 +5,8 @@ import { CameraConfig } from '../../config/types'; import { localize } from '../../localize/localize'; import { PTZCapabilities, PTZMovementType } from '../../types'; import { errorToConsole } from '../../utils/basic'; -import { EntityRegistryManager } from '../../utils/ha/entity-registry'; -import { Entity } from '../../utils/ha/entity-registry/types'; +import { EntityRegistryManager } from '../../utils/ha/registry/entity'; +import { Entity } from '../../utils/ha/registry/entity/types'; import { Camera, CameraInitializationOptions } from '../camera'; import { Capabilities } from '../capabilities'; import { CameraManagerEngine } from '../engine'; @@ -67,9 +67,8 @@ export class FrigateCamera extends Camera { // Entity information is required if the Frigate camera name is missing, or // if the entity requires automatic resolution of motion/occupancy sensors. if (cameraEntity && (!hasCameraName || hasAutoTriggers)) { - try { - entity = await entityRegistryManager.getEntity(hass, cameraEntity); - } catch (e) { + entity = await entityRegistryManager.getEntity(hass, cameraEntity); + if (!entity) { throw new CameraInitializationError(localize('error.no_camera_entity'), config); } } diff --git a/src/camera-manager/frigate/engine-frigate.ts b/src/camera-manager/frigate/engine-frigate.ts index d8d871e5..802874ef 100644 --- a/src/camera-manager/frigate/engine-frigate.ts +++ b/src/camera-manager/frigate/engine-frigate.ts @@ -15,7 +15,7 @@ import { runWhenIdleIfSupported, } from '../../utils/basic'; import { getEntityTitle } from '../../utils/ha'; -import { EntityRegistryManager } from '../../utils/ha/entity-registry'; +import { EntityRegistryManager } from '../../utils/ha/registry/entity'; import { ViewMedia } from '../../view/media'; import { ViewMediaClassifier } from '../../view/media-classifier'; import { RecordingSegmentsCache, RequestCache } from '../cache'; diff --git a/src/card-controller/controller.ts b/src/card-controller/controller.ts index 859f42ab..02ce4624 100644 --- a/src/card-controller/controller.ts +++ b/src/card-controller/controller.ts @@ -2,11 +2,12 @@ import { LovelaceCardEditor } from '@dermotduffy/custom-card-helpers'; import { ReactiveController } from 'lit'; import { CameraManager } from '../camera-manager/manager'; import { FrigateCardConfig } from '../config/types'; -import { EntityRegistryManager } from '../utils/ha/entity-registry'; -import { EntityCache } from '../utils/ha/entity-registry/cache'; +import { + createEntityRegistryCache, + EntityRegistryManager, +} from '../utils/ha/registry/entity'; import { ResolvedMediaCache } from '../utils/ha/resolved-media'; import { ActionsManager } from './actions/actions-manager'; -import { DefaultManager } from './default-manager'; import { AutomationsManager } from './automations-manager'; import { CameraURLManager } from './camera-url-manager'; import { @@ -17,12 +18,14 @@ import { } from './card-element-manager'; import { ConditionsManager, ConditionsManagerListener } from './conditions-manager'; import { ConfigManager } from './config/config-manager'; +import { DefaultManager } from './default-manager'; import { DownloadManager } from './download-manager'; import { ExpandManager } from './expand-manager'; import { FullscreenManager } from './fullscreen-manager'; import { HASSManager } from './hass/hass-manager'; import { InitializationManager } from './initialization-manager'; import { InteractionManager } from './interaction-manager'; +import { KeyboardStateManager } from './keyboard-state-manager'; import { MediaLoadedInfoManager } from './media-info-manager'; import { MediaPlayerManager } from './media-player-manager'; import { MessageManager } from './message-manager'; @@ -57,7 +60,6 @@ import { CardViewAPI, } from './types'; import { ViewManager } from './view/view-manager'; -import { KeyboardStateManager } from './keyboard-state-manager'; export class CardController implements @@ -88,7 +90,9 @@ export class CardController { // These properties may be used in the construction of 'managers' (and should // be created first). - protected _entityRegistryManager = new EntityRegistryManager(new EntityCache()); + protected _entityRegistryManager = new EntityRegistryManager( + createEntityRegistryCache(), + ); protected _resolvedMediaCache = new ResolvedMediaCache(); protected _actionsManager = new ActionsManager(this); diff --git a/src/card-controller/media-player-manager.ts b/src/card-controller/media-player-manager.ts index ff26d201..76095eab 100644 --- a/src/card-controller/media-player-manager.ts +++ b/src/card-controller/media-player-manager.ts @@ -2,7 +2,7 @@ import { CameraConfig, FrigateCardConfig } from '../config/types'; import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const'; import { localize } from '../localize/localize'; import { errorToConsole } from '../utils/basic'; -import { Entity } from '../utils/ha/entity-registry/types'; +import { Entity } from '../utils/ha/registry/entity/types'; import { supportsFeature } from '../utils/ha/update'; import { ViewMedia } from '../view/media'; import { ViewMediaClassifier } from '../view/media-classifier'; diff --git a/src/card-controller/types.ts b/src/card-controller/types.ts index 41884bb5..f55dae66 100644 --- a/src/card-controller/types.ts +++ b/src/card-controller/types.ts @@ -1,6 +1,6 @@ import type { CameraManager } from '../camera-manager/manager'; import type { Automation } from '../config/types'; -import type { EntityRegistryManager } from '../utils/ha/entity-registry'; +import type { EntityRegistryManager } from '../utils/ha/registry/entity'; import type { ResolvedMediaCache } from '../utils/ha/resolved-media'; import type { ActionsManager } from './actions/actions-manager'; import type { AutomationsManager } from './automations-manager'; diff --git a/src/components/menu.ts b/src/components/menu.ts index 1a37fb73..4ced31bb 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -8,7 +8,7 @@ import { MenuController } from '../components-lib/menu-controller.js'; import type { MenuConfig, MenuItem } from '../config/types.js'; import menuStyle from '../scss/menu.scss'; import { frigateCardHasAction } from '../utils/action.js'; -import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js'; +import { EntityRegistryManager } from '../utils/ha/registry/entity/index.js'; import './submenu.js'; @customElement('frigate-card-menu') diff --git a/src/components/submenu.ts b/src/components/submenu.ts index 4bc361f0..2e675920 100644 --- a/src/components/submenu.ts +++ b/src/components/submenu.ts @@ -19,8 +19,8 @@ import { stopEventFromActivatingCardWideActions, } from '../utils/action.js'; import { isHassDifferent, refreshDynamicStateParameters } from '../utils/ha'; -import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js'; import { getEntityStateTranslation } from '../utils/ha/entity-state-translation.js'; +import { EntityRegistryManager } from '../utils/ha/registry/entity/index.js'; import { domainIcon } from '../utils/icons/domain-icon.js'; @customElement('frigate-card-submenu') diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts index a36f92c5..05706f4a 100644 --- a/src/utils/diagnostics.ts +++ b/src/utils/diagnostics.ts @@ -2,7 +2,8 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import pkg from '../../package.json'; import { RawFrigateCardConfig } from '../config/types'; import { getLanguage } from '../localize/localize'; -import { DeviceList, getAllDevices } from './ha/device-registry'; +import { getAllDevices } from './ha/registry/device'; +import { DeviceList } from './ha/registry/device/types'; type FrigateVersions = Record; diff --git a/src/utils/ha/entity-state-translation.ts b/src/utils/ha/entity-state-translation.ts index f09a4fac..1a44ae20 100644 --- a/src/utils/ha/entity-state-translation.ts +++ b/src/utils/ha/entity-state-translation.ts @@ -1,6 +1,6 @@ import { computeDomain, HomeAssistant } from '@dermotduffy/custom-card-helpers'; import { HassEntity } from 'home-assistant-js-websocket'; -import { Entity } from './entity-registry/types'; +import { Entity } from './registry/entity/types'; /** * Get the translation of an entity state. Inspired by: diff --git a/src/utils/ha/entity-registry/cache.ts b/src/utils/ha/registry/cache.ts similarity index 59% rename from src/utils/ha/entity-registry/cache.ts rename to src/utils/ha/registry/cache.ts index 36056805..9b955ba4 100644 --- a/src/utils/ha/entity-registry/cache.ts +++ b/src/utils/ha/registry/cache.ts @@ -1,7 +1,10 @@ -import { Entity } from './types.js'; +export class RegistryCache { + protected _cache: Map = new Map(); + protected _keyCallback: (_data: T) => string; -export class EntityCache { - protected _cache: Map = new Map(); + constructor(keyCallback: (_data: T) => string) { + this._keyCallback = keyCallback; + } /** * Determine if the cache has a given entity_id. @@ -17,11 +20,7 @@ export class EntityCache { * @param func A callback function that returns a boolean. * @returns The first matching value. */ - // public getFirstMatch(func: (arg: T) => boolean): T | null { - // return [...this._cache.values()].find(func) ?? null; - // } - - public getMatches(func: (arg: Entity) => boolean): Entity[] { + public getMatches(func: (arg: T) => boolean): T[] { return [...this._cache.values()].filter(func); } @@ -30,16 +29,16 @@ export class EntityCache { * @param id The entity id. * @returns The entity for this id. */ - public get(id: string): Entity | undefined { - return this._cache.get(id); + public get(id: string): T | null { + return this._cache.get(id) ?? null; } /** * Add a given entity to the cache. * @param input The entity. */ - public set(input: Entity | Entity[]): void { - const _set = (entity: Entity) => this._cache.set(entity.entity_id, entity); + public add(input: T | T[]): void { + const _set = (arg: T) => this._cache.set(this._keyCallback(arg), arg); if (Array.isArray(input)) { input.forEach(_set); diff --git a/src/utils/ha/device-registry.ts b/src/utils/ha/registry/device/index.ts similarity index 57% rename from src/utils/ha/device-registry.ts rename to src/utils/ha/registry/device/index.ts index 0bc1b187..54d39f94 100644 --- a/src/utils/ha/device-registry.ts +++ b/src/utils/ha/registry/device/index.ts @@ -1,14 +1,6 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; -import { z } from 'zod'; -import { homeAssistantWSRequest } from '.'; - -const deviceSchema = z.object({ - model: z.string().nullable(), - config_entries: z.string().array(), - manufacturer: z.string().nullable(), -}); -const deviceListSchema = deviceSchema.array(); -export type DeviceList = z.infer; +import { homeAssistantWSRequest } from '../..'; +import { DeviceList, deviceListSchema } from './types'; /** * Get a list of all entities from the entity registry. May throw. diff --git a/src/utils/ha/registry/device/types.ts b/src/utils/ha/registry/device/types.ts new file mode 100644 index 00000000..a70e6f2e --- /dev/null +++ b/src/utils/ha/registry/device/types.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const deviceSchema = z.object({ + id: z.string(), + model: z.string().nullable(), + config_entries: z.string().array(), + manufacturer: z.string().nullable(), +}); +export type Device = z.infer; + +export const deviceListSchema = deviceSchema.array(); +export type DeviceList = z.infer; diff --git a/src/utils/ha/entity-registry/index.ts b/src/utils/ha/registry/entity/index.ts similarity index 68% rename from src/utils/ha/entity-registry/index.ts rename to src/utils/ha/registry/entity/index.ts index 69057849..191cd243 100644 --- a/src/utils/ha/entity-registry/index.ts +++ b/src/utils/ha/registry/entity/index.ts @@ -1,17 +1,21 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; -import { homeAssistantWSRequest } from '..'; -import { EntityCache } from './cache'; +import { homeAssistantWSRequest } from '../..'; import { Entity, EntityList, entitySchema, entityListSchema } from './types.js'; +import { RegistryCache } from '../cache'; + +export const createEntityRegistryCache = (): RegistryCache => { + return new RegistryCache((entity) => entity.entity_id); +}; // This class manages interactions with entities, caching results and fetching // as necessary. Some calls require every entity to be fetched, which may be -// non-trivial in size (after which it is cached forever). +// non-trivial in size (after which they are cached forever). export class EntityRegistryManager { - protected _cache: EntityCache; + protected _cache: RegistryCache; protected _fetchedEntityList = false; - constructor(cache: EntityCache) { + constructor(cache: RegistryCache) { this._cache = cache; } @@ -21,11 +25,16 @@ export class EntityRegistryManager { return cachedEntity; } - const entity = await homeAssistantWSRequest(hass, entitySchema, { - type: 'config/entity_registry/get', - entity_id: entityID, - }); - this._cache.set(entity); + let entity: Entity | null = null; + try { + entity = await homeAssistantWSRequest(hass, entitySchema, { + type: 'config/entity_registry/get', + entity_id: entityID, + }); + } catch { + return null; + } + this._cache.add(entity); return entity; } @@ -43,15 +52,11 @@ export class EntityRegistryManager { ): Promise> { const output: Map = new Map(); const _storeEntity = async (entityID: string): Promise => { - let entity: Entity | null = null; - try { - entity = await this.getEntity(hass, entityID); - } catch { + const entity = await this.getEntity(hass, entityID); + + if (entity) { // When asked to fetch multiple entities, ignore missing entities (they // will just not feature in the output). - return; - } - if (entity) { output.set(entityID, entity); } }; @@ -66,7 +71,7 @@ export class EntityRegistryManager { const entityList = await homeAssistantWSRequest(hass, entityListSchema, { type: 'config/entity_registry/list', }); - this._cache.set(entityList); + this._cache.add(entityList); this._fetchedEntityList = true; } } diff --git a/src/utils/ha/entity-registry/types.ts b/src/utils/ha/registry/entity/types.ts similarity index 100% rename from src/utils/ha/entity-registry/types.ts rename to src/utils/ha/registry/entity/types.ts diff --git a/tests/camera-manager/browse-media/camera.test.ts b/tests/camera-manager/browse-media/camera.test.ts index eaf96699..d5e76269 100644 --- a/tests/camera-manager/browse-media/camera.test.ts +++ b/tests/camera-manager/browse-media/camera.test.ts @@ -2,10 +2,10 @@ import { describe, expect, it } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { BrowseMediaCamera } from '../../../src/camera-manager/browse-media/camera'; import { CameraManagerEngine } from '../../../src/camera-manager/engine'; -import { EntityRegistryManager } from '../../../src/utils/ha/entity-registry'; -import { Entity } from '../../../src/utils/ha/entity-registry/types'; -import { createCameraConfig, createHASS } from '../../test-utils'; import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher'; +import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity'; +import { Entity } from '../../../src/utils/ha/registry/entity/types'; +import { createCameraConfig, createHASS } from '../../test-utils'; describe('BrowseMediaCamera', () => { describe('should initialize', () => { diff --git a/tests/camera-manager/engine-factory.test.ts b/tests/camera-manager/engine-factory.test.ts index fe0e9ac1..7a38e62c 100644 --- a/tests/camera-manager/engine-factory.test.ts +++ b/tests/camera-manager/engine-factory.test.ts @@ -7,8 +7,10 @@ import { MotionEyeCameraManagerEngine } from '../../src/camera-manager/motioneye import { Engine } from '../../src/camera-manager/types.js'; import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js'; import { CardWideConfig } from '../../src/config/types.js'; -import { EntityRegistryManager } from '../../src/utils/ha/entity-registry'; -import { EntityCache } from '../../src/utils/ha/entity-registry/cache'; +import { + createEntityRegistryCache, + EntityRegistryManager, +} from '../../src/utils/ha/registry/entity/index.js'; import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media'; import { createCameraConfig, @@ -25,7 +27,8 @@ const createFactory = (options?: { cardWideConfig?: CardWideConfig; }): CameraManagerEngineFactory => { return new CameraManagerEngineFactory( - options?.entityRegistryManager ?? new EntityRegistryManager(new EntityCache()), + options?.entityRegistryManager ?? + new EntityRegistryManager(createEntityRegistryCache()), ); }; @@ -40,7 +43,9 @@ describe('getEngineForCamera()', () => { it('from auto detection', async () => { const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' }); - const entityRegistryManager = new EntityRegistryManager(new EntityCache()); + const entityRegistryManager = new EntityRegistryManager( + createEntityRegistryCache(), + ); entityRegistryManager.getEntity = vi .fn() @@ -75,7 +80,9 @@ describe('getEngineForCamera()', () => { it('from auto detection', async () => { const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' }); - const entityRegistryManager = new EntityRegistryManager(new EntityCache()); + const entityRegistryManager = new EntityRegistryManager( + createEntityRegistryCache(), + ); entityRegistryManager.getEntity = vi .fn() @@ -101,7 +108,9 @@ describe('getEngineForCamera()', () => { it('from auto detection', async () => { const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' }); - const entityRegistryManager = new EntityRegistryManager(new EntityCache()); + const entityRegistryManager = new EntityRegistryManager( + createEntityRegistryCache(), + ); entityRegistryManager.getEntity = vi .fn() @@ -121,9 +130,11 @@ describe('getEngineForCamera()', () => { engine: 'auto', webrtc_card: { entity: 'camera.foo' }, }); - const entityRegistryManager = new EntityRegistryManager(new EntityCache()); + const entityRegistryManager = new EntityRegistryManager( + createEntityRegistryCache(), + ); - entityRegistryManager.getEntity = vi.fn().mockRejectedValue(new Error()); + entityRegistryManager.getEntity = vi.fn().mockResolvedValue(null); expect( await createFactory({ @@ -137,6 +148,24 @@ describe('getEngineForCamera()', () => { ).toBe(Engine.Generic); }); + it('from entity not in registry and not in state', async () => { + const config = createCameraConfig({ + engine: 'auto', + webrtc_card: { entity: 'camera.foo' }, + }); + const entityRegistryManager = new EntityRegistryManager( + createEntityRegistryCache(), + ); + entityRegistryManager.getEntity = vi.fn().mockResolvedValue(null); + + expect( + async () => + await createFactory({ + entityRegistryManager: entityRegistryManager, + }).getEngineForCamera(createHASS(), config), + ).rejects.toThrow(/Could not find camera entity/); + }); + it('from webrtc-card url', async () => { const config = createCameraConfig({ engine: 'auto', @@ -167,7 +196,7 @@ describe('getEngineForCamera()', () => { it('should throw error on invalid entity', async () => { const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' }); - const entityRegistryManager = new EntityRegistryManager(new EntityCache()); + const entityRegistryManager = new EntityRegistryManager(createEntityRegistryCache()); entityRegistryManager.getEntity = vi.fn().mockRejectedValue(new Error()); diff --git a/tests/camera-manager/frigate/camera.test.ts b/tests/camera-manager/frigate/camera.test.ts index 470d0b65..81aadf01 100644 --- a/tests/camera-manager/frigate/camera.test.ts +++ b/tests/camera-manager/frigate/camera.test.ts @@ -7,8 +7,8 @@ import { getPTZInfo } from '../../../src/camera-manager/frigate/requests'; import { FrigateEventChange } from '../../../src/camera-manager/frigate/types'; import { StateWatcher } from '../../../src/card-controller/hass/state-watcher'; import { CameraTriggerEventType } from '../../../src/config/types'; -import { EntityRegistryManager } from '../../../src/utils/ha/entity-registry'; -import { Entity } from '../../../src/utils/ha/entity-registry/types'; +import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity'; +import { Entity } from '../../../src/utils/ha/registry/entity/types'; import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils'; vi.mock('../../../src/camera-manager/frigate/requests'); @@ -54,7 +54,7 @@ describe('FrigateCamera', () => { mock(), ); const entityRegistryManager = mock(); - entityRegistryManager.getEntity.mockRejectedValue(null); + entityRegistryManager.getEntity.mockResolvedValue(null); expect( async () => diff --git a/tests/camera-manager/store.test.ts b/tests/camera-manager/store.test.ts index f0184b83..0cfdf350 100644 --- a/tests/camera-manager/store.test.ts +++ b/tests/camera-manager/store.test.ts @@ -5,10 +5,10 @@ import { Capabilities } from '../../src/camera-manager/capabilities.js'; import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js'; import { CameraManagerStore } from '../../src/camera-manager/store.js'; import { Engine } from '../../src/camera-manager/types.js'; -import { EntityRegistryManager } from '../../src/utils/ha/entity-registry/index.js'; +import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js'; +import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/index.js'; import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js'; import { TestViewMedia, createCameraConfig } from '../test-utils.js'; -import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js'; describe('CameraManagerStore', async () => { const configVisible = createCameraConfig({ diff --git a/tests/card-controller/controller.test.ts b/tests/card-controller/controller.test.ts index a211175e..d5268e4f 100644 --- a/tests/card-controller/controller.test.ts +++ b/tests/card-controller/controller.test.ts @@ -28,7 +28,7 @@ import { StyleManager } from '../../src/card-controller/style-manager'; import { TriggersManager } from '../../src/card-controller/triggers-manager'; import { ViewManager } from '../../src/card-controller/view/view-manager'; import { FrigateCardEditor } from '../../src/editor'; -import { EntityRegistryManager } from '../../src/utils/ha/entity-registry'; +import { EntityRegistryManager } from '../../src/utils/ha/registry/entity'; import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media'; vi.mock('../../src/camera-manager/manager'); @@ -55,7 +55,7 @@ vi.mock('../../src/card-controller/status-bar-item-manager'); vi.mock('../../src/card-controller/style-manager'); vi.mock('../../src/card-controller/triggers-manager'); vi.mock('../../src/card-controller/view/view-manager'); -vi.mock('../../src/utils/ha/entity-registry'); +vi.mock('../../src/utils/ha/registry/entity'); vi.mock('../../src/utils/ha/resolved-media'); const createCardElement = (): CardHTMLElement => { diff --git a/tests/card-controller/media-player-manager.test.ts b/tests/card-controller/media-player-manager.test.ts index 85776868..633f0539 100644 --- a/tests/card-controller/media-player-manager.test.ts +++ b/tests/card-controller/media-player-manager.test.ts @@ -3,7 +3,7 @@ import { mock } from 'vitest-mock-extended'; import { MediaPlayerManager } from '../../src/card-controller/media-player-manager'; import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../src/const'; import { ExtendedHomeAssistant } from '../../src/types'; -import { EntityRegistryManager } from '../../src/utils/ha/entity-registry'; +import { EntityRegistryManager } from '../../src/utils/ha/registry/entity'; import { createCameraConfig, createCameraManager, diff --git a/tests/test-utils.ts b/tests/test-utils.ts index d35bbfcb..b66acbcb 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -55,8 +55,8 @@ import { } from '../src/config/types'; import { CapabilitiesRaw, ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types'; import { HassStateDifference } from '../src/utils/ha'; -import { EntityRegistryManager } from '../src/utils/ha/entity-registry'; -import { Entity } from '../src/utils/ha/entity-registry/types'; +import { EntityRegistryManager } from '../src/utils/ha/registry/entity'; +import { Entity } from '../src/utils/ha/registry/entity/types'; import { ViewMedia, ViewMediaType } from '../src/view/media'; import { MediaQueriesResults } from '../src/view/media-queries-results'; import { View, ViewParameters } from '../src/view/view'; diff --git a/tests/utils/diagnostics.test.ts b/tests/utils/diagnostics.test.ts index a020e264..5befe19c 100644 --- a/tests/utils/diagnostics.test.ts +++ b/tests/utils/diagnostics.test.ts @@ -2,7 +2,7 @@ import { HassConfig } from 'home-assistant-js-websocket'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getLanguage } from '../../src/localize/localize'; import { getDiagnostics, getReleaseVersion } from '../../src/utils/diagnostics.js'; -import { getAllDevices } from '../../src/utils/ha/device-registry.js'; +import { getAllDevices } from '../../src/utils/ha/registry/device/index.js'; import { createHASS } from '../test-utils'; vi.mock('../../package.json', () => ({ @@ -14,7 +14,7 @@ vi.mock('../../package.json', () => ({ })); vi.mock('../../src/utils/ha'); vi.mock('../../src/localize/localize.js'); -vi.mock('../../src/utils/ha/device-registry'); +vi.mock('../../src/utils/ha/registry/device/index.js'); describe('getReleaseVersion', () => { it('should get release version', () => { @@ -37,6 +37,7 @@ describe('getDiagnostics', () => { vi.mocked(getAllDevices).mockResolvedValue([ { + id: 'id', model: '4.0.0/0.13.0-aded314', config_entries: [ 'ac4e79d258449a83bc0cf6d47a021c46', @@ -97,6 +98,7 @@ describe('getDiagnostics', () => { it('should fetch diagnostics without device model', async () => { vi.mocked(getAllDevices).mockResolvedValue([ { + id: 'id', model: null, config_entries: [ 'ac4e79d258449a83bc0cf6d47a021c46', diff --git a/tests/utils/ha/registry/cache.test.ts b/tests/utils/ha/registry/cache.test.ts new file mode 100644 index 00000000..ba6ffc65 --- /dev/null +++ b/tests/utils/ha/registry/cache.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest'; +import { RegistryCache } from '../../../../src/utils/ha/registry/cache'; + +interface TestCacheValue { + id: string; + val?: number; +} + +describe('RegistryCache', () => { + describe('has', () => { + it('positive', () => { + const cache = new RegistryCache((arg) => arg.id); + cache.add({ id: 'test' }); + expect(cache.has('test')).toBeTruthy(); + }); + + it('negative', () => { + const cache = new RegistryCache((arg) => arg.id); + cache.add({ id: 'test' }); + expect(cache.has('absent')).toBeFalsy(); + }); + }); + + it('getMatches', () => { + const cache = new RegistryCache((arg) => arg.id); + cache.add([ + { id: 'test-1', val: 1 }, + { id: 'test-5', val: 5 }, + { id: 'test-8', val: 8 }, + ]); + expect(cache.getMatches((obj) => !!obj.val && obj.val >= 5)).toEqual([ + { id: 'test-5', val: 5 }, + { id: 'test-8', val: 8 }, + ]); + }); + + describe('get', () => { + it('positive', () => { + const cache = new RegistryCache((arg) => arg.id); + cache.add({ id: 'test', val: 42 }); + expect(cache.get('test')).toEqual({ id: 'test', val: 42 }); + }); + + it('negative', () => { + const cache = new RegistryCache((arg) => arg.id); + expect(cache.get('test')).toBeNull(); + }); + }); +}); diff --git a/tests/utils/ha/registry/entity/index.test.ts b/tests/utils/ha/registry/entity/index.test.ts new file mode 100644 index 00000000..0f143fb2 --- /dev/null +++ b/tests/utils/ha/registry/entity/index.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { homeAssistantWSRequest } from '../../../../../src/utils/ha'; +import { + createEntityRegistryCache, + EntityRegistryManager, +} from '../../../../../src/utils/ha/registry/entity'; +import { createHASS, createRegistryEntity } from '../../../../test-utils.js'; + +vi.mock('../../../../../src/utils/ha'); + +describe('EntityRegistryManager', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getEntity', () => { + it('should not fetch when cached', async () => { + const cache = createEntityRegistryCache(); + const testEntity = createRegistryEntity({ entity_id: 'test' }); + + cache.add(testEntity); + + const manager = new EntityRegistryManager(cache); + expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity); + + expect(homeAssistantWSRequest).not.toHaveBeenCalled(); + }); + + it('should fetch and cache when not cached', async () => { + const testEntity = createRegistryEntity({ entity_id: 'test' }); + + const manager = new EntityRegistryManager(createEntityRegistryCache()); + vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(testEntity); + + expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity); + expect(homeAssistantWSRequest).toBeCalledTimes(1); + + expect(await manager.getEntity(createHASS(), 'test')).toEqual(testEntity); + expect(homeAssistantWSRequest).toBeCalledTimes(1); + }); + + it('should return null when entity does not exist', async () => { + vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found')); + + const manager = new EntityRegistryManager(createEntityRegistryCache()); + expect(await manager.getEntity(createHASS(), 'missing')).toBeNull(); + }); + }); + + it('getEntities', () => { + const cachedEntity = createRegistryEntity({ entity_id: 'cached' }); + const notCachedEntity = createRegistryEntity({ entity_id: 'not-cached' }); + + const cache = createEntityRegistryCache(); + cache.add(cachedEntity); + + const manager = new EntityRegistryManager(cache); + vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(notCachedEntity); + vi.mocked(homeAssistantWSRequest).mockRejectedValueOnce(new Error('Not found')); + + expect( + manager.getEntities(createHASS(), ['cached', 'not-cached', 'missing']), + ).resolves.toEqual( + new Map([ + ['cached', cachedEntity], + ['not-cached', notCachedEntity], + ]), + ); + }); + + it('fetchEntityList', async () => { + const hass = createHASS(); + const entity = createRegistryEntity({ entity_id: 'cached' }); + vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([entity]); + + const manager = new EntityRegistryManager(createEntityRegistryCache()); + + await manager.fetchEntityList(hass); + + expect(homeAssistantWSRequest).toBeCalledTimes(1); + expect(homeAssistantWSRequest).toBeCalledWith(expect.anything(), expect.anything(), { + type: 'config/entity_registry/list', + }); + + expect(await manager.getEntity(hass, 'cached')).toEqual(entity); + expect(homeAssistantWSRequest).toBeCalledTimes(1); + + await manager.fetchEntityList(hass); + expect(homeAssistantWSRequest).toBeCalledTimes(1); + }); + + it('getMatchingEntities', async () => { + const matchingEntity = createRegistryEntity({ entity_id: 'matching' }); + const notMatchingEntity = createRegistryEntity({ entity_id: 'not-matching' }); + const hass = createHASS(); + + vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce([ + matchingEntity, + notMatchingEntity, + ]); + + const manager = new EntityRegistryManager(createEntityRegistryCache()); + expect( + await manager.getMatchingEntities( + hass, + (entity) => entity.entity_id == 'matching', + ), + ).toEqual([matchingEntity]); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index f5ddd5f1..e85dffcd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,7 +25,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [ 'utils/download.ts', 'utils/embla/**/*.ts', 'utils/endpoint.ts', - 'utils/ha/entity-registry/types.ts', + 'utils/ha/registry/entity/**/*.ts', 'utils/ha/types.ts', 'utils/initializer.ts', 'utils/interaction-mode.ts',