Merge pull request #86 from dermotduffy/camera-names
Much improved auto-detection of frigate camera name
This commit is contained in:
@@ -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).|
|
||||
|
||||
+85
-27
@@ -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,11 +537,28 @@ 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)}">
|
||||
${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=${this._getBrowseMediaQueryParameters()}
|
||||
.browseMediaQueryParameters=${mediaQueryParameters}
|
||||
@frigate-card:change-view=${this._changeViewHandler}
|
||||
>
|
||||
</frigate-card-gallery>`
|
||||
@@ -505,9 +567,8 @@ export class FrigateCard extends LitElement {
|
||||
? html` <frigate-card-viewer
|
||||
.hass=${this._hass}
|
||||
.view=${this._view}
|
||||
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
|
||||
.nextPreviousControlStyle=${this.config.controls?.nextprev ??
|
||||
'thumbnails'}
|
||||
.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}
|
||||
@@ -526,10 +587,7 @@ export class FrigateCard extends LitElement {
|
||||
>
|
||||
</frigate-card-live>`
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
|
||||
</ha-card>`;
|
||||
`;
|
||||
}
|
||||
|
||||
// Show a warning card.
|
||||
|
||||
@@ -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)}">
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
box-sizing: border-box;
|
||||
padding: 10%;
|
||||
}
|
||||
|
||||
.message a {
|
||||
color: var(--primary-text-color, white);
|
||||
}
|
||||
|
||||
+32
-16
@@ -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()
|
||||
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(),
|
||||
.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>;
|
||||
|
||||
Reference in New Issue
Block a user