Add event starring from the card.
This commit is contained in:
Vendored
+7
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 321 KiB |
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user