diff --git a/README.md b/README.md index 0c83763d..006e4dbf 100644 --- a/README.md +++ b/README.md @@ -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).| diff --git a/src/card.ts b/src/card.ts index 8dfcea6f..9900d6e2 100644 --- a/src/card.ts +++ b/src/card.ts @@ -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 { + // 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( + this._hass, + entitySchema, + request, + ); + if (entityResult && entityResult.platform == 'frigate') { + const match = entityResult.unique_id.match(/: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() : ''}
- ${this._view.is('clips') || this._view.is('snapshots') - ? html` - ` - : ``} - ${this._view.is('clip') || this._view.is('snapshot') - ? html` - ` - : ``} - ${this._view.is('live') - ? html` - ` - : ``} + ${until(this._render(), renderProgressIndicator())}
${this.config.menu_mode != 'above' ? this._renderMenu() : ''} `; } + protected async _render(): Promise { + 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` + ` + : ``} + ${this._view.is('clip') || this._view.is('snapshot') + ? html` + ` + : ``} + ${this._view.is('live') + ? html` + ` + : ``} + `; + } + // Show a warning card. private _showWarning(warning: string): TemplateResult { return html` ${warning} `; diff --git a/src/components/gallery.ts b/src/components/gallery.ts index eb4a4ab0..01254b44 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -91,7 +91,6 @@ export class FrigateCardGallery extends LitElement { width: `calc(${100 / this._columns}% - 1.2px)`, }; - console.info(this.clientWidth); return html`