diff --git a/.vscode/i18n-ally-reviews.yml b/.vscode/i18n-ally-reviews.yml
index e79af03c..07f3a096 100644
--- a/.vscode/i18n-ally-reviews.yml
+++ b/.vscode/i18n-ally-reviews.yml
@@ -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'
diff --git a/README.md b/README.md
index 56d96d42..81310a26 100644
--- a/README.md
+++ b/README.md
@@ -1042,6 +1042,13 @@ A dashboard with the card can be cast onto a suitable device (such as the Nest H
+### Event starring
+
+Retain interesting Frigate events forever:
+
+
+
+
## Examples
### Illustrative Expanded Configuration Reference
diff --git a/images/star.gif b/images/star.gif
new file mode 100644
index 00000000..4d04777f
Binary files /dev/null and b/images/star.gif differ
diff --git a/src/components/gallery.ts b/src/components/gallery.ts
index 0204338a..1b347551 100644
--- a/src/components/gallery.ts
+++ b/src/components/gallery.ts
@@ -86,6 +86,7 @@ export class FrigateCardGallery extends LitElement {
.hass=${this.hass}
.view=${this.view}
.galleryConfig=${this.galleryConfig}
+ .cameras=${this.cameras}
>
`;
@@ -116,6 +117,9 @@ export class FrigateCardGalleryCore extends LitElement {
@property({ attribute: false })
public galleryConfig?: GalleryConfig;
+ @property({ attribute: false })
+ public cameras?: Map;
+
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` {
diff --git a/src/components/live.ts b/src/components/live.ts
index c017d908..35006e6e 100644
--- a/src/components/live.ts
+++ b/src/components/live.ts
@@ -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}
>
;
+
/**
* 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` changeDrawer(ev, 'close')}
@frigate-card:carousel:tap=${(ev: CustomEvent) => {
// Send the view change from the source of the tap event, so the
diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts
index 82920299..588dc4f6 100644
--- a/src/components/thumbnail-carousel.ts
+++ b/src/components/thumbnail-carousel.ts
@@ -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;
@@ -28,6 +32,9 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
@property({ attribute: false, hasChanged: contentsChanged })
public target?: FrigateBrowseMediaSource | null;
+ @property({ attribute: false })
+ public cameras?: Map;
+
// 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` `}
- ${this.controls && event?.retain_indefinitely
+ ${this.controls && this.hass && this.clientID
? html` {
+ 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);
+ });
+ }
+ }}
/>`
: ``}
${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;
+ }
}
diff --git a/src/components/timeline.ts b/src/components/timeline.ts
index ace18398..7fa68f7d 100644
--- a/src/components/timeline.ts
+++ b/src/components/timeline.ts
@@ -450,6 +450,7 @@ export class FrigateCardTimeline extends LitElement {
.hass=${this.hass}
.view=${this.view}
.config=${this.timelineConfig.controls.thumbnails}
+ .cameras=${this.cameras}
>
o[i], languages['en']);
}
- if (translated === undefined) {
+ if (!translated) {
translated = string.split('.').reduce((o, i) => o[i], languages['en']);
}
diff --git a/src/scss/thumbnail.scss b/src/scss/thumbnail.scss
index 250282a9..caa64791 100644
--- a/src/scss/thumbnail.scss
+++ b/src/scss/thumbnail.scss
@@ -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 {
diff --git a/src/utils/frigate.ts b/src/utils/frigate.ts
index 47e9e368..ebb226a2 100644
--- a/src/utils/frigate.ts
+++ b/src/utils/frigate.ts
@@ -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;
+const retainResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string(),
+});
+export type RetainResult = z.infer;
+
/**
* 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 {
+ const retainRequest = {
+ type: 'frigate/event/retain',
+ instance_id: client_id,
+ event_id: eventID,
+ retain: retain,
+ };
+ const response = await homeAssistantWSRequest(
+ 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