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 {
|
||||
holdTime: number;
|
||||
connectedCallback(): void;
|
||||
bind(element: Element, options): void;
|
||||
bind(element: Element, options?: AdvancedCameraCardActionHandlerOptions): void;
|
||||
}
|
||||
interface ActionHandlerElement extends HTMLElement {
|
||||
actionHandlerOptions?: AdvancedCameraCardActionHandlerOptions;
|
||||
}
|
||||
|
||||
interface AdvancedCameraCardActionHandlerOptions extends ActionHandlerOptions {
|
||||
export interface AdvancedCameraCardActionHandlerOptions extends ActionHandlerOptions {
|
||||
allowPropagation?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { PTZAction, PTZActionPhase } from '../../config/schema/actions/cust
|
||||
import type { CameraConfig } from '../../config/schema/cameras';
|
||||
import type { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import type { HomeAssistant } from '../../ha/types';
|
||||
import { SEVERITIES } from '../../severity';
|
||||
import {
|
||||
PTZMovementType,
|
||||
type CapabilitiesRaw,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
import { getPTZCapabilitiesFromCameraConfig, mergePTZCapabilities } from '../utils/ptz';
|
||||
import { getPTZInfo } from './requests';
|
||||
import {
|
||||
FRIGATE_SEVERITY_MAP,
|
||||
CARD_SEVERITY_MAP,
|
||||
type FrigateEventChange,
|
||||
type FrigateReviewChange,
|
||||
type PTZInfo,
|
||||
@@ -610,10 +609,7 @@ export class FrigateCamera extends Camera<FrigateCameraInitializationOptions> {
|
||||
|
||||
const reviewConfig = config.triggers.reviews;
|
||||
|
||||
// Map Frigate severity to card severity.
|
||||
const cardSeverity = SEVERITIES.find(
|
||||
(key) => FRIGATE_SEVERITY_MAP[key] === review.after.severity,
|
||||
);
|
||||
const cardSeverity = CARD_SEVERITY_MAP[review.after.severity];
|
||||
|
||||
// Check if this is a description update (GenAI added/changed title or scene)
|
||||
const isDescriptionUpdate =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { Severity } from '../../severity';
|
||||
import { dayToDate } from '../../utils/basic';
|
||||
import type {
|
||||
Engine,
|
||||
@@ -153,6 +154,13 @@ export const FRIGATE_SEVERITY_MAP = {
|
||||
export type FrigateReviewSeverity =
|
||||
(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)
|
||||
const frigateReviewDataSchema = z.object({
|
||||
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 { 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 = (
|
||||
cameraConfig: CameraConfig,
|
||||
action: PTZAction,
|
||||
@@ -16,7 +35,9 @@ export const getConfiguredPTZAction = (
|
||||
},
|
||||
): ActionConfig | ActionConfig[] | null => {
|
||||
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) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfiguredPTZPresetAction } from '../../../camera-manager/utils/ptz';
|
||||
import type { PTZActionConfig } from '../../../config/schema/actions/custom/ptz';
|
||||
import { PTZMovementType } from '../../../types';
|
||||
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
|
||||
// configured action. See:
|
||||
// https://github.com/dermotduffy/advanced-camera-card/issues/2525
|
||||
if (ptzConfiguration.presets?.['home']) {
|
||||
if (getConfiguredPTZPresetAction(ptzConfiguration, 'home')) {
|
||||
await api.getCameraManager().executePTZAction(ptzCameraID, 'preset', {
|
||||
phase: action.ptz_phase,
|
||||
preset: 'home',
|
||||
|
||||
@@ -97,29 +97,25 @@ export class StyleManager {
|
||||
}
|
||||
|
||||
private _setPerformance(): void {
|
||||
const STYLE_DISABLE_MAP = {
|
||||
box_shadow: {
|
||||
cssKey: '--advanced-camera-card-box-shadow-override',
|
||||
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 styles = this._api.getConfigManager().getCardWideConfig()?.performance?.style;
|
||||
if (!styles) {
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -153,7 +153,7 @@ export class TemplateManager implements TemplateRenderer {
|
||||
this._renderTemplateRecursively(hass, item, templateContext),
|
||||
);
|
||||
} else if (isRecord(data)) {
|
||||
const result = {};
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key in data) {
|
||||
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 { ViewModifier } from '../types';
|
||||
|
||||
export class RemoveContextPropertyViewModifier implements ViewModifier {
|
||||
private _key: keyof ViewContext;
|
||||
private _property: PropertyKey;
|
||||
export class RemoveContextPropertyViewModifier<T extends keyof ViewContext>
|
||||
implements ViewModifier
|
||||
{
|
||||
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._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)}
|
||||
?disabled=${!neighbor}
|
||||
?locked=${!!this.locked}
|
||||
@click=${(ev) => {
|
||||
@click=${(ev: Event) => {
|
||||
this._setViewCameraID(neighbor?.id);
|
||||
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) => {
|
||||
let videoDecoded = false;
|
||||
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 { HomeAssistant } from '../ha/types.js';
|
||||
import menuStyle from '../scss/menu.scss?inline';
|
||||
import type { Interaction } from '../types.js';
|
||||
import { hasAction } from '../utils/action.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
import type { SubmenuInteraction } from './submenu/types.js';
|
||||
|
||||
import './icon.js';
|
||||
import './submenu/select-button.js';
|
||||
@@ -76,7 +78,8 @@ export class AdvancedCameraCardMenu extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.submenu=${button}
|
||||
.lockManagerEpoch=${this.lockManagerEpoch}
|
||||
@action=${(ev) => this._controller.handleAction(ev, button)}
|
||||
@action=${(ev: CustomEvent<SubmenuInteraction>) =>
|
||||
this._controller.handleAction(ev, button)}
|
||||
>
|
||||
</advanced-camera-card-submenu-button>`;
|
||||
} else if (button.type === 'custom:advanced-camera-card-menu-submenu-select') {
|
||||
@@ -85,7 +88,8 @@ export class AdvancedCameraCardMenu extends LitElement {
|
||||
.submenuSelect=${button}
|
||||
.entityRegistryManager=${this.entityRegistryManager}
|
||||
.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>`;
|
||||
}
|
||||
@@ -104,7 +108,8 @@ export class AdvancedCameraCardMenu extends LitElement {
|
||||
})}
|
||||
.label=${title ?? ''}
|
||||
?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
|
||||
?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 { StatusBarConfig } from '../config/schema/status-bar.js';
|
||||
import statusStyle from '../scss/status.scss?inline';
|
||||
import type { Interaction } from '../types.js';
|
||||
import { hasAction } from '../utils/action';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
|
||||
@@ -114,7 +115,8 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
||||
class="${classes}"
|
||||
title=${item.title ?? nothing}
|
||||
data-severity=${item.severity ?? ''}
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
@action=${(ev: CustomEvent<Interaction>) =>
|
||||
this._controller.actionHandler(ev, item.actions)}
|
||||
>
|
||||
${item.string}
|
||||
</div>`;
|
||||
@@ -125,7 +127,8 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
||||
class="${classes}"
|
||||
title=${item.title ?? nothing}
|
||||
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>`;
|
||||
} else if (item.type === 'custom:advanced-camera-card-status-bar-image') {
|
||||
return html`<img
|
||||
@@ -134,7 +137,8 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
||||
title=${item.title ?? nothing}
|
||||
src="${item.image}"
|
||||
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 =
|
||||
(await this.entityRegistryManager?.getEntity(this.hass, entityID)) ?? null;
|
||||
|
||||
const optionTitles = {};
|
||||
const optionTitles: Record<string, string> = {};
|
||||
for (const option of options) {
|
||||
const title = getEntityStateTranslation(this.hass, entityID, {
|
||||
...(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 type {
|
||||
@@ -55,15 +55,17 @@ export class ConditionStateManager implements ConditionStateManagerReadonlyInter
|
||||
}
|
||||
|
||||
private _calculateTrueChange(change: ConditionState): ConditionState {
|
||||
const changeState: ConditionState = {};
|
||||
|
||||
for (const key of Object.keys(change)) {
|
||||
if (!isEqual(change[key], this._state[key])) {
|
||||
changeState[key] = change[key];
|
||||
}
|
||||
}
|
||||
|
||||
return changeState;
|
||||
return pickBy(
|
||||
change,
|
||||
(value, key) =>
|
||||
!isEqual(
|
||||
value,
|
||||
this._state[
|
||||
// lodash widens the key to `string`, which cannot index ConditionState.
|
||||
key as keyof ConditionState
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _callListeners = (stateChange: ConditionStateChange): void => {
|
||||
|
||||
+38
-51
@@ -24,7 +24,6 @@ import {
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||
} from './const';
|
||||
import type { Condition } from './schema/condition-trigger/conditions/types';
|
||||
import type {
|
||||
RawAdvancedCameraCardConfig,
|
||||
RawAdvancedCameraCardConfigArray,
|
||||
@@ -430,17 +429,15 @@ export const deleteTransform = function (_value: unknown): number | null | undef
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
const conditionToConditionsTransform = (data: unknown): boolean => {
|
||||
if (
|
||||
typeof data !== 'object' ||
|
||||
!data ||
|
||||
typeof data['conditions'] !== 'object' ||
|
||||
!data['conditions']
|
||||
) {
|
||||
if (!isRecord(data) || !isRecord(data['conditions'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
newConditions.push({
|
||||
@@ -475,23 +472,18 @@ const conditionToConditionsTransform = (data: unknown): boolean => {
|
||||
if (oldConditions['state'] !== undefined && Array.isArray(oldConditions['state'])) {
|
||||
for (const stateCondition of oldConditions['state']) {
|
||||
if (
|
||||
typeof stateCondition === 'object' &&
|
||||
stateCondition &&
|
||||
isRecord(stateCondition) &&
|
||||
(stateCondition['state'] !== undefined ||
|
||||
stateCondition['state_not'] !== undefined ||
|
||||
stateCondition['entity'] !== undefined)
|
||||
) {
|
||||
newConditions.push({
|
||||
condition: 'state' as const,
|
||||
...(stateCondition['state'] && {
|
||||
state: stateCondition['state'],
|
||||
}),
|
||||
...(stateCondition['state_not'] && {
|
||||
...(!!stateCondition['state'] && { state: stateCondition['state'] }),
|
||||
...(!!stateCondition['state_not'] && {
|
||||
state_not: stateCondition['state_not'],
|
||||
}),
|
||||
...(stateCondition['entity'] && {
|
||||
entity_id: stateCondition['entity'],
|
||||
}),
|
||||
...(!!stateCondition['entity'] && { entity_id: stateCondition['entity'] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -570,8 +562,7 @@ const dropTriggerOnlyConditions = (conditions: unknown[]): unknown[] => {
|
||||
for (const condition of conditions) {
|
||||
if (
|
||||
isCompositeCondition(condition) &&
|
||||
typeof condition === 'object' &&
|
||||
condition &&
|
||||
isRecord(condition) &&
|
||||
Array.isArray(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
|
||||
// (`enabled`, and the fields it already shares with its trigger) is preserved,
|
||||
// so promoting never silently discards user configuration.
|
||||
const withoutKeys = (...keys: string[]): Record<string, unknown> => {
|
||||
const withoutKeys = (...keys: string[]): RawAdvancedCameraCardConfig => {
|
||||
const rest = { ...condition };
|
||||
for (const key of keys) {
|
||||
delete rest[key];
|
||||
@@ -1100,8 +1091,7 @@ const callServiceToPerformActionTransform = (data: unknown): boolean => {
|
||||
*/
|
||||
const serviceDataToDataTransform = (data: unknown): boolean => {
|
||||
if (
|
||||
typeof data === 'object' &&
|
||||
data &&
|
||||
isRecord(data) &&
|
||||
data['action'] === 'call-service' &&
|
||||
data['service'] !== undefined &&
|
||||
data['service_data'] !== undefined &&
|
||||
@@ -1205,7 +1195,7 @@ const ptzIncorrectDataToWebRTCDataTransform = (data: unknown): unknown => {
|
||||
};
|
||||
|
||||
const ptzActionsToCamerasGlobalTransform = (data: unknown): unknown => {
|
||||
if (typeof data !== 'object' || !data) {
|
||||
if (!isRecord(data)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1242,7 +1232,7 @@ const ptzActionsToCamerasGlobalTransform = (data: unknown): unknown => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const output = {};
|
||||
const output: RawAdvancedCameraCardConfig = {};
|
||||
|
||||
NON_PRESET_DATA_KEYS.filter((key) => key in data).reduce((obj, key) => {
|
||||
obj[key] = data[key];
|
||||
@@ -1250,36 +1240,31 @@ const ptzActionsToCamerasGlobalTransform = (data: unknown): unknown => {
|
||||
}, output);
|
||||
|
||||
NON_PRESET_ACTION_KEYS.filter((key) => key in data).reduce((obj, key) => {
|
||||
if (typeof data[key] === 'object' && 'tap_action' in data[key]) {
|
||||
obj[key] = data[key]['tap_action'];
|
||||
const action = data[key];
|
||||
if (isRecord(action) && 'tap_action' in action) {
|
||||
obj[key] = action['tap_action'];
|
||||
}
|
||||
return obj;
|
||||
}, output);
|
||||
|
||||
const createPresets = () => {
|
||||
output['presets'] =
|
||||
'presets' in data && typeof data['presets'] === 'object' && !!data['presets']
|
||||
? data['presets']
|
||||
: {};
|
||||
// Returns the preset collection so callers can add to it after it is
|
||||
// attached to the output.
|
||||
const createPresets = (): RawAdvancedCameraCardConfig => {
|
||||
const existing = data['presets'];
|
||||
const presets = isRecord(existing) ? existing : {};
|
||||
output['presets'] = presets;
|
||||
return presets;
|
||||
};
|
||||
|
||||
if (
|
||||
'actions_home' in data &&
|
||||
typeof data['actions_home'] === 'object' &&
|
||||
data['actions_home'] &&
|
||||
'tap_action' in data['actions_home']
|
||||
) {
|
||||
createPresets();
|
||||
output['presets']['home'] = data['actions_home']['tap_action'];
|
||||
} else if (
|
||||
'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'];
|
||||
const actionsHome = data['actions_home'];
|
||||
const dataHome = data['data_home'];
|
||||
|
||||
if (isRecord(actionsHome) && 'tap_action' in actionsHome) {
|
||||
createPresets()['home'] = actionsHome['tap_action'];
|
||||
} else if (isRecord(dataHome) && typeof data['service'] === 'string') {
|
||||
const presets = createPresets();
|
||||
presets['service'] = data['service'];
|
||||
presets['data_home'] = dataHome;
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -1310,7 +1295,7 @@ const ptzControlSettingsTransform = (data: unknown): unknown => {
|
||||
|
||||
return keys
|
||||
.filter((key) => TRANSFORM_KEYS.includes(key))
|
||||
.reduce((obj, key) => {
|
||||
.reduce<RawAdvancedCameraCardConfig>((obj, key) => {
|
||||
obj[key] = data[key];
|
||||
return obj;
|
||||
}, {});
|
||||
@@ -1593,13 +1578,15 @@ const UPGRADES = [
|
||||
deleteWithOverrides('image.layout'),
|
||||
upgradeArrayOfObjects(CONF_OVERRIDES, conditionToConditionsTransform),
|
||||
(data: unknown): boolean => {
|
||||
const elements = isRecord(data) ? data[CONF_ELEMENTS] : null;
|
||||
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
||||
typeof data === 'object' && data ? data[CONF_ELEMENTS] : {},
|
||||
isRecord(elements) ? elements : {},
|
||||
);
|
||||
},
|
||||
(data: unknown): boolean => {
|
||||
const automations = isRecord(data) ? data[CONF_AUTOMATIONS] : null;
|
||||
return upgradeObjectRecursively(conditionToConditionsTransform)(
|
||||
typeof data === 'object' && data ? data[CONF_AUTOMATIONS] : {},
|
||||
isRecord(automations) ? automations : {},
|
||||
);
|
||||
},
|
||||
upgradeArrayOfObjects(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isRecord } from '../../../utils/basic';
|
||||
import type { RawAdvancedCameraCardConfig } from '../../types';
|
||||
import { performActionActionSchema } from '../actions/stock/perform-action';
|
||||
|
||||
export const ptzCameraConfigDefaults = {
|
||||
@@ -14,12 +16,12 @@ export const ptzCameraConfigDefaults = {
|
||||
const dataPTZFormatToFullFormat =
|
||||
(suffix: string) =>
|
||||
(data: unknown): unknown => {
|
||||
if (!data || typeof data !== 'object' || !data['service']) {
|
||||
if (!isRecord(data) || !data['service']) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const service = data['service'];
|
||||
const out = { ...data };
|
||||
const out: RawAdvancedCameraCardConfig = { ...data };
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
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
|
||||
// home button (which activates the first preset) uses it.
|
||||
if (suffix && name === 'home') {
|
||||
const presets =
|
||||
out['presets'] && typeof out['presets'] === 'object' ? out['presets'] : {};
|
||||
const presets = isRecord(out['presets']) ? out['presets'] : {};
|
||||
if (!('home' in presets)) {
|
||||
out['presets'] = {
|
||||
home: {
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ export interface HomeAssistant {
|
||||
[key: string]: unknown;
|
||||
},
|
||||
) => Promise<Response>;
|
||||
hassUrl(path?): string;
|
||||
hassUrl(path?: string): string;
|
||||
sendWS: (msg: MessageBase) => Promise<void>;
|
||||
callWS: <T>(msg: MessageBase) => Promise<T>;
|
||||
}
|
||||
|
||||
+4
-2
@@ -270,13 +270,15 @@ export const getChildrenFromElement = (parent: HTMLElement): HTMLElement[] => {
|
||||
export const recursivelyMergeObjectsNotArrays = <T>(
|
||||
...srcs: (Partial<T> | undefined | null)[]
|
||||
): 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>(
|
||||
...srcs: (Partial<T> | undefined | null)[]
|
||||
): T => {
|
||||
return mergeWith({}, ...srcs, (a, b) =>
|
||||
return mergeWith({}, ...srcs, (a: unknown, b: unknown) =>
|
||||
Array.isArray(a) ? uniq(a.concat(b)) : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
+5
-7
@@ -1,5 +1,6 @@
|
||||
import type { CameraConfig } from '../config/schema/cameras';
|
||||
import type { RawAdvancedCameraCardConfig } from '../config/types';
|
||||
import { isRecord } from './basic';
|
||||
|
||||
/**
|
||||
* Get a camera id.
|
||||
@@ -12,20 +13,17 @@ export function getCameraID(
|
||||
return (
|
||||
(typeof config?.id === 'string' && config.id) ||
|
||||
(typeof config?.camera_entity === 'string' && config.camera_entity) ||
|
||||
(typeof config?.webrtc_card === 'object' &&
|
||||
config.webrtc_card &&
|
||||
(isRecord(config?.webrtc_card) &&
|
||||
((typeof config.webrtc_card['entity'] === 'string' &&
|
||||
config.webrtc_card['entity']) ||
|
||||
(typeof config.webrtc_card['url'] === 'string' && config.webrtc_card['url']))) ||
|
||||
(typeof config?.go2rtc === 'object' &&
|
||||
config.go2rtc &&
|
||||
(isRecord(config?.go2rtc) &&
|
||||
typeof config.go2rtc['url'] === 'string' &&
|
||||
typeof config.go2rtc['stream'] === 'string' &&
|
||||
// Artifical identifier that includes both url / stream.
|
||||
`${config.go2rtc['url']}#${config.go2rtc['stream']}`) ||
|
||||
(typeof config?.frigate === 'object' &&
|
||||
config.frigate &&
|
||||
typeof config?.frigate['camera_name'] === 'string' &&
|
||||
(isRecord(config?.frigate) &&
|
||||
typeof config.frigate['camera_name'] === 'string' &&
|
||||
config.frigate['camera_name']) ||
|
||||
''
|
||||
);
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { MediaLayoutConfig } from '../config/schema/camera/media-layout';
|
||||
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.
|
||||
* @param element The element to update the style for.
|
||||
@@ -17,7 +28,7 @@ export const updateElementStyleFromMediaLayoutConfig = (
|
||||
mediaLayoutConfig?.fit,
|
||||
);
|
||||
|
||||
for (const dimension of ['x', 'y']) {
|
||||
for (const dimension of POSITION_DIMENSIONS) {
|
||||
setOrRemoveStyleProperty(
|
||||
element,
|
||||
!!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(
|
||||
element,
|
||||
!!mediaLayoutConfig?.view_box?.[dimension],
|
||||
|
||||
+6
-4
@@ -143,13 +143,15 @@ export class View {
|
||||
return this;
|
||||
}
|
||||
|
||||
public removeContextProperty(
|
||||
contextKey: keyof ViewContext,
|
||||
removeKey: PropertyKey,
|
||||
public removeContextProperty<T extends keyof ViewContext>(
|
||||
contextKey: T,
|
||||
removeKey: keyof NonNullable<ViewContext[T]>,
|
||||
): View {
|
||||
const contextObj = this.context?.[contextKey];
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
actionHandler,
|
||||
type ActionHandlerInterface,
|
||||
type AdvancedCameraCardActionHandlerOptions,
|
||||
} from '../src/action-handler-directive';
|
||||
import { fireHASSEvent } from '../src/ha/fire-hass-event';
|
||||
import type { ActionHandlerDetail } from '../src/ha/types';
|
||||
@@ -23,7 +24,9 @@ const getActionHandler = (): ActionHandlerInterface => {
|
||||
return el as ActionHandlerInterface;
|
||||
};
|
||||
|
||||
const createBoundElement = (options?: Record<string, unknown>): HTMLElement => {
|
||||
const createBoundElement = (
|
||||
options?: AdvancedCameraCardActionHandlerOptions,
|
||||
): HTMLElement => {
|
||||
const handler = getActionHandler();
|
||||
const element = document.createElement('div');
|
||||
handler.bind(element, options);
|
||||
|
||||
@@ -27,6 +27,9 @@ import {
|
||||
type EventQueryResults,
|
||||
type MediaMetadata,
|
||||
type QueryResults,
|
||||
type RecordingQuery,
|
||||
type RecordingSegmentsQuery,
|
||||
type ReviewQuery,
|
||||
} from '../../src/camera-manager/types.js';
|
||||
import type { CardController } from '../../src/card-controller/controller.js';
|
||||
import type { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||
@@ -752,46 +755,65 @@ describe('CameraManager', () => {
|
||||
});
|
||||
|
||||
describe('generate default queries', () => {
|
||||
it.each([
|
||||
[
|
||||
QueryType.Event as const,
|
||||
'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 setupManagerWithEngine = async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const manager = createCameraManager(api, engine);
|
||||
await manager.initializeCamerasFromConfig();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const manager = createCameraManager(api, engine);
|
||||
await manager.initializeCamerasFromConfig();
|
||||
|
||||
const queries = [{ type: queryType, cameraIDs: new Set(['id']) }];
|
||||
engine[engineMethodName].mockReturnValue(queries);
|
||||
expect(manager[managerMethodName]('id')).toEqual(queries);
|
||||
},
|
||||
);
|
||||
return { engine, manager };
|
||||
};
|
||||
|
||||
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 () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
@@ -42,13 +42,18 @@ const getActionSpy = (
|
||||
};
|
||||
|
||||
const createPlayerElement = (controller?: MediaPlayerController): MediaPlayerElement => {
|
||||
const player = document.createElement('video');
|
||||
player['getMediaPlayerController'] = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
controller ?? mock<MediaPlayerController>({ playback: mock<PlaybackControl>() }),
|
||||
);
|
||||
return player as unknown as MediaPlayerElement;
|
||||
const player: MediaPlayerElement<HTMLVideoElement> = Object.assign(
|
||||
document.createElement('video'),
|
||||
{
|
||||
getMediaPlayerController: vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
controller ??
|
||||
mock<MediaPlayerController>({ playback: mock<PlaybackControl>() }),
|
||||
),
|
||||
},
|
||||
);
|
||||
return player;
|
||||
};
|
||||
|
||||
const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
||||
@@ -914,13 +919,17 @@ describe('MediaActionsController', () => {
|
||||
|
||||
// A player whose media player controller is not ready on first request.
|
||||
const mediaPlayerController = mock<MediaPlayerController>();
|
||||
const player = document.createElement('video');
|
||||
player['getMediaPlayerController'] = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValue(mediaPlayerController);
|
||||
const player: MediaPlayerElement<HTMLVideoElement> = Object.assign(
|
||||
document.createElement('video'),
|
||||
{
|
||||
getMediaPlayerController: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValue(mediaPlayerController),
|
||||
},
|
||||
);
|
||||
const child = createTestSlideNodes({ n: 1 })[0];
|
||||
child.appendChild(player as unknown as MediaPlayerElement);
|
||||
child.appendChild(player);
|
||||
|
||||
controller.setRoot(createParent({ children: [child] }));
|
||||
await controller.setTarget(0, true);
|
||||
|
||||
+1
-1
@@ -3,6 +3,7 @@
|
||||
"target": "es2021",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["es2021", "dom", "dom.iterable"],
|
||||
@@ -12,7 +13,6 @@
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
|
||||
Reference in New Issue
Block a user