Add event starring from the card.

This commit is contained in:
Dermot Duffy
2022-06-28 20:19:32 -07:00
parent 7f430298da
commit 0f04e9383a
15 changed files with 152 additions and 21 deletions
+7
View File
@@ -1,3 +1,10 @@
# Review comments generated by i18n-ally. Please commit this file.
reviews:
error.failed_retain:
locales:
pt-BR:
translation_candidate:
source: en
text: Não foi possível reter o evento
time: '2022-06-28T05:18:48.431Z'
+7
View File
@@ -1042,6 +1042,13 @@ A dashboard with the card can be cast onto a suitable device (such as the Nest H
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/release-4.0.0/images/card-on-nest-hub.jpg" alt="Card on Nest Hub" width="400px">
### Event starring
Retain interesting Frigate events forever:
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/release-4.0.0/images/star.gif" alt="Retain events" width="400px">
## Examples
### Illustrative Expanded Configuration Reference
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

+9 -1
View File
@@ -86,6 +86,7 @@ export class FrigateCardGallery extends LitElement {
.hass=${this.hass}
.view=${this.view}
.galleryConfig=${this.galleryConfig}
.cameras=${this.cameras}
>
</frigate-card-gallery-core>
`;
@@ -116,6 +117,9 @@ export class FrigateCardGalleryCore extends LitElement {
@property({ attribute: false })
public galleryConfig?: GalleryConfig;
@property({ attribute: false })
public cameras?: Map<string, CameraConfig>;
protected _resizeObserver: ResizeObserver;
constructor() {
@@ -207,11 +211,13 @@ export class FrigateCardGalleryCore extends LitElement {
!this.view ||
!this.view.target ||
!this.view.target.children ||
!(this.view.is('clips') || this.view.is('snapshots'))
!(this.view.is('clips') || this.view.is('snapshots')) ||
!this.cameras
) {
return html``;
}
const cameraConfig = this.cameras.get(this.view.camera);
return html`
${this._showBackArrow()
? html` <ha-card
@@ -253,6 +259,8 @@ export class FrigateCardGalleryCore extends LitElement {
.view=${this.view}
.target=${this.view?.target ?? null}
.childIndex=${index}
.hass=${this.hass}
.clientID=${cameraConfig?.frigate.client_id}
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
?controls=${!!this.galleryConfig?.controls.thumbnails.show_controls}
@click=${(ev: Event) => {
+1
View File
@@ -146,6 +146,7 @@ export class FrigateCardLive extends LitElement {
.view=${this.view}
.config=${config.controls.thumbnails}
.browseMediaParams=${browseMediaParams ?? undefined}
.cameras=${this.cameras}
?fetch=${!this._preloaded}
>
<frigate-card-live-carousel
+6
View File
@@ -11,6 +11,7 @@ import { customElement, property } from 'lit/decorators.js';
import surroundThumbnailsStyle from '../scss/surround.scss';
import {
BrowseMediaQueryParameters,
CameraConfig,
FrigateBrowseMediaSource,
FrigateCardError,
FrigateCardView,
@@ -46,6 +47,9 @@ export class FrigateCardSurround extends LitElement {
@property({ attribute: false, hasChanged: contentsChanged })
public browseMediaParams?: BrowseMediaQueryParameters | BrowseMediaQueryParameters[];
@property({ attribute: false })
public cameras?: Map<string, CameraConfig>;
/**
* Fetch thumbnail media when a target is not specified in the view (e.g. for
* the live view).
@@ -137,10 +141,12 @@ export class FrigateCardSurround extends LitElement {
${this.config && this.config.mode !== 'none'
? html` <frigate-card-thumbnail-carousel
slot=${this.config.mode}
.hass=${this.hass}
.config=${this.config}
.view=${this.view}
.target=${this.view.target}
.selected=${this.view.childIndex}
.cameras=${this.cameras}
@frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
// Send the view change from the source of the tap event, so the
+11 -1
View File
@@ -1,10 +1,11 @@
import { HomeAssistant } from 'custom-card-helpers';
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
import { CSSResultGroup, html, PropertyValues, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
import type { CameraConfig, FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
import { isTrueMedia } from '../utils/ha/browse-media';
@@ -20,6 +21,9 @@ export interface ThumbnailCarouselTap {
@customElement('frigate-card-thumbnail-carousel')
export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
@@ -28,6 +32,9 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
@property({ attribute: false, hasChanged: contentsChanged })
public target?: FrigateBrowseMediaSource | null;
@property({ attribute: false })
public cameras?: Map<string, CameraConfig>;
// Thumbnail carousels can expand (e.g. drawer-based carousels after the main
// media loads). The carousel must be re-initialized in these cases, or the
// dynamic sizing fails (and users can scroll past the end of the carousel).
@@ -195,10 +202,13 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
'slide-selected': this._selected === childIndex,
};
const cameraConfig = this.view?.camera ? this.cameras?.get(this.view.camera) : null;
return html` <frigate-card-thumbnail
.hass=${this.hass}
.view=${this.view}
.target=${parent}
.childIndex=${childIndex}
.clientID=${cameraConfig?.frigate.client_id}
?details=${this._config?.show_details}
?controls=${this._config?.show_controls}
class="${classMap(classes)}"
+49 -12
View File
@@ -1,6 +1,8 @@
import { HomeAssistant } from 'custom-card-helpers';
import { format, fromUnixTime } from 'date-fns';
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { localize } from '../localize/localize.js';
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss';
@@ -9,10 +11,11 @@ import thumbnailStyle from '../scss/thumbnail.scss';
import type {
FrigateBrowseMediaSource,
FrigateEvent,
FrigateRecording
FrigateRecording,
} from '../types.js';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import { prettifyTitle } from '../utils/basic.js';
import { errorToConsole, prettifyTitle } from '../utils/basic.js';
import { retainEvent } from '../utils/frigate.js';
import { getEventDurationString } from '../utils/ha/browse-media.js';
import { View } from '../view.js';
@@ -152,6 +155,15 @@ export class FrigateCardThumbnail extends LitElement {
@property({ attribute: true })
public event?: string;
// ================================
// Optional parameters for controls
// ================================
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false })
public clientID?: string;
/**
* Render the element.
* @returns A template to display to the user.
@@ -182,6 +194,11 @@ export class FrigateCardThumbnail extends LitElement {
return;
}
const starClasses = {
star: true,
starred: !!event?.retain_indefinitely,
};
return html` ${event
? html`<frigate-card-thumbnail-feature-event
aria-label="${label ?? ''}"
@@ -194,11 +211,31 @@ export class FrigateCardThumbnail extends LitElement {
title="${label ?? ''}"
.date=${recording ? fromUnixTime(recording.start_time) : undefined}
></frigate-card-thumbnail-feature-recording>`}
${this.controls && event?.retain_indefinitely
${this.controls && this.hass && this.clientID
? html` <ha-icon
class="favorite"
icon="mdi:star"
class="${classMap(starClasses)}"
icon=${event?.retain_indefinitely ? 'mdi:star' : 'mdi:star-outline'}
title=${localize('thumbnail.retain_indefinitely')}
@click=${(ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
if (event && this.hass && this.clientID) {
retainEvent(
this.hass,
this.clientID,
event.id,
!event.retain_indefinitely,
)
.then(() => {
if (event) {
event.retain_indefinitely = !event.retain_indefinitely;
this.requestUpdate();
}
})
.catch((e) => {
errorToConsole(e);
});
}
}}
/></ha-icon>`
: ``}
${this.details && event
@@ -255,11 +292,11 @@ export class FrigateCardThumbnail extends LitElement {
}
declare global {
interface HTMLElementTagNameMap {
"frigate-card-thumbnail": FrigateCardThumbnail
"frigate-card-thumbnail-details-recording": FrigateCardThumbnailDetailsRecording
"frigate-card-thumbnail-details-event": FrigateCardThumbnailDetailsEvent
"frigate-card-thumbnail-feature-recording": FrigateCardThumbnailFeatureRecording
"frigate-card-thumbnail-feature-event": FrigateCardThumbnailFeatureEvent
}
interface HTMLElementTagNameMap {
'frigate-card-thumbnail': FrigateCardThumbnail;
'frigate-card-thumbnail-details-recording': FrigateCardThumbnailDetailsRecording;
'frigate-card-thumbnail-details-event': FrigateCardThumbnailDetailsEvent;
'frigate-card-thumbnail-feature-recording': FrigateCardThumbnailFeatureRecording;
'frigate-card-thumbnail-feature-event': FrigateCardThumbnailFeatureEvent;
}
}
+1
View File
@@ -450,6 +450,7 @@ export class FrigateCardTimeline extends LitElement {
.hass=${this.hass}
.view=${this.view}
.config=${this.timelineConfig.controls.thumbnails}
.cameras=${this.cameras}
>
<frigate-card-timeline-core
.hass=${this.hass}
+1
View File
@@ -109,6 +109,7 @@ export class FrigateCardViewer extends LitElement {
.hass=${this.hass}
.view=${this.view}
.config=${this.viewerConfig.controls.thumbnails}
.cameras=${this.cameras}
>
<frigate-card-viewer-carousel
.hass=${this.hass}
+1
View File
@@ -315,6 +315,7 @@
"duplicate_camera_id": "Duplicate Frigate camera id for the following camera, use the 'id' parameter to uniquely identify cameras",
"empty_response": "Received empty response from Home Assistant for request",
"failed_response": "Failed to receive response from Home Assistant for request",
"failed_retain": "Could not retain event",
"failed_sign": "Could not sign Home Assistant URL",
"image_load_error": "The image could not be loaded",
"invalid_configuration": "Invalid configuration",
+1
View File
@@ -317,6 +317,7 @@
"duplicate_camera_id": "Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",
"empty_response": "Sem resposta do Home Assistant para a solicitação",
"failed_response": "Falha ao receber resposta do Home Assistant para solicitação",
"failed_retain": "",
"failed_sign": "Não foi possível assinar a URL do Home Assistant",
"image_load_error": "A imagem não pôde ser carregada",
"invalid_configuration": "Configuração inválida",
+1 -1
View File
@@ -47,7 +47,7 @@ export function localize(string: string, search = '', replace = ''): string {
translated = string.split('.').reduce((o, i) => o[i], languages['en']);
}
if (translated === undefined) {
if (!translated) {
translated = string.split('.').reduce((o, i) => o[i], languages['en']);
}
+15 -4
View File
@@ -31,16 +31,27 @@
ha-icon {
position: absolute;
border-radius: 50%;
opacity: 0.8;
opacity: 0.5;
color: var(--primary-color);
top: 3px;
cursor: pointer;
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
}
ha-icon:hover {
opacity: 1.0;
}
ha-icon.favorite {
ha-icon.star {
left: 3px;
}
ha-icon.star.starred {
color: gold;
}
ha-icon.timeline {
color: var(--primary-color);
right: 0px;
right: 3px;
}
frigate-card-thumbnail-details-event, frigate-card-thumbnail-details-recording {
+42 -2
View File
@@ -1,6 +1,8 @@
import { HomeAssistant } from 'custom-card-helpers';
import { z } from 'zod';
import { CameraConfig, ExtendedHomeAssistant } from '../types';
import { homeAssistantHTTPRequest } from './ha';
import { localize } from '../localize/localize';
import { CameraConfig, ExtendedHomeAssistant, FrigateCardError } from '../types';
import { homeAssistantHTTPRequest, homeAssistantWSRequest } from './ha';
export const FRIGATE_ICON_SVG_PATH =
'm 4.8759466,22.743573 c 0.0866,0.69274 0.811811,1.16359 0.37885,1.27183 ' +
@@ -50,6 +52,12 @@ const recordingSegmentSchema = z.object({
const recordingSegmentsSchema = recordingSegmentSchema.array();
export type RecordingSegments = z.infer<typeof recordingSegmentsSchema>;
const retainResultSchema = z.object({
success: z.boolean(),
message: z.string(),
});
export type RetainResult = z.infer<typeof retainResultSchema>;
/**
* Get the recordings summary. May throw.
* @param hass The Home Assistant object.
@@ -96,6 +104,38 @@ export const getRecordingSegments = async (
);
};
/**
* Request that Frigate retain an event. May throw.
* @param hass The HomeAssistant object.
* @param client_id The Frigate client_id.
* @param eventID The event ID to retain.
* @param retain `true` to retain or `false` to unretain.
*/
export async function retainEvent(
hass: HomeAssistant,
client_id: string,
eventID: string,
retain: boolean,
): Promise<void> {
const retainRequest = {
type: 'frigate/event/retain',
instance_id: client_id,
event_id: eventID,
retain: retain,
};
const response = await homeAssistantWSRequest<RetainResult>(
hass,
retainResultSchema,
retainRequest,
);
if (!response.success) {
throw new FrigateCardError(localize('error.failed_retain'), {
request: retainRequest,
response: response,
});
}
}
/**
* Get an id that unique identifies a particular camera (not zone, object, etc)
* within a particular Frigate instance. ID will not (necessarily) be unique