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 |
|
| 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.|
|
| `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.|
|
| `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).|
|
| `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
@@ -10,6 +10,7 @@ import {
|
|||||||
import { customElement, property, query, state } from 'lit/decorators';
|
import { customElement, property, query, state } from 'lit/decorators';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
import { until } from 'lit/directives/until';
|
||||||
import {
|
import {
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
LovelaceCardEditor,
|
LovelaceCardEditor,
|
||||||
@@ -19,19 +20,22 @@ import {
|
|||||||
} from 'custom-card-helpers';
|
} from 'custom-card-helpers';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
|
|
||||||
import { MenuButton, frigateCardConfigSchema } from './types';
|
import { entitySchema, frigateCardConfigSchema } from './types';
|
||||||
import type {
|
import type {
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
|
Entity,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
MediaLoadInfo,
|
MediaLoadInfo,
|
||||||
|
MenuButton,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
import { CARD_VERSION } from './const';
|
import { CARD_VERSION } from './const';
|
||||||
import { FrigateCardMenu } from './components/menu';
|
import { FrigateCardMenu } from './components/menu';
|
||||||
import { View } from './view';
|
import { View } from './view';
|
||||||
import { getParseErrorKeys } from './common';
|
import { getParseErrorKeys, homeAssistantWSRequest } from './common';
|
||||||
import { localize } from './localize/localize';
|
import { localize } from './localize/localize';
|
||||||
|
import { renderErrorMessage, renderProgressIndicator } from './components/message';
|
||||||
|
|
||||||
import './editor';
|
import './editor';
|
||||||
import './components/gallery';
|
import './components/gallery';
|
||||||
@@ -134,6 +138,10 @@ export class FrigateCard extends LitElement {
|
|||||||
// Information about the most recently loaded media item.
|
// Information about the most recently loaded media item.
|
||||||
protected _mediaInfo: MediaLoadInfo | null = null;
|
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) {
|
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
|
||||||
this._hass = hass;
|
this._hass = hass;
|
||||||
this._updateMenu();
|
this._updateMenu();
|
||||||
@@ -215,6 +223,49 @@ export class FrigateCard extends LitElement {
|
|||||||
return buttons;
|
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.
|
// Set the object configuration.
|
||||||
public setConfig(inputConfig: FrigateCardConfig): void {
|
public setConfig(inputConfig: FrigateCardConfig): void {
|
||||||
if (!inputConfig) {
|
if (!inputConfig) {
|
||||||
@@ -232,15 +283,6 @@ export class FrigateCard extends LitElement {
|
|||||||
getLovelace().setEditMode(true);
|
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.config = config;
|
||||||
this._entitiesToMonitor = [
|
this._entitiesToMonitor = [
|
||||||
...(this.config.entities || []).map((entity) => entity.entity),
|
...(this.config.entities || []).map((entity) => entity.entity),
|
||||||
@@ -318,10 +360,12 @@ export class FrigateCard extends LitElement {
|
|||||||
if (!this.config.frigate_url) {
|
if (!this.config.frigate_url) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this._view.is('live')) {
|
if (!this._frigateCameraName) {
|
||||||
return `${this.config.frigate_url}/cameras/${this.config.frigate_camera_name}`;
|
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.
|
// 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 {
|
return {
|
||||||
mediaType: this._view.view == 'clips' ? 'clips' : 'snapshots',
|
mediaType: this._view.view == 'clips' ? 'clips' : 'snapshots',
|
||||||
clientId: this.config.frigate_client_id,
|
clientId: this.config.frigate_client_id,
|
||||||
// frigate_camera_name cannot be null, it will be set to a default value
|
cameraName: this._frigateCameraName,
|
||||||
// in setConfig if not specified in the configuration.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
cameraName: this.config.frigate_camera_name!,
|
|
||||||
label: this.config.label,
|
label: this.config.label,
|
||||||
zone: this.config.zone,
|
zone: this.config.zone,
|
||||||
};
|
};
|
||||||
@@ -492,46 +537,59 @@ export class FrigateCard extends LitElement {
|
|||||||
${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
|
${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
|
||||||
<div class="container outer" style="${styleMap(outerStyle)}">
|
<div class="container outer" style="${styleMap(outerStyle)}">
|
||||||
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
|
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
|
||||||
${this._view.is('clips') || this._view.is('snapshots')
|
${until(this._render(), renderProgressIndicator())}
|
||||||
? 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>`
|
|
||||||
: ``}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
|
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
|
||||||
</ha-card>`;
|
</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.
|
// Show a warning card.
|
||||||
private _showWarning(warning: string): TemplateResult {
|
private _showWarning(warning: string): TemplateResult {
|
||||||
return html` <hui-warning> ${warning} </hui-warning> `;
|
return html` <hui-warning> ${warning} </hui-warning> `;
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
width: `calc(${100 / this._columns}% - 1.2px)`,
|
width: `calc(${100 / this._columns}% - 1.2px)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.info(this.clientWidth);
|
|
||||||
return html` <ul class="mdc-image-list frigate-card-gallery">
|
return html` <ul class="mdc-image-list frigate-card-gallery">
|
||||||
${this.view && this.view.previous
|
${this.view && this.view.previous
|
||||||
? html`<li class="mdc-image-list__item" style="${styleMap(styles)}">
|
? html`<li class="mdc-image-list__item" style="${styleMap(styles)}">
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
"could_not_resolve": "Could not resolve media URL",
|
"could_not_resolve": "Could not resolve media URL",
|
||||||
"no_live_camera": "No live camera",
|
"no_live_camera": "No live camera",
|
||||||
"invalid_configuration": "Invalid configuration",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -14,6 +14,8 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
-ms-overflow-style: none; /* Hide scrollbar: IE and Edge */
|
-ms-overflow-style: none; /* Hide scrollbar: IE and Edge */
|
||||||
scrollbar-width: none; /* Hide scrollbar: Firefox */
|
scrollbar-width: none; /* Hide scrollbar: Firefox */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
@@ -54,10 +56,6 @@ ha-card {
|
|||||||
background-color: var(--secondary-background-color, black);
|
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 {
|
frigate-card-gallery, frigate-card-viewer, frigate-card-live {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -87,4 +85,4 @@ frigate-card-gallery {
|
|||||||
}
|
}
|
||||||
:host(:-webkit-full-screen) ha-card {
|
:host(:-webkit-full-screen) ha-card {
|
||||||
@include fullscreen-ha-card;
|
@include fullscreen-ha-card;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10%;
|
padding: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message a {
|
||||||
|
color: var(--primary-text-color, white);
|
||||||
|
}
|
||||||
|
|||||||
+33
-17
@@ -98,15 +98,24 @@ export const frigateCardConfigSchema = z.object({
|
|||||||
nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
|
nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
dimensions: z.object({
|
dimensions: z
|
||||||
aspect_ratio_mode: z.enum(['dynamic', 'static', 'unconstrained']).default('dynamic'),
|
.object({
|
||||||
aspect_ratio:
|
aspect_ratio_mode: z
|
||||||
z.number().array().length(2).or(
|
.enum(['dynamic', 'static', 'unconstrained'])
|
||||||
z.string()
|
.default('dynamic'),
|
||||||
.regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/)
|
aspect_ratio: z
|
||||||
.transform((input) => input.split(/[:\/]/).map((d) => Number(d)))
|
.number()
|
||||||
).default([16, 9]),
|
.array()
|
||||||
}).optional(),
|
.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.
|
// Stock lovelace card config.
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
@@ -136,6 +145,14 @@ export interface BrowseMediaQueryParameters {
|
|||||||
after?: number;
|
after?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BrowseMediaNeighbors {
|
||||||
|
previous: BrowseMediaSource | null;
|
||||||
|
previousIndex: number | null;
|
||||||
|
|
||||||
|
next: BrowseMediaSource | null;
|
||||||
|
nextIndex: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MediaLoadInfo {
|
export interface MediaLoadInfo {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@@ -182,15 +199,14 @@ export const resolvedMediaSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type ResolvedMedia = z.infer<typeof resolvedMediaSchema>;
|
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({
|
export const signedPathSchema = z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
});
|
});
|
||||||
export type SignedPath = z.infer<typeof signedPathSchema>;
|
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