Initial attempt to get camera name from entity unique_id.

This commit is contained in:
Dermot Duffy
2021-09-26 20:36:54 -07:00
parent 70ffc0364d
commit 423bd9fd2b
7 changed files with 155 additions and 79 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ const plugins = [
exclude: 'node_modules/**', exclude: 'node_modules/**',
}), }),
dev && serve(serveopts), dev && serve(serveopts),
!dev && terser(), //!dev && terser(),
]; ];
export default [ export default [
+111 -53
View File
@@ -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> `;
-1
View File
@@ -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)}">
+2 -1
View File
@@ -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"
} }
} }
+2 -4
View File
@@ -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;
+4
View File
@@ -6,3 +6,7 @@
box-sizing: border-box; box-sizing: border-box;
padding: 10%; 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'), 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>;