feat: Make cameras optional (#2343)
This commit is contained in:
+9
-1
@@ -27,7 +27,7 @@ See [Advanced Installation](advanced-installation.md) for other installation res
|
|||||||
|
|
||||||
## Initial configuration
|
## Initial configuration
|
||||||
|
|
||||||
### Minimal configuration
|
### Minimal camera configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
type: custom:advanced-camera-card
|
type: custom:advanced-camera-card
|
||||||
@@ -35,6 +35,14 @@ cameras:
|
|||||||
- camera_entity: camera.office
|
- camera_entity: camera.office
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Minimal folder configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:advanced-camera-card
|
||||||
|
folders:
|
||||||
|
- type: ha
|
||||||
|
```
|
||||||
|
|
||||||
### Video scrubbing configuration
|
### Video scrubbing configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
|
|
||||||
The card supports a myriad of configuration options for simple or complex setups.
|
The card supports a myriad of configuration options for simple or complex setups.
|
||||||
|
|
||||||
### Minimal configuration
|
### Absolute minimum configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:advanced-camera-card
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minimal camera configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
type: custom:advanced-camera-card
|
type: custom:advanced-camera-card
|
||||||
@@ -10,20 +16,30 @@ cameras:
|
|||||||
- camera_entity: camera.office
|
- camera_entity: camera.office
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Minimal folder configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:advanced-camera-card
|
||||||
|
folders:
|
||||||
|
- type: ha
|
||||||
|
```
|
||||||
|
|
||||||
### Configuration blocks
|
### Configuration blocks
|
||||||
|
|
||||||
#### Top-level configuration blocks
|
#### Top-level configuration blocks
|
||||||
|
|
||||||
Only the `cameras` option is required, all other parameters are optional.
|
No top-level option is required. If `cameras` is omitted the card will still
|
||||||
|
render and can operate in `image` or `folders` views depending on configuration.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [`automations`](automations.md) | Take action when conditions are met. |
|
| [`automations`](automations.md) | Take action when conditions are met. |
|
||||||
| [`cameras`](cameras/README.md) | Configures the cameras to be used in the card. At least one camera must be specified. |
|
| [`cameras`](cameras/README.md) | Configures the cameras to be used in the card. Optional. If present, the first listed camera is the default. |
|
||||||
| [`cameras_global`](cameras/README.md) | Global defaults that apply to all cameras from the `cameras` section. |
|
| [`cameras_global`](cameras/README.md) | Global defaults that apply to all cameras from the `cameras` section. |
|
||||||
| `card_id` | An optional ID to uniquely identify this card. For use when actions are being sent to card(s) via [URL actions](../usage/url-actions.md). Must exclusively consist of these characters: `[a-zA-Z0-9_]`. |
|
| `card_id` | An optional ID to uniquely identify this card. For use when actions are being sent to card(s) via [URL actions](../usage/url-actions.md). Must exclusively consist of these characters: `[a-zA-Z0-9_]`. |
|
||||||
| [`dimensions`](dimensions.md) | Configures the overall card dimensions. |
|
| [`dimensions`](dimensions.md) | Configures the overall card dimensions. |
|
||||||
| [`elements`](elements/README.md) | Add custom elements to the card. |
|
| [`elements`](elements/README.md) | Add custom elements to the card. |
|
||||||
|
| [`folders`](folders.md) | Configures folders to be used in the card. Optional. If present, the first listed camera is the default. |
|
||||||
| [`image`](image.md) | Configures the `image` view. |
|
| [`image`](image.md) | Configures the `image` view. |
|
||||||
| [`live`](live.md) | Configures the `live` view. |
|
| [`live`](live.md) | Configures the `live` view. |
|
||||||
| [`media_gallery`](media-gallery.md) | Configures the media gallery. |
|
| [`media_gallery`](media-gallery.md) | Configures the media gallery. |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Cameras
|
# Cameras
|
||||||
|
|
||||||
The `cameras` block configures a list of cameras the card should support. The first listed camera is the default.
|
The `cameras` block configures a list of cameras the card should support. This block is optional. When present, the first listed camera is the default.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cameras:
|
cameras:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ view:
|
|||||||
| `actions` | | [Actions](actions/README.md) to use for all views, individual actions may be overriden by view-specific actions. |
|
| `actions` | | [Actions](actions/README.md) to use for all views, individual actions may be overriden by view-specific actions. |
|
||||||
| `camera_select` | `current` | The [view](view.md?id=supported-views) to show when a new camera is selected (e.g. in the camera menu). If `current` the view is unchanged when a new camera is selected. |
|
| `camera_select` | `current` | The [view](view.md?id=supported-views) to show when a new camera is selected (e.g. in the camera menu). If `current` the view is unchanged when a new camera is selected. |
|
||||||
| `dim` | `false` | Whether or not to 'dim' the brightness of the card (by 25%) if the card `interaction_seconds` has expired (i.e. card has been left unattended for that period of time). |
|
| `dim` | `false` | Whether or not to 'dim' the brightness of the card (by 25%) if the card `interaction_seconds` has expired (i.e. card has been left unattended for that period of time). |
|
||||||
| `default` | `live` | The view to show in the card by default. The default camera is the first one listed. See [Supported Views](view.md?id=supported-views) below. |
|
| `default` | `auto` | The view to show in the card by default. If `auto`, the card will choose `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (screensaver). The default camera is the first one listed. See [Supported Views](view.md?id=supported-views) below. |
|
||||||
| `default_reset` | | The circumstances and behavior that cause the card to reset to the default view. See below. |
|
| `default_reset` | | The circumstances and behavior that cause the card to reset to the default view. See below. |
|
||||||
| `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions.md?id=interaction) or with `reset_after_interaction` to reset the view after the interaction is complete. |
|
| `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions.md?id=interaction) or with `reset_after_interaction` to reset the view after the interaction is complete. |
|
||||||
| `keyboard_shortcuts` | See [usage](../usage/keyboard-shortcuts.md) for defaults. | Configure keyboard shortcuts. See below. |
|
| `keyboard_shortcuts` | See [usage](../usage/keyboard-shortcuts.md) for defaults. | Configure keyboard shortcuts. See below. |
|
||||||
@@ -155,7 +155,7 @@ This card supports several different views.
|
|||||||
| `snapshots` | Shows a gallery of snapshots for this camera. |
|
| `snapshots` | Shows a gallery of snapshots for this camera. |
|
||||||
| `timeline` | Shows an event timeline. |
|
| `timeline` | Shows an event timeline. |
|
||||||
|
|
||||||
The default view is `live`, but can be configured by the `view.default` parameter.
|
The default view is `auto`. It will select `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (screensaver). You can override this with `view.default`.
|
||||||
|
|
||||||
## Fully expanded reference
|
## Fully expanded reference
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ The default view is `live`, but can be configured by the `view.default` paramete
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
view:
|
view:
|
||||||
default: live
|
default: auto
|
||||||
camera_select: current
|
camera_select: current
|
||||||
interaction_seconds: 300
|
interaction_seconds: 300
|
||||||
default_cycle_camera: false
|
default_cycle_camera: false
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
PTZActionPhase,
|
PTZActionPhase,
|
||||||
PTZPanTiltAction,
|
PTZPanTiltAction,
|
||||||
} from '../config/schema/actions/custom/ptz.js';
|
} from '../config/schema/actions/custom/ptz.js';
|
||||||
import { CameraConfig, CamerasConfig, Rotation } from '../config/schema/cameras.js';
|
import { CameraConfig, Rotation } from '../config/schema/cameras.js';
|
||||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import { Endpoint } from '../types.js';
|
import { Endpoint } from '../types.js';
|
||||||
@@ -166,7 +166,7 @@ export class CameraManager {
|
|||||||
// global config (which does have defaults). The merging must happen in this
|
// global config (which does have defaults). The merging must happen in this
|
||||||
// order, to ensure that the defaults in the cameras global config do not
|
// order, to ensure that the defaults in the cameras global config do not
|
||||||
// override the values specified in the per-camera config.
|
// override the values specified in the per-camera config.
|
||||||
const cameras = config.cameras.map((camera) =>
|
const cameras = (config.cameras ?? []).map((camera) =>
|
||||||
recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera),
|
recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async _getEnginesForCameras(
|
protected async _getEnginesForCameras(
|
||||||
camerasConfig: CamerasConfig,
|
camerasConfig: CameraConfig[],
|
||||||
): Promise<Map<CameraConfig, CameraManagerEngine>> {
|
): Promise<Map<CameraConfig, CameraManagerEngine>> {
|
||||||
const output: Map<CameraConfig, CameraManagerEngine> = new Map();
|
const output: Map<CameraConfig, CameraManagerEngine> = new Map();
|
||||||
const engines: Map<Engine, CameraManagerEngine> = new Map();
|
const engines: Map<Engine, CameraManagerEngine> = new Map();
|
||||||
@@ -228,7 +228,7 @@ export class CameraManager {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _initializeCameras(camerasConfig: CamerasConfig): Promise<void> {
|
protected async _initializeCameras(camerasConfig: CameraConfig[]): Promise<void> {
|
||||||
const initializationStartTime = new Date();
|
const initializationStartTime = new Date();
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ interface CapabilitySearchAllAny {
|
|||||||
}
|
}
|
||||||
export type CapabilitySearchKeys = CapabilityKey | CapabilitySearchAllAny;
|
export type CapabilitySearchKeys = CapabilityKey | CapabilitySearchAllAny;
|
||||||
export interface CapabilitySearchOptions {
|
export interface CapabilitySearchOptions {
|
||||||
|
// If true, include a parent camera in results when any of its dependent
|
||||||
|
// (child) cameras have the capability, even if the parent itself doesn't.
|
||||||
inclusive?: boolean;
|
inclusive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ export class EffectAction extends AdvancedCameraCardAction<EffectActionConfig> {
|
|||||||
|
|
||||||
switch (this._action.effect_action) {
|
switch (this._action.effect_action) {
|
||||||
case 'start':
|
case 'start':
|
||||||
api.getEffectsControllerAPI()?.startEffect(this._action.effect);
|
api.getEffectsManager().startEffect(this._action.effect);
|
||||||
break;
|
break;
|
||||||
case 'stop':
|
case 'stop':
|
||||||
api.getEffectsControllerAPI()?.stopEffect(this._action.effect);
|
api.getEffectsManager().stopEffect(this._action.effect);
|
||||||
break;
|
break;
|
||||||
case 'toggle':
|
case 'toggle':
|
||||||
api.getEffectsControllerAPI()?.toggleEffect(this._action.effect);
|
api.getEffectsManager().toggleEffect(this._action.effect);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export class MediaPlayerAction extends AdvancedCameraCardAction<MediaPlayerActio
|
|||||||
const item = view?.queryResults?.getSelectedResult() ?? null;
|
const item = view?.queryResults?.getSelectedResult() ?? null;
|
||||||
|
|
||||||
if (view?.is('live')) {
|
if (view?.is('live')) {
|
||||||
await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view));
|
const cameraID = getStreamCameraID(view);
|
||||||
|
if (cameraID) {
|
||||||
|
await mediaPlayerController.playLive(mediaPlayer, cameraID);
|
||||||
|
}
|
||||||
} else if (view?.isViewerView() && item && ViewItemClassifier.isMedia(item)) {
|
} else if (view?.isViewerView() && item && ViewItemClassifier.isMedia(item)) {
|
||||||
await mediaPlayerController.playMedia(mediaPlayer, item);
|
await mediaPlayerController.playMedia(mediaPlayer, item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ export class SetReviewAction extends AdvancedCameraCardAction<SetReviewActionCon
|
|||||||
viewManager.getEpoch(),
|
viewManager.getEpoch(),
|
||||||
getReviewedQueryFilterFromQuery(view?.query, item),
|
getReviewedQueryFilterFromQuery(view?.query, item),
|
||||||
),
|
),
|
||||||
api
|
api.getEffectsManager().startEffect('check', {
|
||||||
.getEffectsControllerAPI()
|
duration: 0.4,
|
||||||
?.startEffect('check', { duration: 0.4, fadeIn: false }),
|
fadeIn: false,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Trigger UI update to refresh menu icon state
|
// Trigger UI update to refresh menu icon state
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|||||||
// When the camera changes, update the entity to match (only if different
|
// When the camera changes, update the entity to match (only if different
|
||||||
// to avoid race conditions when multiple cards share the same entity).
|
// to avoid race conditions when multiple cards share the same entity).
|
||||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2244
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2244
|
||||||
createInternalCallbackAction((api: CardActionsAPI) =>
|
createInternalCallbackAction(async (api: CardActionsAPI) =>
|
||||||
selectOptionOnEntityIfDifferent(
|
selectOptionOnEntityIfDifferent(
|
||||||
cameraControlEntity,
|
|
||||||
api.getViewManager().getView()?.camera,
|
|
||||||
api,
|
api,
|
||||||
|
cameraControlEntity,
|
||||||
|
api.getViewManager().getView()?.camera ?? undefined,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -76,13 +76,10 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|||||||
`{{ hass.states["${cameraControlEntity}"].state }}`,
|
`{{ hass.states["${cameraControlEntity}"].state }}`,
|
||||||
)
|
)
|
||||||
: // Set the selected option in the entity to the current camera ID.
|
: // Set the selected option in the entity to the current camera ID.
|
||||||
createInternalCallbackAction((api: CardActionsAPI) =>
|
createInternalCallbackAction(async (api: CardActionsAPI) => {
|
||||||
selectOptionOnEntityIfDifferent(
|
const camera = api.getViewManager().getView()?.camera ?? undefined;
|
||||||
cameraControlEntity,
|
return selectOptionOnEntityIfDifferent(api, cameraControlEntity, camera);
|
||||||
api.getViewManager().getView()?.camera,
|
}),
|
||||||
api,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
tag: automationTag,
|
tag: automationTag,
|
||||||
},
|
},
|
||||||
@@ -108,9 +105,9 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectOptionOnEntityIfDifferent = async (
|
const selectOptionOnEntityIfDifferent = async (
|
||||||
entity: string,
|
|
||||||
option: string | undefined,
|
|
||||||
api: CardActionsAPI,
|
api: CardActionsAPI,
|
||||||
|
entity: string,
|
||||||
|
option?: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const hass = api.getHASSManager().getHASS();
|
const hass = api.getHASSManager().getHASS();
|
||||||
const currentState = hass?.states[entity]?.state;
|
const currentState = hass?.states[entity]?.state;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ReactiveController } from 'lit';
|
import { ReactiveController } from 'lit';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { EffectsManager } from './effects/effects-manager';
|
||||||
import { ConditionStateManager } from '../conditions/state-manager';
|
import { ConditionStateManager } from '../conditions/state-manager';
|
||||||
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
||||||
import { DeviceRegistryManager } from '../ha/registry/device';
|
import { DeviceRegistryManager } from '../ha/registry/device';
|
||||||
@@ -8,7 +9,6 @@ import { EntityRegistryManagerLive } from '../ha/registry/entity';
|
|||||||
import { EntityCache, EntityRegistryManager } from '../ha/registry/entity/types';
|
import { EntityCache, EntityRegistryManager } from '../ha/registry/entity/types';
|
||||||
import { ResolvedMediaCache } from '../ha/resolved-media';
|
import { ResolvedMediaCache } from '../ha/resolved-media';
|
||||||
import { LovelaceCardEditor } from '../ha/types';
|
import { LovelaceCardEditor } from '../ha/types';
|
||||||
import { EffectsControllerAPI } from '../types';
|
|
||||||
import { ActionsManager } from './actions/actions-manager';
|
import { ActionsManager } from './actions/actions-manager';
|
||||||
import { AutomationsManager } from './automations-manager';
|
import { AutomationsManager } from './automations-manager';
|
||||||
import { CameraURLManager } from './camera-url-manager';
|
import { CameraURLManager } from './camera-url-manager';
|
||||||
@@ -28,7 +28,6 @@ import { InitializationManager } from './initialization-manager';
|
|||||||
import { InteractionManager } from './interaction-manager';
|
import { InteractionManager } from './interaction-manager';
|
||||||
import { KeyboardStateManager } from './keyboard-state-manager';
|
import { KeyboardStateManager } from './keyboard-state-manager';
|
||||||
import { MediaLoadedInfoManager } from './media-info-manager';
|
import { MediaLoadedInfoManager } from './media-info-manager';
|
||||||
|
|
||||||
import { MediaPlayerManager } from './media-player-manager';
|
import { MediaPlayerManager } from './media-player-manager';
|
||||||
import { MessageManager } from './message-manager';
|
import { MessageManager } from './message-manager';
|
||||||
import { MicrophoneManager } from './microphone-manager';
|
import { MicrophoneManager } from './microphone-manager';
|
||||||
@@ -67,8 +66,6 @@ import {
|
|||||||
import { ViewItemManager } from './view/item-manager';
|
import { ViewItemManager } from './view/item-manager';
|
||||||
import { ViewManager } from './view/view-manager';
|
import { ViewManager } from './view/view-manager';
|
||||||
|
|
||||||
type EffectsControllerAPICallback = () => EffectsControllerAPI | null;
|
|
||||||
|
|
||||||
export class CardController
|
export class CardController
|
||||||
implements
|
implements
|
||||||
CardActionsManagerAPI,
|
CardActionsManagerAPI,
|
||||||
@@ -97,9 +94,8 @@ export class CardController
|
|||||||
CardViewAPI,
|
CardViewAPI,
|
||||||
ReactiveController
|
ReactiveController
|
||||||
{
|
{
|
||||||
protected _effectsControllerAPICallback: EffectsControllerAPICallback;
|
|
||||||
|
|
||||||
protected _conditionStateManager = new ConditionStateManager();
|
protected _conditionStateManager = new ConditionStateManager();
|
||||||
|
protected _effectsManager = new EffectsManager();
|
||||||
|
|
||||||
// These properties may be used in the construction of 'managers' (and should
|
// These properties may be used in the construction of 'managers' (and should
|
||||||
// be created first).
|
// be created first).
|
||||||
@@ -138,7 +134,6 @@ export class CardController
|
|||||||
host: CardHTMLElement,
|
host: CardHTMLElement,
|
||||||
scrollCallback: ScrollCallback,
|
scrollCallback: ScrollCallback,
|
||||||
menuToggleCallback: MenuToggleCallback,
|
menuToggleCallback: MenuToggleCallback,
|
||||||
effectsControllerAPICallback: EffectsControllerAPICallback,
|
|
||||||
) {
|
) {
|
||||||
host.addController(this);
|
host.addController(this);
|
||||||
|
|
||||||
@@ -148,7 +143,6 @@ export class CardController
|
|||||||
scrollCallback,
|
scrollCallback,
|
||||||
menuToggleCallback,
|
menuToggleCallback,
|
||||||
);
|
);
|
||||||
this._effectsControllerAPICallback = effectsControllerAPICallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
@@ -199,8 +193,8 @@ export class CardController
|
|||||||
return this._deviceRegistryManager;
|
return this._deviceRegistryManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEffectsControllerAPI(): EffectsControllerAPI | null {
|
public getEffectsManager(): EffectsManager {
|
||||||
return this._effectsControllerAPICallback();
|
return this._effectsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEntityRegistryManager(): EntityRegistryManager {
|
public getEntityRegistryManager(): EntityRegistryManager {
|
||||||
|
|||||||
+103
-54
@@ -1,4 +1,4 @@
|
|||||||
import { EffectName, EffectsControllerAPI } from '../../types';
|
import { EffectName, EffectsContainer, EffectsManagerInterface } from '../../types';
|
||||||
import { Timer } from '../../utils/timer';
|
import { Timer } from '../../utils/timer';
|
||||||
import { EffectComponent, EffectModule, EffectOptions } from './types';
|
import { EffectComponent, EffectModule, EffectOptions } from './types';
|
||||||
|
|
||||||
@@ -29,38 +29,125 @@ const effectRegistry: Record<EffectName, () => Promise<EffectModule>> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type EffectsContainer = HTMLElement | DocumentFragment;
|
type EffectModuleImporter = (name: EffectName) => Promise<EffectModule | null>;
|
||||||
|
|
||||||
export class EffectsController implements EffectsControllerAPI {
|
const defaultImportEffectModule: EffectModuleImporter = async (name: EffectName) => {
|
||||||
|
const effectModule = await effectRegistry[name]?.();
|
||||||
|
return effectModule ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EffectsManager implements EffectsManagerInterface {
|
||||||
|
private _importer: EffectModuleImporter;
|
||||||
private _importedModules: Map<EffectName, EffectModule> = new Map();
|
private _importedModules: Map<EffectName, EffectModule> = new Map();
|
||||||
private _activeInstances: Map<EffectName, EffectComponent | null> = new Map();
|
|
||||||
private _durationTimers: Map<EffectName, Timer> = new Map();
|
private _durationTimers: Map<EffectName, Timer> = new Map();
|
||||||
private _container: EffectsContainer | null = null;
|
|
||||||
|
|
||||||
public setContainer(container: EffectsContainer | null): void {
|
// Effects that have been requested to start but are still loading or waiting
|
||||||
|
// for a container to be registered (test case: a card without cameras or
|
||||||
|
// folders will initialize very quickly and the card will be loaded before
|
||||||
|
// effects can be started).
|
||||||
|
private _pendingEffects: Map<EffectName, EffectOptions | undefined> = new Map();
|
||||||
|
private _activeEffects: Map<EffectName, EffectComponent | null> = new Map();
|
||||||
|
protected _container: EffectsContainer | null = null;
|
||||||
|
|
||||||
|
constructor(importer: EffectModuleImporter = defaultImportEffectModule) {
|
||||||
|
this._importer = importer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setContainer(container: EffectsContainer): void {
|
||||||
this._container = container;
|
this._container = container;
|
||||||
|
this._startPendingEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeContainer(): void {
|
||||||
|
this._container = null;
|
||||||
|
this._pendingEffects.clear();
|
||||||
|
|
||||||
|
for (const timer of this._durationTimers.values()) {
|
||||||
|
timer.stop();
|
||||||
|
}
|
||||||
|
this._durationTimers.clear();
|
||||||
|
|
||||||
|
for (const instance of this._activeEffects.values()) {
|
||||||
|
instance?.remove();
|
||||||
|
}
|
||||||
|
this._activeEffects.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async startEffect(name: EffectName, options?: EffectOptions): Promise<void> {
|
public async startEffect(name: EffectName, options?: EffectOptions): Promise<void> {
|
||||||
if (!this._container || this._activeInstances.has(name)) {
|
if (this._activeEffects.has(name)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve the slot immediately with null to prevent concurrent starts.
|
// Reserve the slot immediately with null to prevent concurrent starts.
|
||||||
this._activeInstances.set(name, null);
|
this._activeEffects.set(name, null);
|
||||||
|
if (!this._container) {
|
||||||
|
this._pendingEffects.set(name, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._startEffect(name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stopEffect(effect: EffectName): Promise<void> {
|
||||||
|
const timer = this._durationTimers.get(effect);
|
||||||
|
if (timer) {
|
||||||
|
timer.stop();
|
||||||
|
this._durationTimers.delete(effect);
|
||||||
|
}
|
||||||
|
this._pendingEffects.delete(effect);
|
||||||
|
|
||||||
|
if (!this._activeEffects.has(effect)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = this._activeEffects.get(effect);
|
||||||
|
this._activeEffects.delete(effect);
|
||||||
|
|
||||||
|
// If instance is null, it's still loading - just clearing the reservation
|
||||||
|
// will prevent it from appearing (startEffect checks this after import).
|
||||||
|
if (instance) {
|
||||||
|
await instance.startFadeOut();
|
||||||
|
instance.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toggleEffect(name: EffectName, options?: EffectOptions): Promise<void> {
|
||||||
|
if (this._activeEffects.has(name)) {
|
||||||
|
await this.stopEffect(name);
|
||||||
|
} else {
|
||||||
|
await this.startEffect(name, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _importEffectModule(name: EffectName): Promise<EffectModule | null> {
|
||||||
|
const existingModule = this._importedModules.get(name);
|
||||||
|
if (existingModule) {
|
||||||
|
return existingModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectModule = await this._importer(name);
|
||||||
|
if (!effectModule) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._importedModules.set(name, effectModule);
|
||||||
|
return effectModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _startEffect(name: EffectName, options?: EffectOptions): Promise<void> {
|
||||||
const effectModule = await this._importEffectModule(name);
|
const effectModule = await this._importEffectModule(name);
|
||||||
|
|
||||||
// Check if the effect was cancelled during loading.
|
this._pendingEffects.delete(name);
|
||||||
if (!effectModule || !this._activeInstances.has(name)) {
|
|
||||||
this._activeInstances.delete(name);
|
if (!effectModule || !this._activeEffects.has(name) || !this._container) {
|
||||||
|
this._activeEffects.delete(name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectComponent = new effectModule.default();
|
const effectComponent = new effectModule.default();
|
||||||
effectComponent.fadeIn = options?.fadeIn ?? true;
|
effectComponent.fadeIn = options?.fadeIn ?? true;
|
||||||
this._container.appendChild(effectComponent);
|
this._container.appendChild(effectComponent);
|
||||||
this._activeInstances.set(name, effectComponent);
|
this._activeEffects.set(name, effectComponent);
|
||||||
|
|
||||||
const duration = options?.duration;
|
const duration = options?.duration;
|
||||||
if (duration !== undefined) {
|
if (duration !== undefined) {
|
||||||
@@ -76,48 +163,10 @@ export class EffectsController implements EffectsControllerAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stopEffect(effect: EffectName): Promise<void> {
|
private _startPendingEffects(): void {
|
||||||
const timer = this._durationTimers.get(effect);
|
for (const [name, options] of this._pendingEffects.entries()) {
|
||||||
if (timer) {
|
this._pendingEffects.delete(name);
|
||||||
timer.stop();
|
this._startEffect(name, options);
|
||||||
this._durationTimers.delete(effect);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._activeInstances.has(effect)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = this._activeInstances.get(effect);
|
|
||||||
this._activeInstances.delete(effect);
|
|
||||||
|
|
||||||
// If instance is null, it's still loading - just clearing the reservation
|
|
||||||
// will prevent it from appearing (startEffect checks this after import).
|
|
||||||
if (instance) {
|
|
||||||
await instance.startFadeOut();
|
|
||||||
instance.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toggleEffect(name: EffectName, options?: EffectOptions): Promise<void> {
|
|
||||||
if (this._activeInstances.has(name)) {
|
|
||||||
await this.stopEffect(name);
|
|
||||||
} else {
|
|
||||||
await this.startEffect(name, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _importEffectModule(name: EffectName): Promise<EffectModule | null> {
|
|
||||||
const existingModule = this._importedModules.get(name);
|
|
||||||
if (existingModule) {
|
|
||||||
return existingModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectModule = await effectRegistry[name]?.();
|
|
||||||
if (!effectModule) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._importedModules.set(name, effectModule);
|
|
||||||
return effectModule;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
createGeneralAction,
|
createGeneralAction,
|
||||||
createViewAction,
|
createViewAction,
|
||||||
} from '../utils/action.js';
|
} from '../utils/action.js';
|
||||||
import { ViewParameters } from '../view/view';
|
|
||||||
import { CardQueryStringAPI } from './types';
|
import { CardQueryStringAPI } from './types';
|
||||||
import { SubstreamSelectViewModifier } from './view/modifiers/substream-select';
|
import { SubstreamSelectViewModifier } from './view/modifiers/substream-select';
|
||||||
|
import { ViewParametersUserSpecified } from './view/types.js';
|
||||||
|
|
||||||
interface QueryStringViewIntent {
|
interface QueryStringViewIntent {
|
||||||
view?: Partial<ViewParameters> & {
|
view?: ViewParametersUserSpecified & {
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
substream?: string;
|
substream?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export class StatusBarItemManager {
|
|||||||
view?: View | null;
|
view?: View | null;
|
||||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
}): StatusBarItem[] {
|
}): StatusBarItem[] {
|
||||||
const cameraMetadata = options?.view
|
const cameraMetadata = options?.view?.camera
|
||||||
? options?.cameraManager?.getCameraMetadata(options?.view?.camera)
|
? options?.cameraManager?.getCameraMetadata(options.view.camera)
|
||||||
: null;
|
: null;
|
||||||
const engineIcon = cameraMetadata?.engineIcon ?? null;
|
const engineIcon = cameraMetadata?.engineIcon ?? null;
|
||||||
const selectedResult = options?.view?.queryResults?.getSelectedResult();
|
const selectedResult = options?.view?.queryResults?.getSelectedResult();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Automation } from '../config/schema/automations';
|
|||||||
import type { DeviceRegistryManager } from '../ha/registry/device';
|
import type { DeviceRegistryManager } from '../ha/registry/device';
|
||||||
import type { EntityRegistryManager } from '../ha/registry/entity/types';
|
import type { EntityRegistryManager } from '../ha/registry/entity/types';
|
||||||
import type { ResolvedMediaCache } from '../ha/resolved-media';
|
import type { ResolvedMediaCache } from '../ha/resolved-media';
|
||||||
import type { EffectsControllerAPI } from '../types';
|
import type { EffectsManagerInterface } from '../types';
|
||||||
import type { ActionsManager } from './actions/actions-manager';
|
import type { ActionsManager } from './actions/actions-manager';
|
||||||
import type { AutomationsManager } from './automations-manager';
|
import type { AutomationsManager } from './automations-manager';
|
||||||
import type { CameraURLManager } from './camera-url-manager';
|
import type { CameraURLManager } from './camera-url-manager';
|
||||||
@@ -44,7 +44,7 @@ export interface CardActionsAPI {
|
|||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionStateManager(): ConditionStateManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getEffectsControllerAPI(): EffectsControllerAPI | null;
|
getEffectsManager(): EffectsManagerInterface;
|
||||||
getExpandManager(): ExpandManager;
|
getExpandManager(): ExpandManager;
|
||||||
getFoldersManager(): FoldersManager;
|
getFoldersManager(): FoldersManager;
|
||||||
getFullscreenManager(): FullscreenManager;
|
getFullscreenManager(): FullscreenManager;
|
||||||
|
|||||||
+185
-105
@@ -1,15 +1,22 @@
|
|||||||
import { AdvancedCameraCardView, VIEW_DEFAULT } from '../../config/schema/common/const';
|
import { AdvancedCameraCardView } from '../../config/schema/common/const';
|
||||||
import { ViewDisplayMode } from '../../config/schema/common/display';
|
import { ViewDisplayMode } from '../../config/schema/common/display';
|
||||||
import { AdvancedCameraCardConfig } from '../../config/schema/types';
|
import { AdvancedCameraCardConfig } from '../../config/schema/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
|
import { resolveViewName } from '../../view/utils/resolve-default';
|
||||||
import { View, ViewParameters } from '../../view/view';
|
import { View, ViewParameters } from '../../view/view';
|
||||||
import {
|
import {
|
||||||
getCameraIDsForViewName,
|
doesViewRequireCamera,
|
||||||
|
getCameraIDsWithCapabilityForView,
|
||||||
isViewSupportedByCamera,
|
isViewSupportedByCamera,
|
||||||
} from '../../view/view-support';
|
} from '../../view/view-support';
|
||||||
import { CardViewAPI } from '../types';
|
import { CardViewAPI } from '../types';
|
||||||
import { applyViewModifiers } from './modifiers';
|
import { applyViewModifiers } from './modifiers';
|
||||||
import { ViewFactoryOptions, ViewIncompatible, ViewNoCameraError } from './types';
|
import { ViewFactoryOptions, ViewIncompatible } from './types';
|
||||||
|
|
||||||
|
interface ResolvedViewTarget {
|
||||||
|
viewName: AdvancedCameraCardView;
|
||||||
|
cameraID: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export class ViewFactory {
|
export class ViewFactory {
|
||||||
protected _api: CardViewAPI;
|
protected _api: CardViewAPI;
|
||||||
@@ -24,18 +31,38 @@ export class ViewFactory {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neither options.baseView.camera nor options.baseView.view are respected
|
const viewName = this._getDefaultViewName(config);
|
||||||
// here, since this is the default view / camera.
|
|
||||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1564
|
|
||||||
|
|
||||||
let cameraID: string | null = null;
|
return this.getViewByParameters({
|
||||||
const viewName = options?.params?.view ?? config.view.default;
|
params: {
|
||||||
|
...options?.params,
|
||||||
|
view: viewName,
|
||||||
|
camera: this._getDefaultCameraID(config, viewName, options),
|
||||||
|
},
|
||||||
|
baseView: options?.baseView,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getDefaultViewName = (
|
||||||
|
config: AdvancedCameraCardConfig,
|
||||||
|
): AdvancedCameraCardView =>
|
||||||
|
resolveViewName(
|
||||||
|
config.view.default,
|
||||||
|
this._api.getCameraManager(),
|
||||||
|
this._api.getFoldersManager(),
|
||||||
|
);
|
||||||
|
|
||||||
|
protected _getDefaultCameraID(
|
||||||
|
config: AdvancedCameraCardConfig,
|
||||||
|
viewName: AdvancedCameraCardView,
|
||||||
|
options?: ViewFactoryOptions,
|
||||||
|
): string | null {
|
||||||
if (options?.params?.camera) {
|
if (options?.params?.camera) {
|
||||||
cameraID = options.params.camera;
|
return options.params.camera;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
const cameraIDs = [
|
const cameraIDs = [
|
||||||
...getCameraIDsForViewName(
|
...getCameraIDsWithCapabilityForView(
|
||||||
viewName,
|
viewName,
|
||||||
this._api.getCameraManager(),
|
this._api.getCameraManager(),
|
||||||
this._api.getFoldersManager(),
|
this._api.getFoldersManager(),
|
||||||
@@ -49,20 +76,10 @@ export class ViewFactory {
|
|||||||
) {
|
) {
|
||||||
const currentIndex = cameraIDs.indexOf(options.baseView.camera);
|
const currentIndex = cameraIDs.indexOf(options.baseView.camera);
|
||||||
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
|
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
|
||||||
cameraID = cameraIDs[targetIndex];
|
return cameraIDs[targetIndex];
|
||||||
} else {
|
|
||||||
cameraID = cameraIDs[0] ?? null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getViewByParameters({
|
return cameraIDs[0] ?? null;
|
||||||
params: {
|
|
||||||
...options?.params,
|
|
||||||
view: viewName,
|
|
||||||
camera: cameraID,
|
|
||||||
},
|
|
||||||
baseView: options?.baseView,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getViewByParameters(options?: ViewFactoryOptions): View | null {
|
public getViewByParameters(options?: ViewFactoryOptions): View | null {
|
||||||
@@ -71,83 +88,15 @@ export class ViewFactory {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cameraID: string | null =
|
let viewName = this._resolveViewName(config, options);
|
||||||
options?.params?.camera ?? options?.baseView?.camera ?? null;
|
let cameraID = this._resolveCameraID(viewName, options);
|
||||||
let viewName =
|
({ viewName, cameraID } = this._ensureViewCompatibility(
|
||||||
options?.params?.view ?? options?.baseView?.view ?? config.view.default;
|
|
||||||
|
|
||||||
const allCameraIDs = this._api.getCameraManager().getStore().getCameraIDs();
|
|
||||||
|
|
||||||
if (!cameraID || !allCameraIDs.has(cameraID)) {
|
|
||||||
const viewCameraIDs = getCameraIDsForViewName(
|
|
||||||
viewName,
|
viewName,
|
||||||
this._api.getCameraManager(),
|
|
||||||
this._api.getFoldersManager(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset to the default camera.
|
|
||||||
cameraID = viewCameraIDs?.keys().next().value ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cameraID) {
|
|
||||||
const camerasToCapabilities = [
|
|
||||||
...this._api.getCameraManager().getStore().getCameras(),
|
|
||||||
].reduce((acc, [cameraID, camera]) => {
|
|
||||||
const capabilities = camera.getCapabilities()?.getRawCapabilities();
|
|
||||||
if (capabilities) {
|
|
||||||
acc[cameraID] = capabilities;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
throw new ViewNoCameraError(localize('error.no_supported_cameras'), {
|
|
||||||
view: viewName,
|
|
||||||
cameras_capabilities: camerasToCapabilities,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isViewSupportedByCamera(
|
|
||||||
viewName,
|
|
||||||
this._api.getCameraManager(),
|
|
||||||
this._api.getFoldersManager(),
|
|
||||||
cameraID,
|
cameraID,
|
||||||
)
|
config,
|
||||||
) {
|
options,
|
||||||
if (
|
));
|
||||||
options?.failSafe &&
|
const displayMode = this._resolveDisplayMode(viewName, config, options);
|
||||||
isViewSupportedByCamera(
|
|
||||||
VIEW_DEFAULT,
|
|
||||||
this._api.getCameraManager(),
|
|
||||||
this._api.getFoldersManager(),
|
|
||||||
cameraID,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
viewName = VIEW_DEFAULT;
|
|
||||||
} else {
|
|
||||||
const capabilities = this._api
|
|
||||||
.getCameraManager()
|
|
||||||
.getStore()
|
|
||||||
.getCamera(cameraID)
|
|
||||||
?.getCapabilities()
|
|
||||||
?.getRawCapabilities();
|
|
||||||
|
|
||||||
throw new ViewIncompatible(localize('error.no_supported_camera'), {
|
|
||||||
view: viewName,
|
|
||||||
camera: cameraID,
|
|
||||||
...(capabilities && { camera_capabilities: capabilities }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const configuredDisplayMode = this._getDefaultDisplayModeForView(viewName, config);
|
|
||||||
const displayMode =
|
|
||||||
// Prioritize the configured display mode (if present).
|
|
||||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1812
|
|
||||||
(viewName !== options?.baseView?.view ? configuredDisplayMode : null) ??
|
|
||||||
options?.params?.displayMode ??
|
|
||||||
options?.baseView?.displayMode ??
|
|
||||||
configuredDisplayMode ??
|
|
||||||
'single';
|
|
||||||
|
|
||||||
const viewParameters: ViewParameters = {
|
const viewParameters: ViewParameters = {
|
||||||
...options?.params,
|
...options?.params,
|
||||||
@@ -165,22 +114,153 @@ export class ViewFactory {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getDefaultDisplayModeForView(
|
protected _resolveViewName(
|
||||||
|
config: AdvancedCameraCardConfig,
|
||||||
|
options?: ViewFactoryOptions,
|
||||||
|
): AdvancedCameraCardView {
|
||||||
|
if (options?.params?.view !== undefined) {
|
||||||
|
return resolveViewName(
|
||||||
|
options.params.view,
|
||||||
|
this._api.getCameraManager(),
|
||||||
|
this._api.getFoldersManager(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return options?.baseView?.view ?? this._getDefaultViewName(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _resolveCameraID(
|
||||||
|
viewName: AdvancedCameraCardView,
|
||||||
|
options?: ViewFactoryOptions,
|
||||||
|
): string | null {
|
||||||
|
const cameraID = options?.params?.camera ?? options?.baseView?.camera ?? null;
|
||||||
|
const allCameraIDs = this._api.getCameraManager().getStore().getCameraIDs();
|
||||||
|
|
||||||
|
if (cameraID && allCameraIDs.has(cameraID)) {
|
||||||
|
return cameraID;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewCameraIDs = getCameraIDsWithCapabilityForView(
|
||||||
|
viewName,
|
||||||
|
this._api.getCameraManager(),
|
||||||
|
this._api.getFoldersManager(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return viewCameraIDs?.keys().next().value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _ensureViewCompatibility(
|
||||||
|
viewName: AdvancedCameraCardView,
|
||||||
|
cameraID: string | null,
|
||||||
|
config: AdvancedCameraCardConfig,
|
||||||
|
options?: ViewFactoryOptions,
|
||||||
|
): ResolvedViewTarget {
|
||||||
|
if (!cameraID && doesViewRequireCamera(viewName)) {
|
||||||
|
return this._handleNoCameraForView(viewName, config, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cameraID &&
|
||||||
|
!isViewSupportedByCamera(
|
||||||
|
viewName,
|
||||||
|
this._api.getCameraManager(),
|
||||||
|
this._api.getFoldersManager(),
|
||||||
|
cameraID,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return this._handleUnsupportedView(viewName, cameraID, config, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { viewName, cameraID };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleNoCameraForView(
|
||||||
|
viewName: AdvancedCameraCardView,
|
||||||
|
config: AdvancedCameraCardConfig,
|
||||||
|
options?: ViewFactoryOptions,
|
||||||
|
): ResolvedViewTarget {
|
||||||
|
const defaultViewName = this._getDefaultViewName(config);
|
||||||
|
if (options?.failSafe && !doesViewRequireCamera(defaultViewName)) {
|
||||||
|
return { viewName: defaultViewName, cameraID: null };
|
||||||
|
}
|
||||||
|
if (options?.failSafe) {
|
||||||
|
return {
|
||||||
|
viewName: defaultViewName,
|
||||||
|
cameraID: this._api.getCameraManager().getStore().getDefaultCameraID(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new ViewIncompatible(localize('error.no_supported_cameras'), {
|
||||||
|
view: viewName,
|
||||||
|
camera: null,
|
||||||
|
default_view: defaultViewName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleUnsupportedView(
|
||||||
|
viewName: AdvancedCameraCardView,
|
||||||
|
cameraID: string,
|
||||||
|
config: AdvancedCameraCardConfig,
|
||||||
|
options?: ViewFactoryOptions,
|
||||||
|
): ResolvedViewTarget {
|
||||||
|
const defaultViewName = this._getDefaultViewName(config);
|
||||||
|
if (
|
||||||
|
options?.failSafe &&
|
||||||
|
isViewSupportedByCamera(
|
||||||
|
defaultViewName,
|
||||||
|
this._api.getCameraManager(),
|
||||||
|
this._api.getFoldersManager(),
|
||||||
|
cameraID,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return { viewName: defaultViewName, cameraID };
|
||||||
|
}
|
||||||
|
|
||||||
|
const capabilities = this._api
|
||||||
|
.getCameraManager()
|
||||||
|
.getStore()
|
||||||
|
.getCamera(cameraID)
|
||||||
|
?.getCapabilities()
|
||||||
|
?.getRawCapabilities();
|
||||||
|
|
||||||
|
throw new ViewIncompatible(localize('error.no_supported_camera'), {
|
||||||
|
view: viewName,
|
||||||
|
camera: cameraID,
|
||||||
|
default_view: defaultViewName,
|
||||||
|
...(capabilities && { camera_capabilities: capabilities }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _resolveDisplayMode(
|
||||||
|
viewName: AdvancedCameraCardView,
|
||||||
|
config: AdvancedCameraCardConfig,
|
||||||
|
options?: ViewFactoryOptions,
|
||||||
|
): ViewDisplayMode {
|
||||||
|
const configuredDisplayMode = this._getConfiguredDisplayMode(viewName, config);
|
||||||
|
|
||||||
|
// Prioritize the configured display mode (if present).
|
||||||
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1812
|
||||||
|
return (
|
||||||
|
(viewName !== options?.baseView?.view ? configuredDisplayMode : null) ??
|
||||||
|
options?.params?.displayMode ??
|
||||||
|
options?.baseView?.displayMode ??
|
||||||
|
configuredDisplayMode ??
|
||||||
|
'single'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getConfiguredDisplayMode(
|
||||||
viewName: AdvancedCameraCardView,
|
viewName: AdvancedCameraCardView,
|
||||||
config: AdvancedCameraCardConfig,
|
config: AdvancedCameraCardConfig,
|
||||||
): ViewDisplayMode | null {
|
): ViewDisplayMode | null {
|
||||||
let mode: ViewDisplayMode | null = null;
|
|
||||||
switch (viewName) {
|
switch (viewName) {
|
||||||
case 'media':
|
case 'media':
|
||||||
case 'clip':
|
case 'clip':
|
||||||
case 'recording':
|
case 'recording':
|
||||||
case 'snapshot':
|
case 'snapshot':
|
||||||
mode = config.media_viewer.display?.mode ?? null;
|
return config.media_viewer.display?.mode ?? null;
|
||||||
break;
|
|
||||||
case 'live':
|
case 'live':
|
||||||
mode = config.live.display?.mode ?? null;
|
return config.live.display?.mode ?? null;
|
||||||
break;
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return mode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export class SubstreamOnViewModifier implements ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public modify(view: View): void {
|
public modify(view: View): void {
|
||||||
|
if (!view.camera) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dependencies = [
|
const dependencies = [
|
||||||
...this._api
|
...this._api
|
||||||
.getCameraManager()
|
.getCameraManager()
|
||||||
@@ -27,6 +31,13 @@ export class SubstreamOnViewModifier implements ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentOverride = getStreamCameraID(view);
|
const currentOverride = getStreamCameraID(view);
|
||||||
|
|
||||||
|
/* istanbul ignore if: the if path cannot be reached, as there is a
|
||||||
|
view.camera guard at the start of this method and getStreamCameraID will
|
||||||
|
always return non-null as long as camera is present -- @preserve */
|
||||||
|
if (!currentOverride) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const currentIndex = dependencies.indexOf(currentOverride);
|
const currentIndex = dependencies.indexOf(currentOverride);
|
||||||
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
|
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AdvancedCameraCardError } from '../../types.js';
|
|||||||
import { ViewItem } from '../../view/item.js';
|
import { ViewItem } from '../../view/item.js';
|
||||||
import { QueryResults } from '../../view/query-results.js';
|
import { QueryResults } from '../../view/query-results.js';
|
||||||
|
|
||||||
|
import { AdvancedCameraCardUserSpecifiedView } from '../../config/schema/common/const.js';
|
||||||
import { View, ViewParameters } from '../../view/view.js';
|
import { View, ViewParameters } from '../../view/view.js';
|
||||||
|
|
||||||
export interface ViewModifier {
|
export interface ViewModifier {
|
||||||
@@ -26,12 +27,16 @@ export interface QueryExecutorOptions {
|
|||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ViewParametersUserSpecified = Partial<Omit<ViewParameters, 'view'>> & {
|
||||||
|
view?: AdvancedCameraCardUserSpecifiedView;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ViewFactoryOptions {
|
export interface ViewFactoryOptions {
|
||||||
// An existing view to evolve from.
|
// An existing view to evolve from.
|
||||||
baseView?: View | null;
|
baseView?: View | null;
|
||||||
|
|
||||||
// View parameters to set/evolve.
|
// View parameters to set/evolve.
|
||||||
params?: Partial<ViewParameters>;
|
params?: ViewParametersUserSpecified;
|
||||||
|
|
||||||
// Modifiers to the view once created.
|
// Modifiers to the view once created.
|
||||||
modifiers?: ViewModifier[];
|
modifiers?: ViewModifier[];
|
||||||
@@ -70,5 +75,4 @@ export interface ViewManagerInterface {
|
|||||||
hasMajorMediaChange(oldView?: View | null, newView?: View | null): boolean;
|
hasMajorMediaChange(oldView?: View | null, newView?: View | null): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ViewNoCameraError extends AdvancedCameraCardError {}
|
|
||||||
export class ViewIncompatible extends AdvancedCameraCardError {}
|
export class ViewIncompatible extends AdvancedCameraCardError {}
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ export class ViewManager implements ViewManagerInterface {
|
|||||||
|
|
||||||
this._api.getConditionStateManager()?.setState({
|
this._api.getConditionStateManager()?.setState({
|
||||||
view: view?.view,
|
view: view?.view,
|
||||||
camera: view?.camera,
|
camera: view?.camera ?? undefined,
|
||||||
displayMode: view?.displayMode ?? undefined,
|
displayMode: view?.displayMode ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { UnifiedQuery } from '../../view/unified-query';
|
|||||||
import { UnifiedQueryBuilder } from '../../view/unified-query-builder';
|
import { UnifiedQueryBuilder } from '../../view/unified-query-builder';
|
||||||
import { UnifiedQueryRunner } from '../../view/unified-query-runner';
|
import { UnifiedQueryRunner } from '../../view/unified-query-runner';
|
||||||
import { View } from '../../view/view';
|
import { View } from '../../view/view';
|
||||||
|
import { doesViewRequireCamera, isViewSupported } from '../../view/view-support';
|
||||||
import { CardViewAPI } from '../types';
|
import { CardViewAPI } from '../types';
|
||||||
import { MergeContextViewModifier } from './modifiers/merge-context';
|
import { MergeContextViewModifier } from './modifiers/merge-context';
|
||||||
import { RemoveContextPropertyViewModifier } from './modifiers/remove-context-property';
|
import { RemoveContextPropertyViewModifier } from './modifiers/remove-context-property';
|
||||||
@@ -103,7 +104,26 @@ export class ViewQueryExecutor {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const cameraForQuery = view.isGrid() ? undefined : view.camera;
|
// Don't query if the view is not supported (e.g. a camera-based view with
|
||||||
|
// no cameras).
|
||||||
|
if (
|
||||||
|
!isViewSupported(
|
||||||
|
view.view,
|
||||||
|
this._api.getCameraManager(),
|
||||||
|
this._api.getFoldersManager(),
|
||||||
|
view.camera,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't query if the view requires a camera, if it's not in grid mode, but
|
||||||
|
// no camera is provided.
|
||||||
|
if (!view.isGrid() && !view.camera && doesViewRequireCamera(view.view)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraForQuery = view.isGrid() ? undefined : view.camera ?? undefined;
|
||||||
|
|
||||||
const getDefaultQueryModifiers = async () => {
|
const getDefaultQueryModifiers = async () => {
|
||||||
const query = builder.buildDefaultCameraQuery(cameraForQuery, {
|
const query = builder.buildDefaultCameraQuery(cameraForQuery, {
|
||||||
|
|||||||
+3
-6
@@ -9,7 +9,6 @@ import { actionHandler } from './action-handler-directive.js';
|
|||||||
import { CardController } from './card-controller/controller';
|
import { CardController } from './card-controller/controller';
|
||||||
import { MenuButtonController } from './components-lib/menu-button-controller';
|
import { MenuButtonController } from './components-lib/menu-button-controller';
|
||||||
import './components/effects/effects';
|
import './components/effects/effects';
|
||||||
import { AdvancedCameraCardEffects } from './components/effects/effects';
|
|
||||||
import './components/elements.js';
|
import './components/elements.js';
|
||||||
import { AdvancedCameraCardElements } from './components/elements.js';
|
import { AdvancedCameraCardElements } from './components/elements.js';
|
||||||
import './components/loading.js';
|
import './components/loading.js';
|
||||||
@@ -96,12 +95,10 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
// diagnostics starting at the top).
|
// diagnostics starting at the top).
|
||||||
() => this._refMain.value?.scroll({ top: 0 }),
|
() => this._refMain.value?.scroll({ top: 0 }),
|
||||||
() => this._refMenu.value?.toggleMenu(),
|
() => this._refMenu.value?.toggleMenu(),
|
||||||
() => this._refEffects.value ?? null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
protected _menuButtonController = new MenuButtonController();
|
protected _menuButtonController = new MenuButtonController();
|
||||||
|
|
||||||
protected _refEffects: Ref<AdvancedCameraCardEffects> = createRef();
|
|
||||||
protected _refElements: Ref<AdvancedCameraCardElements> = createRef();
|
protected _refElements: Ref<AdvancedCameraCardElements> = createRef();
|
||||||
protected _refMain: Ref<HTMLElement> = createRef();
|
protected _refMain: Ref<HTMLElement> = createRef();
|
||||||
protected _refMenu: Ref<AdvancedCameraCardMenu> = createRef();
|
protected _refMenu: Ref<AdvancedCameraCardMenu> = createRef();
|
||||||
@@ -350,7 +347,7 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
// ensure the hover menu styling continues to work.
|
// ensure the hover menu styling continues to work.
|
||||||
return this._renderInDialogIfNecessary(
|
return this._renderInDialogIfNecessary(
|
||||||
html` <advanced-camera-card-effects
|
html` <advanced-camera-card-effects
|
||||||
${ref(this._refEffects)}
|
.effectsManager=${this._controller.getEffectsManager()}
|
||||||
></advanced-camera-card-effects>
|
></advanced-camera-card-effects>
|
||||||
<ha-card
|
<ha-card
|
||||||
id="ha-card"
|
id="ha-card"
|
||||||
@@ -387,9 +384,9 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
.loaded=${this._controller
|
.loaded=${this._controller
|
||||||
.getInitializationManager()
|
.getInitializationManager()
|
||||||
.wasEverInitialized()}
|
.wasEverInitialized()}
|
||||||
.effectsControllerAPI=${this._config?.performance?.features
|
.effectsManager=${this._config?.performance?.features
|
||||||
.card_loading_effects !== false
|
.card_loading_effects !== false
|
||||||
? this._controller.getEffectsControllerAPI()
|
? this._controller.getEffectsManager()
|
||||||
: undefined}
|
: undefined}
|
||||||
></advanced-camera-card-loading>`
|
></advanced-camera-card-loading>`
|
||||||
: ''}
|
: ''}
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ import { isBeingCasted } from '../utils/casting';
|
|||||||
import { getPTZTarget } from '../utils/ptz';
|
import { getPTZTarget } from '../utils/ptz';
|
||||||
import { getStreamCameraID, hasSubstream } from '../utils/substream';
|
import { getStreamCameraID, hasSubstream } from '../utils/substream';
|
||||||
import { ViewItemClassifier } from '../view/item-classifier';
|
import { ViewItemClassifier } from '../view/item-classifier';
|
||||||
|
import { resolveViewName } from '../view/utils/resolve-default';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { getCameraIDsForViewName, isViewSupportedByCamera } from '../view/view-support';
|
import {
|
||||||
|
getCameraIDsWithCapabilityForView,
|
||||||
|
isViewSupported,
|
||||||
|
} from '../view/view-support';
|
||||||
|
|
||||||
export interface MenuButtonControllerOptions {
|
export interface MenuButtonControllerOptions {
|
||||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
@@ -112,7 +116,13 @@ export class MenuButtonController {
|
|||||||
this._getFoldersButton(config, foldersManager, options?.view),
|
this._getFoldersButton(config, foldersManager, options?.view),
|
||||||
|
|
||||||
...this._dynamicMenuButtons.map((button) => ({
|
...this._dynamicMenuButtons.map((button) => ({
|
||||||
style: this._getStyleFromActions(config, button, options),
|
style: this._getStyleFromActions(
|
||||||
|
config,
|
||||||
|
cameraManager,
|
||||||
|
foldersManager,
|
||||||
|
button,
|
||||||
|
options,
|
||||||
|
),
|
||||||
...button,
|
...button,
|
||||||
})),
|
})),
|
||||||
].filter(isTruthy);
|
].filter(isTruthy);
|
||||||
@@ -175,7 +185,7 @@ export class MenuButtonController {
|
|||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
if (!view) {
|
if (!view?.camera) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +194,7 @@ export class MenuButtonController {
|
|||||||
.getAllDependentCameras(view.camera, 'substream');
|
.getAllDependentCameras(view.camera, 'substream');
|
||||||
|
|
||||||
if (substreamCameraIDs.size && view.is('live')) {
|
if (substreamCameraIDs.size && view.is('live')) {
|
||||||
const substreams = [...substreamCameraIDs].filter(
|
const substreams = Array.from(substreamCameraIDs).filter(
|
||||||
(cameraID) => cameraID !== view.camera,
|
(cameraID) => cameraID !== view.camera,
|
||||||
);
|
);
|
||||||
const streams = [view.camera, ...substreams];
|
const streams = [view.camera, ...substreams];
|
||||||
@@ -239,14 +249,13 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('live', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('live', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:cctv',
|
icon: 'mdi:cctv',
|
||||||
...config.menu.buttons.live,
|
...config.menu.buttons.live,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.live'),
|
title: localize('config.view.views.live'),
|
||||||
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
style: view?.is('live') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('live'),
|
tap_action: createViewAction('live'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
@@ -258,8 +267,7 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('clips', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('clips', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:filmstrip',
|
icon: 'mdi:filmstrip',
|
||||||
...config.menu.buttons.clips,
|
...config.menu.buttons.clips,
|
||||||
@@ -278,8 +286,7 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('snapshots', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('snapshots', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:camera',
|
icon: 'mdi:camera',
|
||||||
...config.menu.buttons.snapshots,
|
...config.menu.buttons.snapshots,
|
||||||
@@ -298,14 +305,13 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('recordings', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('recordings', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:album',
|
icon: 'mdi:album',
|
||||||
...config.menu.buttons.recordings,
|
...config.menu.buttons.recordings,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.recordings'),
|
title: localize('config.view.views.recordings'),
|
||||||
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
style: view?.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('recordings'),
|
tap_action: createViewAction('recordings'),
|
||||||
hold_action: createViewAction('recording'),
|
hold_action: createViewAction('recording'),
|
||||||
}
|
}
|
||||||
@@ -318,14 +324,13 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('reviews', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('reviews', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:play-box-edit-outline',
|
icon: 'mdi:play-box-edit-outline',
|
||||||
...config.menu.buttons.reviews,
|
...config.menu.buttons.reviews,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.reviews'),
|
title: localize('config.view.views.reviews'),
|
||||||
style: view.is('reviews') ? this._getEmphasizedStyle() : {},
|
style: view?.is('reviews') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('reviews'),
|
tap_action: createViewAction('reviews'),
|
||||||
hold_action: createViewAction('review'),
|
hold_action: createViewAction('review'),
|
||||||
}
|
}
|
||||||
@@ -338,14 +343,13 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('gallery', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('gallery', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:play-box-multiple',
|
icon: 'mdi:play-box-multiple',
|
||||||
...config.menu.buttons.gallery,
|
...config.menu.buttons.gallery,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.gallery'),
|
title: localize('config.menu.buttons.gallery'),
|
||||||
style: view.is('gallery') ? this._getEmphasizedStyle() : {},
|
style: view?.is('gallery') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('gallery'),
|
tap_action: createViewAction('gallery'),
|
||||||
hold_action: createViewAction('media'),
|
hold_action: createViewAction('media'),
|
||||||
}
|
}
|
||||||
@@ -358,8 +362,7 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('image', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('image', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:image',
|
icon: 'mdi:image',
|
||||||
...config.menu.buttons.image,
|
...config.menu.buttons.image,
|
||||||
@@ -377,14 +380,13 @@ export class MenuButtonController {
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
return view &&
|
return isViewSupported('timeline', cameraManager, foldersManager, view?.camera)
|
||||||
isViewSupportedByCamera('timeline', cameraManager, foldersManager, view.camera)
|
|
||||||
? {
|
? {
|
||||||
icon: 'mdi:chart-gantt',
|
icon: 'mdi:chart-gantt',
|
||||||
...config.menu.buttons.timeline,
|
...config.menu.buttons.timeline,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.timeline'),
|
title: localize('config.view.views.timeline'),
|
||||||
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
style: view?.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('timeline'),
|
tap_action: createViewAction('timeline'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
@@ -479,11 +481,12 @@ export class MenuButtonController {
|
|||||||
view?: View | null,
|
view?: View | null,
|
||||||
microphoneManager?: MicrophoneManager | null,
|
microphoneManager?: MicrophoneManager | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
if (!view) {
|
const streamCameraID = view ? getStreamCameraID(view) : null;
|
||||||
|
if (!streamCameraID) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const capabilities = cameraManager.getCameraCapabilities(getStreamCameraID(view));
|
const capabilities = cameraManager.getCameraCapabilities(streamCameraID);
|
||||||
|
|
||||||
if (microphoneManager && capabilities?.has('2-way-audio')) {
|
if (microphoneManager && capabilities?.has('2-way-audio')) {
|
||||||
const unavailable =
|
const unavailable =
|
||||||
@@ -557,7 +560,9 @@ export class MenuButtonController {
|
|||||||
if (!view) {
|
if (!view) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const selectedCameraConfig = cameraManager.getStore().getCameraConfig(view.camera);
|
const selectedCameraConfig = view.camera
|
||||||
|
? cameraManager.getStore().getCameraConfig(view.camera)
|
||||||
|
: null;
|
||||||
if (
|
if (
|
||||||
mediaPlayerController?.hasMediaPlayers() &&
|
mediaPlayerController?.hasMediaPlayers() &&
|
||||||
(view.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity))
|
(view.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity))
|
||||||
@@ -659,7 +664,7 @@ export class MenuButtonController {
|
|||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
const viewCameraIDs = view
|
const viewCameraIDs = view
|
||||||
? getCameraIDsForViewName(view.view, cameraManager, foldersManager)
|
? getCameraIDsWithCapabilityForView(view.view, cameraManager, foldersManager)
|
||||||
: null;
|
: null;
|
||||||
if (
|
if (
|
||||||
view?.supportsMultipleDisplayModes() &&
|
view?.supportsMultipleDisplayModes() &&
|
||||||
@@ -754,8 +759,8 @@ export class MenuButtonController {
|
|||||||
foldersManager?: FoldersManager | null,
|
foldersManager?: FoldersManager | null,
|
||||||
view?: View | null,
|
view?: View | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
const folders = [...(foldersManager?.getFolders() ?? [])];
|
const folders = Array.from(foldersManager?.getFolders() ?? []);
|
||||||
if (!folders?.length) {
|
if (!foldersManager?.hasFolders()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,6 +828,8 @@ export class MenuButtonController {
|
|||||||
*/
|
*/
|
||||||
protected _getStyleFromActions(
|
protected _getStyleFromActions(
|
||||||
config: AdvancedCameraCardConfig,
|
config: AdvancedCameraCardConfig,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
foldersManager: FoldersManager,
|
||||||
button: MenuItem,
|
button: MenuItem,
|
||||||
options?: MenuButtonControllerOptions,
|
options?: MenuButtonControllerOptions,
|
||||||
): StyleInfo {
|
): StyleInfo {
|
||||||
@@ -848,7 +855,9 @@ export class MenuButtonController {
|
|||||||
),
|
),
|
||||||
) ||
|
) ||
|
||||||
(action.advanced_camera_card_action === 'default' &&
|
(action.advanced_camera_card_action === 'default' &&
|
||||||
options?.view?.is(config.view.default)) ||
|
options?.view?.is(
|
||||||
|
resolveViewName(config.view.default, cameraManager, foldersManager),
|
||||||
|
)) ||
|
||||||
(action.advanced_camera_card_action === 'fullscreen' &&
|
(action.advanced_camera_card_action === 'fullscreen' &&
|
||||||
!!options?.fullscreenManager?.isInFullscreen()) ||
|
!!options?.fullscreenManager?.isInFullscreen()) ||
|
||||||
(action.advanced_camera_card_action === 'camera_select' &&
|
(action.advanced_camera_card_action === 'camera_select' &&
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ export class TimelineController {
|
|||||||
main: true,
|
main: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (panMode === 'seek-in-camera') {
|
} else if (panMode === 'seek-in-camera' && view.camera) {
|
||||||
newResults = results
|
newResults = results
|
||||||
.clone()
|
.clone()
|
||||||
.selectBestResult(
|
.selectBestResult(
|
||||||
|
|||||||
@@ -1,31 +1,28 @@
|
|||||||
import { LitElement, unsafeCSS } from 'lit';
|
import { LitElement, PropertyValues, unsafeCSS } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { EffectsController } from '../../components-lib/effects/effects-controller';
|
import { EffectsManager } from '../../card-controller/effects/effects-manager';
|
||||||
import { EffectOptions } from '../../components-lib/effects/types';
|
|
||||||
import effectsStyle from '../../scss/effects.scss';
|
import effectsStyle from '../../scss/effects.scss';
|
||||||
import { EffectName, EffectsControllerAPI } from '../../types';
|
|
||||||
|
|
||||||
@customElement('advanced-camera-card-effects')
|
@customElement('advanced-camera-card-effects')
|
||||||
export class AdvancedCameraCardEffects
|
export class AdvancedCameraCardEffects extends LitElement {
|
||||||
extends LitElement
|
@property({ attribute: false })
|
||||||
implements EffectsControllerAPI
|
public effectsManager?: EffectsManager;
|
||||||
{
|
|
||||||
protected _controller = new EffectsController();
|
|
||||||
|
|
||||||
public async startEffect(effect: EffectName, options?: EffectOptions): Promise<void> {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
await this._controller.startEffect(effect, options);
|
if (changedProperties.has('effectsManager')) {
|
||||||
|
const previousManager: EffectsManager | undefined =
|
||||||
|
changedProperties.get('effectsManager');
|
||||||
|
previousManager?.removeContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopEffect(effect: EffectName): void {
|
|
||||||
this._controller.stopEffect(effect);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async toggleEffect(effect: EffectName, options?: EffectOptions): Promise<void> {
|
|
||||||
await this._controller.toggleEffect(effect, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(): void {
|
protected updated(): void {
|
||||||
this._controller.setContainer(this.renderRoot);
|
this.effectsManager?.setContainer(this.renderRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback(): void {
|
||||||
|
this.effectsManager?.removeContainer();
|
||||||
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
|||||||
+17
-1
@@ -10,11 +10,13 @@ import { CameraConfig } from '../config/schema/cameras';
|
|||||||
import { ImageViewConfig } from '../config/schema/image';
|
import { ImageViewConfig } from '../config/schema/image';
|
||||||
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
||||||
import { HomeAssistant } from '../ha/types';
|
import { HomeAssistant } from '../ha/types';
|
||||||
|
import { localize } from '../localize/localize.js';
|
||||||
import imageStyle from '../scss/image.scss';
|
import imageStyle from '../scss/image.scss';
|
||||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
||||||
import './image-updating-player';
|
import './image-updating-player';
|
||||||
import { resolveImageMode } from './image-updating-player';
|
import { resolveImageMode } from './image-updating-player';
|
||||||
import './media-dimensions-container';
|
import './media-dimensions-container';
|
||||||
|
import { renderMessage } from './message.js';
|
||||||
import './zoomer.js';
|
import './zoomer.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-image')
|
@customElement('advanced-camera-card-image')
|
||||||
@@ -81,10 +83,24 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.cameraConfig) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if this image mode requires a camera
|
||||||
|
const mode = resolveImageMode({
|
||||||
|
imageConfig: this.imageConfig,
|
||||||
|
cameraConfig: this.cameraConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mode === 'camera' && !this.cameraConfig) {
|
||||||
|
return renderMessage({
|
||||||
|
type: 'info',
|
||||||
|
message: localize('error.no_camera_for_image'),
|
||||||
|
icon: 'mdi:camera-off',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this._renderContainer(html`
|
return this._renderContainer(html`
|
||||||
<advanced-camera-card-image-updating-player
|
<advanced-camera-card-image-updating-player
|
||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
|
|
||||||
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
if (!cameraIDs?.size || !view) {
|
if (!cameraIDs?.size || !view?.camera) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return Math.max(0, Array.from(cameraIDs).indexOf(view.camera));
|
return Math.max(0, Array.from(cameraIDs).indexOf(view.camera));
|
||||||
@@ -248,6 +248,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cameraID = this.viewFilterCameraID ?? view.camera;
|
const cameraID = this.viewFilterCameraID ?? view.camera;
|
||||||
|
if (!cameraID) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
const currentIndex = cameraIDs.indexOf(cameraID);
|
const currentIndex = cameraIDs.indexOf(cameraID);
|
||||||
|
|
||||||
if (currentIndex < 0) {
|
if (currentIndex < 0) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import loadingStyle from '../scss/loading.scss';
|
import loadingStyle from '../scss/loading.scss';
|
||||||
import { EffectName, EffectsControllerAPI } from '../types';
|
import { EffectName, EffectsManagerInterface } from '../types';
|
||||||
import { getReleaseVersion } from '../utils/diagnostics';
|
import { getReleaseVersion } from '../utils/diagnostics';
|
||||||
import './icon';
|
import './icon';
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const getDateEffect = (): EffectName | null => {
|
|||||||
@customElement('advanced-camera-card-loading')
|
@customElement('advanced-camera-card-loading')
|
||||||
export class AdvancedCameraCardLoading extends LitElement {
|
export class AdvancedCameraCardLoading extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public effectsControllerAPI?: EffectsControllerAPI | null;
|
public effectsManager?: EffectsManagerInterface;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
public loaded = false;
|
public loaded = false;
|
||||||
@@ -60,13 +60,13 @@ export class AdvancedCameraCardLoading extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _startEffect(effect: EffectName): void {
|
private _startEffect(effect: EffectName): void {
|
||||||
this.effectsControllerAPI?.startEffect(effect, { fadeIn: false });
|
this.effectsManager?.startEffect(effect, { fadeIn: false });
|
||||||
this._effectName = effect;
|
this._effectName = effect;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _stopEffect(): void {
|
private _stopEffect(): void {
|
||||||
if (this._effectName) {
|
if (this._effectName) {
|
||||||
this.effectsControllerAPI?.stopEffect(this._effectName);
|
this.effectsManager?.stopEffect(this._effectName);
|
||||||
}
|
}
|
||||||
this._effectName = null;
|
this._effectName = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
? this.config.media_viewer.controls.timeline
|
? this.config.media_viewer.controls.timeline
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const cameraConfig = view
|
const cameraConfig = view?.camera
|
||||||
? this.cameraManager?.getStore().getCameraConfig(view.camera) ?? null
|
? this.cameraManager?.getStore().getCameraConfig(view.camera) ?? null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
.viewItemManager=${this.viewItemManager}
|
.viewItemManager=${this.viewItemManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
${!this.hide && view?.is('image') && cameraConfig
|
${!this.hide && view?.is('image')
|
||||||
? html` <advanced-camera-card-image
|
? html` <advanced-camera-card-image
|
||||||
.imageConfig=${this.config.image}
|
.imageConfig=${this.config.image}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
|
|||||||
@@ -329,7 +329,4 @@ export const cameraConfigSchema = z
|
|||||||
.default(cameraConfigDefault);
|
.default(cameraConfigDefault);
|
||||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||||
|
|
||||||
// Avoid using .nonempty() to avoid changing the inferred type
|
export const camerasConfigSchema = cameraConfigSchema.array().optional();
|
||||||
// (https://github.com/colinhacks/zod#minmaxlength).
|
|
||||||
export const camerasConfigSchema = cameraConfigSchema.array().min(1);
|
|
||||||
export type CamerasConfig = z.infer<typeof camerasConfigSchema>;
|
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ export const STATUS_BAR_PRIORITY_MAX = 100;
|
|||||||
|
|
||||||
export const BUTTON_SIZE_MIN = 20;
|
export const BUTTON_SIZE_MIN = 20;
|
||||||
|
|
||||||
// The default view (may not be supported on all cameras).
|
const VIEWS = [
|
||||||
export const VIEW_DEFAULT = 'live' as const;
|
|
||||||
|
|
||||||
export const VIEWS_USER_SPECIFIED = [
|
|
||||||
'diagnostics',
|
'diagnostics',
|
||||||
'live',
|
'live',
|
||||||
'clip',
|
'clip',
|
||||||
@@ -29,5 +26,7 @@ export const VIEWS_USER_SPECIFIED = [
|
|||||||
'image',
|
'image',
|
||||||
'timeline',
|
'timeline',
|
||||||
] as const;
|
] as const;
|
||||||
|
export type AdvancedCameraCardView = (typeof VIEWS)[number];
|
||||||
|
|
||||||
|
export const VIEWS_USER_SPECIFIED = ['auto', ...VIEWS] as const;
|
||||||
export type AdvancedCameraCardUserSpecifiedView = (typeof VIEWS_USER_SPECIFIED)[number];
|
export type AdvancedCameraCardUserSpecifiedView = (typeof VIEWS_USER_SPECIFIED)[number];
|
||||||
export type AdvancedCameraCardView = AdvancedCameraCardUserSpecifiedView | 'diagnostics';
|
|
||||||
|
|||||||
@@ -13,4 +13,3 @@ export const menuBaseSchema = z.object({
|
|||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
permanent: z.boolean().default(false).optional(),
|
permanent: z.boolean().default(false).optional(),
|
||||||
});
|
});
|
||||||
export type MenuItemBase = z.infer<typeof menuBaseSchema>;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { actionsSchema } from './actions/types';
|
import { actionsSchema } from './actions/types';
|
||||||
import { VIEW_DEFAULT, VIEWS_USER_SPECIFIED } from './common/const';
|
import { VIEWS_USER_SPECIFIED } from './common/const';
|
||||||
|
|
||||||
const keyboardShortcut = z.object({
|
const keyboardShortcut = z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
@@ -46,7 +46,7 @@ export type PTZKeyboardShortcutName =
|
|||||||
| 'ptz_zoom_out';
|
| 'ptz_zoom_out';
|
||||||
|
|
||||||
export const viewConfigDefault = {
|
export const viewConfigDefault = {
|
||||||
default: VIEW_DEFAULT,
|
default: 'auto' as const,
|
||||||
camera_select: 'current' as const,
|
camera_select: 'current' as const,
|
||||||
interaction_seconds: 300,
|
interaction_seconds: 300,
|
||||||
default_reset: {
|
default_reset: {
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
|
|
||||||
protected _viewModes: EditorSelectOption[] = [
|
protected _viewModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
|
{ value: 'auto', label: localize('config.view.views.auto') },
|
||||||
{ value: 'clip', label: localize('config.view.views.clip') },
|
{ value: 'clip', label: localize('config.view.views.clip') },
|
||||||
{ value: 'clips', label: localize('config.view.views.clips') },
|
{ value: 'clips', label: localize('config.view.views.clips') },
|
||||||
{ value: 'folder', label: localize('config.view.views.folder') },
|
{ value: 'folder', label: localize('config.view.views.folder') },
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "Advanced Camera Card",
|
"advanced_camera_card": "Advanced Camera Card",
|
||||||
"advanced_camera_card_description": "An Advanced Camera Card",
|
"advanced_camera_card_description": "An Advanced Camera Card",
|
||||||
"up": "Up",
|
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
"no_folder": "No folder to display",
|
"no_folder": "No folder to display",
|
||||||
@@ -22,6 +21,7 @@
|
|||||||
"medium": "Medium"
|
"medium": "Medium"
|
||||||
},
|
},
|
||||||
"severity": "Severity",
|
"severity": "Severity",
|
||||||
|
"up": "Up",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@@ -628,6 +628,7 @@
|
|||||||
"untrigger_seconds": "Seconds after inactive state change to untrigger"
|
"untrigger_seconds": "Seconds after inactive state change to untrigger"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
|
"auto": "Automatic",
|
||||||
"clip": "Most recent clip",
|
"clip": "Most recent clip",
|
||||||
"clips": "Clips gallery",
|
"clips": "Clips gallery",
|
||||||
"current": "Current view",
|
"current": "Current view",
|
||||||
@@ -738,6 +739,8 @@
|
|||||||
"no_camera_engine": "Could not determine suitable engine for camera",
|
"no_camera_engine": "Could not determine suitable engine for camera",
|
||||||
"no_camera_entity": "Could not find camera entity",
|
"no_camera_entity": "Could not find camera entity",
|
||||||
"no_camera_entity_for_triggers": "A camera entity is required in order to autodetect triggers",
|
"no_camera_entity_for_triggers": "A camera entity is required in order to autodetect triggers",
|
||||||
|
"no_camera_for_image": "No camera configured for camera image mode",
|
||||||
|
"no_camera_for_live": "No cameras are configured for live view",
|
||||||
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
|
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
|
||||||
"no_camera_or_media_for_timeline": "No camera or media available for timeline",
|
"no_camera_or_media_for_timeline": "No camera or media available for timeline",
|
||||||
"no_dashboard_or_view": "Both 'dashboard_path' and 'view_path' parameters are required for the 'dashboard' cast method",
|
"no_dashboard_or_view": "Both 'dashboard_path' and 'view_path' parameters are required for the 'dashboard' cast method",
|
||||||
|
|||||||
+6
-2
@@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { EffectOptions } from './components-lib/effects/types';
|
import type { EffectOptions } from './card-controller/effects/types';
|
||||||
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
|
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
|
||||||
import { Severity } from './severity';
|
import { Severity } from './severity';
|
||||||
|
|
||||||
@@ -217,8 +217,12 @@ export type EffectName =
|
|||||||
| 'shamrocks'
|
| 'shamrocks'
|
||||||
| 'snow';
|
| 'snow';
|
||||||
|
|
||||||
export interface EffectsControllerAPI {
|
export type EffectsContainer = HTMLElement | DocumentFragment;
|
||||||
|
export interface EffectsManagerInterface {
|
||||||
startEffect(name: EffectName, options?: EffectOptions): Promise<void>;
|
startEffect(name: EffectName, options?: EffectOptions): Promise<void>;
|
||||||
stopEffect(effect: EffectName): void;
|
stopEffect(effect: EffectName): void;
|
||||||
toggleEffect(effect: EffectName, options?: EffectOptions): Promise<void>;
|
toggleEffect(effect: EffectName, options?: EffectOptions): Promise<void>;
|
||||||
|
|
||||||
|
setContainer(container: EffectsContainer): void;
|
||||||
|
removeContainer(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ViewItemClassifier } from '../view/item-classifier';
|
|||||||
export const findBestMediaTimeIndex = (
|
export const findBestMediaTimeIndex = (
|
||||||
mediaArray: ViewItem[],
|
mediaArray: ViewItem[],
|
||||||
targetTime: Date,
|
targetTime: Date,
|
||||||
favorCameraID?: string,
|
favorCameraID?: string | null,
|
||||||
): number | null => {
|
): number | null => {
|
||||||
let bestMatch:
|
let bestMatch:
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export const getPTZTarget = (
|
|||||||
};
|
};
|
||||||
} else if (view.is('live')) {
|
} else if (view.is('live')) {
|
||||||
const substreamAwareCameraID = getStreamCameraID(view);
|
const substreamAwareCameraID = getStreamCameraID(view);
|
||||||
|
if (!substreamAwareCameraID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
let type: PTZType = 'digital';
|
let type: PTZType = 'digital';
|
||||||
|
|
||||||
if (options?.type !== 'digital' && options?.cameraManager) {
|
if (options?.type !== 'digital' && options?.cameraManager) {
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ const screenshotElement = (
|
|||||||
|
|
||||||
export const generateScreenshotFilename = (view?: View | null): string => {
|
export const generateScreenshotFilename = (view?: View | null): string => {
|
||||||
if (view?.is('live') || view?.is('image')) {
|
if (view?.is('live') || view?.is('image')) {
|
||||||
return `${view.view}_${view.camera}_${format(
|
return `${view.view}${view.camera ? `_${view.camera}` : ''}_${format(
|
||||||
new Date(),
|
new Date(),
|
||||||
`yyyy-MM-dd-HH-mm-ss`,
|
`yyyy-MM-dd-HH-mm-ss`,
|
||||||
)}.jpg`;
|
)}.jpg`;
|
||||||
} else if (view?.isViewerView()) {
|
} else if (view?.isViewerView()) {
|
||||||
const media = view.queryResults?.getSelectedResult();
|
const media = view.queryResults?.getSelectedResult();
|
||||||
const id = media?.getID() ?? null;
|
const id = media?.getID() ?? null;
|
||||||
return `${view.view}_${view.camera}${id ? `_${id}` : ''}.jpg`;
|
return `${view.view}${view.camera ? `_${view.camera}` : ''}${id ? `_${id}` : ''}.jpg`;
|
||||||
}
|
}
|
||||||
return 'screenshot.jpg';
|
return 'screenshot.jpg';
|
||||||
};
|
};
|
||||||
|
|||||||
+22
-6
@@ -1,18 +1,31 @@
|
|||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
|
|
||||||
export const getStreamCameraID = (view: View, cameraID?: string): string => {
|
/**
|
||||||
return (
|
* Get the effective camera ID for streaming, considering substream overrides.
|
||||||
view.context?.live?.overrides?.get(cameraID ?? view.camera) ??
|
* Returns null if the view has no camera.
|
||||||
cameraID ??
|
*/
|
||||||
view.camera
|
export const getStreamCameraID = (
|
||||||
);
|
view: View,
|
||||||
|
cameraID?: string | null,
|
||||||
|
): string | null => {
|
||||||
|
const baseCameraID = cameraID ?? view.camera;
|
||||||
|
if (!baseCameraID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return view.context?.live?.overrides?.get(baseCameraID) ?? baseCameraID;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasSubstream = (view: View): boolean => {
|
export const hasSubstream = (view: View): boolean => {
|
||||||
|
if (!view.camera) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return getStreamCameraID(view) !== view.camera;
|
return getStreamCameraID(view) !== view.camera;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setSubstream = (view: View, substreamID: string): void => {
|
export const setSubstream = (view: View, substreamID: string): void => {
|
||||||
|
if (!view.camera) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const overrides: Map<string, string> = view.context?.live?.overrides ?? new Map();
|
const overrides: Map<string, string> = view.context?.live?.overrides ?? new Map();
|
||||||
overrides.set(view.camera, substreamID);
|
overrides.set(view.camera, substreamID);
|
||||||
view.mergeInContext({
|
view.mergeInContext({
|
||||||
@@ -21,6 +34,9 @@ export const setSubstream = (view: View, substreamID: string): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const removeSubstream = (view: View): void => {
|
export const removeSubstream = (view: View): void => {
|
||||||
|
if (!view.camera) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const overrides: Map<string, string> | undefined = view.context?.live?.overrides;
|
const overrides: Map<string, string> | undefined = view.context?.live?.overrides;
|
||||||
if (overrides && overrides.has(view.camera)) {
|
if (overrides && overrides.has(view.camera)) {
|
||||||
view.context?.live?.overrides?.delete(view.camera);
|
view.context?.live?.overrides?.delete(view.camera);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { CameraManager } from '../../camera-manager/manager';
|
||||||
|
import { FoldersManager } from '../../card-controller/folders/manager';
|
||||||
|
import {
|
||||||
|
AdvancedCameraCardUserSpecifiedView,
|
||||||
|
AdvancedCameraCardView,
|
||||||
|
} from '../../config/schema/common/const';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a view name that may be 'auto'.
|
||||||
|
* @param viewName The view name.
|
||||||
|
* @param cameraManager The camera manager.
|
||||||
|
* @param foldersManager The folders manager.
|
||||||
|
* @returns A concrete view name.
|
||||||
|
*/
|
||||||
|
export const resolveViewName = (
|
||||||
|
viewName: AdvancedCameraCardUserSpecifiedView,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
foldersManager: FoldersManager,
|
||||||
|
): AdvancedCameraCardView => {
|
||||||
|
if (viewName !== 'auto') {
|
||||||
|
return viewName;
|
||||||
|
}
|
||||||
|
return cameraManager.getStore().getCameraIDs().size
|
||||||
|
? 'live'
|
||||||
|
: foldersManager.hasFolders()
|
||||||
|
? 'folders'
|
||||||
|
: 'image';
|
||||||
|
};
|
||||||
+119
-45
@@ -1,64 +1,120 @@
|
|||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { CapabilitySearchOptions } from '../camera-manager/types';
|
import { CapabilitySearchKeys, CapabilitySearchOptions } from '../camera-manager/types';
|
||||||
import { FoldersManager } from '../card-controller/folders/manager';
|
import { FoldersManager } from '../card-controller/folders/manager';
|
||||||
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
||||||
|
|
||||||
/**
|
type ViewSource = 'camera' | 'folder' | 'any';
|
||||||
* Get cameraIDs that are relevant for a given view name based on camera
|
|
||||||
* capability (if camera specified).
|
interface ViewCapabilityRequirements {
|
||||||
*/
|
src?: ViewSource;
|
||||||
export const getCameraIDsForViewName = (
|
mediaCapabilities?: CapabilitySearchKeys;
|
||||||
|
mediaCapabilitiesInclusive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anyMedia: ViewCapabilityRequirements = {
|
||||||
|
src: 'any',
|
||||||
|
mediaCapabilities: {
|
||||||
|
anyCapabilities: ['clips', 'snapshots', 'recordings', 'reviews'],
|
||||||
|
},
|
||||||
|
mediaCapabilitiesInclusive: false,
|
||||||
|
};
|
||||||
|
const anyFolder = { src: 'folder' as const };
|
||||||
|
const noRequirements = {};
|
||||||
|
|
||||||
|
const generateCameraRequirements = (
|
||||||
|
mediaCapabilities: CapabilitySearchKeys,
|
||||||
|
mediaCapabilitiesInclusive = true,
|
||||||
|
): ViewCapabilityRequirements => {
|
||||||
|
return {
|
||||||
|
src: 'camera',
|
||||||
|
mediaCapabilities,
|
||||||
|
mediaCapabilitiesInclusive,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const VIEW_REQUIREMENTS: Record<AdvancedCameraCardView, ViewCapabilityRequirements> = {
|
||||||
|
live: generateCameraRequirements('live', false),
|
||||||
|
|
||||||
|
clip: generateCameraRequirements('clips'),
|
||||||
|
clips: generateCameraRequirements('clips'),
|
||||||
|
snapshot: generateCameraRequirements('snapshots'),
|
||||||
|
snapshots: generateCameraRequirements('snapshots'),
|
||||||
|
recording: generateCameraRequirements('recordings'),
|
||||||
|
recordings: generateCameraRequirements('recordings'),
|
||||||
|
review: generateCameraRequirements('reviews'),
|
||||||
|
reviews: generateCameraRequirements('reviews'),
|
||||||
|
|
||||||
|
gallery: anyMedia,
|
||||||
|
media: anyMedia,
|
||||||
|
timeline: anyMedia,
|
||||||
|
|
||||||
|
folder: anyFolder,
|
||||||
|
folders: anyFolder,
|
||||||
|
|
||||||
|
image: noRequirements,
|
||||||
|
diagnostics: noRequirements,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isViewAvailable = (
|
||||||
|
view: AdvancedCameraCardView,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
foldersManager: FoldersManager,
|
||||||
|
): boolean => {
|
||||||
|
const req = VIEW_REQUIREMENTS[view];
|
||||||
|
const hasCameras = cameraManager.getStore().getCameraIDs().size > 0;
|
||||||
|
const hasFolders = foldersManager.hasFolders();
|
||||||
|
|
||||||
|
if (req.src === 'camera') {
|
||||||
|
return hasCameras;
|
||||||
|
}
|
||||||
|
if (req.src === 'folder') {
|
||||||
|
return hasFolders;
|
||||||
|
}
|
||||||
|
if (req.src === 'any') {
|
||||||
|
return hasCameras || hasFolders;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doesViewRequireCamera = (view: AdvancedCameraCardView): boolean => {
|
||||||
|
return VIEW_REQUIREMENTS[view].src === 'camera';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCameraIDsWithCapabilityForView = (
|
||||||
viewName: AdvancedCameraCardView,
|
viewName: AdvancedCameraCardView,
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
cameraID?: string,
|
cameraID?: string,
|
||||||
): Set<string> => {
|
): Set<string> => {
|
||||||
const folder = foldersManager.getFolder();
|
const requirements = VIEW_REQUIREMENTS[viewName];
|
||||||
|
const allCameras = cameraManager.getStore().getCameraIDs();
|
||||||
|
|
||||||
switch (viewName) {
|
if (requirements.src !== 'camera' && requirements.src !== 'any') {
|
||||||
case 'diagnostics':
|
if (requirements.src === 'folder' && !foldersManager.hasFolders()) {
|
||||||
case 'image':
|
return new Set();
|
||||||
return cameraManager.getStore().getCameraIDs();
|
}
|
||||||
|
return allCameras;
|
||||||
|
}
|
||||||
|
|
||||||
case 'folder':
|
if (requirements.src === 'any' && foldersManager.hasFolders()) {
|
||||||
case 'folders':
|
return allCameras;
|
||||||
return folder ? cameraManager.getStore().getCameraIDs() : new Set();
|
}
|
||||||
|
|
||||||
case 'live':
|
|
||||||
case 'clip':
|
|
||||||
case 'clips':
|
|
||||||
case 'snapshot':
|
|
||||||
case 'snapshots':
|
|
||||||
case 'recording':
|
|
||||||
case 'recordings':
|
|
||||||
case 'review':
|
|
||||||
case 'reviews':
|
|
||||||
const options: CapabilitySearchOptions = {
|
const options: CapabilitySearchOptions = {
|
||||||
inclusive: viewName !== 'live',
|
inclusive: !!requirements.mediaCapabilitiesInclusive,
|
||||||
};
|
};
|
||||||
const capability =
|
const capability = requirements.mediaCapabilities;
|
||||||
viewName === 'clip'
|
|
||||||
? 'clips'
|
/* istanbul ignore next: this path is currently unreachable given the mapping
|
||||||
: viewName === 'snapshot'
|
in VIEW_REQUIREMENTS includes mediaCapabilities for all camera or 'any'
|
||||||
? 'snapshots'
|
related views -- @preserve */
|
||||||
: viewName === 'recording'
|
if (!capability) {
|
||||||
? 'recordings'
|
return allCameras;
|
||||||
: viewName === 'review'
|
}
|
||||||
? 'reviews'
|
|
||||||
: viewName;
|
|
||||||
return cameraID
|
return cameraID
|
||||||
? cameraManager.getStore().getAllDependentCameras(cameraID, capability, options)
|
? cameraManager.getStore().getAllDependentCameras(cameraID, capability, options)
|
||||||
: cameraManager.getStore().getCameraIDsWithCapability(capability, options);
|
: cameraManager.getStore().getCameraIDsWithCapability(capability, options);
|
||||||
|
|
||||||
case 'gallery':
|
|
||||||
case 'media':
|
|
||||||
case 'timeline':
|
|
||||||
return folder
|
|
||||||
? cameraManager.getStore().getCameraIDs()
|
|
||||||
: cameraManager.getStore().getCameraIDsWithCapability({
|
|
||||||
anyCapabilities: ['clips', 'snapshots', 'recordings', 'reviews'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isViewSupportedByCamera = (
|
export const isViewSupportedByCamera = (
|
||||||
@@ -67,5 +123,23 @@ export const isViewSupportedByCamera = (
|
|||||||
foldersManager: FoldersManager,
|
foldersManager: FoldersManager,
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return !!getCameraIDsForViewName(view, cameraManager, foldersManager, cameraID)?.size;
|
return !!getCameraIDsWithCapabilityForView(
|
||||||
|
view,
|
||||||
|
cameraManager,
|
||||||
|
foldersManager,
|
||||||
|
cameraID,
|
||||||
|
)?.size;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isViewSupported = (
|
||||||
|
viewName: AdvancedCameraCardView,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
foldersManager: FoldersManager,
|
||||||
|
cameraID?: string | null,
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
isViewAvailable(viewName, cameraManager, foldersManager) &&
|
||||||
|
(!cameraID ||
|
||||||
|
isViewSupportedByCamera(viewName, cameraManager, foldersManager, cameraID))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-4
@@ -16,7 +16,7 @@ declare module 'view' {
|
|||||||
|
|
||||||
interface ViewEvolveParameters {
|
interface ViewEvolveParameters {
|
||||||
view?: AdvancedCameraCardView;
|
view?: AdvancedCameraCardView;
|
||||||
camera?: string;
|
camera?: string | null;
|
||||||
query?: UnifiedQuery | null;
|
query?: UnifiedQuery | null;
|
||||||
queryResults?: QueryResults | null;
|
queryResults?: QueryResults | null;
|
||||||
context?: ViewContext | null;
|
context?: ViewContext | null;
|
||||||
@@ -25,7 +25,6 @@ interface ViewEvolveParameters {
|
|||||||
|
|
||||||
export interface ViewParameters extends ViewEvolveParameters {
|
export interface ViewParameters extends ViewEvolveParameters {
|
||||||
view: AdvancedCameraCardView;
|
view: AdvancedCameraCardView;
|
||||||
camera: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mergeViewContext = (
|
export const mergeViewContext = (
|
||||||
@@ -37,7 +36,7 @@ export const mergeViewContext = (
|
|||||||
|
|
||||||
export class View {
|
export class View {
|
||||||
public view: AdvancedCameraCardView;
|
public view: AdvancedCameraCardView;
|
||||||
public camera: string;
|
public camera: string | null;
|
||||||
public query: UnifiedQuery | null;
|
public query: UnifiedQuery | null;
|
||||||
public queryResults: QueryResults | null;
|
public queryResults: QueryResults | null;
|
||||||
public context: ViewContext | null;
|
public context: ViewContext | null;
|
||||||
@@ -45,7 +44,7 @@ export class View {
|
|||||||
|
|
||||||
constructor(params: ViewParameters) {
|
constructor(params: ViewParameters) {
|
||||||
this.view = params.view;
|
this.view = params.view;
|
||||||
this.camera = params.camera;
|
this.camera = params.camera ?? null;
|
||||||
this.query = params.query ?? null;
|
this.query = params.query ?? null;
|
||||||
this.queryResults = params.queryResults ?? null;
|
this.queryResults = params.queryResults ?? null;
|
||||||
this.context = params.context ?? null;
|
this.context = params.context ?? null;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { CardController } from '../../src/card-controller/controller.js';
|
import { CardController } from '../../src/card-controller/controller.js';
|
||||||
import { sortItems } from '../../src/card-controller/view/sort.js';
|
import { sortItems } from '../../src/card-controller/view/sort.js';
|
||||||
import { CameraConfig } from '../../src/config/schema/cameras.js';
|
import { CameraConfig } from '../../src/config/schema/cameras.js';
|
||||||
|
import { advancedCameraCardConfigSchema } from '../../src/config/schema/types.js';
|
||||||
import { HomeAssistant } from '../../src/ha/types.js';
|
import { HomeAssistant } from '../../src/ha/types.js';
|
||||||
import { QuerySource } from '../../src/query-source.js';
|
import { QuerySource } from '../../src/query-source.js';
|
||||||
import { Endpoint, PTZMovementType } from '../../src/types.js';
|
import { Endpoint, PTZMovementType } from '../../src/types.js';
|
||||||
@@ -340,6 +341,21 @@ describe('CameraManager', () => {
|
|||||||
expect(manager.getStore().getCameraCount()).toBe(0);
|
expect(manager.getStore().getCameraCount()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('without cameras in config', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
advancedCameraCardConfigSchema.parse({
|
||||||
|
type: 'advanced-camera-card' as const,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const manager = new CameraManager(api);
|
||||||
|
|
||||||
|
await manager.initializeCamerasFromConfig();
|
||||||
|
expect(manager.getStore().getCameraCount()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('without an id', async () => {
|
it('without an id', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { EffectAction } from '../../../../src/card-controller/actions/actions/effect';
|
import { EffectAction } from '../../../../src/card-controller/actions/actions/effect';
|
||||||
import { createEffectAction } from '../../../../src/utils/action';
|
import { createEffectAction } from '../../../../src/utils/action';
|
||||||
import { createCardAPI } from '../../../test-utils';
|
import { createCardAPI } from '../../../test-utils';
|
||||||
@@ -11,7 +11,7 @@ describe('EffectAction', () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(api.getEffectsControllerAPI()?.startEffect).toHaveBeenCalledWith('snow');
|
expect(api.getEffectsManager().startEffect).toHaveBeenCalledWith('snow');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call stopEffect when action is stop', async () => {
|
it('should call stopEffect when action is stop', async () => {
|
||||||
@@ -21,7 +21,7 @@ describe('EffectAction', () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(api.getEffectsControllerAPI()?.stopEffect).toHaveBeenCalledWith('snow');
|
expect(api.getEffectsManager().stopEffect).toHaveBeenCalledWith('snow');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call toggleEffect when action is toggle', async () => {
|
it('should call toggleEffect when action is toggle', async () => {
|
||||||
@@ -31,16 +31,7 @@ describe('EffectAction', () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(api.getEffectsControllerAPI()?.toggleEffect).toHaveBeenCalledWith('snow');
|
expect(api.getEffectsManager().toggleEffect).toHaveBeenCalledWith('snow');
|
||||||
});
|
|
||||||
|
|
||||||
it('should not throw when effectsControllerAPI is null', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getEffectsControllerAPI).mockReturnValue(null);
|
|
||||||
const actionConfig = createEffectAction('snow', 'start');
|
|
||||||
const action = new EffectAction({}, actionConfig);
|
|
||||||
|
|
||||||
await expect(action.execute(api)).resolves.toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a no-op stop method', async () => {
|
it('should have a no-op stop method', async () => {
|
||||||
|
|||||||
@@ -101,4 +101,28 @@ describe('should handle media_player action', () => {
|
|||||||
|
|
||||||
expect(api.getMediaPlayerManager().playMedia).not.toBeCalled();
|
expect(api.getMediaPlayerManager().playMedia).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('to not play live without camera', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: null,
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const action = new MediaPlayerAction(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
advanced_camera_card_action: 'media_player',
|
||||||
|
media_player_action: 'play',
|
||||||
|
media_player: 'this_is_a_media_player',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await action.execute(api);
|
||||||
|
|
||||||
|
expect(api.getMediaPlayerManager().playLive).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
|
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
|
||||||
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
||||||
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
||||||
import { ConditionStateManager } from '../../../src/conditions/state-manager';
|
import { ConditionStateManager } from '../../../src/conditions/state-manager';
|
||||||
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
|
import { Automation } from '../../../src/config/schema/automations';
|
||||||
|
import { AdvancedCameraCardCondition } from '../../../src/config/schema/conditions/types';
|
||||||
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
||||||
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
|
|
||||||
import { createGeneralAction } from '../../../src/utils/action';
|
import { createGeneralAction } from '../../../src/utils/action';
|
||||||
|
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a ConfigManager test setup with real AutomationsManager and ConditionStateManager.
|
* Create a ConfigManager test setup with real AutomationsManager and ConditionStateManager.
|
||||||
@@ -92,7 +94,7 @@ describe('ConfigManager', () => {
|
|||||||
it('invalid configuration with hint', () => {
|
it('invalid configuration with hint', () => {
|
||||||
const manager = new ConfigManager(createCardAPI());
|
const manager = new ConfigManager(createCardAPI());
|
||||||
expect(() => manager.setConfig({})).toThrowError(
|
expect(() => manager.setConfig({})).toThrowError(
|
||||||
'Invalid configuration: [\n "cameras",\n "type"\n]',
|
'Invalid configuration: [\n "type"\n]',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,7 +292,7 @@ describe('ConfigManager', () => {
|
|||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
conditions: [TEST_CONDITIONS.FULLSCREEN_ON],
|
conditions: [TEST_CONDITIONS.FULLSCREEN_ON],
|
||||||
delete: ['cameras'],
|
delete: ['type'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -472,7 +474,7 @@ describe('ConfigManager', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.setConfig(config as any);
|
manager.setConfig(config);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Verify keyboard shortcuts automations were added initially with ptz_home
|
// Verify keyboard shortcuts automations were added initially with ptz_home
|
||||||
@@ -501,9 +503,10 @@ describe('ConfigManager', () => {
|
|||||||
// This confirms the override removed them (directly verified through add calls)
|
// This confirms the override removed them (directly verified through add calls)
|
||||||
const addCalls = addAutomationsSpy.mock.calls;
|
const addCalls = addAutomationsSpy.mock.calls;
|
||||||
const hasKeyboardShortcut = addCalls.some((call) =>
|
const hasKeyboardShortcut = addCalls.some((call) =>
|
||||||
call[0].some((automation: any) =>
|
call[0].some((automation: Automation) =>
|
||||||
automation.conditions?.some(
|
automation.conditions?.some(
|
||||||
(cond: any) => cond.condition === 'key' && cond.key === 'h',
|
(cond: AdvancedCameraCardCondition) =>
|
||||||
|
cond.condition === 'key' && cond.key === 'h',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -524,7 +527,7 @@ describe('ConfigManager', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.setConfig(config as any);
|
manager.setConfig(config);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Verify initial payload (folders may be populated with defaults, so
|
// Verify initial payload (folders may be populated with defaults, so
|
||||||
@@ -594,7 +597,7 @@ describe('ConfigManager', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.setConfig(config as any);
|
manager.setConfig(config);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Verify automations were added initially
|
// Verify automations were added initially
|
||||||
@@ -652,7 +655,7 @@ describe('ConfigManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initial set should register remote control automations
|
// Initial set should register remote control automations
|
||||||
manager.setConfig(config as any);
|
manager.setConfig(config);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Verify remote-control automations were added initially with config condition
|
// Verify remote-control automations were added initially with config condition
|
||||||
@@ -682,9 +685,9 @@ describe('ConfigManager', () => {
|
|||||||
// Verify new automations don't contain remote-control config conditions
|
// Verify new automations don't contain remote-control config conditions
|
||||||
const addCalls = addAutomationsSpy.mock.calls;
|
const addCalls = addAutomationsSpy.mock.calls;
|
||||||
const hasRemoteControl = addCalls.some((call) =>
|
const hasRemoteControl = addCalls.some((call) =>
|
||||||
call[0].some((automation: any) =>
|
call[0].some((automation: Automation) =>
|
||||||
automation.conditions?.some(
|
automation.conditions?.some(
|
||||||
(cond: any) =>
|
(cond: AdvancedCameraCardCondition) =>
|
||||||
cond.condition === 'config' &&
|
cond.condition === 'config' &&
|
||||||
cond.paths?.includes('remote_control.entities.camera'),
|
cond.paths?.includes('remote_control.entities.camera'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -313,13 +313,16 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('should select option on entity', () => {
|
describe('should select option on entity', () => {
|
||||||
it('should select option when camera differs from entity state', () => {
|
it('should select option when camera differs from entity state', async () => {
|
||||||
const hass = createHASS({
|
const hass = createHASS({
|
||||||
'input_select.camera': createStateEntity({
|
'input_select.camera': createStateEntity({
|
||||||
state: 'camera.one',
|
state: 'camera.one',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera.two' }]),
|
||||||
|
);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
@@ -349,7 +352,7 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
INTERNAL_CALLBACK_ACTION,
|
INTERNAL_CALLBACK_ACTION,
|
||||||
);
|
);
|
||||||
|
|
||||||
cameraSyncAction.callback(api);
|
await cameraSyncAction.callback(api);
|
||||||
expect(hass.callService).toBeCalledWith(
|
expect(hass.callService).toBeCalledWith(
|
||||||
'input_select',
|
'input_select',
|
||||||
'select_option',
|
'select_option',
|
||||||
@@ -369,6 +372,9 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera.one' }]),
|
||||||
|
);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
@@ -441,7 +447,47 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
expect(hass.callService).not.toBeCalled();
|
expect(hass.callService).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw when hass is undefined', () => {
|
it('should not select option when view exists but has no camera', () => {
|
||||||
|
const hass = createHASS({
|
||||||
|
'input_select.camera': createStateEntity({
|
||||||
|
state: 'camera.one',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
remote_control: {
|
||||||
|
entities: {
|
||||||
|
camera: 'input_select.camera',
|
||||||
|
camera_priority: 'card',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: null,
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
|
// Test the 'camera' condition callback (automation index 1)
|
||||||
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
cameraSyncAction.callback(api);
|
||||||
|
expect(hass.callService).not.toBeCalled();
|
||||||
|
|
||||||
|
// Also test the 'initialized' condition callback (automation index 2)
|
||||||
|
const initializedSyncAction = vi.mocked(api.getAutomationsManager().addAutomations)
|
||||||
|
.mock.calls[0][0][2].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
initializedSyncAction.callback(api);
|
||||||
|
expect(hass.callService).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw when hass is undefined', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
@@ -472,12 +518,15 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Should not throw and obviously not call service (as hass is null)
|
// Should not throw and obviously not call service (as hass is null)
|
||||||
cameraSyncAction.callback(api);
|
await cameraSyncAction.callback(api);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select option when entity state is undefined', () => {
|
it('should select option when entity state is undefined', async () => {
|
||||||
const hass = createHASS({});
|
const hass = createHASS({});
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera.two' }]),
|
||||||
|
);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
@@ -506,7 +555,7 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
INTERNAL_CALLBACK_ACTION,
|
INTERNAL_CALLBACK_ACTION,
|
||||||
);
|
);
|
||||||
|
|
||||||
cameraSyncAction.callback(api);
|
await cameraSyncAction.callback(api);
|
||||||
expect(hass.callService).toBeCalledWith(
|
expect(hass.callService).toBeCalledWith(
|
||||||
'input_select',
|
'input_select',
|
||||||
'select_option',
|
'select_option',
|
||||||
@@ -519,13 +568,16 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select option on initialization with card priority', () => {
|
it('should select option on initialization with card priority', async () => {
|
||||||
const hass = createHASS({
|
const hass = createHASS({
|
||||||
'input_select.camera': createStateEntity({
|
'input_select.camera': createStateEntity({
|
||||||
state: 'camera.one',
|
state: 'camera.one',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera.two' }]),
|
||||||
|
);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
@@ -553,7 +605,7 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
expect(isAdvancedCameraCardCustomAction(initAction)).toBeTruthy();
|
expect(isAdvancedCameraCardCustomAction(initAction)).toBeTruthy();
|
||||||
expect(initAction.advanced_camera_card_action).toBe(INTERNAL_CALLBACK_ACTION);
|
expect(initAction.advanced_camera_card_action).toBe(INTERNAL_CALLBACK_ACTION);
|
||||||
|
|
||||||
initAction.callback(api);
|
await initAction.callback(api);
|
||||||
expect(hass.callService).toBeCalledWith(
|
expect(hass.callService).toBeCalledWith(
|
||||||
'input_select',
|
'input_select',
|
||||||
'select_option',
|
'select_option',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { CameraManager } from '../../src/camera-manager/manager';
|
import { CameraManager } from '../../src/camera-manager/manager';
|
||||||
import { ActionsManager } from '../../src/card-controller/actions/actions-manager';
|
import { ActionsManager } from '../../src/card-controller/actions/actions-manager';
|
||||||
import { AutomationsManager } from '../../src/card-controller/automations-manager';
|
import { AutomationsManager } from '../../src/card-controller/automations-manager';
|
||||||
@@ -34,7 +33,6 @@ import { AdvancedCameraCardEditor } from '../../src/editor';
|
|||||||
import { DeviceRegistryManager } from '../../src/ha/registry/device';
|
import { DeviceRegistryManager } from '../../src/ha/registry/device';
|
||||||
import { EntityRegistryManagerLive } from '../../src/ha/registry/entity';
|
import { EntityRegistryManagerLive } from '../../src/ha/registry/entity';
|
||||||
import { ResolvedMediaCache } from '../../src/ha/resolved-media';
|
import { ResolvedMediaCache } from '../../src/ha/resolved-media';
|
||||||
import { EffectsControllerAPI } from '../../src/types';
|
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager');
|
vi.mock('../../src/camera-manager/manager');
|
||||||
vi.mock('../../src/card-controller/actions/actions-manager');
|
vi.mock('../../src/card-controller/actions/actions-manager');
|
||||||
@@ -74,7 +72,7 @@ const createCardElement = (): CardHTMLElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createController = (): CardController => {
|
const createController = (): CardController => {
|
||||||
return new CardController(createCardElement(), vi.fn(), vi.fn(), vi.fn());
|
return new CardController(createCardElement(), vi.fn(), vi.fn());
|
||||||
};
|
};
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
@@ -87,15 +85,8 @@ describe('CardController', () => {
|
|||||||
const element = createCardElement();
|
const element = createCardElement();
|
||||||
const scrollCallback = vi.fn();
|
const scrollCallback = vi.fn();
|
||||||
const menuToggleCallback = vi.fn();
|
const menuToggleCallback = vi.fn();
|
||||||
const effectsControllerAPI = mock<EffectsControllerAPI>();
|
|
||||||
const effectsControllerAPICallback = vi.fn().mockReturnValue(effectsControllerAPI);
|
|
||||||
|
|
||||||
const controller = new CardController(
|
const controller = new CardController(element, scrollCallback, menuToggleCallback);
|
||||||
element,
|
|
||||||
scrollCallback,
|
|
||||||
menuToggleCallback,
|
|
||||||
effectsControllerAPICallback,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(CardElementManager).toBeCalledWith(
|
expect(CardElementManager).toBeCalledWith(
|
||||||
controller,
|
controller,
|
||||||
@@ -103,7 +94,7 @@ describe('CardController', () => {
|
|||||||
scrollCallback,
|
scrollCallback,
|
||||||
menuToggleCallback,
|
menuToggleCallback,
|
||||||
);
|
);
|
||||||
expect(controller.getEffectsControllerAPI()).toBe(effectsControllerAPI);
|
expect(controller.getEffectsManager()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('accessors', () => {
|
describe('accessors', () => {
|
||||||
|
|||||||
+66
-39
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { EffectsController } from '../../../src/components-lib/effects/effects-controller';
|
import { EffectsManager } from '../../../src/card-controller/effects/effects-manager';
|
||||||
import { EffectComponent } from '../../../src/components-lib/effects/types';
|
import { EffectComponent } from '../../../src/card-controller/effects/types';
|
||||||
import { EffectName } from '../../../src/types';
|
import { EffectName } from '../../../src/types';
|
||||||
import { flushPromises } from '../../test-utils';
|
import { flushPromises } from '../../test-utils';
|
||||||
|
|
||||||
@@ -36,14 +36,14 @@ const createMockEffectComponent = (): EffectComponent => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('EffectsController', () => {
|
describe('EffectsManager', () => {
|
||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
let controller: EffectsController;
|
let manager: EffectsManager;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
controller = new EffectsController();
|
manager = new EffectsManager();
|
||||||
controller.setContainer(container);
|
manager.setContainer(container);
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ describe('EffectsController', () => {
|
|||||||
it('should load and start snow effect', async () => {
|
it('should load and start snow effect', async () => {
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -64,7 +64,7 @@ describe('EffectsController', () => {
|
|||||||
it('should load and start hearts effect', async () => {
|
it('should load and start hearts effect', async () => {
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
await controller.startEffect('hearts');
|
await manager.startEffect('hearts');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -72,7 +72,7 @@ describe('EffectsController', () => {
|
|||||||
it('should load and start shamrocks effect', async () => {
|
it('should load and start shamrocks effect', async () => {
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
await controller.startEffect('shamrocks');
|
await manager.startEffect('shamrocks');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -80,7 +80,7 @@ describe('EffectsController', () => {
|
|||||||
it('should load and start fireworks effect', async () => {
|
it('should load and start fireworks effect', async () => {
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
await controller.startEffect('fireworks');
|
await manager.startEffect('fireworks');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -88,7 +88,7 @@ describe('EffectsController', () => {
|
|||||||
it('should load and start ghost effect', async () => {
|
it('should load and start ghost effect', async () => {
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
await controller.startEffect('ghost');
|
await manager.startEffect('ghost');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -96,13 +96,13 @@ describe('EffectsController', () => {
|
|||||||
it('should load and start check effect', async () => {
|
it('should load and start check effect', async () => {
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
await controller.startEffect('check');
|
await manager.startEffect('check');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stop effect automatically after duration', async () => {
|
it('should stop effect automatically after duration', async () => {
|
||||||
const effectPromise = controller.startEffect('snow', { duration: 1 });
|
const effectPromise = manager.startEffect('snow', { duration: 1 });
|
||||||
|
|
||||||
// Wait for the effect to be added to the DOM.
|
// Wait for the effect to be added to the DOM.
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -117,56 +117,61 @@ describe('EffectsController', () => {
|
|||||||
|
|
||||||
it('should clean up timer when stopped manually', async () => {
|
it('should clean up timer when stopped manually', async () => {
|
||||||
// Don't await startEffect as it waits for the duration
|
// Don't await startEffect as it waits for the duration
|
||||||
controller.startEffect('snow', { duration: 10 });
|
manager.startEffect('snow', { duration: 10 });
|
||||||
|
|
||||||
// Wait for the effect to be added to the DOM.
|
// Wait for the effect to be added to the DOM.
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
await controller.stopEffect('snow');
|
await manager.stopEffect('snow');
|
||||||
|
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set fadeIn to true by default', async () => {
|
it('should set fadeIn to true by default', async () => {
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
|
|
||||||
const effectElement = container.children[0] as EffectComponent;
|
const effectElement = container.children[0] as EffectComponent;
|
||||||
expect(effectElement.fadeIn).toBe(true);
|
expect(effectElement.fadeIn).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set fadeIn from options', async () => {
|
it('should set fadeIn from options', async () => {
|
||||||
await controller.startEffect('snow', { fadeIn: false });
|
await manager.startEffect('snow', { fadeIn: false });
|
||||||
|
|
||||||
const effectElement = container.children[0] as EffectComponent;
|
const effectElement = container.children[0] as EffectComponent;
|
||||||
expect(effectElement.fadeIn).toBe(false);
|
expect(effectElement.fadeIn).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not start effect if container is null', async () => {
|
it('should queue effects until a container is available', async () => {
|
||||||
controller.setContainer(null);
|
manager.removeContainer();
|
||||||
|
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
|
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
|
manager.setContainer(container);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not start effect if already active', async () => {
|
it('should not start effect if already active', async () => {
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
|
|
||||||
// Should only have one child since the second call is ignored.
|
// Should only have one child since the second call is ignored.
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow starting different effects simultaneously', async () => {
|
it('should allow starting different effects simultaneously', async () => {
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
await controller.startEffect('hearts');
|
await manager.startEffect('hearts');
|
||||||
|
|
||||||
expect(container.children.length).toBe(2);
|
expect(container.children.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return early if effect module cannot be loaded', async () => {
|
it('should return early if effect module cannot be loaded', async () => {
|
||||||
// Use an effect name that doesn't exist in the registry
|
// Use an effect name that doesn't exist in the registry
|
||||||
await controller.startEffect('unknown' as EffectName);
|
await manager.startEffect('unknown' as EffectName);
|
||||||
|
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
});
|
});
|
||||||
@@ -174,10 +179,10 @@ describe('EffectsController', () => {
|
|||||||
|
|
||||||
describe('stopEffect', () => {
|
describe('stopEffect', () => {
|
||||||
it('should stop an active effect', async () => {
|
it('should stop an active effect', async () => {
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
const effectElement = container.children[0] as EffectComponent;
|
const effectElement = container.children[0] as EffectComponent;
|
||||||
|
|
||||||
await controller.stopEffect('snow');
|
await manager.stopEffect('snow');
|
||||||
|
|
||||||
expect(effectElement.startFadeOut).toHaveBeenCalled();
|
expect(effectElement.startFadeOut).toHaveBeenCalled();
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
@@ -186,25 +191,25 @@ describe('EffectsController', () => {
|
|||||||
it('should do nothing if effect is not active', async () => {
|
it('should do nothing if effect is not active', async () => {
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
|
|
||||||
await controller.stopEffect('snow');
|
await manager.stopEffect('snow');
|
||||||
|
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow starting effect again after stopping', async () => {
|
it('should allow starting effect again after stopping', async () => {
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
await controller.stopEffect('snow');
|
await manager.stopEffect('snow');
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel effect when stopped during loading', async () => {
|
it('should cancel effect when stopped during loading', async () => {
|
||||||
// Start the effect but don't await it - simulates the loading phase.
|
// Start the effect but don't await it - simulates the loading phase.
|
||||||
const startPromise = controller.startEffect('snow');
|
const startPromise = manager.startEffect('snow');
|
||||||
|
|
||||||
// Stop the effect immediately while it's still loading.
|
// Stop the effect immediately while it's still loading.
|
||||||
await controller.stopEffect('snow');
|
await manager.stopEffect('snow');
|
||||||
|
|
||||||
// Wait for start to complete.
|
// Wait for start to complete.
|
||||||
await startPromise;
|
await startPromise;
|
||||||
@@ -212,26 +217,48 @@ describe('EffectsController', () => {
|
|||||||
// The effect should not appear since it was stopped during loading.
|
// The effect should not appear since it was stopped during loading.
|
||||||
expect(container.children.length).toBe(0);
|
expect(container.children.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should cancel queued effects', async () => {
|
||||||
|
manager.removeContainer();
|
||||||
|
await manager.startEffect('snow');
|
||||||
|
await manager.stopEffect('snow');
|
||||||
|
|
||||||
|
manager.setContainer(container);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(container.children.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear active effects and timers when container is removed', async () => {
|
||||||
|
manager.startEffect('snow', { duration: 10 });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(container.children.length).toBe(1);
|
||||||
|
|
||||||
|
manager.removeContainer();
|
||||||
|
|
||||||
|
expect(container.children.length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toggleEffect', () => {
|
describe('toggleEffect', () => {
|
||||||
it('should start effect when not active', async () => {
|
it('should start effect when not active', async () => {
|
||||||
await controller.toggleEffect('snow');
|
await manager.toggleEffect('snow');
|
||||||
|
|
||||||
expect(container.children.length).toBe(1);
|
expect(container.children.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stop effect when active', async () => {
|
it('should stop effect when active', async () => {
|
||||||
await controller.startEffect('snow');
|
await manager.startEffect('snow');
|
||||||
const effectElement = container.children[0] as EffectComponent;
|
const effectElement = container.children[0] as EffectComponent;
|
||||||
|
|
||||||
await controller.toggleEffect('snow');
|
await manager.toggleEffect('snow');
|
||||||
|
|
||||||
expect(effectElement.startFadeOut).toHaveBeenCalled();
|
expect(effectElement.startFadeOut).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass options when starting effect', async () => {
|
it('should pass options when starting effect', async () => {
|
||||||
await controller.toggleEffect('snow', { fadeIn: false });
|
await manager.toggleEffect('snow', { fadeIn: false });
|
||||||
|
|
||||||
const effectElement = container.children[0] as EffectComponent;
|
const effectElement = container.children[0] as EffectComponent;
|
||||||
expect(effectElement.fadeIn).toBe(false);
|
expect(effectElement.fadeIn).toBe(false);
|
||||||
@@ -239,10 +266,10 @@ describe('EffectsController', () => {
|
|||||||
|
|
||||||
it('should cancel effect when toggled during loading', async () => {
|
it('should cancel effect when toggled during loading', async () => {
|
||||||
// Start the effect but don't await it - simulates the loading phase.
|
// Start the effect but don't await it - simulates the loading phase.
|
||||||
const startPromise = controller.startEffect('snow');
|
const startPromise = manager.startEffect('snow');
|
||||||
|
|
||||||
// Toggle the effect immediately while it's still loading.
|
// Toggle the effect immediately while it's still loading.
|
||||||
await controller.toggleEffect('snow');
|
await manager.toggleEffect('snow');
|
||||||
|
|
||||||
// Wait for start to complete.
|
// Wait for start to complete.
|
||||||
await startPromise;
|
await startPromise;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { FoldersManager } from '../../../src/card-controller/folders/manager';
|
||||||
import { ViewFactory } from '../../../src/card-controller/view/factory';
|
import { ViewFactory } from '../../../src/card-controller/view/factory';
|
||||||
import { ViewModifier } from '../../../src/card-controller/view/types';
|
import { ViewModifier } from '../../../src/card-controller/view/types';
|
||||||
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
|
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
|
||||||
@@ -9,6 +11,7 @@ import {
|
|||||||
createCapabilities,
|
createCapabilities,
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
createConfig,
|
createConfig,
|
||||||
|
createFolder,
|
||||||
createStore,
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
@@ -20,7 +23,7 @@ describe('getViewDefault', () => {
|
|||||||
expect(factory.getViewDefault()).toBeNull();
|
expect(factory.getViewDefault()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if no cameras support view', () => {
|
it('should throw when no cameras support view', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
@@ -47,6 +50,23 @@ describe('getViewDefault', () => {
|
|||||||
expect(() => factory.getViewDefault()).toThrowError(/No cameras support this view/);
|
expect(() => factory.getViewDefault()).toThrowError(/No cameras support this view/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use folders view as default when folders exist without cameras', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getFoldersManager).mockReturnValue(mock<FoldersManager>());
|
||||||
|
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
|
||||||
|
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(createFolder());
|
||||||
|
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore([]));
|
||||||
|
|
||||||
|
const factory = new ViewFactory(api);
|
||||||
|
const view = factory.getViewDefault();
|
||||||
|
|
||||||
|
expect(view?.view).toBe('folders');
|
||||||
|
expect(view?.camera).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create view', () => {
|
it('should create view', () => {
|
||||||
const factory = new ViewFactory(createPopulatedAPI());
|
const factory = new ViewFactory(createPopulatedAPI());
|
||||||
const view = factory.getViewDefault();
|
const view = factory.getViewDefault();
|
||||||
@@ -217,8 +237,6 @@ describe('getViewByParameters', () => {
|
|||||||
const factory = new ViewFactory(api);
|
const factory = new ViewFactory(api);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
factory.getViewByParameters({
|
factory.getViewByParameters({
|
||||||
// Since no camera is specified, and no camera supports the capabilities
|
|
||||||
// necessary for this view, the view will be null.
|
|
||||||
params: {
|
params: {
|
||||||
view: 'snapshots',
|
view: 'snapshots',
|
||||||
},
|
},
|
||||||
@@ -226,6 +244,56 @@ describe('getViewByParameters', () => {
|
|||||||
).toThrowError(/No cameras support this view/);
|
).toThrowError(/No cameras support this view/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should handle no camera for view with failsafe', () => {
|
||||||
|
it('should choose default view when it does not require a camera', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore([]));
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'diagnostics',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const factory = new ViewFactory(api);
|
||||||
|
const view = factory.getViewByParameters({
|
||||||
|
params: {
|
||||||
|
view: 'live',
|
||||||
|
},
|
||||||
|
failSafe: true,
|
||||||
|
});
|
||||||
|
expect(view?.is('diagnostics')).toBeTruthy();
|
||||||
|
expect(view?.camera).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should choose default view and camera when it requires a camera', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||||
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera.one' }]),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'live',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const factory = new ViewFactory(api);
|
||||||
|
const view = factory.getViewByParameters({
|
||||||
|
params: {
|
||||||
|
view: 'snapshots',
|
||||||
|
},
|
||||||
|
failSafe: true,
|
||||||
|
});
|
||||||
|
expect(view?.is('live')).toBeTruthy();
|
||||||
|
expect(view?.camera).toBe('camera.one');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('should handle unsupported view', () => {
|
describe('should handle unsupported view', () => {
|
||||||
it('should throw without failsafe', () => {
|
it('should throw without failsafe', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
@@ -285,6 +353,24 @@ describe('getViewByParameters', () => {
|
|||||||
});
|
});
|
||||||
expect(view?.is('live')).toBeTruthy();
|
expect(view?.is('live')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw and omit capabilities when camera object is missing from store', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const store = createStore([{ cameraID: 'camera.one' }]);
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
||||||
|
vi.spyOn(store, 'getCamera').mockReturnValue(null);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
|
|
||||||
|
const factory = new ViewFactory(api);
|
||||||
|
expect(() =>
|
||||||
|
factory.getViewByParameters({
|
||||||
|
params: {
|
||||||
|
camera: 'camera.one',
|
||||||
|
view: 'snapshots',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrowError(/The selected camera or media does not support this view/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call modifiers', () => {
|
it('should call modifiers', () => {
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ const createAPIWithSubstreams = (
|
|||||||
config?: RawAdvancedCameraCardConfig,
|
config?: RawAdvancedCameraCardConfig,
|
||||||
): CardController => {
|
): CardController => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
const store = createStore([
|
||||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
||||||
createStore([
|
|
||||||
{
|
{
|
||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: createCapabilities({
|
capabilities: createCapabilities({
|
||||||
@@ -42,8 +40,8 @@ const createAPIWithSubstreams = (
|
|||||||
substream: true,
|
substream: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]),
|
]);
|
||||||
);
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
||||||
return api;
|
return api;
|
||||||
};
|
};
|
||||||
@@ -97,11 +95,25 @@ describe('should turn on substream', () => {
|
|||||||
expect(hasSubstream(view)).toBe(false);
|
expect(hasSubstream(view)).toBe(false);
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
const cameraManager = createCameraManager(createStore([]));
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
|
|
||||||
const modifier = new SubstreamOnViewModifier(api);
|
const modifier = new SubstreamOnViewModifier(api);
|
||||||
modifier.modify(view);
|
modifier.modify(view);
|
||||||
|
|
||||||
expect(hasSubstream(view)).toBe(false);
|
expect(hasSubstream(view)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('without camera', () => {
|
||||||
|
const view = createView({
|
||||||
|
camera: null,
|
||||||
|
view: 'live',
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
const modifier = new SubstreamOnViewModifier(api);
|
||||||
|
modifier.modify(view);
|
||||||
|
|
||||||
|
expect(hasSubstream(view)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ describe('ViewQueryExecutor', () => {
|
|||||||
const store = createStore([
|
const store = createStore([
|
||||||
{
|
{
|
||||||
cameraID: 'camera.office',
|
cameraID: 'camera.office',
|
||||||
capabilities: createCapabilities({ [mode]: true }),
|
capabilities: createCapabilities({ live: true, [mode]: true }),
|
||||||
config: createCameraConfig({
|
config: createCameraConfig({
|
||||||
media: {
|
media: {
|
||||||
type: mode === 'reviews' ? 'reviews' : 'events',
|
type: mode === 'reviews' ? 'reviews' : 'events',
|
||||||
@@ -546,6 +546,34 @@ describe('ViewQueryExecutor', () => {
|
|||||||
expect(view.query).toBeNull();
|
expect(view.query).toBeNull();
|
||||||
expect(view.queryResults).toBeNull();
|
expect(view.queryResults).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle non-grid view without a camera', async () => {
|
||||||
|
const api = createPopulatedAPI();
|
||||||
|
const viewQueryExecutor = new ViewQueryExecutor(api);
|
||||||
|
const view = createView({
|
||||||
|
view: 'live',
|
||||||
|
camera: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||||
|
applyViewModifiers(view, modifiers);
|
||||||
|
|
||||||
|
expect(view.query).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle diagnostics view without a camera', async () => {
|
||||||
|
const api = createPopulatedAPI();
|
||||||
|
const viewQueryExecutor = new ViewQueryExecutor(api);
|
||||||
|
const view = createView({
|
||||||
|
view: 'diagnostics',
|
||||||
|
camera: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||||
|
applyViewModifiers(view, modifiers);
|
||||||
|
|
||||||
|
expect(view.query).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with a timeline view', () => {
|
describe('with a timeline view', () => {
|
||||||
@@ -799,6 +827,7 @@ describe('ViewQueryExecutor', () => {
|
|||||||
'should execute default folder query with %s view',
|
'should execute default folder query with %s view',
|
||||||
async (viewName: AdvancedCameraCardView) => {
|
async (viewName: AdvancedCameraCardView) => {
|
||||||
const api = createPopulatedAPI();
|
const api = createPopulatedAPI();
|
||||||
|
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
|
||||||
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue({
|
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue({
|
||||||
type: 'ha',
|
type: 'ha',
|
||||||
id: 'office',
|
id: 'office',
|
||||||
@@ -827,6 +856,7 @@ describe('ViewQueryExecutor', () => {
|
|||||||
|
|
||||||
it('should execute default folder query with folder view and handle null results', async () => {
|
it('should execute default folder query with folder view and handle null results', async () => {
|
||||||
const api = createPopulatedAPI();
|
const api = createPopulatedAPI();
|
||||||
|
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
|
||||||
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(null);
|
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(null);
|
||||||
vi.mocked(api.getFoldersManager().getDefaultQueryParameters).mockReturnValue(
|
vi.mocked(api.getFoldersManager().getDefaultQueryParameters).mockReturnValue(
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import { ViewMedia, ViewMediaType } from '../../src/view/item.js';
|
|||||||
import { QueryResults } from '../../src/view/query-results.js';
|
import { QueryResults } from '../../src/view/query-results.js';
|
||||||
import { UnifiedQuery } from '../../src/view/unified-query.js';
|
import { UnifiedQuery } from '../../src/view/unified-query.js';
|
||||||
import {
|
import {
|
||||||
getCameraIDsForViewName,
|
getCameraIDsWithCapabilityForView,
|
||||||
isViewSupportedByCamera,
|
isViewSupported,
|
||||||
} from '../../src/view/view-support.js';
|
} from '../../src/view/view-support.js';
|
||||||
import { View } from '../../src/view/view.js';
|
import { View } from '../../src/view/view.js';
|
||||||
import {
|
import {
|
||||||
@@ -89,6 +89,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
controller = new MenuButtonController();
|
controller = new MenuButtonController();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -507,7 +508,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have live menu button', () => {
|
describe('should have live menu button', () => {
|
||||||
it('when in live view', () => {
|
it('when in live view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'live' }),
|
view: createView({ view: 'live' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -526,7 +527,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in live view', () => {
|
it('when not in live view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'clips' }),
|
view: createView({ view: 'clips' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -545,7 +546,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -561,7 +562,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have clips menu button', () => {
|
describe('should have clips menu button', () => {
|
||||||
it('when in clips view', () => {
|
it('when in clips view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'clips' }),
|
view: createView({ view: 'clips' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -581,7 +582,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in clips view', () => {
|
it('when not in clips view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -600,7 +601,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -614,9 +615,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have snapshots menu button', () => {
|
describe('should have snapshots menu button', () => {
|
||||||
it('when in snapshots view', () => {
|
it('when in snapshots view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockImplementation(
|
vi.mocked(isViewSupported).mockImplementation((view) => view !== 'reviews');
|
||||||
(view) => view !== 'reviews',
|
|
||||||
);
|
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'snapshots' }),
|
view: createView({ view: 'snapshots' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -642,7 +641,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in snapshots view', () => {
|
it('when not in snapshots view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -667,7 +666,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -683,7 +682,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have reviews menu button', () => {
|
describe('should have reviews menu button', () => {
|
||||||
it('when in reviews view', () => {
|
it('when in reviews view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'reviews' }),
|
view: createView({ view: 'reviews' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -709,7 +708,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in reviews view', () => {
|
it('when not in reviews view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -734,7 +733,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -748,7 +747,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have gallery menu button', () => {
|
describe('should have gallery menu button', () => {
|
||||||
it('when in gallery view', () => {
|
it('when in gallery view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'gallery' }),
|
view: createView({ view: 'gallery' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -774,7 +773,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in gallery view', () => {
|
it('when not in gallery view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -799,7 +798,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -813,7 +812,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have recordings menu button', () => {
|
describe('should have recordings menu button', () => {
|
||||||
it('when in recordings view', () => {
|
it('when in recordings view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'recordings' }),
|
view: createView({ view: 'recordings' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -839,7 +838,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in recordings view', () => {
|
it('when not in recordings view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -864,7 +863,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -880,7 +879,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have image menu button', () => {
|
describe('should have image menu button', () => {
|
||||||
it('when in image view', () => {
|
it('when in image view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
|
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'image' }),
|
view: createView({ view: 'image' }),
|
||||||
@@ -900,7 +899,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in image view', () => {
|
it('when not in image view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
|
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'live' }),
|
view: createView({ view: 'live' }),
|
||||||
@@ -920,7 +919,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
});
|
});
|
||||||
@@ -936,7 +935,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have timeline button', () => {
|
describe('should have timeline button', () => {
|
||||||
it('when in timeline view', () => {
|
it('when in timeline view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'timeline' }),
|
view: createView({ view: 'timeline' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -958,7 +957,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not in timeline view', () => {
|
it('when not in timeline view', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
|
vi.mocked(isViewSupported).mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'live' }),
|
view: createView({ view: 'live' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -980,7 +979,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when not supported', () => {
|
it('when not supported', () => {
|
||||||
const viewManager = mock<ViewManager>();
|
const viewManager = mock<ViewManager>();
|
||||||
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
|
vi.mocked(isViewSupported).mockReturnValue(false);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
view: createView({ view: 'live' }),
|
view: createView({ view: 'live' }),
|
||||||
viewManager: viewManager,
|
viewManager: viewManager,
|
||||||
@@ -1640,7 +1639,7 @@ describe('MenuButtonController', () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mocked(getCameraIDsForViewName).mockReturnValue(
|
vi.mocked(getCameraIDsWithCapabilityForView).mockReturnValue(
|
||||||
new Set(['camera-1', 'camera-2']),
|
new Set(['camera-1', 'camera-2']),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1928,6 +1927,7 @@ describe('MenuButtonController', () => {
|
|||||||
describe('should have folders button', () => {
|
describe('should have folders button', () => {
|
||||||
it('should have no folders button without folders', () => {
|
it('should have no folders button without folders', () => {
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
foldersManager.hasFolders.mockReturnValue(true);
|
||||||
foldersManager.getFolders.mockReturnValue(new Map().entries());
|
foldersManager.getFolders.mockReturnValue(new Map().entries());
|
||||||
|
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
@@ -1945,6 +1945,7 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('should have a folders button for a single folder outside the folder view', () => {
|
it('should have a folders button for a single folder outside the folder view', () => {
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
foldersManager.hasFolders.mockReturnValue(true);
|
||||||
foldersManager.getFolders.mockReturnValue(
|
foldersManager.getFolders.mockReturnValue(
|
||||||
new Map([['folder-0', createFolder({ id: 'folder-0' })]]).entries(),
|
new Map([['folder-0', createFolder({ id: 'folder-0' })]]).entries(),
|
||||||
);
|
);
|
||||||
@@ -1968,6 +1969,7 @@ describe('MenuButtonController', () => {
|
|||||||
it('should have a folders button for a single folder inside the folder view', () => {
|
it('should have a folders button for a single folder inside the folder view', () => {
|
||||||
const folder = createFolder({ id: 'folder-0' });
|
const folder = createFolder({ id: 'folder-0' });
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
foldersManager.hasFolders.mockReturnValue(true);
|
||||||
foldersManager.getFolders.mockReturnValue(
|
foldersManager.getFolders.mockReturnValue(
|
||||||
new Map([['folder-0', folder]]).entries(),
|
new Map([['folder-0', folder]]).entries(),
|
||||||
);
|
);
|
||||||
@@ -2006,6 +2008,7 @@ describe('MenuButtonController', () => {
|
|||||||
['folder-0', createFolder({ id: 'folder-0' })],
|
['folder-0', createFolder({ id: 'folder-0' })],
|
||||||
['folder-selected', selectedFolder],
|
['folder-selected', selectedFolder],
|
||||||
]);
|
]);
|
||||||
|
foldersManager.hasFolders.mockReturnValue(true);
|
||||||
foldersManager.getFolders.mockReturnValue(folders.entries());
|
foldersManager.getFolders.mockReturnValue(folders.entries());
|
||||||
|
|
||||||
const selectedFolderNode: FolderQuery = {
|
const selectedFolderNode: FolderQuery = {
|
||||||
@@ -2079,6 +2082,7 @@ describe('MenuButtonController', () => {
|
|||||||
['folder-0', createFolder({ id: 'folder-0' })],
|
['folder-0', createFolder({ id: 'folder-0' })],
|
||||||
['folder-1', createFolder({ id: 'folder-1' })],
|
['folder-1', createFolder({ id: 'folder-1' })],
|
||||||
]);
|
]);
|
||||||
|
foldersManager.hasFolders.mockReturnValue(true);
|
||||||
foldersManager.getFolders.mockReturnValue(folders.entries());
|
foldersManager.getFolders.mockReturnValue(folders.entries());
|
||||||
|
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
@@ -2202,9 +2206,10 @@ describe('MenuButtonController', () => {
|
|||||||
...dynamicButton,
|
...dynamicButton,
|
||||||
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'default' },
|
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'default' },
|
||||||
};
|
};
|
||||||
|
const cameraManager = createCameraManager(createStore([{ cameraID: 'camera-1' }]));
|
||||||
controller.addDynamicMenuButton(button);
|
controller.addDynamicMenuButton(button);
|
||||||
|
|
||||||
expect(calculateButtons(controller)).toContainEqual({
|
expect(calculateButtons(controller, { cameraManager })).toContainEqual({
|
||||||
...button,
|
...button,
|
||||||
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
|
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
|
||||||
});
|
});
|
||||||
@@ -2346,4 +2351,12 @@ describe('MenuButtonController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle view without camera', () => {
|
||||||
|
const buttons = calculateButtons(controller, {
|
||||||
|
view: createView({ camera: null }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buttons).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ describe('config defaults', () => {
|
|||||||
type: 'advanced-camera-card',
|
type: 'advanced-camera-card',
|
||||||
view: {
|
view: {
|
||||||
camera_select: 'current',
|
camera_select: 'current',
|
||||||
default: 'live',
|
default: 'auto',
|
||||||
keyboard_shortcuts: {
|
keyboard_shortcuts: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
ptz_down: {
|
ptz_down: {
|
||||||
|
|||||||
+3
-7
@@ -28,6 +28,7 @@ import { CardElementManager } from '../src/card-controller/card-element-manager'
|
|||||||
import { ConfigManager } from '../src/card-controller/config/config-manager';
|
import { ConfigManager } from '../src/card-controller/config/config-manager';
|
||||||
import { CardController } from '../src/card-controller/controller';
|
import { CardController } from '../src/card-controller/controller';
|
||||||
import { DefaultManager } from '../src/card-controller/default-manager';
|
import { DefaultManager } from '../src/card-controller/default-manager';
|
||||||
|
import { EffectsManager } from '../src/card-controller/effects/effects-manager';
|
||||||
import { ExpandManager } from '../src/card-controller/expand-manager';
|
import { ExpandManager } from '../src/card-controller/expand-manager';
|
||||||
import { FoldersManager } from '../src/card-controller/folders/manager';
|
import { FoldersManager } from '../src/card-controller/folders/manager';
|
||||||
import { FolderQuery } from '../src/card-controller/folders/types';
|
import { FolderQuery } from '../src/card-controller/folders/types';
|
||||||
@@ -71,12 +72,7 @@ import { Entity, EntityRegistryManager } from '../src/ha/registry/entity/types';
|
|||||||
import { CurrentUser, HassStateDifference, HomeAssistant } from '../src/ha/types';
|
import { CurrentUser, HassStateDifference, HomeAssistant } from '../src/ha/types';
|
||||||
import { QuerySource } from '../src/query-source';
|
import { QuerySource } from '../src/query-source';
|
||||||
import { Severity } from '../src/severity';
|
import { Severity } from '../src/severity';
|
||||||
import {
|
import { CapabilitiesRaw, Interaction, MediaLoadedInfo } from '../src/types';
|
||||||
CapabilitiesRaw,
|
|
||||||
EffectsControllerAPI,
|
|
||||||
Interaction,
|
|
||||||
MediaLoadedInfo,
|
|
||||||
} from '../src/types';
|
|
||||||
import {
|
import {
|
||||||
EventViewMedia,
|
EventViewMedia,
|
||||||
ReviewViewMedia,
|
ReviewViewMedia,
|
||||||
@@ -658,7 +654,7 @@ export const createCardAPI = (): CardController => {
|
|||||||
api.getCardElementManager.mockReturnValue(mock<CardElementManager>());
|
api.getCardElementManager.mockReturnValue(mock<CardElementManager>());
|
||||||
api.getConditionStateManager.mockReturnValue(mock<ConditionStateManager>());
|
api.getConditionStateManager.mockReturnValue(mock<ConditionStateManager>());
|
||||||
api.getConfigManager.mockReturnValue(mock<ConfigManager>());
|
api.getConfigManager.mockReturnValue(mock<ConfigManager>());
|
||||||
api.getEffectsControllerAPI.mockReturnValue(mock<EffectsControllerAPI>());
|
api.getEffectsManager.mockReturnValue(mock<EffectsManager>());
|
||||||
api.getEntityRegistryManager.mockReturnValue(mock<EntityRegistryManager>());
|
api.getEntityRegistryManager.mockReturnValue(mock<EntityRegistryManager>());
|
||||||
api.getExpandManager.mockReturnValue(mock<ExpandManager>());
|
api.getExpandManager.mockReturnValue(mock<ExpandManager>());
|
||||||
api.getFoldersManager.mockReturnValue(mock<FoldersManager>());
|
api.getFoldersManager.mockReturnValue(mock<FoldersManager>());
|
||||||
|
|||||||
@@ -120,6 +120,14 @@ describe('getPTZTarget', () => {
|
|||||||
type: 'digital',
|
type: 'digital',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('without cameraID', () => {
|
||||||
|
const view = createView({
|
||||||
|
view: 'live',
|
||||||
|
camera: null,
|
||||||
|
});
|
||||||
|
expect(getPTZTarget(view, { cameraManager: createCameraManager() })).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('in non-media views', () => {
|
describe('in non-media views', () => {
|
||||||
|
|||||||
@@ -123,4 +123,16 @@ describe('generateScreenshotTitle', () => {
|
|||||||
|
|
||||||
expect(generateScreenshotFilename(view)).toBe('media_camera-1.jpg');
|
expect(generateScreenshotFilename(view)).toBe('media_camera-1.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get title for live view without camera', () => {
|
||||||
|
expect(generateScreenshotFilename(createView({ view: 'live', camera: null }))).toBe(
|
||||||
|
'live_2023-06-13-21-54-01.jpg',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get title for viewer view without camera', () => {
|
||||||
|
expect(generateScreenshotFilename(createView({ view: 'media', camera: null }))).toBe(
|
||||||
|
'media.jpg',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
getStreamCameraID,
|
getStreamCameraID,
|
||||||
hasSubstream,
|
hasSubstream,
|
||||||
removeSubstream,
|
removeSubstream,
|
||||||
|
setSubstream,
|
||||||
} from '../../src/utils/substream';
|
} from '../../src/utils/substream';
|
||||||
import { View } from '../../src/view/view';
|
import { View } from '../../src/view/view';
|
||||||
import { createView } from '../test-utils';
|
import { createView } from '../test-utils';
|
||||||
@@ -62,6 +63,25 @@ describe('hasSubstream/getStreamCameraID', () => {
|
|||||||
expect(getStreamCameraID(view, 'camera3')).toBe('camera3');
|
expect(getStreamCameraID(view, 'camera3')).toBe('camera3');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should correctly handle null cameras', () => {
|
||||||
|
expect(getStreamCameraID(createView({ camera: null }))).toBeNull();
|
||||||
|
expect(hasSubstream(createView({ camera: null }))).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSubstream', () => {
|
||||||
|
it('should set substream', () => {
|
||||||
|
const view = createView({ camera: 'camera1' });
|
||||||
|
setSubstream(view, 'substream1');
|
||||||
|
expect(view.context?.live?.overrides?.get('camera1')).toBe('substream1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without a camera', () => {
|
||||||
|
const view = createView({ camera: null });
|
||||||
|
setSubstream(view, 'foo');
|
||||||
|
expect(view.context).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removeSubstream', () => {
|
describe('removeSubstream', () => {
|
||||||
@@ -100,4 +120,12 @@ describe('removeSubstream', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not remove substream without camera', () => {
|
||||||
|
const view = createView({
|
||||||
|
camera: null,
|
||||||
|
});
|
||||||
|
removeSubstream(view);
|
||||||
|
expect(view.context).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { CameraManager } from '../../../src/camera-manager/manager';
|
||||||
|
import { FoldersManager } from '../../../src/card-controller/folders/manager';
|
||||||
|
import { resolveViewName } from '../../../src/view/utils/resolve-default';
|
||||||
|
import { createStore } from '../../test-utils';
|
||||||
|
|
||||||
|
describe('resolveViewName', () => {
|
||||||
|
it('should return the view name directly if not auto', () => {
|
||||||
|
expect(resolveViewName('live', mock<CameraManager>(), mock<FoldersManager>())).toBe(
|
||||||
|
'live',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return live if auto and cameras are present', () => {
|
||||||
|
const cameraManager = mock<CameraManager>();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera-1' }]),
|
||||||
|
);
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
|
expect(resolveViewName('auto', cameraManager, foldersManager)).toBe('live');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return folders if auto, no cameras, but folders are present', () => {
|
||||||
|
const cameraManager = mock<CameraManager>();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(resolveViewName('auto', cameraManager, foldersManager)).toBe('folders');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return image if auto and nothing is present', () => {
|
||||||
|
const cameraManager = mock<CameraManager>();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
vi.mocked(foldersManager.hasFolders).mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(resolveViewName('auto', cameraManager, foldersManager)).toBe('image');
|
||||||
|
});
|
||||||
|
});
|
||||||
+132
-38
@@ -4,18 +4,19 @@ import { FoldersManager } from '../../src/card-controller/folders/manager';
|
|||||||
import { AdvancedCameraCardView } from '../../src/config/schema/common/const';
|
import { AdvancedCameraCardView } from '../../src/config/schema/common/const';
|
||||||
import { CapabilityKey } from '../../src/types';
|
import { CapabilityKey } from '../../src/types';
|
||||||
import {
|
import {
|
||||||
getCameraIDsForViewName,
|
getCameraIDsWithCapabilityForView,
|
||||||
|
isViewAvailable,
|
||||||
|
isViewSupported,
|
||||||
isViewSupportedByCamera,
|
isViewSupportedByCamera,
|
||||||
} from '../../src/view/view-support';
|
} from '../../src/view/view-support';
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
createCapabilities,
|
createCapabilities,
|
||||||
createFolder,
|
|
||||||
createStore,
|
createStore,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
describe('getCameraIDsForViewName', () => {
|
describe('getCameraIDsWithCapabilityForView', () => {
|
||||||
describe('views that are always supported', () => {
|
describe('views that are always supported', () => {
|
||||||
it.each([['diagnostics' as const], ['image' as const]])(
|
it.each([['diagnostics' as const], ['image' as const]])(
|
||||||
'%s',
|
'%s',
|
||||||
@@ -32,14 +33,24 @@ describe('getCameraIDsForViewName', () => {
|
|||||||
);
|
);
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
|
|
||||||
new Set(['camera-1', 'camera-2']),
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
|
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
|
||||||
).toEqual(new Set(['camera-1', 'camera-2']));
|
).toEqual(new Set(['camera-1', 'camera-2']));
|
||||||
expect(
|
expect(
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
|
getCameraIDsWithCapabilityForView(
|
||||||
|
viewName,
|
||||||
|
cameraManager,
|
||||||
|
foldersManager,
|
||||||
|
'camera-1',
|
||||||
|
),
|
||||||
|
).toEqual(new Set(['camera-1', 'camera-2']));
|
||||||
|
expect(
|
||||||
|
getCameraIDsWithCapabilityForView(
|
||||||
|
viewName,
|
||||||
|
cameraManager,
|
||||||
|
foldersManager,
|
||||||
|
'camera-2',
|
||||||
|
),
|
||||||
).toEqual(new Set(['camera-1', 'camera-2']));
|
).toEqual(new Set(['camera-1', 'camera-2']));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -76,9 +87,9 @@ describe('getCameraIDsForViewName', () => {
|
|||||||
);
|
);
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
|
expect(
|
||||||
new Set(['camera-2']),
|
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
|
||||||
);
|
).toEqual(new Set(['camera-2']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@@ -106,20 +117,14 @@ describe('getCameraIDsForViewName', () => {
|
|||||||
);
|
);
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
|
|
||||||
new Set(['camera-1', 'camera-2']),
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
|
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
|
||||||
).toEqual(new Set(['camera-1', 'camera-2']));
|
).toEqual(new Set(['camera-2', 'camera-1']));
|
||||||
expect(
|
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
|
|
||||||
).toEqual(new Set(['camera-2']));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('views that respect a folder', () => {
|
describe('folder views', () => {
|
||||||
describe('should return cameras when a folder is present', () => {
|
describe('should return all cameras when a folder is present', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['folder' as const],
|
['folder' as const],
|
||||||
['folders' as const],
|
['folders' as const],
|
||||||
@@ -140,16 +145,10 @@ describe('getCameraIDsForViewName', () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
vi.mocked(foldersManager.getFolder).mockReturnValue(createFolder());
|
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
|
||||||
|
|
||||||
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
|
|
||||||
new Set(['camera-1', 'camera-2']),
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
|
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
|
||||||
).toEqual(new Set(['camera-1', 'camera-2']));
|
|
||||||
expect(
|
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
|
|
||||||
).toEqual(new Set(['camera-1', 'camera-2']));
|
).toEqual(new Set(['camera-1', 'camera-2']));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -175,19 +174,60 @@ describe('getCameraIDsForViewName', () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
const foldersManager = mock<FoldersManager>();
|
const foldersManager = mock<FoldersManager>();
|
||||||
vi.mocked(foldersManager.getFolder).mockReturnValue(null);
|
vi.mocked(foldersManager.hasFolders).mockReturnValue(false);
|
||||||
|
|
||||||
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
|
expect(
|
||||||
new Set(),
|
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
|
||||||
|
).toEqual(new Set());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isViewAvailable', () => {
|
||||||
|
it('should return true when cameras are present for camera view', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera-1' }]),
|
||||||
);
|
);
|
||||||
expect(
|
const foldersManager = mock<FoldersManager>();
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
|
expect(isViewAvailable('live', cameraManager, foldersManager)).toBe(true);
|
||||||
).toEqual(new Set());
|
|
||||||
expect(
|
|
||||||
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
|
|
||||||
).toEqual(new Set());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false when cameras are missing for camera view', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
expect(isViewAvailable('live', cameraManager, foldersManager)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return true when folders are present for folder view', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
|
||||||
|
expect(isViewAvailable('folder', cameraManager, foldersManager)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when folders are missing for folder view', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
vi.mocked(foldersManager.hasFolders).mockReturnValue(false);
|
||||||
|
expect(isViewAvailable('folder', cameraManager, foldersManager)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when either covers it', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
|
||||||
|
expect(isViewAvailable('gallery', cameraManager, foldersManager)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for diagnostics and image view', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
expect(isViewAvailable('diagnostics', cameraManager, foldersManager)).toBe(true);
|
||||||
|
expect(isViewAvailable('image', cameraManager, foldersManager)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,3 +266,57 @@ describe('isViewSupportedByCamera', () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isViewSupported', () => {
|
||||||
|
it('should return true when view is available and camera is null', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([{ cameraID: 'camera-1' }]),
|
||||||
|
);
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
|
expect(isViewSupported('live', cameraManager, foldersManager, null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when view is available and camera is supported', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera-1',
|
||||||
|
capabilities: createCapabilities({ live: true }),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
|
expect(isViewSupported('live', cameraManager, foldersManager, 'camera-1')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when view is available but camera is not supported', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera-1',
|
||||||
|
capabilities: createCapabilities({ live: false }),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
|
expect(isViewSupported('live', cameraManager, foldersManager, 'camera-1')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when view is not available', () => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
|
||||||
|
const foldersManager = mock<FoldersManager>();
|
||||||
|
|
||||||
|
expect(isViewSupported('live', cameraManager, foldersManager, null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user