Add birdseye support.

This commit is contained in:
Dermot Duffy
2021-10-10 20:21:21 -07:00
parent da2508de21
commit bb9c27e34c
7 changed files with 101 additions and 93 deletions
+5 -5
View File
@@ -63,19 +63,19 @@ lovelace:
## Options ## Options
### Required ### Basic
| Option | Default | Description | | Option | Default | Description |
| ------------- | - | --------------------------------------------- | | ------------- | - | --------------------------------------------- |
| `camera_entity` | | The Frigate camera entity to use in the live camera view.| | `camera_entity` | | The optional Frigate camera entity to use in the `frigate` live provider view. Also used to automatically detect the `frigate_camera_name`.|
| `frigate_camera_name` | Autodetected from `camera_entity` if that is specified. | The Frigate camera name to use when communicating with the Frigate server, e.g. for viewing clips/snapshots or the JSMPEG live view. To view the birdseye view set this to `birdseye` and use the `frigate-jsmpeg` live provider.|
| `view_default` | `live` | The view to show by default. See [views](#views) below.|
### Optional ### Optional
| Option | Default | Description | | Option | Default | Description |
| ------------- | --------------------------------------------- | - | | ------------- | --------------------------------------------- | - |
| `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.|
| `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).|
| `view_timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.| | `view_timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
| `frigate_url` | | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. | | `frigate_url` | | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. |
@@ -87,7 +87,7 @@ lovelace:
|Live Provider|Latency|Frame Rate|Installation|Description| |Live Provider|Latency|Frame Rate|Installation|Description|
| -- | -- | -- | -- | -- | | -- | -- | -- | -- | -- |
|`frigate`|High|High|Builtin|Use the built-in Home Assistant camera stream from Frigate (RTMP). The camera doesn't even need to be a Frigate camera! | |`frigate`|High|High|Builtin|Use the built-in Home Assistant camera stream from Frigate (RTMP). The camera doesn't even need to be a Frigate camera! |
|`frigate-jsmpeg`|Lower|Low|Builtin|Stream the JSMPEG stream from Frigate (proxied via the Frigate integration). See [note below on the required integration version](#jsmpeg-troubleshooting) for this live provider to function.| |`frigate-jsmpeg`|Lower|Low|Builtin|Stream the JSMPEG stream from Frigate (proxied via the Frigate integration). See [note below on the required integration version](#jsmpeg-troubleshooting) for this live provider to function. This is the only live provider that can view the Frigate `birdseye` view.|
|`webrtc`|Lowest|High|Separate installation required|Uses [WebRTC](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc).| |`webrtc`|Lowest|High|Separate installation required|Uses [WebRTC](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc).|
### Appearance ### Appearance
+6 -6
View File
@@ -19,17 +19,17 @@
"custom-card-helpers": "^1.8.0", "custom-card-helpers": "^1.8.0",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"home-assistant-js-websocket": "^5.11.1", "home-assistant-js-websocket": "^5.11.1",
"lit": "^2.0.0", "lit": "^2.0.2",
"screenfull": "^5.1.0", "screenfull": "^5.1.0",
"zod": "^3.9.5" "zod": "^3.9.8"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.15.5", "@babel/core": "^7.15.8",
"@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.15.4", "@babel/plugin-proposal-decorators": "^7.15.8",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@typescript-eslint/eslint-plugin": "^4.32.0", "@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.32.0", "@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
+48 -41
View File
@@ -33,7 +33,6 @@ import { CARD_VERSION } from './const';
import { FrigateCardMenu, MENU_HEIGHT } from './components/menu'; import { FrigateCardMenu, MENU_HEIGHT } from './components/menu';
import { View } from './view'; import { View } from './view';
import { import {
getParseErrorKeys,
homeAssistantWSRequest, homeAssistantWSRequest,
shouldUpdateBasedOnHass, shouldUpdateBasedOnHass,
} from './common'; } from './common';
@@ -221,30 +220,32 @@ export class FrigateCard extends LitElement {
return this.config.frigate_camera_name; return this.config.frigate_camera_name;
} }
// Option 2: Find entity unique_id in registry. if (this.config.camera_entity) {
const request = { // Option 2: Find entity unique_id in registry.
type: 'config/entity_registry/get', const request = {
entity_id: this.config.camera_entity, type: 'config/entity_registry/get',
}; entity_id: this.config.camera_entity,
try { };
const entityResult = await homeAssistantWSRequest<Entity>( try {
this._hass, const entityResult = await homeAssistantWSRequest<Entity>(
entitySchema, this._hass,
request, entitySchema,
); request,
if (entityResult && entityResult.platform == 'frigate') { );
const match = entityResult.unique_id.match(/:camera:(?<camera>[^:]+)$/); if (entityResult && entityResult.platform == 'frigate') {
if (match && match.groups) { const match = entityResult.unique_id.match(/:camera:(?<camera>[^:]+)$/);
return match.groups['camera']; if (match && match.groups) {
return match.groups['camera'];
}
} }
} catch (e: any) {
// Pass.
} }
} catch (e: any) {
// Pass.
}
// Option 3: Guess from the entity_id. // Option 3: Guess from the entity_id.
if (this.config.camera_entity.includes('.')) { if (this.config.camera_entity.includes('.')) {
return this.config.camera_entity.split('.', 2)[1]; return this.config.camera_entity.split('.', 2)[1];
}
} }
return null; return null;
@@ -287,11 +288,13 @@ export class FrigateCard extends LitElement {
getLovelace().setEditMode(true); getLovelace().setEditMode(true);
} }
this._frigateCameraName = null;
this.config = config; this.config = config;
this._entitiesToMonitor = [
...(this.config.update_entities || []), this._entitiesToMonitor = this.config.update_entities || [];
this.config.camera_entity, if (this.config.camera_entity) {
]; this._entitiesToMonitor.push(this.config.camera_entity);
}
this._changeView(); this._changeView();
} }
@@ -636,21 +639,25 @@ export class FrigateCard extends LitElement {
</frigate-card-live> </frigate-card-live>
` `
: ``} : ``}
<frigate-card-elements ${this.config.elements
.hass=${this._hass} ? html`
.elements=${this.config.elements} <frigate-card-elements
@frigate-card:message=${this._messageHandler} .hass=${this._hass}
@frigate-card:menu-add=${(e) => { .elements=${this.config.elements}
this._menu.addButton(e.detail); @frigate-card:message=${this._messageHandler}
}} @frigate-card:menu-add=${(e) => {
@frigate-card:menu-remove=${(e) => { this._menu.addButton(e.detail);
this._menu.removeButton(e.detail); }}
}} @frigate-card:menu-remove=${(e) => {
@frigate-card:state-request=${(e) => { this._menu.removeButton(e.detail);
e.view = this._view; }}
}} @frigate-card:state-request=${(e) => {
> e.view = this._view;
</frigate-card-elements> }}
>
</frigate-card-elements>
`
: ``}
</div> </div>
`; `;
} }
+2 -2
View File
@@ -68,10 +68,10 @@ export class FrigateCardViewerFrigate extends LitElement {
protected hass!: HomeAssistant & ExtendedHomeAssistant; protected hass!: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false }) @property({ attribute: false })
protected cameraEntity!: string; protected cameraEntity?: string;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!(this.cameraEntity in this.hass.states)) { if (!this.cameraEntity || !(this.cameraEntity in this.hass.states)) {
return dispatchMessageEvent( return dispatchMessageEvent(
this, this,
localize('error.no_live_camera'), localize('error.no_live_camera'),
+32 -32
View File
@@ -9,10 +9,10 @@ import type { FrigateCardConfig } from './types';
import frigate_card_editor_style from './scss/editor.scss'; import frigate_card_editor_style from './scss/editor.scss';
const options = { const options = {
required: { basic: {
icon: 'cog', icon: 'cog',
name: localize('editor.required'), name: localize('editor.basic'),
secondary: localize('editor.required_secondary'), secondary: localize('editor.basic_secondary'),
show: true, show: true,
}, },
optional: { optional: {
@@ -146,14 +146,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return html` return html`
<div class="card-config"> <div class="card-config">
<div class="option" @click=${this._toggleOption} .option=${'required'}> <div class="option" @click=${this._toggleOption} .option=${'basic'}>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.required.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.basic.icon}`}></ha-icon>
<div class="title">${options.required.name}</div> <div class="title">${options.basic.name}</div>
</div> </div>
<div class="secondary">${options.required.secondary}</div> <div class="secondary">${options.basic.secondary}</div>
</div> </div>
${options.required.show ${options.basic.show
? html` ? html`
<div class="values"> <div class="values">
<paper-dropdown-menu <paper-dropdown-menu
@@ -172,6 +172,30 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
})} })}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-input
label=${localize('editor.frigate_camera_name')}
.value=${this._config?.frigate_camera_name || ''}
.configValue=${'frigate_camera_name'}
@value-changed=${this._valueChanged}
></paper-input>
<paper-dropdown-menu
label=${localize('editor.default_view')}
@value-changed=${this._valueChanged}
.configValue=${'view_default'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(viewModes).indexOf(
this._config?.view_default || '',
)}
>
${Object.keys(viewModes).map((key) => {
return html`
<paper-item .label="${key}"> ${viewModes[key]} </paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
` `
: ''} : ''}
@@ -184,12 +208,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div> </div>
${options.optional.show ${options.optional.show
? html` <div class="values"> ? html` <div class="values">
<paper-input
label=${localize('editor.frigate_camera_name')}
.value=${this._config?.frigate_camera_name || ''}
.configValue=${'frigate_camera_name'}
@value-changed=${this._valueChanged}
></paper-input>
<paper-dropdown-menu <paper-dropdown-menu
.label=${localize('editor.live_provider')} .label=${localize('editor.live_provider')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@@ -208,24 +226,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
})} })}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-dropdown-menu
label=${localize('editor.default_view')}
@value-changed=${this._valueChanged}
.configValue=${'view_default'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(viewModes).indexOf(
this._config?.view_default || '',
)}
>
${Object.keys(viewModes).map((key) => {
return html`
<paper-item .label="${key}"> ${viewModes[key]} </paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-input <paper-input
label=${localize('editor.frigate_client_id')} label=${localize('editor.frigate_client_id')}
.value=${this._config?.frigate_client_id || ''} .value=${this._config?.frigate_client_id || ''}
+6 -6
View File
@@ -11,8 +11,8 @@
"no_clip": "No recent clip" "no_clip": "No recent clip"
}, },
"editor": { "editor": {
"required": "Required", "basic": "Basic",
"required_secondary": "Required options for this card to function", "basic_secondary": "Basic options for most users",
"optional": "Optional", "optional": "Optional",
"optional_secondary": "Optional configuration to tweak card behavior", "optional_secondary": "Optional configuration to tweak card behavior",
"appearance": "Appearance", "appearance": "Appearance",
@@ -21,8 +21,8 @@
"webrtc_secondary": "Optional WebRTC parameters", "webrtc_secondary": "Optional WebRTC parameters",
"advanced": "Advanced", "advanced": "Advanced",
"advanced_secondary": "Advanced options", "advanced_secondary": "Advanced options",
"camera_entity": "Camera Entity (Required)", "camera_entity": "Camera Entity (Optional)",
"frigate_camera_name": "Frigate camera name (Optional)", "frigate_camera_name": "Frigate camera name (Optional, autodetected from entity)",
"default_view": "Default view (Optional)", "default_view": "Default view (Optional)",
"frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)", "frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)",
"view_timeout": "View timeout (seconds)", "view_timeout": "View timeout (seconds)",
@@ -92,10 +92,10 @@
"unknown": "Unknown error", "unknown": "Unknown error",
"troubleshooting": "Check troubleshooting", "troubleshooting": "Check troubleshooting",
"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": "The camera_entity parameter must be set and valid for this live provider",
"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", "no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate_camera_name",
"could_not_render_elements": "Could not render picture elements", "could_not_render_elements": "Could not render picture elements",
"invalid_elements_config": "Invalid picture elements configuration" "invalid_elements_config": "Invalid picture elements configuration"
} }
+2 -1
View File
@@ -244,7 +244,8 @@ const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>; export type PictureElements = z.infer<typeof pictureElementsSchema>;
export const frigateCardConfigSchema = z.object({ export const frigateCardConfigSchema = z.object({
camera_entity: z.string(), camera_entity: z.string().optional(),
// No URL validation to allow relative URLs within HA (e.g. addons). // No URL validation to allow relative URLs within HA (e.g. addons).
frigate_url: z.string().optional(), frigate_url: z.string().optional(),
frigate_client_id: z.string().optional().default('frigate'), frigate_client_id: z.string().optional().default('frigate'),