chore: Enable noImplicitAny across the codebase (#2667)

This commit is contained in:
Dermot Duffy
2026-08-09 13:22:57 -07:00
committed by GitHub
parent 6807fd7690
commit a8ce6acb44
27 changed files with 311 additions and 180 deletions
+2 -2
View File
@@ -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;
}
+2 -6
View File
@@ -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 =
+8
View File
@@ -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(),
+22 -1
View File
@@ -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) {
+2 -1
View File
@@ -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',
+18 -22
View File
@@ -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(
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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);
}}
+1 -1
View File
@@ -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(
+8 -3
View File
@@ -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}
+7 -3
View File
@@ -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)}
/>`;
}
})}
+1 -1
View File
@@ -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
View File
@@ -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(
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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']) ||
''
);
+13 -2
View File
@@ -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
View File
@@ -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;
}