Merge pull request #86 from dermotduffy/camera-names

Much improved auto-detection of frigate camera name
This commit is contained in:
Dermot Duffy
2021-09-26 21:02:27 -07:00
committed by GitHub
7 changed files with 155 additions and 79 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ lovelace:
| Option | Default | Description |
| ------------- | --------------------------------------------- | - |
| `frigate_camera_name` | The string after the "camera." in the `camera_entity` option (above). | This parameter allows the camera name heuristic to be overriden for cases where the entity name does not cleanly map to the Frigate camera name (e.g. when the Frigate camera name is capitalized, but the entity name is lower case). This camera name is used for communicating with the Frigate backend, e.g. for fetching events. |
| `frigate_camera_name` | The Frigate camera name Home Assistant associates with that camera entity, if none then the string after the `camera.` in the `camera_entity` field. | This parameter allows the Frigate camera name to be overriden. This name is used for communicating with the Frigate backend, e.g. for fetching events. |
| `live_provider` | `frigate` | The means through which the live camera view is displayed. See [Live Provider](#live-provider) below.|
| `view_default` | `live` | The view to show by default. See [views](#views) below.|
| `frigate_client_id` | `frigate` | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://blakeblackshear.github.io/frigate/usage/home-assistant/#multiple-instance-support).|
+111 -53
View File
@@ -10,6 +10,7 @@ import {
import { customElement, property, query, state } from 'lit/decorators';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { until } from 'lit/directives/until';
import {
HomeAssistant,
LovelaceCardEditor,
@@ -19,19 +20,22 @@ import {
} from 'custom-card-helpers';
import screenfull from 'screenfull';
import { MenuButton, frigateCardConfigSchema } from './types';
import { entitySchema, frigateCardConfigSchema } from './types';
import type {
BrowseMediaQueryParameters,
Entity,
ExtendedHomeAssistant,
FrigateCardConfig,
MediaLoadInfo,
MenuButton,
} from './types';
import { CARD_VERSION } from './const';
import { FrigateCardMenu } from './components/menu';
import { View } from './view';
import { getParseErrorKeys } from './common';
import { getParseErrorKeys, homeAssistantWSRequest } from './common';
import { localize } from './localize/localize';
import { renderErrorMessage, renderProgressIndicator } from './components/message';
import './editor';
import './components/gallery';
@@ -134,6 +138,10 @@ export class FrigateCard extends LitElement {
// Information about the most recently loaded media item.
protected _mediaInfo: MediaLoadInfo | null = null;
// The frigate camera name to use (may be manually specified or automatically
// derived).
protected _frigateCameraName: string | null = null;
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
this._hass = hass;
this._updateMenu();
@@ -215,6 +223,49 @@ export class FrigateCard extends LitElement {
return buttons;
}
protected async _getFrigateCameraName(): Promise<string | null> {
// No camera name specified, apply two heuristics in this order:
// - Get the entity information and pull out the camera name from the unique_id.
// - Apply basic entity name guesswork.
if (!this._hass || !this.config) {
return null;
}
// Option 1: Name specified in config -> done!
if (this.config.frigate_camera_name) {
return this.config.frigate_camera_name;
}
// Option 2: Find entity unique_id in registry.
const request = {
type: 'config/entity_registry/get',
entity_id: this.config.camera_entity,
};
try {
const entityResult = await homeAssistantWSRequest<Entity>(
this._hass,
entitySchema,
request,
);
if (entityResult && entityResult.platform == 'frigate') {
const match = entityResult.unique_id.match(/:camera:(?<camera>[^:]+)$/);
if (match && match.groups) {
return match.groups['camera'];
}
}
} catch (e: any) {
// Pass.
}
// Option 3: Guess from the entity_id.
if (this.config.camera_entity.includes('.')) {
return this.config.camera_entity.split('.', 2)[1];
}
return null;
}
// Set the object configuration.
public setConfig(inputConfig: FrigateCardConfig): void {
if (!inputConfig) {
@@ -232,15 +283,6 @@ export class FrigateCard extends LitElement {
getLovelace().setEditMode(true);
}
if (!config.frigate_camera_name) {
// No camera name specified, so just assume it's the same as the entity name.
if (config.camera_entity.includes('.')) {
config.frigate_camera_name = config.camera_entity.split('.', 2)[1];
} else {
throw new Error(localize('error.invalid_configuration') + ': camera_entity');
}
}
this.config = config;
this._entitiesToMonitor = [
...(this.config.entities || []).map((entity) => entity.entity),
@@ -318,10 +360,12 @@ export class FrigateCard extends LitElement {
if (!this.config.frigate_url) {
return null;
}
if (this._view.is('live')) {
return `${this.config.frigate_url}/cameras/${this.config.frigate_camera_name}`;
if (!this._frigateCameraName) {
return this.config.frigate_url;
} else if (this._view.is('live')) {
return `${this.config.frigate_url}/cameras/${this._frigateCameraName}`;
}
return `${this.config.frigate_url}/events?camera=${this.config.frigate_camera_name}`;
return `${this.config.frigate_url}/events?camera=${this._frigateCameraName}`;
}
// Record interactions with the card.
@@ -352,14 +396,15 @@ export class FrigateCard extends LitElement {
`;
}
protected _getBrowseMediaQueryParameters(): BrowseMediaQueryParameters {
protected _getBrowseMediaQueryParameters(): BrowseMediaQueryParameters | null {
if (!this._frigateCameraName) {
return null;
}
return {
mediaType: this._view.view == 'clips' ? 'clips' : 'snapshots',
clientId: this.config.frigate_client_id,
// frigate_camera_name cannot be null, it will be set to a default value
// in setConfig if not specified in the configuration.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
cameraName: this.config.frigate_camera_name!,
cameraName: this._frigateCameraName,
label: this.config.label,
zone: this.config.zone,
};
@@ -492,46 +537,59 @@ export class FrigateCard extends LitElement {
${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
<div class="container outer" style="${styleMap(outerStyle)}">
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
${this._view.is('clips') || this._view.is('snapshots')
? html` <frigate-card-gallery
.hass=${this._hass}
.view=${this._view}
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
@frigate-card:change-view=${this._changeViewHandler}
>
</frigate-card-gallery>`
: ``}
${this._view.is('clip') || this._view.is('snapshot')
? html` <frigate-card-viewer
.hass=${this._hass}
.view=${this._view}
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
.nextPreviousControlStyle=${this.config.controls?.nextprev ??
'thumbnails'}
.autoplayClip=${this.config.autoplay_clip}
@frigate-card:change-view=${this._changeViewHandler}
@frigate-card:media-load=${this._mediaLoadHandler}
@frigate-card:pause=${this._pauseHandler}
@frigate-card:play=${this._playHandler}
>
</frigate-card-viewer>`
: ``}
${this._view.is('live')
? html` <frigate-card-live
.hass=${this._hass}
.config=${this.config}
@frigate-card:media-load=${this._mediaLoadHandler}
@frigate-card:pause=${this._pauseHandler}
@frigate-card:play=${this._playHandler}
>
</frigate-card-live>`
: ``}
${until(this._render(), renderProgressIndicator())}
</div>
</div>
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
</ha-card>`;
}
protected async _render(): Promise<TemplateResult> {
if (!this._frigateCameraName) {
this._frigateCameraName = await this._getFrigateCameraName();
}
const mediaQueryParameters = this._getBrowseMediaQueryParameters();
if (!this._frigateCameraName || !mediaQueryParameters) {
return renderErrorMessage(localize('error.no_frigate_camera_name'));
}
return html`
${this._view.is('clips') || this._view.is('snapshots')
? html` <frigate-card-gallery
.hass=${this._hass}
.view=${this._view}
.browseMediaQueryParameters=${mediaQueryParameters}
@frigate-card:change-view=${this._changeViewHandler}
>
</frigate-card-gallery>`
: ``}
${this._view.is('clip') || this._view.is('snapshot')
? html` <frigate-card-viewer
.hass=${this._hass}
.view=${this._view}
.browseMediaQueryParameters=${mediaQueryParameters}
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
.autoplayClip=${this.config.autoplay_clip}
@frigate-card:change-view=${this._changeViewHandler}
@frigate-card:media-load=${this._mediaLoadHandler}
@frigate-card:pause=${this._pauseHandler}
@frigate-card:play=${this._playHandler}
>
</frigate-card-viewer>`
: ``}
${this._view.is('live')
? html` <frigate-card-live
.hass=${this._hass}
.config=${this.config}
@frigate-card:media-load=${this._mediaLoadHandler}
@frigate-card:pause=${this._pauseHandler}
@frigate-card:play=${this._playHandler}
>
</frigate-card-live>`
: ``}
`;
}
// Show a warning card.
private _showWarning(warning: string): TemplateResult {
return html` <hui-warning> ${warning} </hui-warning> `;
-1
View File
@@ -91,7 +91,6 @@ export class FrigateCardGallery extends LitElement {
width: `calc(${100 / this._columns}% - 1.2px)`,
};
console.info(this.clientWidth);
return html` <ul class="mdc-image-list frigate-card-gallery">
${this.view && this.view.previous
? html`<li class="mdc-image-list__item" style="${styleMap(styles)}">
+2 -1
View File
@@ -94,6 +94,7 @@
"could_not_resolve": "Could not resolve media URL",
"no_live_camera": "No live camera",
"invalid_configuration": "Invalid configuration",
"missing_webrtc": "WebRTC component not found"
"missing_webrtc": "WebRTC component not found",
"no_frigate_camera_name": "Cannot derive frigate_camera_name, you may need to set it manually"
}
}
+2 -4
View File
@@ -14,6 +14,8 @@
overflow: auto;
-ms-overflow-style: none; /* Hide scrollbar: IE and Edge */
scrollbar-width: none; /* Hide scrollbar: Firefox */
display: flex;
align-items: center;
}
/* Hide scrollbar for Chrome, Safari and Opera */
@@ -54,10 +56,6 @@ ha-card {
background-color: var(--secondary-background-color, black);
}
ha-card a {
color: var(--primary-text-color, white);
}
frigate-card-gallery, frigate-card-viewer, frigate-card-live {
width: 100%;
display: block;
+4
View File
@@ -6,3 +6,7 @@
box-sizing: border-box;
padding: 10%;
}
.message a {
color: var(--primary-text-color, white);
}
+33 -17
View File
@@ -98,15 +98,24 @@ export const frigateCardConfigSchema = z.object({
nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
})
.optional(),
dimensions: z.object({
aspect_ratio_mode: z.enum(['dynamic', 'static', 'unconstrained']).default('dynamic'),
aspect_ratio:
z.number().array().length(2).or(
z.string()
.regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/)
.transform((input) => input.split(/[:\/]/).map((d) => Number(d)))
).default([16, 9]),
}).optional(),
dimensions: z
.object({
aspect_ratio_mode: z
.enum(['dynamic', 'static', 'unconstrained'])
.default('dynamic'),
aspect_ratio: z
.number()
.array()
.length(2)
.or(
z
.string()
.regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/)
.transform((input) => input.split(/[:\/]/).map((d) => Number(d))),
)
.default([16, 9]),
})
.optional(),
// Stock lovelace card config.
type: z.string(),
@@ -136,6 +145,14 @@ export interface BrowseMediaQueryParameters {
after?: number;
}
export interface BrowseMediaNeighbors {
previous: BrowseMediaSource | null;
previousIndex: number | null;
next: BrowseMediaSource | null;
nextIndex: number | null;
}
export interface MediaLoadInfo {
width: number;
height: number;
@@ -182,15 +199,14 @@ export const resolvedMediaSchema = z.object({
});
export type ResolvedMedia = z.infer<typeof resolvedMediaSchema>;
export interface BrowseMediaNeighbors {
previous: BrowseMediaSource | null;
previousIndex: number | null;
next: BrowseMediaSource | null;
nextIndex: number | null;
}
export const signedPathSchema = z.object({
path: z.string(),
});
export type SignedPath = z.infer<typeof signedPathSchema>;
export const entitySchema = z.object({
entity_id: z.string(),
unique_id: z.string(),
platform: z.string(),
});
export type Entity = z.infer<typeof entitySchema>;