chore: Enable noImplicitAny across the codebase (#2667)
This commit is contained in:
@@ -15,13 +15,13 @@ import { Timer } from './utils/timer.js';
|
|||||||
export interface ActionHandlerInterface extends HTMLElement {
|
export interface ActionHandlerInterface extends HTMLElement {
|
||||||
holdTime: number;
|
holdTime: number;
|
||||||
connectedCallback(): void;
|
connectedCallback(): void;
|
||||||
bind(element: Element, options): void;
|
bind(element: Element, options?: AdvancedCameraCardActionHandlerOptions): void;
|
||||||
}
|
}
|
||||||
interface ActionHandlerElement extends HTMLElement {
|
interface ActionHandlerElement extends HTMLElement {
|
||||||
actionHandlerOptions?: AdvancedCameraCardActionHandlerOptions;
|
actionHandlerOptions?: AdvancedCameraCardActionHandlerOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdvancedCameraCardActionHandlerOptions extends ActionHandlerOptions {
|
export interface AdvancedCameraCardActionHandlerOptions extends ActionHandlerOptions {
|
||||||
allowPropagation?: boolean;
|
allowPropagation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { PTZAction, PTZActionPhase } from '../../config/schema/actions/cust
|
|||||||
import type { CameraConfig } from '../../config/schema/cameras';
|
import type { CameraConfig } from '../../config/schema/cameras';
|
||||||
import type { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
import type { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||||
import type { HomeAssistant } from '../../ha/types';
|
import type { HomeAssistant } from '../../ha/types';
|
||||||
import { SEVERITIES } from '../../severity';
|
|
||||||
import {
|
import {
|
||||||
PTZMovementType,
|
PTZMovementType,
|
||||||
type CapabilitiesRaw,
|
type CapabilitiesRaw,
|
||||||
@@ -24,7 +23,7 @@ import {
|
|||||||
import { getPTZCapabilitiesFromCameraConfig, mergePTZCapabilities } from '../utils/ptz';
|
import { getPTZCapabilitiesFromCameraConfig, mergePTZCapabilities } from '../utils/ptz';
|
||||||
import { getPTZInfo } from './requests';
|
import { getPTZInfo } from './requests';
|
||||||
import {
|
import {
|
||||||
FRIGATE_SEVERITY_MAP,
|
CARD_SEVERITY_MAP,
|
||||||
type FrigateEventChange,
|
type FrigateEventChange,
|
||||||
type FrigateReviewChange,
|
type FrigateReviewChange,
|
||||||
type PTZInfo,
|
type PTZInfo,
|
||||||
@@ -610,10 +609,7 @@ export class FrigateCamera extends Camera<FrigateCameraInitializationOptions> {
|
|||||||
|
|
||||||
const reviewConfig = config.triggers.reviews;
|
const reviewConfig = config.triggers.reviews;
|
||||||
|
|
||||||
// Map Frigate severity to card severity.
|
const cardSeverity = CARD_SEVERITY_MAP[review.after.severity];
|
||||||
const cardSeverity = SEVERITIES.find(
|
|
||||||
(key) => FRIGATE_SEVERITY_MAP[key] === review.after.severity,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if this is a description update (GenAI added/changed title or scene)
|
// Check if this is a description update (GenAI added/changed title or scene)
|
||||||
const isDescriptionUpdate =
|
const isDescriptionUpdate =
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { Severity } from '../../severity';
|
||||||
import { dayToDate } from '../../utils/basic';
|
import { dayToDate } from '../../utils/basic';
|
||||||
import type {
|
import type {
|
||||||
Engine,
|
Engine,
|
||||||
@@ -153,6 +154,13 @@ export const FRIGATE_SEVERITY_MAP = {
|
|||||||
export type FrigateReviewSeverity =
|
export type FrigateReviewSeverity =
|
||||||
(typeof FRIGATE_SEVERITY_MAP)[keyof typeof FRIGATE_SEVERITY_MAP];
|
(typeof FRIGATE_SEVERITY_MAP)[keyof typeof FRIGATE_SEVERITY_MAP];
|
||||||
|
|
||||||
|
// Maps Frigate severity to card severity. Frigate has no equivalent of the
|
||||||
|
// card's `low` severity.
|
||||||
|
export const CARD_SEVERITY_MAP = {
|
||||||
|
alert: 'high',
|
||||||
|
detection: 'medium',
|
||||||
|
} as const satisfies Record<FrigateReviewSeverity, Severity>;
|
||||||
|
|
||||||
// Review data schema (only fields we need for display)
|
// Review data schema (only fields we need for display)
|
||||||
const frigateReviewDataSchema = z.object({
|
const frigateReviewDataSchema = z.object({
|
||||||
objects: z.string().array().optional(),
|
objects: z.string().array().optional(),
|
||||||
|
|||||||
@@ -7,6 +7,25 @@ import type { ActionConfig } from '../../config/schema/actions/types';
|
|||||||
import type { CameraConfig } from '../../config/schema/cameras';
|
import type { CameraConfig } from '../../config/schema/cameras';
|
||||||
import { PTZMovementType, type PTZCapabilities } from '../../types';
|
import { PTZMovementType, type PTZCapabilities } from '../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the action configured for a named PTZ preset.
|
||||||
|
* @param ptzConfig The camera's PTZ config.
|
||||||
|
* @param preset The preset name.
|
||||||
|
* @returns The configured action, or `null` if the preset is not configured.
|
||||||
|
*/
|
||||||
|
export const getConfiguredPTZPresetAction = (
|
||||||
|
ptzConfig: CameraConfig['ptz'],
|
||||||
|
preset: string,
|
||||||
|
): ActionConfig | null => {
|
||||||
|
const presets = ptzConfig.presets;
|
||||||
|
if (!presets) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = Object.entries(presets).find(([name]) => name === preset)?.[1];
|
||||||
|
return typeof action === 'object' ? action : null;
|
||||||
|
};
|
||||||
|
|
||||||
export const getConfiguredPTZAction = (
|
export const getConfiguredPTZAction = (
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
action: PTZAction,
|
action: PTZAction,
|
||||||
@@ -16,7 +35,9 @@ export const getConfiguredPTZAction = (
|
|||||||
},
|
},
|
||||||
): ActionConfig | ActionConfig[] | null => {
|
): ActionConfig | ActionConfig[] | null => {
|
||||||
if (action === 'preset') {
|
if (action === 'preset') {
|
||||||
return (options?.preset ? cameraConfig.ptz.presets?.[options.preset] : null) ?? null;
|
return options?.preset
|
||||||
|
? getConfiguredPTZPresetAction(cameraConfig.ptz, options.preset)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.phase) {
|
if (options?.phase) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getConfiguredPTZPresetAction } from '../../../camera-manager/utils/ptz';
|
||||||
import type { PTZActionConfig } from '../../../config/schema/actions/custom/ptz';
|
import type { PTZActionConfig } from '../../../config/schema/actions/custom/ptz';
|
||||||
import { PTZMovementType } from '../../../types';
|
import { PTZMovementType } from '../../../types';
|
||||||
import { getPTZTarget, ptzActionToCapabilityKey } from '../../../utils/ptz';
|
import { getPTZTarget, ptzActionToCapabilityKey } from '../../../utils/ptz';
|
||||||
@@ -62,7 +63,7 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
|||||||
// and the home button always targets `presets[0]`, ignoring the
|
// and the home button always targets `presets[0]`, ignoring the
|
||||||
// configured action. See:
|
// configured action. See:
|
||||||
// https://github.com/dermotduffy/advanced-camera-card/issues/2525
|
// https://github.com/dermotduffy/advanced-camera-card/issues/2525
|
||||||
if (ptzConfiguration.presets?.['home']) {
|
if (getConfiguredPTZPresetAction(ptzConfiguration, 'home')) {
|
||||||
await api.getCameraManager().executePTZAction(ptzCameraID, 'preset', {
|
await api.getCameraManager().executePTZAction(ptzCameraID, 'preset', {
|
||||||
phase: action.ptz_phase,
|
phase: action.ptz_phase,
|
||||||
preset: 'home',
|
preset: 'home',
|
||||||
|
|||||||
@@ -97,29 +97,25 @@ export class StyleManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _setPerformance(): void {
|
private _setPerformance(): void {
|
||||||
const STYLE_DISABLE_MAP = {
|
const styles = this._api.getConfigManager().getCardWideConfig()?.performance?.style;
|
||||||
box_shadow: {
|
if (!styles) {
|
||||||
cssKey: '--advanced-camera-card-box-shadow-override',
|
return;
|
||||||
value: 'none',
|
|
||||||
},
|
|
||||||
border_radius: {
|
|
||||||
cssKey: '--advanced-camera-card-border-radius-override',
|
|
||||||
value: '0px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const element = this._api.getCardElementManager().getElement();
|
|
||||||
const performance = this._api.getConfigManager().getCardWideConfig()?.performance;
|
|
||||||
|
|
||||||
const styles = performance?.style ?? {};
|
|
||||||
for (const configKey of Object.keys(styles)) {
|
|
||||||
const mapping = STYLE_DISABLE_MAP[configKey];
|
|
||||||
setOrRemoveStyleProperty(
|
|
||||||
element,
|
|
||||||
!styles[configKey],
|
|
||||||
mapping.cssKey,
|
|
||||||
mapping.value,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const element = this._api.getCardElementManager().getElement();
|
||||||
|
|
||||||
|
setOrRemoveStyleProperty(
|
||||||
|
element,
|
||||||
|
!styles.box_shadow,
|
||||||
|
'--advanced-camera-card-box-shadow-override',
|
||||||
|
'none',
|
||||||
|
);
|
||||||
|
setOrRemoveStyleProperty(
|
||||||
|
element,
|
||||||
|
!styles.border_radius,
|
||||||
|
'--advanced-camera-card-border-radius-override',
|
||||||
|
'0px',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isAspectRatioEnforced(
|
private _isAspectRatioEnforced(
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export class TemplateManager implements TemplateRenderer {
|
|||||||
this._renderTemplateRecursively(hass, item, templateContext),
|
this._renderTemplateRecursively(hass, item, templateContext),
|
||||||
);
|
);
|
||||||
} else if (isRecord(data)) {
|
} else if (isRecord(data)) {
|
||||||
const result = {};
|
const result: Record<string, unknown> = {};
|
||||||
for (const key in data) {
|
for (const key in data) {
|
||||||
result[key] = this._renderTemplateRecursively(hass, data[key], templateContext);
|
result[key] = this._renderTemplateRecursively(hass, data[key], templateContext);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import type { ViewContext } from 'view';
|
|||||||
import type { View } from '../../../view/view';
|
import type { View } from '../../../view/view';
|
||||||
import type { ViewModifier } from '../types';
|
import type { ViewModifier } from '../types';
|
||||||
|
|
||||||
export class RemoveContextPropertyViewModifier implements ViewModifier {
|
export class RemoveContextPropertyViewModifier<T extends keyof ViewContext>
|
||||||
private _key: keyof ViewContext;
|
implements ViewModifier
|
||||||
private _property: PropertyKey;
|
{
|
||||||
|
private _key: T;
|
||||||
|
private _property: keyof NonNullable<ViewContext[T]>;
|
||||||
|
|
||||||
constructor(key: keyof ViewContext, property: PropertyKey) {
|
constructor(key: T, property: keyof NonNullable<ViewContext[T]>) {
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._property = property;
|
this._property = property;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// The package ships no types of its own, and no DefinitelyTyped package exists.
|
||||||
|
declare module '@cycjimmy/jsmpeg-player' {
|
||||||
|
namespace JSMpeg {
|
||||||
|
// Options forwarded to the underlying JSMpeg player.
|
||||||
|
// See: https://github.com/phoboslab/jsmpeg#usage
|
||||||
|
interface PlayerOptions {
|
||||||
|
audio?: boolean;
|
||||||
|
audioBufferSize?: number;
|
||||||
|
autoplay?: boolean;
|
||||||
|
chunkSize?: number;
|
||||||
|
disableGl?: boolean;
|
||||||
|
disableWebAssembly?: boolean;
|
||||||
|
maxAudioLag?: number;
|
||||||
|
pauseWhenHidden?: boolean;
|
||||||
|
preserveDrawingBuffer?: boolean;
|
||||||
|
progressive?: boolean;
|
||||||
|
protocols?: string[];
|
||||||
|
reconnectInterval?: number;
|
||||||
|
throttled?: boolean;
|
||||||
|
video?: boolean;
|
||||||
|
videoBufferSize?: number;
|
||||||
|
onPause?: (player: Player) => void;
|
||||||
|
onPlay?: (player: Player) => void;
|
||||||
|
onVideoDecode?: (decoder: unknown, elapsedTime: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options for the wrapper element that hosts the canvas and play button.
|
||||||
|
interface VideoElementOptions {
|
||||||
|
autoplay?: boolean;
|
||||||
|
canvas?: HTMLCanvasElement;
|
||||||
|
poster?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player {
|
||||||
|
paused: boolean;
|
||||||
|
volume: number;
|
||||||
|
|
||||||
|
play(): void;
|
||||||
|
pause(): void;
|
||||||
|
stop(): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoElement {
|
||||||
|
constructor(
|
||||||
|
wrapper: HTMLElement | string,
|
||||||
|
videoUrl: string,
|
||||||
|
videoOptions?: VideoElementOptions,
|
||||||
|
playerOptions?: PlayerOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
player: Player | null;
|
||||||
|
|
||||||
|
play(): void;
|
||||||
|
pause(): void;
|
||||||
|
stop(): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default JSMpeg;
|
||||||
|
}
|
||||||
@@ -396,7 +396,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
.autoHideState=${resolveAutoHideState(!!this.call)}
|
.autoHideState=${resolveAutoHideState(!!this.call)}
|
||||||
?disabled=${!neighbor}
|
?disabled=${!neighbor}
|
||||||
?locked=${!!this.locked}
|
?locked=${!!this.locked}
|
||||||
@click=${(ev) => {
|
@click=${(ev: Event) => {
|
||||||
this._setViewCameraID(neighbor?.id);
|
this._setViewCameraID(neighbor?.id);
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createJSMPEGPlayer(url: string): Promise<JSMpeg.VideoElement> {
|
private async _createJSMPEGPlayer(url: string): Promise<void> {
|
||||||
this._jsmpegVideoPlayer = await new Promise<JSMpeg.VideoElement>((resolve) => {
|
this._jsmpegVideoPlayer = await new Promise<JSMpeg.VideoElement>((resolve) => {
|
||||||
let videoDecoded = false;
|
let videoDecoded = false;
|
||||||
const player = new JSMpeg.VideoElement(
|
const player = new JSMpeg.VideoElement(
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import { getEntityTitle } from '../ha/get-entity-title.js';
|
|||||||
import type { EntityRegistryManager } from '../ha/registry/entity/types.js';
|
import type { EntityRegistryManager } from '../ha/registry/entity/types.js';
|
||||||
import type { HomeAssistant } from '../ha/types.js';
|
import type { HomeAssistant } from '../ha/types.js';
|
||||||
import menuStyle from '../scss/menu.scss?inline';
|
import menuStyle from '../scss/menu.scss?inline';
|
||||||
|
import type { Interaction } from '../types.js';
|
||||||
import { hasAction } from '../utils/action.js';
|
import { hasAction } from '../utils/action.js';
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
|
import type { SubmenuInteraction } from './submenu/types.js';
|
||||||
|
|
||||||
import './icon.js';
|
import './icon.js';
|
||||||
import './submenu/select-button.js';
|
import './submenu/select-button.js';
|
||||||
@@ -76,7 +78,8 @@ export class AdvancedCameraCardMenu extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.submenu=${button}
|
.submenu=${button}
|
||||||
.lockManagerEpoch=${this.lockManagerEpoch}
|
.lockManagerEpoch=${this.lockManagerEpoch}
|
||||||
@action=${(ev) => this._controller.handleAction(ev, button)}
|
@action=${(ev: CustomEvent<SubmenuInteraction>) =>
|
||||||
|
this._controller.handleAction(ev, button)}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-submenu-button>`;
|
</advanced-camera-card-submenu-button>`;
|
||||||
} else if (button.type === 'custom:advanced-camera-card-menu-submenu-select') {
|
} else if (button.type === 'custom:advanced-camera-card-menu-submenu-select') {
|
||||||
@@ -85,7 +88,8 @@ export class AdvancedCameraCardMenu extends LitElement {
|
|||||||
.submenuSelect=${button}
|
.submenuSelect=${button}
|
||||||
.entityRegistryManager=${this.entityRegistryManager}
|
.entityRegistryManager=${this.entityRegistryManager}
|
||||||
.lockManagerEpoch=${this.lockManagerEpoch}
|
.lockManagerEpoch=${this.lockManagerEpoch}
|
||||||
@action=${(ev) => this._controller.handleAction(ev, button)}
|
@action=${(ev: CustomEvent<SubmenuInteraction>) =>
|
||||||
|
this._controller.handleAction(ev, button)}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-submenu-select-button>`;
|
</advanced-camera-card-submenu-select-button>`;
|
||||||
}
|
}
|
||||||
@@ -104,7 +108,8 @@ export class AdvancedCameraCardMenu extends LitElement {
|
|||||||
})}
|
})}
|
||||||
.label=${title ?? ''}
|
.label=${title ?? ''}
|
||||||
?disabled=${this._controller.shouldButtonBeInert(button)}
|
?disabled=${this._controller.shouldButtonBeInert(button)}
|
||||||
@action=${(ev) => this._controller.handleAction(ev, button)}
|
@action=${(ev: CustomEvent<Interaction>) =>
|
||||||
|
this._controller.handleAction(ev, button)}
|
||||||
>
|
>
|
||||||
<advanced-camera-card-icon
|
<advanced-camera-card-icon
|
||||||
?allow-override-non-active-styles=${true}
|
?allow-override-non-active-styles=${true}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { StatusBarController } from '../components-lib/status-bar-controller';
|
|||||||
import type { StatusBarItem } from '../config/schema/actions/types.js';
|
import type { StatusBarItem } from '../config/schema/actions/types.js';
|
||||||
import type { StatusBarConfig } from '../config/schema/status-bar.js';
|
import type { StatusBarConfig } from '../config/schema/status-bar.js';
|
||||||
import statusStyle from '../scss/status.scss?inline';
|
import statusStyle from '../scss/status.scss?inline';
|
||||||
|
import type { Interaction } from '../types.js';
|
||||||
import { hasAction } from '../utils/action';
|
import { hasAction } from '../utils/action';
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
|
|
||||||
@@ -114,7 +115,8 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
|||||||
class="${classes}"
|
class="${classes}"
|
||||||
title=${item.title ?? nothing}
|
title=${item.title ?? nothing}
|
||||||
data-severity=${item.severity ?? ''}
|
data-severity=${item.severity ?? ''}
|
||||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
@action=${(ev: CustomEvent<Interaction>) =>
|
||||||
|
this._controller.actionHandler(ev, item.actions)}
|
||||||
>
|
>
|
||||||
${item.string}
|
${item.string}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -125,7 +127,8 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
|||||||
class="${classes}"
|
class="${classes}"
|
||||||
title=${item.title ?? nothing}
|
title=${item.title ?? nothing}
|
||||||
data-severity=${item.severity ?? ''}
|
data-severity=${item.severity ?? ''}
|
||||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
@action=${(ev: CustomEvent<Interaction>) =>
|
||||||
|
this._controller.actionHandler(ev, item.actions)}
|
||||||
></advanced-camera-card-icon>`;
|
></advanced-camera-card-icon>`;
|
||||||
} else if (item.type === 'custom:advanced-camera-card-status-bar-image') {
|
} else if (item.type === 'custom:advanced-camera-card-status-bar-image') {
|
||||||
return html`<img
|
return html`<img
|
||||||
@@ -134,7 +137,8 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
|||||||
title=${item.title ?? nothing}
|
title=${item.title ?? nothing}
|
||||||
src="${item.image}"
|
src="${item.image}"
|
||||||
data-severity=${item.severity ?? ''}
|
data-severity=${item.severity ?? ''}
|
||||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
@action=${(ev: CustomEvent<Interaction>) =>
|
||||||
|
this._controller.actionHandler(ev, item.actions)}
|
||||||
/>`;
|
/>`;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
|||||||
const entity =
|
const entity =
|
||||||
(await this.entityRegistryManager?.getEntity(this.hass, entityID)) ?? null;
|
(await this.entityRegistryManager?.getEntity(this.hass, entityID)) ?? null;
|
||||||
|
|
||||||
const optionTitles = {};
|
const optionTitles: Record<string, string> = {};
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
const title = getEntityStateTranslation(this.hass, entityID, {
|
const title = getEntityStateTranslation(this.hass, entityID, {
|
||||||
...(entity && { entity: entity }),
|
...(entity && { entity: entity }),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isEqual } from 'lodash-es';
|
import { isEqual, pickBy } from 'lodash-es';
|
||||||
|
|
||||||
import { SerialRunner } from '../../utils/concurrency/serial-runner';
|
import { SerialRunner } from '../../utils/concurrency/serial-runner';
|
||||||
import type {
|
import type {
|
||||||
@@ -55,15 +55,17 @@ export class ConditionStateManager implements ConditionStateManagerReadonlyInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _calculateTrueChange(change: ConditionState): ConditionState {
|
private _calculateTrueChange(change: ConditionState): ConditionState {
|
||||||
const changeState: ConditionState = {};
|
return pickBy(
|
||||||
|
change,
|
||||||
for (const key of Object.keys(change)) {
|
(value, key) =>
|
||||||
if (!isEqual(change[key], this._state[key])) {
|
!isEqual(
|
||||||
changeState[key] = change[key];
|
value,
|
||||||
}
|
this._state[
|
||||||
}
|
// lodash widens the key to `string`, which cannot index ConditionState.
|
||||||
|
key as keyof ConditionState
|
||||||
return changeState;
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _callListeners = (stateChange: ConditionStateChange): void => {
|
private _callListeners = (stateChange: ConditionStateChange): void => {
|
||||||
|
|||||||
+38
-51
@@ -24,7 +24,6 @@ import {
|
|||||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||||
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||||
} from './const';
|
} from './const';
|
||||||
import type { Condition } from './schema/condition-trigger/conditions/types';
|
|
||||||
import type {
|
import type {
|
||||||
RawAdvancedCameraCardConfig,
|
RawAdvancedCameraCardConfig,
|
||||||
RawAdvancedCameraCardConfigArray,
|
RawAdvancedCameraCardConfigArray,
|
||||||
@@ -430,17 +429,15 @@ export const deleteTransform = function (_value: unknown): number | null | undef
|
|||||||
* @returns `true` if the configuration was modified.
|
* @returns `true` if the configuration was modified.
|
||||||
*/
|
*/
|
||||||
const conditionToConditionsTransform = (data: unknown): boolean => {
|
const conditionToConditionsTransform = (data: unknown): boolean => {
|
||||||
if (
|
if (!isRecord(data) || !isRecord(data['conditions'])) {
|
||||||
typeof data !== 'object' ||
|
|
||||||
!data ||
|
|
||||||
typeof data['conditions'] !== 'object' ||
|
|
||||||
!data['conditions']
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldConditions = data['conditions'];
|
const oldConditions = data['conditions'];
|
||||||
const newConditions: Condition[] = [];
|
|
||||||
|
// The legacy values are copied across unvalidated; the schema rejects
|
||||||
|
// anything malformed when the migrated configuration is later parsed.
|
||||||
|
const newConditions: RawAdvancedCameraCardConfig[] = [];
|
||||||
|
|
||||||
if (oldConditions['view'] !== undefined) {
|
if (oldConditions['view'] !== undefined) {
|
||||||
newConditions.push({
|
newConditions.push({
|
||||||
@@ -475,23 +472,18 @@ const conditionToConditionsTransform = (data: unknown): boolean => {
|
|||||||
if (oldConditions['state'] !== undefined && Array.isArray(oldConditions['state'])) {
|
if (oldConditions['state'] !== undefined && Array.isArray(oldConditions['state'])) {
|
||||||
for (const stateCondition of oldConditions['state']) {
|
for (const stateCondition of oldConditions['state']) {
|
||||||
if (
|
if (
|
||||||
typeof stateCondition === 'object' &&
|
isRecord(stateCondition) &&
|
||||||
stateCondition &&
|
|
||||||
(stateCondition['state'] !== undefined ||
|
(stateCondition['state'] !== undefined ||
|
||||||
stateCondition['state_not'] !== undefined ||
|
stateCondition['state_not'] !== undefined ||
|
||||||
stateCondition['entity'] !== undefined)
|
stateCondition['entity'] !== undefined)
|
||||||
) {
|
) {
|
||||||
newConditions.push({
|
newConditions.push({
|
||||||
condition: 'state' as const,
|
condition: 'state' as const,
|
||||||
...(stateCondition['state'] && {
|
...(!!stateCondition['state'] && { state: stateCondition['state'] }),
|
||||||
state: stateCondition['state'],
|
...(!!stateCondition['state_not'] && {
|
||||||
}),
|
|
||||||
...(stateCondition['state_not'] && {
|
|
||||||
state_not: stateCondition['state_not'],
|
state_not: stateCondition['state_not'],
|
||||||
}),
|
}),
|
||||||
...(stateCondition['entity'] && {
|
...(!!stateCondition['entity'] && { entity_id: stateCondition['entity'] }),
|
||||||
entity_id: stateCondition['entity'],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,8 +562,7 @@ const dropTriggerOnlyConditions = (conditions: unknown[]): unknown[] => {
|
|||||||
for (const condition of conditions) {
|
for (const condition of conditions) {
|
||||||
if (
|
if (
|
||||||
isCompositeCondition(condition) &&
|
isCompositeCondition(condition) &&
|
||||||
typeof condition === 'object' &&
|
isRecord(condition) &&
|
||||||
condition &&
|
|
||||||
Array.isArray(condition['conditions'])
|
Array.isArray(condition['conditions'])
|
||||||
) {
|
) {
|
||||||
const inner = dropTriggerOnlyConditions(condition['conditions']);
|
const inner = dropTriggerOnlyConditions(condition['conditions']);
|
||||||
@@ -594,7 +585,7 @@ const rewriteConditionAsTrigger = (condition: unknown): unknown => {
|
|||||||
// Only the renamed fields are consumed; anything else the condition carries
|
// Only the renamed fields are consumed; anything else the condition carries
|
||||||
// (`enabled`, and the fields it already shares with its trigger) is preserved,
|
// (`enabled`, and the fields it already shares with its trigger) is preserved,
|
||||||
// so promoting never silently discards user configuration.
|
// so promoting never silently discards user configuration.
|
||||||
const withoutKeys = (...keys: string[]): Record<string, unknown> => {
|
const withoutKeys = (...keys: string[]): RawAdvancedCameraCardConfig => {
|
||||||
const rest = { ...condition };
|
const rest = { ...condition };
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
delete rest[key];
|
delete rest[key];
|
||||||
@@ -1100,8 +1091,7 @@ const callServiceToPerformActionTransform = (data: unknown): boolean => {
|
|||||||
*/
|
*/
|
||||||
const serviceDataToDataTransform = (data: unknown): boolean => {
|
const serviceDataToDataTransform = (data: unknown): boolean => {
|
||||||
if (
|
if (
|
||||||
typeof data === 'object' &&
|
isRecord(data) &&
|
||||||
data &&
|
|
||||||
data['action'] === 'call-service' &&
|
data['action'] === 'call-service' &&
|
||||||
data['service'] !== undefined &&
|
data['service'] !== undefined &&
|
||||||
data['service_data'] !== undefined &&
|
data['service_data'] !== undefined &&
|
||||||
@@ -1205,7 +1195,7 @@ const ptzIncorrectDataToWebRTCDataTransform = (data: unknown): unknown => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ptzActionsToCamerasGlobalTransform = (data: unknown): unknown => {
|
const ptzActionsToCamerasGlobalTransform = (data: unknown): unknown => {
|
||||||
if (typeof data !== 'object' || !data) {
|
if (!isRecord(data)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1242,7 +1232,7 @@ const ptzActionsToCamerasGlobalTransform = (data: unknown): unknown => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = {};
|
const output: RawAdvancedCameraCardConfig = {};
|
||||||
|
|
||||||
NON_PRESET_DATA_KEYS.filter((key) => key in data).reduce((obj, key) => {
|
NON_PRESET_DATA_KEYS.filter((key) => key in data).reduce((obj, key) => {
|
||||||
obj[key] = data[key];
|
obj[key] = data[key];
|
||||||
@@ -1250,36 +1240,31 @@ const ptzActionsToCamerasGlobalTransform = (data: unknown): unknown => {
|
|||||||
}, output);
|
}, output);
|
||||||
|
|
||||||
NON_PRESET_ACTION_KEYS.filter((key) => key in data).reduce((obj, key) => {
|
NON_PRESET_ACTION_KEYS.filter((key) => key in data).reduce((obj, key) => {
|
||||||
if (typeof data[key] === 'object' && 'tap_action' in data[key]) {
|
const action = data[key];
|
||||||
obj[key] = data[key]['tap_action'];
|
if (isRecord(action) && 'tap_action' in action) {
|
||||||
|
obj[key] = action['tap_action'];
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}, output);
|
}, output);
|
||||||
|
|
||||||
const createPresets = () => {
|
// Returns the preset collection so callers can add to it after it is
|
||||||
output['presets'] =
|
// attached to the output.
|
||||||
'presets' in data && typeof data['presets'] === 'object' && !!data['presets']
|
const createPresets = (): RawAdvancedCameraCardConfig => {
|
||||||
? data['presets']
|
const existing = data['presets'];
|
||||||
: {};
|
const presets = isRecord(existing) ? existing : {};
|
||||||
|
output['presets'] = presets;
|
||||||
|
return presets;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
const actionsHome = data['actions_home'];
|
||||||
'actions_home' in data &&
|
const dataHome = data['data_home'];
|
||||||
typeof data['actions_home'] === 'object' &&
|
|
||||||
data['actions_home'] &&
|
if (isRecord(actionsHome) && 'tap_action' in actionsHome) {
|
||||||
'tap_action' in data['actions_home']
|
createPresets()['home'] = actionsHome['tap_action'];
|
||||||
) {
|
} else if (isRecord(dataHome) && typeof data['service'] === 'string') {
|
||||||
createPresets();
|
const presets = createPresets();
|
||||||
output['presets']['home'] = data['actions_home']['tap_action'];
|
presets['service'] = data['service'];
|
||||||
} else if (
|
presets['data_home'] = dataHome;
|
||||||
'data_home' in data &&
|
|
||||||
typeof data['data_home'] === 'object' &&
|
|
||||||
data['data_home'] &&
|
|
||||||
typeof data['service'] === 'string'
|
|
||||||
) {
|
|
||||||
createPresets();
|
|
||||||
output['presets']['service'] = data['service'];
|
|
||||||
output['presets']['data_home'] = data['data_home'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
@@ -1310,7 +1295,7 @@ const ptzControlSettingsTransform = (data: unknown): unknown => {
|
|||||||
|
|
||||||
return keys
|
return keys
|
||||||
.filter((key) => TRANSFORM_KEYS.includes(key))
|
.filter((key) => TRANSFORM_KEYS.includes(key))
|
||||||
.reduce((obj, key) => {
|
.reduce<RawAdvancedCameraCardConfig>((obj, key) => {
|
||||||
obj[key] = data[key];
|
obj[key] = data[key];
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -1593,13 +1578,15 @@ const UPGRADES = [
|
|||||||
deleteWithOverrides('image.layout'),
|
deleteWithOverrides('image.layout'),
|
||||||
upgradeArrayOfObjects(CONF_OVERRIDES, conditionToConditionsTransform),
|
upgradeArrayOfObjects(CONF_OVERRIDES, conditionToConditionsTransform),
|
||||||
(data: unknown): boolean => {
|
(data: unknown): boolean => {
|
||||||
|
const elements = isRecord(data) ? data[CONF_ELEMENTS] : null;
|
||||||
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
||||||
typeof data === 'object' && data ? data[CONF_ELEMENTS] : {},
|
isRecord(elements) ? elements : {},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(data: unknown): boolean => {
|
(data: unknown): boolean => {
|
||||||
|
const automations = isRecord(data) ? data[CONF_AUTOMATIONS] : null;
|
||||||
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
||||||
typeof data === 'object' && data ? data[CONF_AUTOMATIONS] : {},
|
isRecord(automations) ? automations : {},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
upgradeArrayOfObjects(
|
upgradeArrayOfObjects(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { isRecord } from '../../../utils/basic';
|
||||||
|
import type { RawAdvancedCameraCardConfig } from '../../types';
|
||||||
import { performActionActionSchema } from '../actions/stock/perform-action';
|
import { performActionActionSchema } from '../actions/stock/perform-action';
|
||||||
|
|
||||||
export const ptzCameraConfigDefaults = {
|
export const ptzCameraConfigDefaults = {
|
||||||
@@ -14,12 +16,12 @@ export const ptzCameraConfigDefaults = {
|
|||||||
const dataPTZFormatToFullFormat =
|
const dataPTZFormatToFullFormat =
|
||||||
(suffix: string) =>
|
(suffix: string) =>
|
||||||
(data: unknown): unknown => {
|
(data: unknown): unknown => {
|
||||||
if (!data || typeof data !== 'object' || !data['service']) {
|
if (!isRecord(data) || !data['service']) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = data['service'];
|
const service = data['service'];
|
||||||
const out = { ...data };
|
const out: RawAdvancedCameraCardConfig = { ...data };
|
||||||
|
|
||||||
for (const key of Object.keys(data)) {
|
for (const key of Object.keys(data)) {
|
||||||
const webrtc = key.match(/^data_(start|end)_(.+)$/);
|
const webrtc = key.match(/^data_(start|end)_(.+)$/);
|
||||||
@@ -34,8 +36,7 @@ const dataPTZFormatToFullFormat =
|
|||||||
// Route `data_home` into a `home` preset listed first so the PTZ
|
// Route `data_home` into a `home` preset listed first so the PTZ
|
||||||
// home button (which activates the first preset) uses it.
|
// home button (which activates the first preset) uses it.
|
||||||
if (suffix && name === 'home') {
|
if (suffix && name === 'home') {
|
||||||
const presets =
|
const presets = isRecord(out['presets']) ? out['presets'] : {};
|
||||||
out['presets'] && typeof out['presets'] === 'object' ? out['presets'] : {};
|
|
||||||
if (!('home' in presets)) {
|
if (!('home' in presets)) {
|
||||||
out['presets'] = {
|
out['presets'] = {
|
||||||
home: {
|
home: {
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ export interface HomeAssistant {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
},
|
},
|
||||||
) => Promise<Response>;
|
) => Promise<Response>;
|
||||||
hassUrl(path?): string;
|
hassUrl(path?: string): string;
|
||||||
sendWS: (msg: MessageBase) => Promise<void>;
|
sendWS: (msg: MessageBase) => Promise<void>;
|
||||||
callWS: <T>(msg: MessageBase) => Promise<T>;
|
callWS: <T>(msg: MessageBase) => Promise<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -270,13 +270,15 @@ export const getChildrenFromElement = (parent: HTMLElement): HTMLElement[] => {
|
|||||||
export const recursivelyMergeObjectsNotArrays = <T>(
|
export const recursivelyMergeObjectsNotArrays = <T>(
|
||||||
...srcs: (Partial<T> | undefined | null)[]
|
...srcs: (Partial<T> | undefined | null)[]
|
||||||
): T => {
|
): T => {
|
||||||
return mergeWith({}, ...srcs, (_a, b) => (Array.isArray(b) ? b : undefined));
|
return mergeWith({}, ...srcs, (_a: unknown, b: unknown) =>
|
||||||
|
Array.isArray(b) ? b : undefined,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const recursivelyMergeObjectsConcatenatingArraysUniquely = <T>(
|
export const recursivelyMergeObjectsConcatenatingArraysUniquely = <T>(
|
||||||
...srcs: (Partial<T> | undefined | null)[]
|
...srcs: (Partial<T> | undefined | null)[]
|
||||||
): T => {
|
): T => {
|
||||||
return mergeWith({}, ...srcs, (a, b) =>
|
return mergeWith({}, ...srcs, (a: unknown, b: unknown) =>
|
||||||
Array.isArray(a) ? uniq(a.concat(b)) : undefined,
|
Array.isArray(a) ? uniq(a.concat(b)) : undefined,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-7
@@ -1,5 +1,6 @@
|
|||||||
import type { CameraConfig } from '../config/schema/cameras';
|
import type { CameraConfig } from '../config/schema/cameras';
|
||||||
import type { RawAdvancedCameraCardConfig } from '../config/types';
|
import type { RawAdvancedCameraCardConfig } from '../config/types';
|
||||||
|
import { isRecord } from './basic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a camera id.
|
* Get a camera id.
|
||||||
@@ -12,20 +13,17 @@ export function getCameraID(
|
|||||||
return (
|
return (
|
||||||
(typeof config?.id === 'string' && config.id) ||
|
(typeof config?.id === 'string' && config.id) ||
|
||||||
(typeof config?.camera_entity === 'string' && config.camera_entity) ||
|
(typeof config?.camera_entity === 'string' && config.camera_entity) ||
|
||||||
(typeof config?.webrtc_card === 'object' &&
|
(isRecord(config?.webrtc_card) &&
|
||||||
config.webrtc_card &&
|
|
||||||
((typeof config.webrtc_card['entity'] === 'string' &&
|
((typeof config.webrtc_card['entity'] === 'string' &&
|
||||||
config.webrtc_card['entity']) ||
|
config.webrtc_card['entity']) ||
|
||||||
(typeof config.webrtc_card['url'] === 'string' && config.webrtc_card['url']))) ||
|
(typeof config.webrtc_card['url'] === 'string' && config.webrtc_card['url']))) ||
|
||||||
(typeof config?.go2rtc === 'object' &&
|
(isRecord(config?.go2rtc) &&
|
||||||
config.go2rtc &&
|
|
||||||
typeof config.go2rtc['url'] === 'string' &&
|
typeof config.go2rtc['url'] === 'string' &&
|
||||||
typeof config.go2rtc['stream'] === 'string' &&
|
typeof config.go2rtc['stream'] === 'string' &&
|
||||||
// Artifical identifier that includes both url / stream.
|
// Artifical identifier that includes both url / stream.
|
||||||
`${config.go2rtc['url']}#${config.go2rtc['stream']}`) ||
|
`${config.go2rtc['url']}#${config.go2rtc['stream']}`) ||
|
||||||
(typeof config?.frigate === 'object' &&
|
(isRecord(config?.frigate) &&
|
||||||
config.frigate &&
|
typeof config.frigate['camera_name'] === 'string' &&
|
||||||
typeof config?.frigate['camera_name'] === 'string' &&
|
|
||||||
config.frigate['camera_name']) ||
|
config.frigate['camera_name']) ||
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import type { MediaLayoutConfig } from '../config/schema/camera/media-layout';
|
import type { MediaLayoutConfig } from '../config/schema/camera/media-layout';
|
||||||
import { setOrRemoveStyleProperty } from './basic';
|
import { setOrRemoveStyleProperty } from './basic';
|
||||||
|
|
||||||
|
const POSITION_DIMENSIONS: (keyof NonNullable<MediaLayoutConfig['position']>)[] = [
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
];
|
||||||
|
const VIEW_BOX_EDGES: (keyof NonNullable<MediaLayoutConfig['view_box']>)[] = [
|
||||||
|
'top',
|
||||||
|
'bottom',
|
||||||
|
'left',
|
||||||
|
'right',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update element style from a media configuration.
|
* Update element style from a media configuration.
|
||||||
* @param element The element to update the style for.
|
* @param element The element to update the style for.
|
||||||
@@ -17,7 +28,7 @@ export const updateElementStyleFromMediaLayoutConfig = (
|
|||||||
mediaLayoutConfig?.fit,
|
mediaLayoutConfig?.fit,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const dimension of ['x', 'y']) {
|
for (const dimension of POSITION_DIMENSIONS) {
|
||||||
setOrRemoveStyleProperty(
|
setOrRemoveStyleProperty(
|
||||||
element,
|
element,
|
||||||
!!mediaLayoutConfig?.position?.[dimension],
|
!!mediaLayoutConfig?.position?.[dimension],
|
||||||
@@ -26,7 +37,7 @@ export const updateElementStyleFromMediaLayoutConfig = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const dimension of ['top', 'bottom', 'left', 'right']) {
|
for (const dimension of VIEW_BOX_EDGES) {
|
||||||
setOrRemoveStyleProperty(
|
setOrRemoveStyleProperty(
|
||||||
element,
|
element,
|
||||||
!!mediaLayoutConfig?.view_box?.[dimension],
|
!!mediaLayoutConfig?.view_box?.[dimension],
|
||||||
|
|||||||
+6
-4
@@ -143,13 +143,15 @@ export class View {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeContextProperty(
|
public removeContextProperty<T extends keyof ViewContext>(
|
||||||
contextKey: keyof ViewContext,
|
contextKey: T,
|
||||||
removeKey: PropertyKey,
|
removeKey: keyof NonNullable<ViewContext[T]>,
|
||||||
): View {
|
): View {
|
||||||
const contextObj = this.context?.[contextKey];
|
const contextObj = this.context?.[contextKey];
|
||||||
if (contextObj) {
|
if (contextObj) {
|
||||||
delete contextObj[removeKey];
|
// Cannot use a regular 'delete' here as TypeScript cannot directly index
|
||||||
|
// `contextObj` while its type is still generic.
|
||||||
|
Reflect.deleteProperty(contextObj, removeKey);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
actionHandler,
|
actionHandler,
|
||||||
type ActionHandlerInterface,
|
type ActionHandlerInterface,
|
||||||
|
type AdvancedCameraCardActionHandlerOptions,
|
||||||
} from '../src/action-handler-directive';
|
} from '../src/action-handler-directive';
|
||||||
import { fireHASSEvent } from '../src/ha/fire-hass-event';
|
import { fireHASSEvent } from '../src/ha/fire-hass-event';
|
||||||
import type { ActionHandlerDetail } from '../src/ha/types';
|
import type { ActionHandlerDetail } from '../src/ha/types';
|
||||||
@@ -23,7 +24,9 @@ const getActionHandler = (): ActionHandlerInterface => {
|
|||||||
return el as ActionHandlerInterface;
|
return el as ActionHandlerInterface;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createBoundElement = (options?: Record<string, unknown>): HTMLElement => {
|
const createBoundElement = (
|
||||||
|
options?: AdvancedCameraCardActionHandlerOptions,
|
||||||
|
): HTMLElement => {
|
||||||
const handler = getActionHandler();
|
const handler = getActionHandler();
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
handler.bind(element, options);
|
handler.bind(element, options);
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import {
|
|||||||
type EventQueryResults,
|
type EventQueryResults,
|
||||||
type MediaMetadata,
|
type MediaMetadata,
|
||||||
type QueryResults,
|
type QueryResults,
|
||||||
|
type RecordingQuery,
|
||||||
|
type RecordingSegmentsQuery,
|
||||||
|
type ReviewQuery,
|
||||||
} from '../../src/camera-manager/types.js';
|
} from '../../src/camera-manager/types.js';
|
||||||
import type { CardController } from '../../src/card-controller/controller.js';
|
import type { CardController } from '../../src/card-controller/controller.js';
|
||||||
import type { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
import type { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||||
@@ -752,46 +755,65 @@ describe('CameraManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('generate default queries', () => {
|
describe('generate default queries', () => {
|
||||||
it.each([
|
const setupManagerWithEngine = async () => {
|
||||||
[
|
const api = createCardAPI();
|
||||||
QueryType.Event as const,
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
'generateDefaultEventQuery',
|
|
||||||
'generateDefaultEventQueries',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
QueryType.Recording as const,
|
|
||||||
'generateDefaultRecordingQuery',
|
|
||||||
'generateDefaultRecordingQueries',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
QueryType.RecordingSegments as const,
|
|
||||||
'generateDefaultRecordingSegmentsQuery',
|
|
||||||
'generateDefaultRecordingSegmentsQueries',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
QueryType.Review as const,
|
|
||||||
'generateDefaultReviewQuery',
|
|
||||||
'generateDefaultReviewQueries',
|
|
||||||
],
|
|
||||||
])(
|
|
||||||
'basic %s',
|
|
||||||
async (
|
|
||||||
queryType: string,
|
|
||||||
engineMethodName: string,
|
|
||||||
managerMethodName: string,
|
|
||||||
) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
|
|
||||||
const engine = mock<CameraManagerEngine>();
|
const engine = mock<CameraManagerEngine>();
|
||||||
const manager = createCameraManager(api, engine);
|
const manager = createCameraManager(api, engine);
|
||||||
await manager.initializeCamerasFromConfig();
|
await manager.initializeCamerasFromConfig();
|
||||||
|
|
||||||
const queries = [{ type: queryType, cameraIDs: new Set(['id']) }];
|
return { engine, manager };
|
||||||
engine[engineMethodName].mockReturnValue(queries);
|
};
|
||||||
expect(manager[managerMethodName]('id')).toEqual(queries);
|
|
||||||
},
|
it('should generate default event queries', async () => {
|
||||||
);
|
const { engine, manager } = await setupManagerWithEngine();
|
||||||
|
const queries: EventQuery[] = [baseEventQuery];
|
||||||
|
|
||||||
|
engine.generateDefaultEventQuery.mockReturnValue(queries);
|
||||||
|
|
||||||
|
expect(manager.generateDefaultEventQueries('id')).toEqual(queries);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate default recording queries', async () => {
|
||||||
|
const { engine, manager } = await setupManagerWithEngine();
|
||||||
|
const queries: RecordingQuery[] = [baseRecordingQuery];
|
||||||
|
|
||||||
|
engine.generateDefaultRecordingQuery.mockReturnValue(queries);
|
||||||
|
|
||||||
|
expect(manager.generateDefaultRecordingQueries('id')).toEqual(queries);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate default recording segments queries', async () => {
|
||||||
|
const { engine, manager } = await setupManagerWithEngine();
|
||||||
|
const queries: RecordingSegmentsQuery[] = [
|
||||||
|
{
|
||||||
|
type: QueryType.RecordingSegments,
|
||||||
|
cameraIDs: new Set(['id']),
|
||||||
|
start: new Date(),
|
||||||
|
end: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
engine.generateDefaultRecordingSegmentsQuery.mockReturnValue(queries);
|
||||||
|
|
||||||
|
expect(manager.generateDefaultRecordingSegmentsQueries('id')).toEqual(queries);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate default review queries', async () => {
|
||||||
|
const { engine, manager } = await setupManagerWithEngine();
|
||||||
|
const queries: ReviewQuery[] = [
|
||||||
|
{
|
||||||
|
source: QuerySource.Camera,
|
||||||
|
type: QueryType.Review,
|
||||||
|
cameraIDs: new Set(['id']),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
engine.generateDefaultReviewQuery.mockReturnValue(queries);
|
||||||
|
|
||||||
|
expect(manager.generateDefaultReviewQueries('id')).toEqual(queries);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle missing camera', async () => {
|
it('should handle missing camera', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
|||||||
@@ -42,13 +42,18 @@ const getActionSpy = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createPlayerElement = (controller?: MediaPlayerController): MediaPlayerElement => {
|
const createPlayerElement = (controller?: MediaPlayerController): MediaPlayerElement => {
|
||||||
const player = document.createElement('video');
|
const player: MediaPlayerElement<HTMLVideoElement> = Object.assign(
|
||||||
player['getMediaPlayerController'] = vi
|
document.createElement('video'),
|
||||||
.fn()
|
{
|
||||||
.mockResolvedValue(
|
getMediaPlayerController: vi
|
||||||
controller ?? mock<MediaPlayerController>({ playback: mock<PlaybackControl>() }),
|
.fn()
|
||||||
);
|
.mockResolvedValue(
|
||||||
return player as unknown as MediaPlayerElement;
|
controller ??
|
||||||
|
mock<MediaPlayerController>({ playback: mock<PlaybackControl>() }),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return player;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
||||||
@@ -914,13 +919,17 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
// A player whose media player controller is not ready on first request.
|
// A player whose media player controller is not ready on first request.
|
||||||
const mediaPlayerController = mock<MediaPlayerController>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
const player = document.createElement('video');
|
const player: MediaPlayerElement<HTMLVideoElement> = Object.assign(
|
||||||
player['getMediaPlayerController'] = vi
|
document.createElement('video'),
|
||||||
.fn()
|
{
|
||||||
.mockResolvedValueOnce(null)
|
getMediaPlayerController: vi
|
||||||
.mockResolvedValue(mediaPlayerController);
|
.fn()
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValue(mediaPlayerController),
|
||||||
|
},
|
||||||
|
);
|
||||||
const child = createTestSlideNodes({ n: 1 })[0];
|
const child = createTestSlideNodes({ n: 1 })[0];
|
||||||
child.appendChild(player as unknown as MediaPlayerElement);
|
child.appendChild(player);
|
||||||
|
|
||||||
controller.setRoot(createParent({ children: [child] }));
|
controller.setRoot(createParent({ children: [child] }));
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|||||||
+1
-1
@@ -3,6 +3,7 @@
|
|||||||
"target": "es2021",
|
"target": "es2021",
|
||||||
"module": "es2020",
|
"module": "es2020",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"allowJs": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"lib": ["es2021", "dom", "dom.iterable"],
|
"lib": ["es2021", "dom", "dom.iterable"],
|
||||||
@@ -12,7 +13,6 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": false,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user