From b1ab849981fbc638861f404a7aa422cea1abe5c8 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 28 Aug 2021 20:56:06 -0700 Subject: [PATCH 1/3] Use media_browser websockets. --- src/editor.ts | 25 ++- src/frigate-hass-card.scss | 14 +- src/frigate-hass-card.ts | 381 ++++++++++++++++++++++--------------- src/types.ts | 68 ++++--- 4 files changed, 299 insertions(+), 189 deletions(-) diff --git a/src/editor.ts b/src/editor.ts index 5c617388..d7a6b410 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -145,12 +145,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor })} - ` : ''} @@ -221,6 +215,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor })} + + !v.startsWith('_')); + } + // Set the object configuration. public setConfig(inputConfig: FrigateCardConfig): void { if (!inputConfig) { @@ -301,8 +310,7 @@ export class FrigateCard extends LitElement { const parseResult = frigateCardConfigSchema.safeParse(inputConfig); if (!parseResult.success) { - const errors = parseResult.error.format(); - const keys = Object.keys(errors).filter((v) => !v.startsWith('_')); + const keys = this._getParseErrorKeys(parseResult.error); throw new Error(localize('common.invalid_configuration') + ': ' + keys.join(', ')); } const config = parseResult.data; @@ -339,19 +347,19 @@ export class FrigateCard extends LitElement { protected _changeView( view?: FrigateCardView | undefined, - event?: FrigateEvent | undefined, + mediaSource?: BrowseMediaSource | undefined, ): void { if (view !== undefined) { this._viewMode = view; } else { this._viewMode = this.config.view_default; if (['clip', 'snapshot'].includes(this.config.view_default)) { - this._requestedEvent = null; + this._requestedMediaSource = null; } } - this._eventBeingShown = null; - if (event !== undefined) { - this._requestedEvent = event; + this._mediaBeingShown = null; + if (mediaSource !== undefined) { + this._requestedMediaSource = mediaSource; } } @@ -379,50 +387,72 @@ export class FrigateCard extends LitElement { return true; } - // Get FrigateEvents from the Frigate server API. - protected async _getEvents({ - has_clip = false, - has_snapshot = false, - limit = 100, - }: GetEventsParameters): Promise { - let url = `${this.config.frigate_url}/api/events?camera=${this.config.frigate_camera_name}`; - if (has_clip) { - url += `&has_clip=1`; - } - if (has_snapshot) { - url += `&has_snapshot=1`; - } - if (limit > 0) { - url += `&limit=${limit}`; + // Make a websocket request to Home Assistant. + protected async _makeWSRequest( + schema: ZodSchema, + request: MessageBase, + ): Promise { + if (!this._hass) { + return null; } - if (this.config.label) { - url += `&label=${this.config.label}`; - } - if (this.config.zone) { - url += `&zone=${this.config.zone}`; - } + const response = await this._hass.callWS(request); - const response = await fetch(url); - if (response.ok) { - let raw_json; - try { - raw_json = await response.json(); - } catch (e: any) { - console.warn(e); - throw new Error(`Could not JSON decode Frigate API response: ${e}`); - } - try { - return frigateGetEventsResponseSchema.parse(raw_json); - } catch (e: any) { - console.warn(e); - throw new Error(`Frigate events were malformed: ${e}`); - } - } else { - const error_message = `Frigate API request failed with status: ${response.status}`; + if (!response) { + const error_message = `Received empty response from Home Assistant for request ${JSON.stringify( + request, + )}`; console.warn(error_message); throw new Error(error_message); } + const parseResult = schema.safeParse(response); + if (!parseResult.success) { + const keys = this._getParseErrorKeys(parseResult.error); + const error_message = + `Received invalid response from Home Assistant for request ${JSON.stringify( + request, + )}, ` + `invalid keys were '${keys}'`; + console.warn(error_message); + throw new Error(error_message); + } + return parseResult.data; + } + + // Browse Frigate media. + protected async _browseMedia( + want_clips?: boolean, + before?: number, + after?: number, + ): Promise { + // Defined in: + // https://github.com/blakeblackshear/frigate-hass-integration/blob/master/custom_components/frigate/media_source.py + const request = { + type: 'media_source/browse_media', + media_content_id: [ + 'media-source://frigate', + this.config.frigate_client_id, + 'event-search', + want_clips ? 'clips' : 'snapshots', + '', // Name/Title to render (not necessary here) + after ? String(after) : '', + before ? String(before) : '', + this.config.frigate_camera_name, + this.config.label, + this.config.zone, + ].join('/'), + }; + return this._makeWSRequest(browseMediaSourceSchema, request); + } + + // Resolve Frigate media identifier to a real URL. + protected async _resolveMedia( + mediaSource: BrowseMediaSource, + ): Promise { + const request = { + type: 'media_source/resolve_media', + media_content_id: mediaSource.media_content_id, + }; + return this._makeWSRequest(resolvedMediaSchema, request); } // Render an attention grabbing icon. @@ -442,41 +472,23 @@ export class FrigateCard extends LitElement { protected _renderError(error: string): TemplateResult { return this._renderAttentionIcon( 'mdi:alert-circle', - html`${error}. See troubleshooting.`, + html`${ + error ? `${error} .` : `Unknown error` + }Check troubleshooting.`, ); } - // Generate a human-readable title from an event. - // MediaBrowser title: 2021-08-12 19:20:14 [10s, Person 76%] - protected _getEventTitle(event: FrigateEvent): string { - const date = dayjs.unix(event.end_time).tz('UTC').local(); - - const iso_datetime = date.format('YYYY-MM-DD HH:mm:ss'); - const duration = Math.trunc( - event.end_time > event.start_time ? event.end_time - event.start_time : 0, - ); - const score = Math.trunc(event.top_score * 100); - - // Capitalize the label. - const label = event.label.charAt(0).toUpperCase() + event.label.slice(1); - - return `${iso_datetime} [${duration}s, ${label} ${score}%]`; - } - // Render Frigate events into a card gallery. protected async _renderEvents(): Promise { const want_clips = this._viewMode == 'clips'; - let events; + let media; try { - events = await this._getEvents({ - has_clip: want_clips, - has_snapshot: !want_clips, - }); + media = await this._browseMedia(want_clips); } catch (e: any) { return this._renderError(e.message); } - - if (!events.length) { + const firstMediaItem = this._getFirstTrueMediaItem(media); + if (!firstMediaItem) { return this._renderAttentionIcon( want_clips ? 'mdi:filmstrip-off' : 'mdi:camera-off', want_clips ? 'No clips' : 'No snapshots', @@ -484,20 +496,22 @@ export class FrigateCard extends LitElement { } return html`
    - ${events.map( - (event) => html`
  • -
    - { - this._changeView(want_clips ? 'clip' : 'snapshot', event); - }} - /> -
    -
  • `, + ${media.children.map((mediaSource) => + mediaSource.can_expand + ? '' + : html`
  • +
    + { + this._changeView(want_clips ? 'clip' : 'snapshot', mediaSource); + }} + /> +
    +
  • `, )}
`; } @@ -584,7 +598,11 @@ export class FrigateCard extends LitElement { this._changeView(name); break; case 'frigate-ui': - window.open(this._getFrigateURLFromContext()); + const frigate_url = this._getFrigateURLFromContext(); + if (frigate_url) { + window.open(frigate_url); + break; + } break; case 'motion': if (this.config.motion_entity) { @@ -596,46 +614,76 @@ export class FrigateCard extends LitElement { } } - protected _getFrigateURLFromContext(): string { - if (this._eventBeingShown) { - return `${this.config.frigate_url}/events/${this._eventBeingShown.id}`; + // Extract the Frigate event id from the resolved media. Unfortunately, there + // is no way to attach metadata to BrowseMediaSource so this must suffice. + protected _extractEventIDFromResolvedMedia( + resolvedMedia: ResolvedMedia, + ): string | null { + // Example: /api/frigate/frigate/clips/camera-1630123639.21596-l1y9af.jpg?authSig=[large_string] + const result = resolvedMedia.url.match(/-(?[\w]+)\.(jpg|m3u8|mp4)($|\?)/i); + if (result && result.groups) { + return result.groups['id'] || null; + } + return null; + } + + protected _extractEventStartTimeFromBrowseMedia( + browseMedia: BrowseMediaSource, + ): number | null { + // Example: 2021-08-27 20:57:22 [10s, Person 76%] + const result = browseMedia.title.match(/^(?.+) \[/); + if (result && result.groups) { + const iso_datetime_str = result.groups['iso_datetime']; + if (iso_datetime_str) { + const iso_datetime = dayjs(iso_datetime_str, 'YYYY-MM-DD HH:mm:ss', true); + if (iso_datetime.isValid()) { + return iso_datetime.unix(); + } + } + } + return null; + } + + // Get the Frigate UI url. + protected _getFrigateURLFromContext(): string | null { + if (!this.config.frigate_url) { + return null; + } + if (this._mediaBeingShown) { + const eventID = this._extractEventIDFromResolvedMedia( + this._mediaBeingShown.resolvedMedia, + ); + if (eventID) { + return `${this.config.frigate_url}/events/${eventID}`; + } } return `${this.config.frigate_url}/cameras/${this.config.frigate_camera_name}`; } - protected _getClipURLFromEvent(event: FrigateEvent): string | null { - if (!event.has_clip) { - return null; - } - return `${this.config.frigate_url}/vod/event/${event.id}/index.m3u8`; - } - - protected _getSnapshotURLFromEvent(event: FrigateEvent): string | null { - if (!event.has_snapshot) { - return null; - } - return `${this.config.frigate_url}/clips/${event.camera}-${event.id}.jpg`; + // From a BrowseMediaSource item extract the first true media item (i.e. a + // clip/snapshot, not a folder). + protected _getFirstTrueMediaItem(media: BrowseMediaSource): BrowseMediaSource | null { + return media.children?.find((mediaSource) => !mediaSource.can_expand) || null; } // Render the player for a saved clip. protected async _renderClipPlayer(): Promise { - let event: FrigateEvent, events: FrigateGetEventsResponse; + let mediaSource: BrowseMediaSource; let autoplay = true; - if (this._requestedEvent) { - event = this._requestedEvent; + if (this._requestedMediaSource) { + mediaSource = this._requestedMediaSource; } else { + let media; try { - events = await this._getEvents({ - has_clip: true, - limit: 1, - }); + media = await this._browseMedia(true); } catch (e: any) { return this._renderError(e.message); } - if (!events.length) { - return this._renderAttentionIcon('mdi:camera-off', 'No recent clip'); + const firstMediaItem = this._getFirstTrueMediaItem(media); + if (!firstMediaItem) { + return this._renderAttentionIcon('mdi:filmstrip-off', 'No recent clip'); } - event = events[0]; + mediaSource = firstMediaItem; // In this block, no clip has been manually selected, so this is loading // the most recent clip on card load. In this mode, autoplay of the clip @@ -645,19 +693,21 @@ export class FrigateCard extends LitElement { autoplay = this.config.autoplay_clip; } - const clipURL = this._getClipURLFromEvent(event); - if (!clipURL) { - // Frigate has returned an event without a clip, even though it was - // specifically asked only for events with clips. - return this._renderAttentionIcon('mdi:camera-off', 'No recent clip'); + const resolvedMedia = await this._resolveMedia(mediaSource); + if (!resolvedMedia) { + // Home Assistant could not resolve media item. + return this._renderError('Could not resolve clip URL'); } - this._eventBeingShown = event; + this._mediaBeingShown = { + browseMedia: mediaSource, + resolvedMedia: resolvedMedia, + }; return html` { + const startTime = this._extractEventStartTimeFromBrowseMedia(snapshot); + if (startTime) { + try { + // Fetch clips within the same second (same camera/zone/label, etc). + const clipsAtSameTime = await this._browseMedia(true, startTime + 1, startTime); + if (clipsAtSameTime) { + return this._getFirstTrueMediaItem(clipsAtSameTime); + } + } catch (e: any) { + // Pass. This is best effort. + } + } + return null; + } + // Render a snapshot. protected async _renderSnapshotViewer(): Promise { - let event: FrigateEvent, events: FrigateGetEventsResponse; - if (this._requestedEvent) { - event = this._requestedEvent; + let mediaSource: BrowseMediaSource; + if (this._requestedMediaSource) { + mediaSource = this._requestedMediaSource; } else { + let media; try { - events = await this._getEvents({ - has_snapshot: true, - limit: 1, - }); + media = await this._browseMedia(false); } catch (e: any) { return this._renderError(e.message); } - if (!events.length) { - return this._renderAttentionIcon('mdi:filmstrip-off', 'No recent snapshots'); + const firstMediaItem = this._getFirstTrueMediaItem(media); + if (!firstMediaItem) { + return this._renderAttentionIcon('mdi:camera-off', 'No recent snapshots'); } - event = events[0]; + mediaSource = firstMediaItem; } - const snapshotURL = this._getSnapshotURLFromEvent(event); - if (!snapshotURL) { - // Frigate has returned an event without a snapshot, even though it was - // specifically asked only for events with snapshots. - return this._renderAttentionIcon('mdi:filmstrip-off', 'No recent snapshots'); + const resolvedMedia = await this._resolveMedia(mediaSource); + if (!resolvedMedia) { + // Home Assistant could not resolve media item. + return this._renderError('Could not resolve snapshot URL'); } - this._eventBeingShown = event; + this._mediaBeingShown = { + browseMedia: mediaSource, + resolvedMedia: resolvedMedia, + }; return html` { - if (event.has_clip) { - this._changeView('clip', event); - } + // Get clips potentially related to this snapshot. + this._findRelatedClips(mediaSource).then((relatedClip) => { + if (relatedClip) { + this._changeView('clip', relatedClip); + } + }) }} />`; } diff --git a/src/types.ts b/src/types.ts index a2fe31dd..980386dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,7 +36,9 @@ export type FrigateMenuMode = typeof FRIGATE_MENU_MODES[number]; export const frigateCardConfigSchema = z.object({ camera_entity: z.string(), motion_entity: z.string().optional(), - frigate_url: z.string().url(), + // No URL validation to allow relative URLs within HA (e.g. addons). + frigate_url: z.string().optional(), + frigate_client_id: z.string().optional().default("frigate"), frigate_camera_name: z.string().optional(), view_default: z.enum(FRIGATE_CARD_VIEWS).optional().default('live'), @@ -64,36 +66,54 @@ export const frigateCardConfigSchema = z.object({ }); export type FrigateCardConfig = z.infer; -export interface GetEventsParameters { - has_clip?: boolean; - has_snapshot?: boolean; - limit?: number; -} - export interface ControlVideosParameters { stop: boolean; control_live?: boolean; control_clip?: boolean; } +export interface MediaBeingShown { + browseMedia: BrowseMediaSource; + resolvedMedia: ResolvedMedia; +} + /** - * Frigate API types. + * Media Browser API types. */ -export const frigateEventSchema = z.object({ - camera: z.string(), - end_time: z.number(), - false_positive: z.boolean(), - has_clip: z.boolean(), - has_snapshot: z.boolean(), - id: z.string(), - label: z.string(), - start_time: z.number(), - thumbnail: z.string(), - top_score: z.number(), - zones: z.string().array(), -}); -export type FrigateEvent = z.infer; +// Recursive type, cannot use type interference: +// See: https://github.com/colinhacks/zod#recursive-types +// +// Server side data-type defined here: https://github.com/home-assistant/core/blob/dev/homeassistant/components/media_player/__init__.py +export interface BrowseMediaSource { + title: string; + media_class: string; + media_content_type: string; + media_content_id: string; + can_play: boolean; + can_expand: boolean; + children_media_class: string | null; + thumbnail: string | null + children?: BrowseMediaSource[] | null; +} -export const frigateGetEventsResponseSchema = z.array(frigateEventSchema); -export type FrigateGetEventsResponse = z.infer; +export const browseMediaSourceSchema: z.ZodSchema = z.lazy(() => + z.object({ + title: z.string(), + media_class: z.string(), + media_content_type: z.string(), + media_content_id: z.string(), + can_play: z.boolean(), + can_expand: z.boolean(), + children_media_class: z.string().nullable(), + thumbnail: z.string().nullable(), + children: z.array(browseMediaSourceSchema).nullable().optional(), + }) +); + +// Server side data-type defined here: https://github.com/home-assistant/core/blob/dev/homeassistant/components/media_source/models.py +export const resolvedMediaSchema = z.object({ + url: z.string(), + mime_type: z.string(), +}); +export type ResolvedMedia = z.infer; \ No newline at end of file From e0fe90835742415b95731ca483d73ce96eefddf4 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 28 Aug 2021 23:35:47 -0700 Subject: [PATCH 2/3] Allow dynamic menu contents. --- src/frigate-hass-card-menu.scss | 5 + src/frigate-hass-card.ts | 195 ++++++++++++++------------------ src/types.ts | 6 + 3 files changed, 93 insertions(+), 113 deletions(-) diff --git a/src/frigate-hass-card-menu.scss b/src/frigate-hass-card-menu.scss index a2a46c31..4a1607da 100644 --- a/src/frigate-hass-card-menu.scss +++ b/src/frigate-hass-card-menu.scss @@ -43,3 +43,8 @@ ha-icon-button.button { /* Buttons can always be clicked */ pointer-events: auto; } + +ha-icon-button.emphasized-button { + @extend ha-icon-button, .button; + color: var(--primary-color, white); +} \ No newline at end of file diff --git a/src/frigate-hass-card.ts b/src/frigate-hass-card.ts index 09ddd2ec..78de4f83 100644 --- a/src/frigate-hass-card.ts +++ b/src/frigate-hass-card.ts @@ -1,4 +1,3 @@ -// TODO Don't show button if no Frigate url. /* eslint-disable @typescript-eslint/no-explicit-any */ import { LitElement, @@ -10,6 +9,7 @@ import { PropertyValues, state, unsafeCSS, + query, } from 'lit-element'; import { NodePart } from 'lit-html'; @@ -30,6 +30,7 @@ import frigate_card_menu_style from './frigate-hass-card-menu.scss'; import { browseMediaSourceSchema, frigateCardConfigSchema, + MenuButton, resolvedMediaSchema, } from './types'; import type { @@ -111,59 +112,47 @@ export class FrigateCardMenu extends LitElement { @property({ attribute: false }) protected expand = false; - @property({ attribute: false }) - protected motionEntity: string | null = null; - - @property({ attribute: false }) - public hass: HomeAssistant | null = null; - @property({ attribute: false }) protected actionCallback: FrigateCardMenuCallback | null = null; - protected shouldUpdate(changedProps: PropertyValues): boolean { - const oldHass = changedProps.get('hass') as HomeAssistant | undefined; - if (oldHass) { - return shouldUpdateBasedOnHass(this.hass, oldHass, [this.motionEntity]); - } - return true; - } - - // Render the Frigate menu button. - protected _renderFrigateButton(): TemplateResult { - return html` { - if (this.menuMode == 'hidden') { - this.expand = !this.expand; - } else { - this._callAction('default'); - } - }} - >`; - } + @property({ attribute: false }) + public buttons: Map = new Map(); // Call the callback. protected _callAction(name: string): void { + if (name == 'frigate' && this.menuMode == 'hidden') { + this.expand = !this.expand; + return; + } + if (this.actionCallback) { this.actionCallback(name); } } + // Render a menu button. + protected _renderButton(name: string, button: MenuButton): TemplateResult { + return html` this._callAction(name)} + >`; + } + + // Render the Frigate menu button. + protected _renderFrigateButton(name: string, button: MenuButton): TemplateResult { + const icon = + this.menuMode != 'hidden' || this.expand + ? 'mdi:alpha-f-box' + : 'mdi:alpha-f-box-outline'; + + return this._renderButton(name, Object.assign({}, button, { icon: icon })); + } + // Render the menu. protected render(): TemplateResult | void | ((part: NodePart) => Promise) { - let motionIcon: string | null = null; - if (this.motionEntity && this.hass) { - motionIcon = - this.hass.states[this.motionEntity]?.state == 'on' - ? 'mdi:motion-sensor' - : 'mdi:walk'; - } - let menuClass = 'frigate-card-menu-full'; if (['hidden', 'overlay'].includes(this.menuMode)) { if (this.menuMode == 'overlay' || this.expand) { @@ -175,63 +164,15 @@ export class FrigateCardMenu extends LitElement { return html`
- ${this._renderFrigateButton()} - ${this.menuMode != 'hidden' || this.expand - ? html` - { - this.expand = false; - this._callAction('live'); - }} - > - { - this.expand = false; - this._callAction('clips'); - }} - > - { - this.expand = false; - this._callAction('snapshots'); - }} - > - { - this.expand = false; - this._callAction('frigate-ui'); - }} - > - ${!motionIcon - ? html`` - : html` { - this.expand = false; - this._callAction('motion'); - }} - >`} - ` - : ``} + ${Array.from(this.buttons.keys()).map((name) => { + const button = this.buttons.get(name); + if (button) { + return name === 'frigate' + ? this._renderFrigateButton(name, button) + : this._renderButton(name, button); + } + return html``; + })}
`; } @@ -259,16 +200,7 @@ export class FrigateCard extends LitElement { this._webrtcElement.hass = hass; } this._hass = hass; - - // Manually set hass in the menu. This is to allow the menu to update, - // without necessarily re-rendering the entire card (re-rendering interrupts - // clip playing). - const menu = this.shadowRoot?.getElementById( - FrigateCardMenu.FRIGATE_CARD_MENU_ID, - ) as FrigateCardMenu; - if (menu) { - menu.hass = hass; - } + this._updateMenu(); } @property({ attribute: false }) @@ -297,6 +229,44 @@ export class FrigateCard extends LitElement { // Whether or not there is an active clip being played. protected _clipPlaying = false; + @query(FrigateCardMenu.FRIGATE_CARD_MENU_ID) + _menu!: FrigateCardMenu | null; + + protected _updateMenu(): void { + // Manually set hass in the menu. This is to allow the menu to update, + // without necessarily re-rendering the entire card (re-rendering interrupts + // clip playing). + if (!this._menu || !this._hass) { + return; + } + + this._menu.buttons = this._getMenuButtons(); + } + + protected _getMenuButtons(): Map { + const buttons: Map = new Map(); + + buttons.set('frigate', { description: 'Frigate Menu' }); + buttons.set('live', { icon: 'mdi:cctv', description: 'View Live' }); + buttons.set('clips', { icon: 'mdi:filmstrip', description: 'View Clips' }); + buttons.set('snapshots', { icon: 'mdi:camera', description: 'View Snapshots' }); + if (this.config.frigate_url) { + buttons.set('frigate_ui', { icon: 'mdi:web', description: 'View Frigate UI' }); + } + + if (this._hass && this.config.motion_entity) { + const on = this._hass.states[this.config.motion_entity]?.state == 'on'; + const motionIcon = on ? 'mdi:motion-sensor' : 'mdi:walk'; + + buttons.set('motion', { + icon: motionIcon, + description: 'View Motion Sensor', + emphasize: on, + }); + } + return buttons; + } + protected _getParseErrorKeys(error: z.ZodError): string[] { const errors = error.format(); return Object.keys(errors).filter((v) => !v.startsWith('_')); @@ -579,7 +549,7 @@ export class FrigateCard extends LitElement { protected _menuActionHandler(name: string): void { switch (name) { - case 'default': + case 'frigate': this._controlVideos({ stop: true, control_clip: true }); this._controlVideos({ stop: true, control_live: true }); this._changeView(); @@ -597,7 +567,7 @@ export class FrigateCard extends LitElement { this._controlVideos({ stop: true, control_clip: true, control_live: true }); this._changeView(name); break; - case 'frigate-ui': + case 'frigate_ui': const frigate_url = this._getFrigateURLFromContext(); if (frigate_url) { window.open(frigate_url); @@ -806,7 +776,7 @@ export class FrigateCard extends LitElement { if (relatedClip) { this._changeView('clip', relatedClip); } - }) + }); }} />`; } @@ -848,10 +818,9 @@ export class FrigateCard extends LitElement { return html` `; } diff --git a/src/types.ts b/src/types.ts index 980386dc..bf9b1227 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,6 +77,12 @@ export interface MediaBeingShown { resolvedMedia: ResolvedMedia; } +export interface MenuButton { + icon?: string; + description: string; + emphasize?: boolean; +} + /** * Media Browser API types. */ From a67eebd6cde2cef4a47bad835c13a72dce0a9bf6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 28 Aug 2021 23:49:46 -0700 Subject: [PATCH 3/3] Update README with updated options. --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8a7ab21d..237624d7 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ lovelace: | Option | Default | Description | | ------------- | - | --------------------------------------------- | | `camera_entity` | | The Frigate camera entity to use in the live camera view.| -| `frigate_url` | | The URL of the frigate server. Must be manually specified, as the URL from the underlying device is not available to Lovelace cards.| ### Optional @@ -64,7 +63,9 @@ lovelace: | `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. | | `view_default` | `live` | The view to show by default. See [views](#views) below.| | `menu_mode` | `hidden` | The menu mode to show by default. See [menu modes](#menu-modes) 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).| | `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. | | `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.| ### Advanced @@ -140,8 +141,8 @@ do). ### Getting from a snapshot to a clip -Clicking on a snapshot will take the user to the clip associated with the -snapshot (if any). +Clicking on a snapshot will take the user to a clip that was taken at the ~same +time as the snapshot (if any). ### Getting event details @@ -200,18 +201,22 @@ This card supports full editing via the Lovelace card editor. Additional arbitra ## Troubleshooting -### Failed to fetch / Cannot load clips or snapshots +### Failed to fetch + +**Note:** This error should no longer be possible >= v0.1.5 . `Failed to fetch` is a generic error indicating your browser (and this card) could not communicate with the Frigate server specified in the card configuration. This could be for any number of reasons (e.g. incorrect URL, -incorrect port, broken DNS, etc). +incorrect port, broken DNS, etc). If the 'globe' icon in the menu bar of the card also doesn't open the Frigate UI, the address entered is probably incorrect/inaccessible. #### Mixed content +**Note:** This error should no longer be possible >= v0.1.5 . + If you are accessing your Home Assistant instance over `https`, you will likely receive this error unless you have configured the card to also communicate with Frigate via `https` (e.g. via a reverse proxy). This is because the browser is