Prettier formatting.

This commit is contained in:
Dermot Duffy
2021-08-17 20:36:27 -07:00
parent 0e3290998a
commit 0497c92779
11 changed files with 365 additions and 309 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ module.exports = {
semi: true, semi: true,
trailingComma: 'all', trailingComma: 'all',
singleQuote: true, singleQuote: true,
printWidth: 120, printWidth: 89,
tabWidth: 2, tabWidth: 2,
embeddedLanguageFormatting: 'auto',
}; };
+4 -4
View File
@@ -5,7 +5,7 @@ import babel from 'rollup-plugin-babel';
import { terser } from 'rollup-plugin-terser'; import { terser } from 'rollup-plugin-terser';
import serve from 'rollup-plugin-serve'; import serve from 'rollup-plugin-serve';
import json from '@rollup/plugin-json'; import json from '@rollup/plugin-json';
import styles from "rollup-plugin-styles"; import styles from 'rollup-plugin-styles';
const dev = process.env.ROLLUP_WATCH; const dev = process.env.ROLLUP_WATCH;
@@ -24,10 +24,10 @@ const plugins = [
modules: false, modules: false,
// Behavior of inject mode, without actually injecting style // Behavior of inject mode, without actually injecting style
// into <head>. // into <head>.
mode: ["inject", () => undefined], mode: ['inject', () => undefined],
sass: { sass: {
includePaths: ["./node_modules/"] includePaths: ['./node_modules/'],
} },
}), }),
nodeResolve({}), nodeResolve({}),
commonjs(), commonjs(),
+31 -8
View File
@@ -1,9 +1,15 @@
import { directive, PropertyPart } from 'lit-html'; import { directive, PropertyPart } from 'lit-html';
import { ActionHandlerDetail, ActionHandlerOptions } from 'custom-card-helpers/dist/types'; import {
ActionHandlerDetail,
ActionHandlerOptions,
} from 'custom-card-helpers/dist/types';
import { fireEvent } from 'custom-card-helpers'; import { fireEvent } from 'custom-card-helpers';
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; const isTouch =
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0;
interface ActionHandler extends HTMLElement { interface ActionHandler extends HTMLElement {
holdTime: number; holdTime: number;
@@ -49,7 +55,15 @@ class ActionHandler extends HTMLElement implements ActionHandler {
this.appendChild(this.ripple); this.appendChild(this.ripple);
this.ripple.primary = true; this.ripple.primary = true;
['touchcancel', 'mouseout', 'mouseup', 'touchmove', 'mousewheel', 'wheel', 'scroll'].forEach(ev => { [
'touchcancel',
'mouseout',
'mouseup',
'touchmove',
'mousewheel',
'wheel',
'scroll',
].forEach((ev) => {
document.addEventListener( document.addEventListener(
ev, ev,
() => { () => {
@@ -111,7 +125,10 @@ class ActionHandler extends HTMLElement implements ActionHandler {
if (this.held) { if (this.held) {
fireEvent(element, 'action', { action: 'hold' }); fireEvent(element, 'action', { action: 'hold' });
} else if (options.hasDoubleClick) { } else if (options.hasDoubleClick) {
if ((ev.type === 'click' && (ev as MouseEvent).detail < 2) || !this.dblClickTimeout) { if (
(ev.type === 'click' && (ev as MouseEvent).detail < 2) ||
!this.dblClickTimeout
) {
this.dblClickTimeout = window.setTimeout(() => { this.dblClickTimeout = window.setTimeout(() => {
this.dblClickTimeout = undefined; this.dblClickTimeout = undefined;
fireEvent(element, 'action', { action: 'tap' }); fireEvent(element, 'action', { action: 'tap' });
@@ -175,7 +192,10 @@ const getActionHandler = (): ActionHandler => {
return actionhandler as ActionHandler; return actionhandler as ActionHandler;
}; };
export const actionHandlerBind = (element: ActionHandlerElement, options: ActionHandlerOptions): void => { export const actionHandlerBind = (
element: ActionHandlerElement,
options: ActionHandlerOptions,
): void => {
const actionhandler: ActionHandler = getActionHandler(); const actionhandler: ActionHandler = getActionHandler();
if (!actionhandler) { if (!actionhandler) {
return; return;
@@ -183,6 +203,9 @@ export const actionHandlerBind = (element: ActionHandlerElement, options: Action
actionhandler.bind(element, options); actionhandler.bind(element, options);
}; };
export const actionHandler = directive((options: ActionHandlerOptions = {}) => (part: PropertyPart): void => { export const actionHandler = directive(
actionHandlerBind(part.committer.element as ActionHandlerElement, options); (options: ActionHandlerOptions = {}) =>
}); (part: PropertyPart): void => {
actionHandlerBind(part.committer.element as ActionHandlerElement, options);
},
);
+95 -85
View File
@@ -13,7 +13,7 @@ import { HomeAssistant, fireEvent, LovelaceCardEditor } from 'custom-card-helper
import { FrigateCardConfig } from './types'; import { FrigateCardConfig } from './types';
import frigate_card_editor_style from './frigate-card-editor.scss' import frigate_card_editor_style from './frigate-card-editor.scss';
const options = { const options = {
required: { required: {
@@ -63,12 +63,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return true; return true;
} }
protected _getEntities(domain: string) : string[] { protected _getEntities(domain: string): string[] {
if (!this.hass) { if (!this.hass) {
return []; return [];
} }
const entities = Object.keys( const entities = Object.keys(this.hass.states).filter(
this.hass.states).filter(eid => eid.substr(0, eid.indexOf('.')) === domain); (eid) => eid.substr(0, eid.indexOf('.')) === domain,
);
entities.sort(); entities.sort();
// Add a blank entry to unset a selection. // Add a blank entry to unset a selection.
@@ -89,24 +90,24 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
const binarySensorEntities = this._getEntities('binary_sensor'); const binarySensorEntities = this._getEntities('binary_sensor');
const webrtcCameraEntity = const webrtcCameraEntity =
this._config?.webrtc && (this._config?.webrtc as any).entity ? this._config?.webrtc && (this._config?.webrtc as any).entity
(this._config?.webrtc as any).entity : ? (this._config?.webrtc as any).entity
''; : '';
const viewModes = { const viewModes = {
"": "", '': '',
"live": "Live view", live: 'Live view',
"clips": "Clip gallery", clips: 'Clip gallery',
"snapshots": "Snapshot gallery", snapshots: 'Snapshot gallery',
"clip": "Latest clip", clip: 'Latest clip',
"snapshot": "Latest snapshot", snapshot: 'Latest snapshot',
} };
const liveProvider = { const liveProvider = {
"": "", '': '',
"frigate": "Frigate", frigate: 'Frigate',
"webrtc": "WebRTC", webrtc: 'WebRTC',
} };
return html` return html`
<div class="card-config"> <div class="card-config">
@@ -125,11 +126,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.configValue=${'camera_entity'} .configValue=${'camera_entity'}
> >
<paper-listbox slot="dropdown-content" .selected=${cameraEntities.indexOf(this._config?.camera_entity || '')}> <paper-listbox
${cameraEntities.map(entity => { slot="dropdown-content"
return html` .selected=${cameraEntities.indexOf(
<paper-item>${entity}</paper-item> this._config?.camera_entity || '',
`; )}
>
${cameraEntities.map((entity) => {
return html` <paper-item>${entity}</paper-item> `;
})} })}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
@@ -150,18 +154,20 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="secondary">${options.optional.secondary}</div> <div class="secondary">${options.optional.secondary}</div>
</div> </div>
${options.optional.show ${options.optional.show
? html` ? html` <div class="values">
<div class="values">
<paper-dropdown-menu <paper-dropdown-menu
label="Motion Entity (Optional)" label="Motion Entity (Optional)"
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.configValue=${'motion_entity'} .configValue=${'motion_entity'}
> >
<paper-listbox slot="dropdown-content" .selected=${binarySensorEntities.indexOf(this._config?.motion_entity || '')}> <paper-listbox
${binarySensorEntities.map(entity => { slot="dropdown-content"
return html` .selected=${binarySensorEntities.indexOf(
<paper-item>${entity}</paper-item> this._config?.motion_entity || '',
`; )}
>
${binarySensorEntities.map((entity) => {
return html` <paper-item>${entity}</paper-item> `;
})} })}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
@@ -172,16 +178,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
<paper-dropdown-menu <paper-dropdown-menu
label="Default view (Optional)" label="Default view (Optional)"
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.configValue=${'view_default'} .configValue=${'view_default'}
> >
<paper-listbox slot="dropdown-content" .selected=${Object.keys(viewModes).indexOf(this._config?.view_default || '')}> <paper-listbox
${Object.keys(viewModes).map(key => { slot="dropdown-content"
.selected=${Object.keys(viewModes).indexOf(
this._config?.view_default || '',
)}
>
${Object.keys(viewModes).map((key) => {
return html` return html`
<paper-item .label="${key}"> <paper-item .label="${key}"> ${viewModes[key]} </paper-item>
${viewModes[key]}
</paper-item>
`; `;
})} })}
</paper-listbox> </paper-listbox>
@@ -190,13 +199,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
label="View timeout (seconds)" label="View timeout (seconds)"
prevent-invalid-input prevent-invalid-input
allowed-pattern="[0-9]" allowed-pattern="[0-9]"
.value=${this._config?.view_timeout ? String(this._config.view_timeout) : ''} .value=${this._config?.view_timeout
? String(this._config.view_timeout)
: ''}
.configValue=${'view_timeout'} .configValue=${'view_timeout'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
</div>` </div>`
: '' : ''}
}
<div class="option" @click=${this._toggleOption} .option=${'advanced'}> <div class="option" @click=${this._toggleOption} .option=${'advanced'}>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.advanced.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.advanced.icon}`}></ha-icon>
@@ -205,25 +215,23 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="secondary">${options.advanced.secondary}</div> <div class="secondary">${options.advanced.secondary}</div>
</div> </div>
${options.advanced.show ${options.advanced.show
? html` ? html` <div class="values">
<div class="values"> <paper-input
<paper-input label="Frigate zone filter (Optional)"
label="Frigate zone filter (Optional)" .value=${this._config?.zone || ''}
.value=${this._config?.zone || ''} .configValue=${'zone'}
.configValue=${'zone'} @value-changed=${this._valueChanged}
@value-changed=${this._valueChanged} ></paper-input>
></paper-input> </div>
</div> <div class="values">
<div class="values"> <paper-input
<paper-input label="Frigate label/object filter (Optional)"
label="Frigate label/object filter (Optional)" .value=${this._config?.label || ''}
.value=${this._config?.label || ''} .configValue=${'label'}
.configValue=${'label'} @value-changed=${this._valueChanged}
@value-changed=${this._valueChanged} ></paper-input>
></paper-input> </div>`
</div>` : ''}
: ''
}
<div class="option" @click=${this._toggleOption} .option=${'webrtc'}> <div class="option" @click=${this._toggleOption} .option=${'webrtc'}>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.webrtc.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.webrtc.icon}`}></ha-icon>
@@ -232,39 +240,41 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="secondary">${options.webrtc.secondary}</div> <div class="secondary">${options.webrtc.secondary}</div>
</div> </div>
${options.webrtc.show ${options.webrtc.show
? html` ? html` <div class="values">
<div class="values">
<paper-dropdown-menu <paper-dropdown-menu
label="WebRTC/Frigate Live provider (Optional)" label="WebRTC/Frigate Live provider (Optional)"
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.configValue=${'live_provider'} .configValue=${'live_provider'}
> >
<paper-listbox slot="dropdown-content" .selected=${Object.keys(liveProvider).indexOf(this._config?.live_provider || '')}> <paper-listbox
${Object.keys(liveProvider).map(key => { slot="dropdown-content"
.selected=${Object.keys(liveProvider).indexOf(
this._config?.live_provider || '',
)}
>
${Object.keys(liveProvider).map((key) => {
return html` return html`
<paper-item .label="${key}" <paper-item .label="${key}">${liveProvider[key]} </paper-item>
>${liveProvider[key]}
</paper-item>
`; `;
})} })}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-dropdown-menu <paper-dropdown-menu
label="WebRTC Camera Entity (To use with WebRTC Live Provider)" label="WebRTC Camera Entity (To use with WebRTC Live Provider)"
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.configValue=${'webrtc.entity'} .configValue=${'webrtc.entity'}
> >
<paper-listbox slot="dropdown-content" .selected=${cameraEntities.indexOf(webrtcCameraEntity)}> <paper-listbox
${cameraEntities.map(entity => { slot="dropdown-content"
return html` .selected=${cameraEntities.indexOf(webrtcCameraEntity)}
<paper-item>${entity}</paper-item> >
`; ${cameraEntities.map((entity) => {
return html` <paper-item>${entity}</paper-item> `;
})} })}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
</div>` </div>`
: '' : ''}
}
</div> </div>
`; `;
} }
@@ -314,7 +324,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
const parts = key.split('.', 2); const parts = key.split('.', 2);
const configName = parts[0]; const configName = parts[0];
if (!(configName in newConfig)) { if (!(configName in newConfig)) {
newConfig[configName] = {} newConfig[configName] = {};
} }
objectTarget = newConfig[configName]; objectTarget = newConfig[configName];
key = parts[1]; key = parts[1];
@@ -327,7 +337,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
} else if (target.value === '') { } else if (target.value === '') {
delete objectTarget[key]; delete objectTarget[key];
} else { } else {
objectTarget[key] = (target.checked !== undefined ? target.checked : target.value); objectTarget[key] = target.checked !== undefined ? target.checked : target.value;
} }
this._config = newConfig; this._config = newConfig;
fireEvent(this, 'config-changed', { config: this._config }); fireEvent(this, 'config-changed', { config: this._config });
+1 -1
View File
@@ -25,7 +25,7 @@
height: 1em; height: 1em;
padding-bottom: 0.5em; padding-bottom: 0.5em;
padding-left: 0.5em; padding-left: 0.5em;
color: rgba(255,255,255,0.7); color: rgba(255, 255, 255, 0.7);
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
} }
+3 -3
View File
@@ -16,8 +16,8 @@
top: 0px; top: 0px;
height: 100%; height: 100%;
-ms-overflow-style: none; // Hide scrollbar: IE and Edge -ms-overflow-style: none; // Hide scrollbar: IE and Edge
scrollbar-width: none; // Hide scrollbar: Firefox scrollbar-width: none; // Hide scrollbar: Firefox
} }
.frigate-card-attention { .frigate-card-attention {
@@ -29,7 +29,7 @@
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */
.frigate-card-gallery::-webkit-scrollbar { .frigate-card-gallery::-webkit-scrollbar {
display: none; display: none;
} }
.frigate-card-image-list { .frigate-card-image-list {
+188 -184
View File
@@ -11,7 +11,7 @@ import {
unsafeCSS, unsafeCSS,
} from 'lit-element'; } from 'lit-element';
import { NodePart } from "lit-html"; import { NodePart } from 'lit-html';
import { until } from 'lit-html/directives/until.js'; import { until } from 'lit-html/directives/until.js';
import { import {
@@ -23,11 +23,18 @@ import {
import './editor'; import './editor';
import frigate_card_style from './frigate-card.scss' import frigate_card_style from './frigate-card.scss';
import frigate_card_menu_style from './frigate-card-menu.scss' import frigate_card_menu_style from './frigate-card-menu.scss';
import { frigateCardConfigSchema, frigateGetEventsResponseSchema } from './types'; import { frigateCardConfigSchema, frigateGetEventsResponseSchema } from './types';
import type { FrigateCardView, FrigateCardConfig, FrigateEvent, FrigateGetEventsResponse, GetEventsParameters, ControlVideosParameters } from './types'; import type {
FrigateCardView,
FrigateCardConfig,
FrigateEvent,
FrigateGetEventsResponse,
GetEventsParameters,
ControlVideosParameters,
} from './types';
import { CARD_VERSION } from './const'; import { CARD_VERSION } from './const';
import { localize } from './localize/localize'; import { localize } from './localize/localize';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -69,7 +76,7 @@ function shouldUpdateBasedOnHass(
} }
if (oldHass) { if (oldHass) {
for(let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
const entity = entities[i]; const entity = entities[i];
if (!entity) { if (!entity) {
continue; continue;
@@ -105,20 +112,21 @@ export class FrigateCardMenu extends LitElement {
return shouldUpdateBasedOnHass( return shouldUpdateBasedOnHass(
this.hass, this.hass,
changedProps.get('hass') as HomeAssistant | undefined, changedProps.get('hass') as HomeAssistant | undefined,
[this.motionEntity]); [this.motionEntity],
);
} }
// Render the Frigate menu button. // Render the Frigate menu button.
protected _renderFrigateButton(): TemplateResult { protected _renderFrigateButton(): TemplateResult {
return html` return html` <ha-icon-button
<ha-icon-button class="button"
class="button" icon=${this.expand ? 'mdi:alpha-f-box' : 'mdi:alpha-f-box-outline'}
icon=${this.expand ? "mdi:alpha-f-box" : "mdi:alpha-f-box-outline"} data-toggle="tooltip"
data-toggle="tooltip" title="Frigate menu" title="Frigate menu"
@click=${() => { @click=${() => {
this.expand = !this.expand; this.expand = !this.expand;
}} }}
></ha-icon-button>`; ></ha-icon-button>`;
} }
// Call the callback. // Call the callback.
@@ -131,76 +139,77 @@ export class FrigateCardMenu extends LitElement {
// Render the menu. // Render the menu.
protected render(): TemplateResult | void | ((part: NodePart) => Promise<void>) { protected render(): TemplateResult | void | ((part: NodePart) => Promise<void>) {
if (!this.expand) { if (!this.expand) {
return html` return html` <div class="frigate-card-menu">${this._renderFrigateButton()}</div>`;
<div class="frigate-card-menu">
${this._renderFrigateButton()}
</div>`;
} }
let motionIcon: string | null = null; let motionIcon: string | null = null;
if (this.motionEntity && this.hass) { if (this.motionEntity && this.hass) {
motionIcon = this.hass.states[this.motionEntity]?.state == "on" ? "mdi:motion-sensor" : "mdi:walk"; motionIcon =
this.hass.states[this.motionEntity]?.state == 'on'
? 'mdi:motion-sensor'
: 'mdi:walk';
} }
return html` return html`
<div class="frigate-card-menu-container"> <div class="frigate-card-menu-container">
<div <div class="frigate-card-menu-expanded">
class="frigate-card-menu-expanded"
>
${this._renderFrigateButton()} ${this._renderFrigateButton()}
<ha-icon-button <ha-icon-button
class="button" class="button"
icon="mdi:cctv" icon="mdi:cctv"
data-toggle="tooltip" title="View live" data-toggle="tooltip"
title="View live"
@click=${() => { @click=${() => {
this.expand = false; this.expand = false;
this._callAction("live"); this._callAction('live');
}} }}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class="button" class="button"
icon = "mdi:filmstrip" icon="mdi:filmstrip"
data-toggle="tooltip" title="View clips" data-toggle="tooltip"
title="View clips"
@click=${() => { @click=${() => {
this.expand = false; this.expand = false;
this._callAction("clips"); this._callAction('clips');
}} }}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class="button" class="button"
icon = "mdi:camera" icon="mdi:camera"
data-toggle="tooltip" title="View snapshots" data-toggle="tooltip"
title="View snapshots"
@click=${() => { @click=${() => {
this.expand = false; this.expand = false;
this._callAction("snapshots"); this._callAction('snapshots');
}} }}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class="button" class="button"
icon = "mdi:web" icon="mdi:web"
data-toggle="tooltip" title="View Frigate UI" data-toggle="tooltip"
title="View Frigate UI"
@click=${() => { @click=${() => {
this.expand = false; this.expand = false;
this._callAction("frigate-ui"); this._callAction('frigate-ui');
}} }}
></ha-icon-button> ></ha-icon-button>
${!motionIcon ? html`` : html` ${!motionIcon
<ha-icon-button ? html``
data-toggle="tooltip" title="View motion sensor" : html` <ha-icon-button
class="button" data-toggle="tooltip"
icon="${motionIcon}" title="View motion sensor"
@click=${() => { class="button"
this.expand = false; icon="${motionIcon}"
this._callAction("motion"); @click=${() => {
}} this.expand = false;
></ha-icon-button>` this._callAction('motion');
} }}
></ha-icon-button>`}
</div> </div>
${this.heading ? html` ${this.heading
<div class="frigate-card-menu-title"> ? html` <div class="frigate-card-menu-title">${this.heading}</div> `
${this.heading} : ``}
</div>
` : ``}
</div> </div>
`; `;
} }
@@ -211,11 +220,9 @@ export class FrigateCardMenu extends LitElement {
} }
} }
// Main FrigateCard class. // Main FrigateCard class.
@customElement('frigate-card') @customElement('frigate-card')
export class FrigateCard extends LitElement { export class FrigateCard extends LitElement {
// Get the configuration element. // Get the configuration element.
public static async getConfigElement(): Promise<LovelaceCardEditor> { public static async getConfigElement(): Promise<LovelaceCardEditor> {
return document.createElement('frigate-card-editor'); return document.createElement('frigate-card-editor');
@@ -239,7 +246,7 @@ export class FrigateCard extends LitElement {
public config!: FrigateCardConfig; public config!: FrigateCardConfig;
@property({ attribute: false }) @property({ attribute: false })
protected _viewMode: FrigateCardView = "live"; protected _viewMode: FrigateCardView = 'live';
@property({ attribute: false }) @property({ attribute: false })
protected _viewEvent: FrigateEvent | null = null; protected _viewEvent: FrigateEvent | null = null;
@@ -248,7 +255,7 @@ export class FrigateCard extends LitElement {
protected _showMenu = false; protected _showMenu = false;
@state() @state()
protected _heading: string | null = null; protected _heading: string | null = null;
protected _interactionTimerID: number | null = null; protected _interactionTimerID: number | null = null;
protected _webrtcElement: any | null = null; protected _webrtcElement: any | null = null;
@@ -261,9 +268,9 @@ export class FrigateCard extends LitElement {
const parseResult = frigateCardConfigSchema.safeParse(inputConfig); const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
if (!parseResult.success) { if (!parseResult.success) {
const errors = parseResult.error.format() const errors = parseResult.error.format();
const keys = Object.keys(errors).filter(v => !v.startsWith("_")); const keys = Object.keys(errors).filter((v) => !v.startsWith('_'));
throw new Error(localize('common.invalid_configuration') + ": " + keys.join(", ")); throw new Error(localize('common.invalid_configuration') + ': ' + keys.join(', '));
} }
const config = parseResult.data; const config = parseResult.data;
@@ -273,14 +280,14 @@ export class FrigateCard extends LitElement {
if (!config.frigate_camera_name) { if (!config.frigate_camera_name) {
// No camera name specified, so just assume it's the same as the entity name. // No camera name specified, so just assume it's the same as the entity name.
if (config.camera_entity.includes(".")) { if (config.camera_entity.includes('.')) {
config.frigate_camera_name = config.camera_entity.split('.', 2)[1] config.frigate_camera_name = config.camera_entity.split('.', 2)[1];
} else { } else {
throw new Error(localize('common.invalid_configuration') + ": camera_entity"); throw new Error(localize('common.invalid_configuration') + ': camera_entity');
} }
} }
if (config.live_provider == "webrtc") { if (config.live_provider == 'webrtc') {
// Create a WebRTC element (https://github.com/AlexxIT/WebRTC) // Create a WebRTC element (https://github.com/AlexxIT/WebRTC)
const webrtcElement = customElements.get('webrtc-camera'); const webrtcElement = customElements.get('webrtc-camera');
if (webrtcElement) { if (webrtcElement) {
@@ -300,8 +307,8 @@ export class FrigateCard extends LitElement {
// Set the view mode to the configured default. // Set the view mode to the configured default.
protected _setViewModeToDefault(): void { protected _setViewModeToDefault(): void {
this._viewMode = this.config.view_default; this._viewMode = this.config.view_default;
if (["clip", "snapshot"].includes(this._viewMode)) { if (['clip', 'snapshot'].includes(this._viewMode)) {
this._viewEvent = null; this._viewEvent = null;
} }
} }
@@ -317,7 +324,8 @@ export class FrigateCard extends LitElement {
return shouldUpdateBasedOnHass( return shouldUpdateBasedOnHass(
this._hass, this._hass,
changedProps.get('_hass') as HomeAssistant | undefined, changedProps.get('_hass') as HomeAssistant | undefined,
[this.config.camera_entity, this.config.motion_entity]); [this.config.camera_entity, this.config.motion_entity],
);
} }
// Get FrigateEvents from the Frigate server API. // Get FrigateEvents from the Frigate server API.
@@ -328,13 +336,13 @@ export class FrigateCard extends LitElement {
}: GetEventsParameters): Promise<FrigateGetEventsResponse> { }: GetEventsParameters): Promise<FrigateGetEventsResponse> {
let url = `${this.config.frigate_url}/api/events?camera=${this.config.frigate_camera_name}`; let url = `${this.config.frigate_url}/api/events?camera=${this.config.frigate_camera_name}`;
if (has_clip) { if (has_clip) {
url += `&has_clip=1` url += `&has_clip=1`;
} }
if (has_snapshot) { if (has_snapshot) {
url += `&has_snapshot=1` url += `&has_snapshot=1`;
} }
if (limit > 0) { if (limit > 0) {
url += `&limit=${limit}` url += `&limit=${limit}`;
} }
if (this.config.label) { if (this.config.label) {
@@ -349,13 +357,13 @@ export class FrigateCard extends LitElement {
let raw_json; let raw_json;
try { try {
raw_json = await response.json(); raw_json = await response.json();
} catch(e) { } catch (e) {
console.warn(e); console.warn(e);
throw new Error(`Could not JSON decode Frigate API response: ${e}`); throw new Error(`Could not JSON decode Frigate API response: ${e}`);
} }
try { try {
return frigateGetEventsResponseSchema.parse(raw_json); return frigateGetEventsResponseSchema.parse(raw_json);
} catch(e) { } catch (e) {
console.warn(e); console.warn(e);
throw new Error(`Frigate events were malformed: ${e}`); throw new Error(`Frigate events were malformed: ${e}`);
} }
@@ -367,28 +375,27 @@ export class FrigateCard extends LitElement {
} }
protected _renderAttentionIcon(icon: string): TemplateResult { protected _renderAttentionIcon(icon: string): TemplateResult {
return html` return html` <div class="frigate-card-attention">
<div class="frigate-card-attention"> <ha-icon icon="${icon}"> </ha-icon>
<ha-icon icon="${icon}"> </div>`;
</ha-icon>
</div>`;
} }
// Render an embedded error situation. // Render an embedded error situation.
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _renderError(_error: string) : TemplateResult { protected _renderError(_error: string): TemplateResult {
return this._renderAttentionIcon("mdi:alert-circle"); return this._renderAttentionIcon('mdi:alert-circle');
} }
// Generate a human-readable title from an event. // Generate a human-readable title from an event.
// MediaBrowser title: 2021-08-12 19:20:14 [10s, Person 76%] // MediaBrowser title: 2021-08-12 19:20:14 [10s, Person 76%]
protected _getEventTitle(event: FrigateEvent) : string { protected _getEventTitle(event: FrigateEvent): string {
const date = dayjs.unix(event.end_time).tz("UTC").local(); const date = dayjs.unix(event.end_time).tz('UTC').local();
const iso_datetime = date.format("YYYY-MM-DD HH:mm:ss"); const iso_datetime = date.format('YYYY-MM-DD HH:mm:ss');
const duration = Math.trunc(event.end_time > event.start_time ? const duration = Math.trunc(
event.end_time - event.start_time : 0); event.end_time > event.start_time ? event.end_time - event.start_time : 0,
const score = Math.trunc(event.top_score*100); );
const score = Math.trunc(event.top_score * 100);
// Capitalize the label. // Capitalize the label.
const label = event.label.charAt(0).toUpperCase() + event.label.slice(1); const label = event.label.charAt(0).toUpperCase() + event.label.slice(1);
@@ -397,8 +404,8 @@ export class FrigateCard extends LitElement {
} }
// Render Frigate events into a card gallery. // Render Frigate events into a card gallery.
protected async _renderEvents() : Promise<TemplateResult> { protected async _renderEvents(): Promise<TemplateResult> {
const want_clips = (this._viewMode == "clips"); const want_clips = this._viewMode == 'clips';
let events; let events;
try { try {
events = await this._getEvents({ events = await this._getEvents({
@@ -410,45 +417,45 @@ export class FrigateCard extends LitElement {
} }
if (!events.length) { if (!events.length) {
return this._renderAttentionIcon(want_clips ? "mdi:filmstrip-off" : "mdi:camera-off"); return this._renderAttentionIcon(
want_clips ? 'mdi:filmstrip-off' : 'mdi:camera-off',
);
} }
return html` return html` <ul class="mdc-image-list frigate-card-image-list">
<ul class= "mdc-image-list frigate-card-image-list"> ${events.map(
${events.map(event => html` (event) => html` <li class="mdc-image-list__item">
<li class="mdc-image-list__item"> <div class="mdc-image-list__image-aspect-container">
<div class="mdc-image-list__image-aspect-container"> <img
<img data-toggle="tooltip"
data-toggle="tooltip" title="${this._getEventTitle(event)}" title="${this._getEventTitle(event)}"
class="mdc-image-list__image" class="mdc-image-list__image"
src="data:image/png;base64,${event.thumbnail}" src="data:image/png;base64,${event.thumbnail}"
@click=${() => { @click=${() => {
this._showMenu = false; this._showMenu = false;
this._viewEvent = event; this._viewEvent = event;
this._viewMode = want_clips ? "clip" : "snapshot"; this._viewMode = want_clips ? 'clip' : 'snapshot';
}} }}
> />
</div> </div>
</li>`)} </li>`,
</ul>`; )}
</ul>`;
} }
// Render a progress spinner while content loads. // Render a progress spinner while content loads.
protected _renderProgressIndicator(): TemplateResult { protected _renderProgressIndicator(): TemplateResult {
return html` return html` <div class="frigate-card-attention">
<div class="frigate-card-attention"> <ha-circular-progress active="true" size="large"></ha-circular-progress>
<ha-circular-progress </div>`;
active="true"
size="large"
></ha-circular-progress>
</div>`
} }
// Stop/Play video controls. // Stop/Play video controls.
protected _controlVideos({ protected _controlVideos({
stop, stop,
control_live = false, control_live = false,
control_clip = false}: ControlVideosParameters): void { control_clip = false,
}: ControlVideosParameters): void {
const controlVideo = (stop: boolean, is_live: boolean, video: HTMLVideoElement) => { const controlVideo = (stop: boolean, is_live: boolean, video: HTMLVideoElement) => {
if (video) { if (video) {
if (stop) { if (stop) {
@@ -464,7 +471,7 @@ export class FrigateCard extends LitElement {
video.play(); video.play();
} }
} }
} };
if (!this.shadowRoot) { if (!this.shadowRoot) {
return; return;
} }
@@ -472,8 +479,8 @@ export class FrigateCard extends LitElement {
controlVideo( controlVideo(
stop, stop,
false, false,
this.shadowRoot?. this.shadowRoot?.querySelector('video.frigate-card-viewer') as HTMLVideoElement,
querySelector('video.frigate-card-viewer') as HTMLVideoElement); );
} }
if (control_live) { if (control_live) {
// Don't have direct access to the live video player as it is buried in // Don't have direct access to the live video player as it is buried in
@@ -482,50 +489,49 @@ export class FrigateCard extends LitElement {
controlVideo( controlVideo(
stop, stop,
true, true,
this.shadowRoot?. this.shadowRoot?.querySelector('webrtc-camera video') as HTMLVideoElement,
querySelector('webrtc-camera video') as HTMLVideoElement );
)
} else { } else {
controlVideo( controlVideo(
stop, stop,
true, true,
this.shadowRoot?. this.shadowRoot
querySelector('ha-camera-stream')?.shadowRoot?. ?.querySelector('ha-camera-stream')
querySelector('ha-hls-player')?.shadowRoot?. ?.shadowRoot?.querySelector('ha-hls-player')
querySelector('video') as HTMLVideoElement ?.shadowRoot?.querySelector('video') as HTMLVideoElement,
) );
} }
} }
} }
protected _menuActionHandler(name: string): void { protected _menuActionHandler(name: string): void {
switch (name) { switch (name) {
case "live": case 'live':
this._controlVideos({stop: true, control_clip: true}); this._controlVideos({ stop: true, control_clip: true });
this._controlVideos({stop: false, control_live: true}); this._controlVideos({ stop: false, control_live: true });
this._viewMode = name; this._viewMode = name;
this._heading = null; this._heading = null;
break; break;
case "clips": case 'clips':
this._controlVideos({stop: true, control_live: true}); this._controlVideos({ stop: true, control_live: true });
this._viewMode = name; this._viewMode = name;
this._heading = null; this._heading = null;
break; break;
case "snapshots": case 'snapshots':
this._controlVideos({stop: true, control_clip: true, control_live: true}); this._controlVideos({ stop: true, control_clip: true, control_live: true });
this._viewMode = name; this._viewMode = name;
this._heading = null; this._heading = null;
break; break;
case "frigate-ui": case 'frigate-ui':
window.open(this.config.frigate_url); window.open(this.config.frigate_url);
break; break;
case "motion": case 'motion':
if (this.config.motion_entity) { if (this.config.motion_entity) {
fireEvent(this, "hass-more-info", {entityId: this.config.motion_entity}); fireEvent(this, 'hass-more-info', { entityId: this.config.motion_entity });
} }
break; break;
default: default:
console.warn("Unknown Frigate card menu option.") console.warn('Unknown Frigate card menu option.');
} }
} }
@@ -538,23 +544,21 @@ export class FrigateCard extends LitElement {
try { try {
events = await this._getEvents({ events = await this._getEvents({
has_clip: true, has_clip: true,
limit: 1 limit: 1,
}); });
} catch (e) { } catch (e) {
return this._renderError(e); return this._renderError(e);
} }
if (!events.length) { if (!events.length) {
return this._renderAttentionIcon("mdi:camera-off"); return this._renderAttentionIcon('mdi:camera-off');
} }
event = events[0]; event = events[0];
} }
this._heading = this._getEventTitle(event); this._heading = this._getEventTitle(event);
const url = `${this.config.frigate_url}/clips/` + const url = `${this.config.frigate_url}/clips/` + `${event.camera}-${event.id}.mp4`;
`${event.camera}-${event.id}.mp4`; return html` <video class="frigate-card-viewer" autoplay muted controls>
return html` <source src="${url}" type="video/mp4" />
<video class="frigate-card-viewer" autoplay muted controls> </video>`;
<source src="${url}" type="video/mp4">
</video>`
} }
// Render a snapshot. // Render a snapshot.
@@ -566,19 +570,19 @@ export class FrigateCard extends LitElement {
try { try {
events = await this._getEvents({ events = await this._getEvents({
has_snapshot: true, has_snapshot: true,
limit: 1 limit: 1,
}); });
} catch (e) { } catch (e) {
return this._renderError(e); return this._renderError(e);
} }
if (!events.length) { if (!events.length) {
return this._renderAttentionIcon("mdi:filmstrip-off"); return this._renderAttentionIcon('mdi:filmstrip-off');
} }
event = events[0]; event = events[0];
} }
this._heading = this._getEventTitle(event); this._heading = this._getEventTitle(event);
const url = `${this.config.frigate_url}/clips/${event.camera}-${event.id}.jpg`; const url = `${this.config.frigate_url}/clips/${event.camera}-${event.id}.jpg`;
return html`<img class="frigate-card-viewer" src="${url}">` return html`<img class="frigate-card-viewer" src="${url}" />`;
} }
// Render the live viewer. // Render the live viewer.
@@ -586,25 +590,21 @@ export class FrigateCard extends LitElement {
// is always rendered (but sometimes hidden). // is always rendered (but sometimes hidden).
protected _renderLiveViewer(): TemplateResult { protected _renderLiveViewer(): TemplateResult {
if (!this._hass || !(this.config.camera_entity in this._hass.states)) { if (!this._hass || !(this.config.camera_entity in this._hass.states)) {
return this._renderError("mdi:camera-off") return this._renderError('mdi:camera-off');
} }
if (this._webrtcElement) { if (this._webrtcElement) {
return html` return html` <div class=${this._viewMode == 'live' ? 'visible' : 'invisible'}>
<div ${this._webrtcElement}
class=${this._viewMode == "live" ? 'visible' : 'invisible'} </div>`;
>
${this._webrtcElement}
</div>`;
} }
return html` return html` <ha-camera-stream
<ha-camera-stream .hass=${this._hass}
.hass=${this._hass} .stateObj=${this._hass.states[this.config.camera_entity]}
.stateObj=${this._hass.states[this.config.camera_entity]} .controls=${true}
.controls=${true} .muted=${true}
.muted=${true} class=${this._viewMode == 'live' ? 'visible' : 'invisible'}
class=${this._viewMode == "live" ? 'visible' : 'invisible'} >
> </ha-camera-stream>`;
</ha-camera-stream>`;
} }
// Record interactions with the card. // Record interactions with the card.
@@ -633,25 +633,33 @@ export class FrigateCard extends LitElement {
return html` return html`
<ha-card @click=${this._interactionHandler}> <ha-card @click=${this._interactionHandler}>
</frigate-card-menu> </frigate-card-menu>
${this._viewMode == "clips" ? ${
html`<div class="frigate-card-gallery"> this._viewMode == 'clips'
${until(this._renderEvents(), this._renderProgressIndicator())} ? html`<div class="frigate-card-gallery">
</div>` : `` ${until(this._renderEvents(), this._renderProgressIndicator())}
</div>`
: ``
} }
${this._viewMode == "snapshots" ? ${
html`<div class="frigate-card-gallery"> this._viewMode == 'snapshots'
${until(this._renderEvents(), this._renderProgressIndicator())} ? html`<div class="frigate-card-gallery">
</div>` : `` ${until(this._renderEvents(), this._renderProgressIndicator())}
</div>`
: ``
} }
${this._viewMode == "clip" ? ${
html`<div class="frigate-card-viewer"> this._viewMode == 'clip'
${until(this._renderClipPlayer(), this._renderProgressIndicator())} ? html`<div class="frigate-card-viewer">
</div>` : `` ${until(this._renderClipPlayer(), this._renderProgressIndicator())}
</div>`
: ``
} }
${this._viewMode == "snapshot" ? ${
html`<div class="frigate-card-viewer"> this._viewMode == 'snapshot'
${until(this._renderSnapshotViewer(), this._renderProgressIndicator())} ? html`<div class="frigate-card-viewer">
</div>` : `` ${until(this._renderSnapshotViewer(), this._renderProgressIndicator())}
</div>`
: ``
} }
${this._renderLiveViewer()} ${this._renderLiveViewer()}
<frigate-card-menu <frigate-card-menu
@@ -665,9 +673,7 @@ export class FrigateCard extends LitElement {
// Show a warning card. // Show a warning card.
private _showWarning(warning: string): TemplateResult { private _showWarning(warning: string): TemplateResult {
return html` return html` <hui-warning> ${warning} </hui-warning> `;
<hui-warning> ${warning} </hui-warning>
`;
} }
// Show an error card. // Show an error card.
@@ -679,9 +685,7 @@ export class FrigateCard extends LitElement {
origConfig: this.config, origConfig: this.config,
}); });
return html` return html` ${errorCard} `;
${errorCard}
`;
} }
// Return compiled CSS styles (thus safe to use with unsafeCSS). // Return compiled CSS styles (thus safe to use with unsafeCSS).
+27 -9
View File
@@ -1,5 +1,10 @@
import { ActionConfig, LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from 'custom-card-helpers'; import {
import { number, z } from "zod"; ActionConfig,
LovelaceCard,
LovelaceCardConfig,
LovelaceCardEditor,
} from 'custom-card-helpers';
import { number, z } from 'zod';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -12,7 +17,13 @@ declare global {
* Internal types. * Internal types.
*/ */
export const FRIGATE_CARD_VIEWS = ["live", "clip", "clips", "snapshot", "snapshots"] as const; export const FRIGATE_CARD_VIEWS = [
'live',
'clip',
'clips',
'snapshot',
'snapshots',
] as const;
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number]; export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
export const frigateCardConfigSchema = z.object({ export const frigateCardConfigSchema = z.object({
@@ -20,10 +31,18 @@ export const frigateCardConfigSchema = z.object({
motion_entity: z.string().optional(), motion_entity: z.string().optional(),
frigate_url: z.string().url(), frigate_url: z.string().url(),
frigate_camera_name: z.string().optional(), frigate_camera_name: z.string().optional(),
view_default: z.enum(FRIGATE_CARD_VIEWS).optional().default("live"), view_default: z.enum(FRIGATE_CARD_VIEWS).optional().default('live'),
view_timeout: z.number().or(z.string().regex(/^\d+$/).transform(val => Number(val))).optional(), view_timeout: z
live_provider: z.enum(["frigate", "webrtc"]).default("frigate"), .number()
.or(
z
.string()
.regex(/^\d+$/)
.transform((val) => Number(val)),
)
.optional(),
live_provider: z.enum(['frigate', 'webrtc']).default('frigate'),
webrtc: z.object({}).passthrough().optional(), webrtc: z.object({}).passthrough().optional(),
label: z.string().optional(), label: z.string().optional(),
zone: z.string().optional(), zone: z.string().optional(),
@@ -33,10 +52,9 @@ export const frigateCardConfigSchema = z.object({
show_warning: z.boolean().optional(), show_warning: z.boolean().optional(),
show_error: z.boolean().optional(), show_error: z.boolean().optional(),
test_gui: z.boolean().optional(), test_gui: z.boolean().optional(),
}) });
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>; export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
export interface GetEventsParameters { export interface GetEventsParameters {
has_clip?: boolean; has_clip?: boolean;
has_snapshot?: boolean; has_snapshot?: boolean;
@@ -65,7 +83,7 @@ export const frigateEventSchema = z.object({
thumbnail: z.string(), thumbnail: z.string(),
top_score: z.number(), top_score: z.number(),
zones: z.string().array(), zones: z.string().array(),
}) });
export type FrigateEvent = z.infer<typeof frigateEventSchema>; export type FrigateEvent = z.infer<typeof frigateEventSchema>;
export const frigateGetEventsResponseSchema = z.array(frigateEventSchema); export const frigateGetEventsResponseSchema = z.array(frigateEventSchema);