Initial skeletal Frigate lovelace card.
This commit is contained in:
@@ -161,16 +161,15 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO You need to replace all instances of "action-handler-boilerplate" with "action-handler-<your card name>"
|
||||
customElements.define('action-handler-boilerplate', ActionHandler);
|
||||
customElements.define('action-handler-frigate-card', ActionHandler);
|
||||
|
||||
const getActionHandler = (): ActionHandler => {
|
||||
const body = document.body;
|
||||
if (body.querySelector('action-handler-boilerplate')) {
|
||||
return body.querySelector('action-handler-boilerplate') as ActionHandler;
|
||||
if (body.querySelector('action-handler-frigate-card')) {
|
||||
return body.querySelector('action-handler-frigate-card') as ActionHandler;
|
||||
}
|
||||
|
||||
const actionhandler = document.createElement('action-handler-boilerplate');
|
||||
const actionhandler = document.createElement('action-handler-frigate-card');
|
||||
body.appendChild(actionhandler);
|
||||
|
||||
return actionhandler as ActionHandler;
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
CSSResult,
|
||||
TemplateResult,
|
||||
css,
|
||||
PropertyValues,
|
||||
internalProperty,
|
||||
} from 'lit-element';
|
||||
import {
|
||||
HomeAssistant,
|
||||
hasConfigOrEntityChanged,
|
||||
hasAction,
|
||||
ActionHandlerEvent,
|
||||
handleAction,
|
||||
LovelaceCardEditor,
|
||||
getLovelace,
|
||||
} from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types
|
||||
|
||||
import './editor';
|
||||
|
||||
import type { BoilerplateCardConfig } from './types';
|
||||
import { actionHandler } from './action-handler-directive';
|
||||
import { CARD_VERSION } from './const';
|
||||
import { localize } from './localize/localize';
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
console.info(
|
||||
`%c BOILERPLATE-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
|
||||
'color: orange; font-weight: bold; background: black',
|
||||
'color: white; font-weight: bold; background: dimgray',
|
||||
);
|
||||
|
||||
// This puts your card into the UI card picker dialog
|
||||
(window as any).customCards = (window as any).customCards || [];
|
||||
(window as any).customCards.push({
|
||||
type: 'boilerplate-card',
|
||||
name: 'Boilerplate Card',
|
||||
description: 'A template custom card for you to create something awesome',
|
||||
});
|
||||
|
||||
// TODO Name your custom element
|
||||
@customElement('boilerplate-card')
|
||||
export class BoilerplateCard extends LitElement {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
return document.createElement('boilerplate-card-editor');
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
return {};
|
||||
}
|
||||
|
||||
// TODO Add any properities that should cause your element to re-render here
|
||||
// https://lit-element.polymer-project.org/guide/properties
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@internalProperty() private config!: BoilerplateCardConfig;
|
||||
|
||||
// https://lit-element.polymer-project.org/guide/properties#accessors-custom
|
||||
public setConfig(config: BoilerplateCardConfig): void {
|
||||
// TODO Check for required fields and that they are of the proper format
|
||||
if (!config) {
|
||||
throw new Error(localize('common.invalid_configuration'));
|
||||
}
|
||||
|
||||
if (config.test_gui) {
|
||||
getLovelace().setEditMode(true);
|
||||
}
|
||||
|
||||
this.config = {
|
||||
name: 'Boilerplate',
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
// https://lit-element.polymer-project.org/guide/lifecycle#shouldupdate
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (!this.config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasConfigOrEntityChanged(this, changedProps, false);
|
||||
}
|
||||
|
||||
// https://lit-element.polymer-project.org/guide/templates
|
||||
protected render(): TemplateResult | void {
|
||||
// TODO Check for stateObj or other necessary things and render a warning if missing
|
||||
if (this.config.show_warning) {
|
||||
return this._showWarning(localize('common.show_warning'));
|
||||
}
|
||||
|
||||
if (this.config.show_error) {
|
||||
return this._showError(localize('common.show_error'));
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.config.name}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.config.hold_action),
|
||||
hasDoubleClick: hasAction(this.config.double_tap_action),
|
||||
})}
|
||||
tabindex="0"
|
||||
.label=${`Boilerplate: ${this.config.entity || 'No Entity Defined'}`}
|
||||
></ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent): void {
|
||||
if (this.hass && this.config && ev.detail.action) {
|
||||
handleAction(this, this.hass, this.config, ev.detail.action);
|
||||
}
|
||||
}
|
||||
|
||||
private _showWarning(warning: string): TemplateResult {
|
||||
return html`
|
||||
<hui-warning>${warning}</hui-warning>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showError(error: string): TemplateResult {
|
||||
const errorCard = document.createElement('hui-error-card');
|
||||
errorCard.setConfig({
|
||||
type: 'error',
|
||||
error,
|
||||
origConfig: this.config,
|
||||
});
|
||||
|
||||
return html`
|
||||
${errorCard}
|
||||
`;
|
||||
}
|
||||
|
||||
// https://lit-element.polymer-project.org/guide/styles
|
||||
static get styles(): CSSResult {
|
||||
return css``;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const CARD_VERSION = '1.3.2';
|
||||
export const CARD_VERSION = '0.0.1';
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
declare module '*.scss';
|
||||
+8
-9
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
@@ -8,11 +7,11 @@ import {
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
internalProperty,
|
||||
state,
|
||||
} from 'lit-element';
|
||||
import { HomeAssistant, fireEvent, LovelaceCardEditor, ActionConfig } from 'custom-card-helpers';
|
||||
|
||||
import { BoilerplateCardConfig } from './types';
|
||||
import { FrigateCardConfig } from './types';
|
||||
|
||||
const options = {
|
||||
required: {
|
||||
@@ -55,15 +54,15 @@ const options = {
|
||||
},
|
||||
};
|
||||
|
||||
@customElement('boilerplate-card-editor')
|
||||
export class BoilerplateCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
@customElement('frigate-card-editor')
|
||||
export class FrigateCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@internalProperty() private _config?: BoilerplateCardConfig;
|
||||
@internalProperty() private _toggle?: boolean;
|
||||
@internalProperty() private _helpers?: any;
|
||||
@state() private _config?: FrigateCardConfig;
|
||||
@state() private _toggle?: boolean;
|
||||
@state() private _helpers?: any;
|
||||
private _initialized = false;
|
||||
|
||||
public setConfig(config: BoilerplateCardConfig): void {
|
||||
public setConfig(config: FrigateCardConfig): void {
|
||||
this._config = config;
|
||||
|
||||
this.loadCardHelpers();
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
@use "@material/image-list/mdc-image-list";
|
||||
@use "@material/image-list";
|
||||
|
||||
.frigate-card-viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.frigate-card-gallery {
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
|
||||
left: 55px;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
|
||||
-ms-overflow-style: none; // Hide scrollbar: IE and Edge
|
||||
scrollbar-width: none; // Hide scrollbar: Firefox
|
||||
}
|
||||
|
||||
.frigate-card-exception {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.frigate-card-gallery::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.frigate-card-image-list {
|
||||
@include image-list.standard-columns(4);
|
||||
@include image-list.shape-radius(5px);
|
||||
}
|
||||
|
||||
.frigate-card-image-list-icon-overlay {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.frigate-card-image-list-highlight {
|
||||
// Put border inside.
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
|
||||
opacity: 0.5;
|
||||
|
||||
border-style: dashed;
|
||||
border-width: 1px;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.frigate-card-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.frigate-card-navbar {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.frigate-card-statusbar {
|
||||
@extend .frigate-card-navbar;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
ha-icon-button.button {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
opacity: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 50%;
|
||||
padding: 0px;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.visibile {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
// TODO: Can I use ajv (https://ajv.js.org/guide/typescript.html) to verify
|
||||
// event return matches the TS interface?
|
||||
|
||||
// TODO Does each event contain thumbnail?
|
||||
|
||||
// TODO comments per method.
|
||||
|
||||
// TODO: Stop the live view video when I hide it.
|
||||
|
||||
// TODO Check for HA state presence and validity before using it, otherwise warn.
|
||||
|
||||
// TODO Add material tooltips
|
||||
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
CSSResult,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
state,
|
||||
unsafeCSS,
|
||||
} from 'lit-element';
|
||||
|
||||
import { until } from 'lit-html/directives/until.js';
|
||||
import {
|
||||
HomeAssistant,
|
||||
ActionHandlerEvent,
|
||||
handleAction,
|
||||
LovelaceCardEditor,
|
||||
getLovelace,
|
||||
} from 'custom-card-helpers';
|
||||
|
||||
import './editor';
|
||||
|
||||
import style from './frigate-card.scss'
|
||||
|
||||
import type { FrigateCardConfig, FrigateEvent, GetEventsParameters } from './types';
|
||||
import { actionHandler } from './action-handler-directive';
|
||||
import { CARD_VERSION } from './const';
|
||||
import { localize } from './localize/localize';
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
console.info(
|
||||
`%c FRIGATE-HASS-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
|
||||
'color: pink; font-weight: bold; background: black',
|
||||
'color: white; font-weight: bold; background: dimgray',
|
||||
);
|
||||
|
||||
// This puts your card into the UI card picker dialog
|
||||
(window as any).customCards = (window as any).customCards || [];
|
||||
(window as any).customCards.push({
|
||||
type: 'frigate-card',
|
||||
name: 'Frigate Card',
|
||||
description: 'A lovelace card for use with Frigate',
|
||||
});
|
||||
|
||||
enum FrigateCardView {
|
||||
LIVE, // Show the live camera.
|
||||
CLIP, // Show a clip video.
|
||||
CLIPS, // Show the clips gallery.
|
||||
SNAPSHOT, // Show a snapshot.
|
||||
SNAPSHOTS, // Show the snapshots gallery.
|
||||
}
|
||||
|
||||
@customElement('frigate-card')
|
||||
export class FrigateCard extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._viewMode = FrigateCardView.LIVE;
|
||||
this._viewEvent = null;
|
||||
this._interactionTimerID = null;
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
return document.createElement('frigate-card-editor');
|
||||
}
|
||||
|
||||
public static getStubConfig(): Record<string, string> {
|
||||
return {};
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
public config!: FrigateCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected _viewMode: FrigateCardView;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected _viewEvent: FrigateEvent | null;
|
||||
|
||||
protected _interactionTimerID: number | null;
|
||||
|
||||
public setConfig(inputConfig: FrigateCardConfig): void {
|
||||
if (!inputConfig) {
|
||||
throw new Error(localize('common.invalid_configuration:'));
|
||||
}
|
||||
// inputConfig is not extensible, need to make a copy to allow
|
||||
// modifications.
|
||||
const cardConfig = Object.assign({
|
||||
name: 'Frigate'
|
||||
}, inputConfig);
|
||||
|
||||
if (cardConfig.test_gui) {
|
||||
getLovelace().setEditMode(true);
|
||||
}
|
||||
|
||||
if (!cardConfig.frigate_url) {
|
||||
throw new Error(localize('common.invalid_configuration_missing') + ": frigate_url");
|
||||
}
|
||||
|
||||
if (!cardConfig.frigate_camera_name) {
|
||||
// No camera name specified, so just assume it's the same as the entity name.
|
||||
if (cardConfig.camera_entity.includes(".")) {
|
||||
cardConfig.frigate_camera_name = cardConfig.camera_entity.split('.', 2)[1]
|
||||
} else {
|
||||
throw new Error(localize('common.invalid_configuration_missing') + ": camera");
|
||||
}
|
||||
}
|
||||
|
||||
if (cardConfig.timeout_ms) {
|
||||
if (isNaN(Number(cardConfig.timeout_ms))) {
|
||||
throw new Error(localize('common.invalid_configuration') + ": timeout_ms");
|
||||
}
|
||||
}
|
||||
|
||||
if (cardConfig.default_view) {
|
||||
if (!["live", "clips", "clip", "snapshots", "snapshot"].includes(cardConfig.default_view)) {
|
||||
throw new Error(localize('common.invalid_configuration') + ": default_view");
|
||||
}
|
||||
}
|
||||
|
||||
this.config = cardConfig;
|
||||
this._setViewModeToDefault();
|
||||
}
|
||||
|
||||
protected _setViewModeToDefault(): void {
|
||||
if (this.config.default_view == "live") {
|
||||
this._viewMode = FrigateCardView.LIVE;
|
||||
} else if (this.config.default_view == "clips") {
|
||||
this._viewMode = FrigateCardView.CLIPS;
|
||||
} else if (this.config.default_view == "clip") {
|
||||
this._viewMode = FrigateCardView.CLIP;
|
||||
} else if (this.config.default_view == "snapshots") {
|
||||
this._viewMode = FrigateCardView.SNAPSHOTS;
|
||||
} else if (this.config.default_view == "snapshot") {
|
||||
this._viewMode = FrigateCardView.SNAPSHOT;
|
||||
}
|
||||
}
|
||||
|
||||
// == RTC experimentation ==
|
||||
// const div = document.createElement("div");
|
||||
// const webrtcElement = customElements.get('webrtc-camera');
|
||||
// const webrtc = new webrtcElement();
|
||||
// webrtc.setConfig({ "entity": "camera.landing_rtsp" });
|
||||
// webrtc.hass = this.hass;
|
||||
// div.appendChild(webrtc);
|
||||
// this.renderRoot.appendChild(div);
|
||||
// ==
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (!this.config || !this.hass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cameraEntity = this.config.camera_entity;
|
||||
const motionEntity = this.config.motion_entity;
|
||||
|
||||
if (!cameraEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (changedProps.has('config')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
||||
|
||||
if (oldHass) {
|
||||
if (oldHass.states[cameraEntity] !== this.hass.states[cameraEntity]) {
|
||||
return true;
|
||||
}
|
||||
if (motionEntity && oldHass.states[motionEntity] !== this.hass.states[motionEntity]) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async _getEvents({
|
||||
has_clip = false,
|
||||
has_snapshot = false,
|
||||
limit = 100,
|
||||
}: GetEventsParameters): Promise<FrigateEvent[]> {
|
||||
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}`
|
||||
}
|
||||
|
||||
if (this.config.label) {
|
||||
url += `&label=${this.config.label}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
// TODO: Catch when json decoding fails.
|
||||
throw new Error(`Frigate API request failed with status: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected async _renderEvents() : Promise<TemplateResult> {
|
||||
const want_clips = this._viewMode == FrigateCardView.CLIPS;
|
||||
|
||||
const events = await this._getEvents({
|
||||
has_clip: want_clips,
|
||||
has_snapshot: !want_clips,
|
||||
});
|
||||
|
||||
if (!events.length) {
|
||||
return html`
|
||||
<div class="frigate-card-exception">
|
||||
<ha-icon
|
||||
icon="${want_clips ? "mdi:filmstrip-off" : "mdi:camera-off"}"
|
||||
></ha-icon>
|
||||
</div>`
|
||||
}
|
||||
|
||||
return html`
|
||||
<ul class= "mdc-image-list frigate-card-image-list">
|
||||
${events.map(event => html`
|
||||
<li class="mdc-image-list__item">
|
||||
<div class="mdc-image-list__image-aspect-container">
|
||||
<img
|
||||
class="mdc-image-list__image"
|
||||
src="data:image/png;base64,${event.thumbnail}"
|
||||
@click=${() => {
|
||||
this._viewEvent = event;
|
||||
this._viewMode = want_clips ?
|
||||
FrigateCardView.CLIP : FrigateCardView.SNAPSHOT
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</li>`)}
|
||||
</ul>`;
|
||||
}
|
||||
|
||||
protected _renderProgressIndicator(): TemplateResult {
|
||||
return html`
|
||||
<div class="frigate-card-exception">
|
||||
<ha-circular-progress
|
||||
active="true"
|
||||
size="large"
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
}
|
||||
|
||||
protected _renderNavigationBar(): TemplateResult {
|
||||
return html`
|
||||
<div class="frigate-card-navbar" >
|
||||
<ha-icon-button
|
||||
class="button"
|
||||
icon="mdi:cctv"
|
||||
@click=${() => this._viewMode = FrigateCardView.LIVE}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class="button"
|
||||
icon = "mdi:filmstrip"
|
||||
@click=${() => this._viewMode = FrigateCardView.CLIPS}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class="button"
|
||||
icon = "mdi:camera"
|
||||
@click=${() => this._viewMode = FrigateCardView.SNAPSHOTS}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
}
|
||||
|
||||
protected async _renderClipPlayer(): Promise<TemplateResult> {
|
||||
let event: FrigateEvent;
|
||||
if (!this._viewEvent) {
|
||||
const events = await this._getEvents({
|
||||
has_clip: true,
|
||||
limit: 1
|
||||
});
|
||||
if (!events.length) {
|
||||
return html`
|
||||
<div class="frigate-card-exception">
|
||||
<ha-icon
|
||||
icon="mdi:camera-off"
|
||||
></ha-icon>
|
||||
</div>`
|
||||
}
|
||||
event = events[0];
|
||||
} else {
|
||||
event = this._viewEvent;
|
||||
}
|
||||
|
||||
const url = `${this.config.frigate_url}/clips/` +
|
||||
`${event.camera}-${event.id}.mp4`;
|
||||
return html`
|
||||
<video class="frigate-card-viewer" autoplay controls>
|
||||
<source src="${url}" type="video/mp4">
|
||||
</video>`
|
||||
}
|
||||
|
||||
protected _renderSnapshotViewer(): TemplateResult {
|
||||
if (!this._viewEvent) {
|
||||
return html``
|
||||
}
|
||||
const url = `${this.config.frigate_url}/clips/` +
|
||||
`${this._viewEvent.camera}-${this._viewEvent.id}.jpg`;
|
||||
return html`<img class="frigate-card-viewer" src="${url}">`
|
||||
}
|
||||
|
||||
protected _renderStatusBar(): TemplateResult {
|
||||
if (!this.config.motion_entity || !(this.config.motion_entity in this.hass.states)) {
|
||||
return html``;
|
||||
}
|
||||
const icon = this.hass.states[this.config.motion_entity].state == "on" ?
|
||||
"mdi:motion-sensor" : "mdi:walk"
|
||||
return html`
|
||||
<div class="frigate-card-statusbar ${
|
||||
this._viewMode == FrigateCardView.LIVE ? 'visible' : 'invisible'}
|
||||
">
|
||||
<ha-icon-button
|
||||
class="button"
|
||||
icon="${icon}"
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
}
|
||||
|
||||
protected _renderLiveViewer(): TemplateResult {
|
||||
return html`
|
||||
<ha-camera-stream
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass.states[this.config.camera_entity]}
|
||||
.controls=${true}
|
||||
.muted=${true}
|
||||
class=${this._viewMode == FrigateCardView.LIVE ? 'visible' : 'invisible'}
|
||||
>
|
||||
</ha-camera-stream>`;
|
||||
}
|
||||
|
||||
protected _interactionHandler(): void {
|
||||
if (!this.config.timeout_ms) {
|
||||
return;
|
||||
}
|
||||
if (this._interactionTimerID) {
|
||||
window.clearTimeout(this._interactionTimerID);
|
||||
}
|
||||
this._interactionTimerID = window.setTimeout(() => {
|
||||
this._interactionTimerID = null;
|
||||
this._setViewModeToDefault();
|
||||
}, this.config.timeout_ms);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (this.config.show_warning) {
|
||||
return this._showWarning(localize('common.show_warning'));
|
||||
}
|
||||
if (this.config.show_error) {
|
||||
return this._showError(localize('common.show_error'));
|
||||
}
|
||||
|
||||
// TODO: Add latest snapshot fetch functionality.
|
||||
return html`
|
||||
<div
|
||||
class="frigate-card-container"
|
||||
@click=${this._interactionHandler}
|
||||
>
|
||||
${this._renderNavigationBar()}
|
||||
${this._viewMode == FrigateCardView.CLIPS ?
|
||||
html`<div class="frigate-card-gallery">
|
||||
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||
</div>` : ``
|
||||
}
|
||||
${this._viewMode == FrigateCardView.SNAPSHOTS ?
|
||||
html`<div class="frigate-card-gallery">
|
||||
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||
</div>` : ``
|
||||
}
|
||||
${this._viewMode == FrigateCardView.CLIP ?
|
||||
html`<div class="frigate-card-viewer">
|
||||
${until(this._renderClipPlayer(), this._renderProgressIndicator())}
|
||||
</div>` : ``
|
||||
}
|
||||
${this._viewMode == FrigateCardView.SNAPSHOT ?
|
||||
this._renderSnapshotViewer() : ``
|
||||
}
|
||||
${this._renderStatusBar()}
|
||||
${this._renderLiveViewer()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// private _handleAction(ev: ActionHandlerEvent): void {
|
||||
// if (this.hass && this.config && ev.detail.action) {
|
||||
// handleAction(this, this.hass, this.config, ev.detail.action);
|
||||
// }
|
||||
// }
|
||||
|
||||
private _showWarning(warning: string): TemplateResult {
|
||||
return html`
|
||||
<hui-warning> ${warning} </hui-warning>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showError(error: string): TemplateResult {
|
||||
const errorCard = document.createElement('hui-error-card');
|
||||
errorCard.setConfig({
|
||||
type: 'error',
|
||||
error,
|
||||
origConfig: this.config,
|
||||
});
|
||||
|
||||
return html`
|
||||
${errorCard}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
// CSS is compiled from frigate-card.scss, so this is safe.
|
||||
return unsafeCSS(style);
|
||||
}
|
||||
|
||||
static getCardSize(): number {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"version": "Version",
|
||||
"invalid_configuration": "Invalid configuration",
|
||||
"invalid_configuration_missing": "Missing parameter in configuration",
|
||||
"show_warning": "Show Warning",
|
||||
"show_error": "Show Error"
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"version": "Versjon",
|
||||
"invalid_configuration": "Ikke gyldig konfiguration",
|
||||
"show_warning": "Vis advarsel"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import * as en from './languages/en.json';
|
||||
import * as nb from './languages/nb.json';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const languages: any = {
|
||||
en: en,
|
||||
nb: nb,
|
||||
};
|
||||
|
||||
export function localize(string: string, search = '', replace = ''): string {
|
||||
|
||||
+30
-5
@@ -1,21 +1,46 @@
|
||||
import { ActionConfig, LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from 'custom-card-helpers';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'boilerplate-card-editor': LovelaceCardEditor;
|
||||
'frigate-card-editor': LovelaceCardEditor;
|
||||
'hui-error-card': LovelaceCard;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add your configuration elements here for type-checking
|
||||
export interface BoilerplateCardConfig extends LovelaceCardConfig {
|
||||
export interface FrigateCardConfig extends LovelaceCardConfig {
|
||||
type: string;
|
||||
name?: string;
|
||||
|
||||
camera_entity: string;
|
||||
motion_entity: string | null;
|
||||
frigate_url: string;
|
||||
frigate_camera_name?: string | null;
|
||||
default_view: string | null;
|
||||
timeout_ms?: number | null;
|
||||
|
||||
show_warning?: boolean;
|
||||
show_error?: boolean;
|
||||
test_gui?: boolean;
|
||||
entity?: string;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface FrigateEvent {
|
||||
camera: string;
|
||||
end_time: number;
|
||||
false_positive: boolean;
|
||||
has_clip: boolean;
|
||||
has_snapshot: boolean;
|
||||
id: string;
|
||||
label: string;
|
||||
start_time: number;
|
||||
thumbnail: string;
|
||||
top_score: number;
|
||||
zones: string[];
|
||||
}
|
||||
|
||||
export interface GetEventsParameters {
|
||||
has_clip?: boolean;
|
||||
has_snapshot?: boolean;
|
||||
limit?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user