Files
advanced-camera-card/src/components/views.ts
T
Dermot Duffy fc32727860 feat: Add support for Frigate reviews / detections [initial PR] (#2315)
- Add support for Frigate reviews / detections.
 - Add support for GenAI metadata.
- Significant internal refactor to more flexible "UnifiedQuery" to allow
mixing cameras with simple metadata and review metadata (e.g. a timeline
view of a Frigate camera with reviews, and a Reolink camera with simple
metadata).
 - Add support for folder media as camera media.

There are a few more PRs to commit prior to this going live, but
commiting this for now due to the scale of the change.

BREAKING CHANGE: `media_type` and `events_type` are retired under
`live`, `viewer` and `timeline` configuration sections, instead media
type is associated (once) with the camera under `media`.
2026-01-19 13:32:50 -08:00

248 lines
8.6 KiB
TypeScript

import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { CameraManager } from '../camera-manager/manager.js';
import { FoldersManager } from '../card-controller/folders/manager.js';
import { MicrophoneState } from '../card-controller/types.js';
import { ViewItemManager } from '../card-controller/view/item-manager.js';
import { ViewManagerEpoch } from '../card-controller/view/types.js';
import { ConditionStateManagerReadonlyInterface } from '../conditions/types.js';
import { AdvancedCameraCardConfig, CardWideConfig } from '../config/schema/types.js';
import { RawAdvancedCameraCardConfig } from '../config/types.js';
import { DeviceRegistryManager } from '../ha/registry/device/index.js';
import { ResolvedMediaCache } from '../ha/resolved-media.js';
import { HomeAssistant } from '../ha/types.js';
import viewsStyle from '../scss/views.scss';
import './surround.js';
// As a special case: The diagnostics view is not dynamically loaded in case
// something goes wrong.
import './diagnostics.js';
@customElement('advanced-camera-card-views')
export class AdvancedCameraCardViews extends LitElement {
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false })
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public cameraManager?: CameraManager;
@property({ attribute: false })
public foldersManager?: FoldersManager;
@property({ attribute: false })
public viewItemManager?: ViewItemManager;
@property({ attribute: false })
public config?: AdvancedCameraCardConfig;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
@property({ attribute: false })
public rawConfig?: RawAdvancedCameraCardConfig;
@property({ attribute: false })
public resolvedMediaCache?: ResolvedMediaCache;
@property({ attribute: false })
public hide?: boolean;
@property({ attribute: false })
public microphoneState?: MicrophoneState;
@property({ attribute: false })
public triggeredCameraIDs?: Set<string>;
@property({ attribute: false })
public deviceRegistryManager?: DeviceRegistryManager;
@property({ attribute: false })
public conditionStateManager?: ConditionStateManagerReadonlyInterface;
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('viewManagerEpoch') || changedProps.has('config')) {
const view = this.viewManagerEpoch?.manager.getView();
if (view?.is('live') || this._shouldLivePreload()) {
import('./live/index.js');
}
if (view?.isGalleryView()) {
import('./gallery/gallery.js');
} else if (view?.isViewerView()) {
import('./viewer/index.js');
} else if (view?.is('image')) {
import('./image.js');
} else if (view?.is('timeline')) {
import('./timeline.js');
}
}
if (changedProps.has('hide')) {
if (this.hide) {
this.setAttribute('hidden', '');
} else {
this.removeAttribute('hidden');
}
}
}
protected _shouldLivePreload(): boolean {
const view = this.viewManagerEpoch?.manager.getView();
return (
// Special case: Never preload for diagnostics -- we want that to be as
// minimal as possible.
!!this.config?.live.preload && !view?.is('diagnostics')
);
}
protected render(): TemplateResult | void {
// Only essential items should be added to the below list, since we want the
// overall views pane to render in ~almost all cases (e.g. for a camera
// initialization error to display, `view` and `cameraConfig` may both be
// undefined, but we still want to render).
if (!this.hass || !this.config || !this.cardWideConfig) {
return html``;
}
const view = this.viewManagerEpoch?.manager.getView();
// Render but hide the live view if there's a message, or if it's preload
// mode and the view is not live.
const liveClasses = {
hidden: this._shouldLivePreload() && !view?.is('live'),
};
const overallClasses = {
hidden: !!this.hide,
};
const thumbnailConfig = view?.is('live')
? this.config.live.controls.thumbnails
: view?.isViewerView()
? this.config.media_viewer.controls.thumbnails
: view?.is('timeline')
? this.config.timeline.controls.thumbnails
: undefined;
const miniTimelineConfig = view?.is('live')
? this.config.live.controls.timeline
: view?.isViewerView()
? this.config.media_viewer.controls.timeline
: undefined;
const cameraConfig = view
? this.cameraManager?.getStore().getCameraConfig(view.camera) ?? null
: null;
return html` <advanced-camera-card-surround
class="${classMap(overallClasses)}"
.hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch}
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
.cameraManager=${this.cameraManager}
.foldersManager=${this.foldersManager}
.conditionStateManager=${this.conditionStateManager}
.viewItemManager=${this.viewItemManager}
.cardWideConfig=${this.cardWideConfig}
>
${!this.hide && view?.is('image') && cameraConfig
? html` <advanced-camera-card-image
.imageConfig=${this.config.image}
.viewManagerEpoch=${this.viewManagerEpoch}
.hass=${this.hass}
.cameraConfig=${cameraConfig}
.cameraManager=${this.cameraManager}
>
</advanced-camera-card-image>`
: ``}
${!this.hide && view?.isGalleryView()
? html` <advanced-camera-card-gallery
.hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch}
.galleryConfig=${this.config.media_gallery}
.cameraManager=${this.cameraManager}
.foldersManager=${this.foldersManager}
.viewItemManager=${this.viewItemManager}
.cardWideConfig=${this.cardWideConfig}
.conditionStateManager=${this.conditionStateManager}
>
</advanced-camera-card-gallery>`
: ``}
${!this.hide && view?.isViewerView()
? html`
<advanced-camera-card-viewer
.hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch}
.viewerConfig=${this.config.media_viewer}
.resolvedMediaCache=${this.resolvedMediaCache}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
.viewItemManager=${this.viewItemManager}
>
</advanced-camera-card-viewer>
`
: ``}
${!this.hide && view?.is('timeline')
? html` <advanced-camera-card-timeline
.hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch}
.timelineConfig=${this.config.timeline}
.cameraManager=${this.cameraManager}
.conditionStateManager=${this.conditionStateManager}
.foldersManager=${this.foldersManager}
.viewItemManager=${this.viewItemManager}
.cardWideConfig=${this.cardWideConfig}
>
</advanced-camera-card-timeline>`
: ``}
${!this.hide && view?.is('diagnostics')
? html` <advanced-camera-card-diagnostics
.hass=${this.hass}
.rawConfig=${this.rawConfig}
.deviceRegistryManager=${this.deviceRegistryManager}
>
</advanced-camera-card-diagnostics>`
: ``}
${
// Note: Subtle difference in condition below vs the other views in
// order to always render the live view for live.preload mode.
this._shouldLivePreload() || (!this.hide && view?.is('live'))
? html`
<advanced-camera-card-live
.hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch}
.liveConfig=${this.config.live}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
.microphoneState=${this.microphoneState}
.triggeredCameraIDs=${this.triggeredCameraIDs}
class="${classMap(liveClasses)}"
>
</advanced-camera-card-live>
`
: ``
}
</advanced-camera-card-surround>`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(viewsStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'advanced-camera-card-views': AdvancedCameraCardViews;
}
}