Initial attempt to get camera name from entity unique_id.
This commit is contained in:
+1
-1
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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