diff --git a/src/card-controller/config/config-manager.ts b/src/card-controller/config/config-manager.ts index dbfb2af9..dcb16331 100644 --- a/src/card-controller/config/config-manager.ts +++ b/src/card-controller/config/config-manager.ts @@ -7,7 +7,12 @@ import { type AdvancedCameraCardConfig, type CardWideConfig, } from '../../config/schema/types.js'; -import type { RawAdvancedCameraCardConfig } from '../../config/types.js'; +import type { + PartialAdvancedCameraCardConfig, + RawAdvancedCameraCardConfig, +} from '../../config/types.js'; +import { computeDomain } from '../../ha/compute-domain.js'; +import type { HomeAssistant } from '../../ha/types.js'; import { localize } from '../../localize/localize.js'; import { getParseError } from '../../utils/zod/parse-errors.js'; import { InitializationAspect } from '../initialization-manager.js'; @@ -36,6 +41,32 @@ export class ConfigManager { this._api = api; } + public static getEntitySuggestion( + hass: HomeAssistant, + entityId: string, + ): { config: PartialAdvancedCameraCardConfig } | null { + if (computeDomain(entityId) !== 'camera' || !hass.states[entityId]) { + return null; + } + return { + config: { + type: 'custom:advanced-camera-card', + cameras: [{ camera_entity: entityId }], + }, + }; + } + + public static getStubConfig(entities: string[]): PartialAdvancedCameraCardConfig { + const cameraEntity = entities.find((entity) => computeDomain(entity) === 'camera'); + return { + cameras: [ + { + camera_entity: cameraEntity ?? 'camera.demo', + }, + ], + }; + } + public hasConfig(): boolean { return !!this.getConfig(); } diff --git a/src/card-controller/controller.ts b/src/card-controller/controller.ts index e9b3b3ea..3d43e300 100644 --- a/src/card-controller/controller.ts +++ b/src/card-controller/controller.ts @@ -2,7 +2,6 @@ import type { ReactiveController } from 'lit'; import { CameraManager } from '../camera-manager/manager'; import { ConditionStateManager } from '../condition-trigger/conditions/state-manager'; -import type { AdvancedCameraCardConfig } from '../config/schema/types'; import { DeviceRegistryManager } from '../ha/registry/device'; import { DeviceCache } from '../ha/registry/device/types'; import { EntityRegistryManagerLive } from '../ha/registry/entity'; @@ -303,20 +302,6 @@ export class CardController return this._statusBarItemManager; } - public static getStubConfig(entities: string[]): AdvancedCameraCardConfig { - const cameraEntity = entities.find((element) => element.startsWith('camera.')); - return { - cameras: [ - { - camera_entity: cameraEntity ?? 'camera.demo', - }, - ], - // Need to use 'as unknown' to convince Typescript that this really isn't a - // mistake, despite the miniscule size of the configuration vs the full type - // description. - } as unknown as AdvancedCameraCardConfig; - } - public getStyleManager(): StyleManager { return this._styleManager; } diff --git a/src/card.ts b/src/card.ts index eeafe07d..ae687d53 100644 --- a/src/card.ts +++ b/src/card.ts @@ -14,6 +14,7 @@ import { styleMap } from 'lit/directives/style-map.js'; import 'web-dialog'; import { actionHandler } from './action-handler-directive.js'; +import { ConfigManager } from './card-controller/config/config-manager'; import { CardController } from './card-controller/controller'; import type { IssueKey, IssueTriggerEventData } from './card-controller/issues/types.js'; import { resolveAutoHideState, type AutoHideState } from './components-lib/auto-hide.js'; @@ -47,7 +48,10 @@ import type { ConditionStateManagerGetEvent } from './condition-trigger/conditio import type { StatusBarItem } from './config/schema/actions/types.js'; import type { MenuItem } from './config/schema/elements/custom/menu/types.js'; import type { AdvancedCameraCardConfig } from './config/schema/types.js'; -import type { RawAdvancedCameraCardConfig } from './config/types.js'; +import type { + PartialAdvancedCameraCardConfig, + RawAdvancedCameraCardConfig, +} from './config/types.js'; import { REPO_URL } from './const.js'; import type { HomeAssistant, LovelaceCardEditor } from './ha/types.js'; import { localize } from './localize/localize.js'; @@ -97,6 +101,7 @@ console.info( description: localize('common.advanced_camera_card_description'), preview: true, documentationURL: REPO_URL, + getEntitySuggestion: ConfigManager.getEntitySuggestion, }); // Expose currently-connected card instances on `window.advancedCameraCards` for @@ -172,8 +177,8 @@ class AdvancedCameraCard extends LitElement { public static getStubConfig( _: HomeAssistant, entities: string[], - ): AdvancedCameraCardConfig { - return CardController.getStubConfig(entities); + ): PartialAdvancedCameraCardConfig { + return ConfigManager.getStubConfig(entities); } public setConfig(config: RawAdvancedCameraCardConfig): void { diff --git a/src/config/types.ts b/src/config/types.ts index ca5a5d6e..d7feac16 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,2 +1,14 @@ +import type { PartialDeep } from 'type-fest'; + +import type { AdvancedCameraCardConfig } from './schema/types'; + export type RawAdvancedCameraCardConfig = Record; export type RawAdvancedCameraCardConfigArray = RawAdvancedCameraCardConfig[]; + +// A partial config used for pre-parsed configs (e.g. a stub config). Nested +// fields are optional because their defaults are applied when the config is +// parsed. +export type PartialAdvancedCameraCardConfig = PartialDeep< + AdvancedCameraCardConfig, + { recurseIntoArrays: true } +>; diff --git a/tests/card-controller/config/config-manager.test.ts b/tests/card-controller/config/config-manager.test.ts index 41b50ac0..397eaabd 100644 --- a/tests/card-controller/config/config-manager.test.ts +++ b/tests/card-controller/config/config-manager.test.ts @@ -11,7 +11,13 @@ import type { Automation } from '../../../src/config/schema/automations'; import type { Trigger } from '../../../src/config/schema/condition-trigger/triggers/types'; import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types'; import { createGeneralAction } from '../../../src/utils/action'; -import { createCardAPI, createConfig, flushPromises } from '../../test-utils'; +import { + createCardAPI, + createConfig, + createHASS, + createStateEntity, + flushPromises, +} from '../../test-utils'; /** * Create a ConfigManager test setup with real AutomationsManager and ConditionStateManager. @@ -75,6 +81,44 @@ describe('ConfigManager', () => { vi.restoreAllMocks(); }); + describe('getEntitySuggestion', () => { + it('should suggest the card for a camera entity', () => { + const hass = createHASS({ 'camera.office': createStateEntity() }); + expect(ConfigManager.getEntitySuggestion(hass, 'camera.office')).toEqual({ + config: { + type: 'custom:advanced-camera-card', + cameras: [{ camera_entity: 'camera.office' }], + }, + }); + }); + + it('should not suggest the card for a non-camera entity', () => { + const hass = createHASS({ 'binary_sensor.motion': createStateEntity() }); + expect(ConfigManager.getEntitySuggestion(hass, 'binary_sensor.motion')).toBeNull(); + }); + + it('should not suggest the card for an unknown entity', () => { + const hass = createHASS({}); + expect(ConfigManager.getEntitySuggestion(hass, 'camera.office')).toBeNull(); + }); + }); + + describe('getStubConfig', () => { + it('should handle with camera entities', () => { + expect( + ConfigManager.getStubConfig(['camera.office', 'binary_sensor.motion']), + ).toEqual({ + cameras: [{ camera_entity: 'camera.office' }], + }); + }); + + it('should handle without camera entities', () => { + expect(ConfigManager.getStubConfig(['binary_sensor.motion'])).toEqual({ + cameras: [{ camera_entity: 'camera.demo' }], + }); + }); + }); + describe('should handle error when', () => { it('should handle no input', () => { const manager = new ConfigManager(createCardAPI()); diff --git a/tests/card-controller/controller.test.ts b/tests/card-controller/controller.test.ts index 854ec6be..952718f3 100644 --- a/tests/card-controller/controller.test.ts +++ b/tests/card-controller/controller.test.ts @@ -302,22 +302,6 @@ describe('CardController', () => { ); }); - describe('getStubConfig', () => { - it('should handle with camera entities', () => { - expect( - CardController.getStubConfig(['camera.office', 'binary_sensor.motion']), - ).toEqual({ - cameras: [{ camera_entity: 'camera.office' }], - }); - }); - - it('should handle without camera entities', () => { - expect(CardController.getStubConfig(['binary_sensor.motion'])).toEqual({ - cameras: [{ camera_entity: 'camera.demo' }], - }); - }); - }); - it('should return getQueryStringManager', () => { expect(createController().getQueryStringManager()).toBe( vi.mocked(QueryStringManager).mock.instances[0],