Prettier formatting.
This commit is contained in:
+2
-1
@@ -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
@@ -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(),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
(options: ActionHandlerOptions = {}) =>
|
||||||
|
(part: PropertyPart): void => {
|
||||||
actionHandlerBind(part.committer.element as ActionHandlerElement, options);
|
actionHandlerBind(part.committer.element as ActionHandlerElement, options);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
+68
-58
@@ -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>
|
||||||
@@ -176,12 +182,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
@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,8 +215,7 @@ 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 || ''}
|
||||||
@@ -222,8 +231,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
@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,19 +240,21 @@ 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>
|
||||||
@@ -254,17 +264,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
@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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+151
-147
@@ -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,16 +112,17 @@ 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" title="Frigate menu"
|
data-toggle="tooltip"
|
||||||
|
title="Frigate menu"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.expand = !this.expand;
|
this.expand = !this.expand;
|
||||||
}}
|
}}
|
||||||
@@ -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
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="View motion sensor"
|
||||||
class="button"
|
class="button"
|
||||||
icon="${motionIcon}"
|
icon="${motionIcon}"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.expand = false;
|
this.expand = false;
|
||||||
this._callAction("motion");
|
this._callAction('motion');
|
||||||
}}
|
}}
|
||||||
></ha-icon-button>`
|
></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;
|
||||||
@@ -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,7 +307,7 @@ 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}">
|
|
||||||
</ha-icon>
|
|
||||||
</div>`;
|
</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" title="${this._getEventTitle(event)}"
|
data-toggle="tooltip"
|
||||||
|
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,23 +590,19 @@ 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
|
|
||||||
class=${this._viewMode == "live" ? 'visible' : 'invisible'}
|
|
||||||
>
|
|
||||||
${this._webrtcElement}
|
${this._webrtcElement}
|
||||||
</div>`;
|
</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>`;
|
||||||
}
|
}
|
||||||
@@ -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'
|
||||||
|
? html`<div class="frigate-card-gallery">
|
||||||
${until(this._renderEvents(), this._renderProgressIndicator())}
|
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||||
</div>` : ``
|
</div>`
|
||||||
|
: ``
|
||||||
}
|
}
|
||||||
${this._viewMode == "snapshots" ?
|
${
|
||||||
html`<div class="frigate-card-gallery">
|
this._viewMode == 'snapshots'
|
||||||
|
? html`<div class="frigate-card-gallery">
|
||||||
${until(this._renderEvents(), this._renderProgressIndicator())}
|
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||||
</div>` : ``
|
</div>`
|
||||||
|
: ``
|
||||||
}
|
}
|
||||||
${this._viewMode == "clip" ?
|
${
|
||||||
html`<div class="frigate-card-viewer">
|
this._viewMode == 'clip'
|
||||||
|
? html`<div class="frigate-card-viewer">
|
||||||
${until(this._renderClipPlayer(), this._renderProgressIndicator())}
|
${until(this._renderClipPlayer(), this._renderProgressIndicator())}
|
||||||
</div>` : ``
|
</div>`
|
||||||
|
: ``
|
||||||
}
|
}
|
||||||
${this._viewMode == "snapshot" ?
|
${
|
||||||
html`<div class="frigate-card-viewer">
|
this._viewMode == 'snapshot'
|
||||||
|
? html`<div class="frigate-card-viewer">
|
||||||
${until(this._renderSnapshotViewer(), this._renderProgressIndicator())}
|
${until(this._renderSnapshotViewer(), this._renderProgressIndicator())}
|
||||||
</div>` : ``
|
</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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user