Initial editor support.

This commit is contained in:
Dermot Duffy
2022-01-14 21:31:16 -08:00
parent b03c8b4020
commit c40c63862e
10 changed files with 417 additions and 120 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ const plugins = [
exclude: 'node_modules/**', exclude: 'node_modules/**',
}), }),
dev && serve(serveopts), dev && serve(serveopts),
!dev && terser(), //!dev && terser(),
]; ];
export default [ export default [
+2 -2
View File
@@ -429,8 +429,8 @@ export class FrigateCard extends LitElement {
} }
}; };
if (this.config.camera && Array.isArray(this.config.camera)) { if (this.config.cameras && Array.isArray(this.config.cameras)) {
await Promise.all(this.config.camera.map(addCameraConfig.bind(this))); await Promise.all(this.config.cameras.map(addCameraConfig.bind(this)));
} }
if (!cameras.size) { if (!cameras.size) {
+16 -3
View File
@@ -287,7 +287,8 @@ export function convertActionToFrigateCardCustomAction(
*/ */
export function createFrigateCardCustomAction( export function createFrigateCardCustomAction(
action: FrigateCardAction, action: FrigateCardAction,
camera?: string): FrigateCardCustomAction | undefined { camera?: string,
): FrigateCardCustomAction | undefined {
if (action == 'camera_select') { if (action == 'camera_select') {
if (!camera) { if (!camera) {
return undefined; return undefined;
@@ -296,12 +297,12 @@ export function createFrigateCardCustomAction(
action: 'fire-dom-event', action: 'fire-dom-event',
frigate_card_action: action, frigate_card_action: action,
camera: camera, camera: camera,
} };
} }
return { return {
action: 'fire-dom-event', action: 'fire-dom-event',
frigate_card_action: action, frigate_card_action: action,
} };
} }
/** /**
@@ -418,3 +419,15 @@ export function refreshCameraConfigDynamicParameters(
config.icon = config.icon ?? (state ? stateIcon(state) : 'mdi:video'); config.icon = config.icon ?? (state ? stateIcon(state) : 'mdi:video');
return config; return config;
} }
/**
* Move an element within an array.
* @param target Target array.
* @param from From index.
* @param to To index.
*/
export function arrayMove(target: unknown[], from: number, to: number): void {
const element = target[from];
target.splice(from, 1);
target.splice(to, 0, element);
}
+3 -1
View File
@@ -1,7 +1,9 @@
// TODO editor // TODO editor
// TODO editor: fill in a new camera and keep focus
// TODO editor: fill in a new camera then backspace it away
// TODO webrtc entities in camera section? // TODO webrtc entities in camera section?
// TODO conditional elements based on camera name (requires event changed to propagate upwards) // TODO conditional elements based on camera name (requires event changed to propagate upwards)
// TODO change url to frigate_url? // TODO change url to frigate_url? Would need to also fix upgrade logic to refer to new name.
// TODO Remove media load event warning // TODO Remove media load event warning
// TODO readme // TODO readme
// TODO search for TODOs // TODO search for TODOs
+105 -28
View File
@@ -1,14 +1,16 @@
import delve from 'dlv'; import delve from 'dlv';
import { dset } from 'dset'; import { dset } from 'dset';
import { import {
CONF_CAMERAS,
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
CONF_CAMERAS_ARRAY_CAMERA_NAME,
CONF_CAMERAS_ARRAY_CLIENT_ID,
CONF_CAMERAS_ARRAY_LABEL,
CONF_CAMERAS_ARRAY_URL,
CONF_CAMERAS_ARRAY_ZONE,
CONF_EVENT_VIEWER_AUTOPLAY_CLIP, CONF_EVENT_VIEWER_AUTOPLAY_CLIP,
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_FRIGATE_CAMERA_NAME,
CONF_FRIGATE_CLIENT_ID,
CONF_FRIGATE_LABEL,
CONF_FRIGATE_URL,
CONF_FRIGATE_ZONE,
CONF_IMAGE_SRC, CONF_IMAGE_SRC,
CONF_LIVE_PRELOAD, CONF_LIVE_PRELOAD,
CONF_LIVE_PROVIDER, CONF_LIVE_PROVIDER,
@@ -18,7 +20,7 @@ import {
CONF_VIEW_TIMEOUT, CONF_VIEW_TIMEOUT,
CONF_VIEW_UPDATE_ENTITIES, CONF_VIEW_UPDATE_ENTITIES,
} from './const'; } from './const';
import { RawFrigateCardConfig } from './types'; import { RawFrigateCardConfig, RawFrigateCardConfigArray } from './types';
/** /**
* Set a configuration value. * Set a configuration value.
@@ -26,12 +28,13 @@ import { RawFrigateCardConfig } from './types';
* @param key The key to the property to set. * @param key The key to the property to set.
* @param value The value to set. * @param value The value to set.
*/ */
export const setConfigValue = ( export const setConfigValue = (
obj: RawFrigateCardConfig, obj: RawFrigateCardConfig,
key: string, keys: string | (string|number)[],
value: unknown, value: unknown,
): void => { ): void => {
dset(obj, key, value); dset(obj, keys, value);
}; };
/** /**
@@ -42,10 +45,14 @@ export const setConfigValue = (
*/ */
export const getConfigValue = ( export const getConfigValue = (
obj: RawFrigateCardConfig, obj: RawFrigateCardConfig,
key: string, keys: string | (string|number)[],
def?: unknown, def?: unknown,
): unknown => { ): unknown => {
return delve(obj, key, def); // Need to manually split the key apart to use delve array accesses by number.
if (typeof(keys) === 'string') {
keys = keys.split('.');
}
return delve(obj, keys, def);
}; };
/** /**
@@ -93,19 +100,23 @@ export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean
/** /**
* Remove empty sections from a configuration. * Remove empty sections from a configuration.
* @param obj Configuration object. * @param obj Configuration object.
* @returns `true` if the configuration was modified.
*/ */
export const trimConfig = function (obj: RawFrigateCardConfig): void { export const trimConfig = function (obj: RawFrigateCardConfig): boolean {
const keys = Object.keys(obj); const keys = Object.keys(obj);
let modified = false;
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i]; const key = keys[i];
if (typeof obj[key] === 'object' && obj[key] != null) { if (typeof obj[key] === 'object' && obj[key] != null) {
trimConfig(obj[key] as RawFrigateCardConfig); modified ||= trimConfig(obj[key] as RawFrigateCardConfig);
if (!Object.keys(obj[key] as RawFrigateCardConfig).length) { if (!Object.keys(obj[key] as RawFrigateCardConfig).length) {
delete obj[key]; delete obj[key];
modified = true;
} }
} }
} }
return modified;
}; };
/** /**
@@ -128,6 +139,32 @@ const isNotObject = function (value: unknown) {
/** /**
* Move a property from one location to another. * Move a property from one location to another.
* @param obj The configuration object in which the property resides.
* @param oldPath The old property path.
* @param newPath The new property path.
* @param transform An optional transform for the value.
* @returns `true` if the configuration was modified.
*/
export const moveConfigValue = (
obj: RawFrigateCardConfig,
oldPath: string,
newPath: string,
transform?: (valueIn: unknown) => (unknown),
): boolean => {
let value = getConfigValue(obj, oldPath);
if (transform) {
value = transform(value);
}
if (typeof value !== 'undefined') {
deleteConfigValue(obj, oldPath);
setConfigValue(obj, newPath, value);
return true;
}
return false;
};
/**
* Upgrade by moving a property from one location to another.
* @param oldPath The old property path. * @param oldPath The old property path.
* @param newPath The new property path. * @param newPath The new property path.
* @param transform An optional transform for the value. * @param transform An optional transform for the value.
@@ -136,29 +173,66 @@ const isNotObject = function (value: unknown) {
const upgradeMoveTo = function ( const upgradeMoveTo = function (
oldPath: string, oldPath: string,
newPath: string, newPath: string,
transform?: (valueIn: unknown) => unknown, transform?: (valueIn: unknown) => (unknown),
): (obj: RawFrigateCardConfig) => boolean { ): (obj: RawFrigateCardConfig) => boolean {
return function (obj: RawFrigateCardConfig): boolean { return function (obj: RawFrigateCardConfig): boolean {
let value = getConfigValue(obj, oldPath); return moveConfigValue(obj, oldPath, newPath, transform);
if (transform) {
value = transform(value);
}
if (typeof value !== 'undefined') {
deleteConfigValue(obj, oldPath);
setConfigValue(obj, newPath, value);
return true;
}
return false;
}; };
}; };
/**
* Sanitize a potentially-unsafe key segment.
* @param key A string key.
* @returns A safe key.
*/
// const sanitizeKeySegment = (key: string): string => {
// return key.replace(/\.+/g, '_');
// }
const upgradeToMultipleCameras = (): ((obj: RawFrigateCardConfig) => boolean) => {
return function (obj: RawFrigateCardConfig): boolean {
let modified = false;
const camera = {}
const imports = {
'camera_entity': CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
'frigate.camera_name': CONF_CAMERAS_ARRAY_CAMERA_NAME,
'frigate.client_id': CONF_CAMERAS_ARRAY_CLIENT_ID,
'frigate.label': CONF_CAMERAS_ARRAY_LABEL,
'frigate.url': CONF_CAMERAS_ARRAY_URL,
'frigate.zone': CONF_CAMERAS_ARRAY_ZONE,
}
Object.keys(imports).forEach((key) => {
const oldValue = getConfigValue(obj, key);
if (oldValue !== undefined) {
camera[imports[key]] = oldValue;
deleteConfigValue(obj, key)
modified = true;
}
})
if (modified) {
let cameras = getConfigValue(obj, CONF_CAMERAS) as RawFrigateCardConfigArray;
if (!Array.isArray(cameras)) {
// Note: This will replace `cameras` if it already exists and isn't an
// array.
cameras = []
}
cameras.push(camera);
setConfigValue(obj, CONF_CAMERAS, cameras)
trimConfig(obj);
}
return modified;
}
}
const UPGRADES = [ const UPGRADES = [
// v1.2.1 -> v2.0.0 // v1.2.1 -> v2.0.0
upgradeMoveTo('frigate_url', CONF_FRIGATE_URL), upgradeMoveTo('frigate_url', 'frigate.url'),
upgradeMoveTo('frigate_client_id', CONF_FRIGATE_CLIENT_ID), upgradeMoveTo('frigate_client_id', 'frigate.client_id'),
upgradeMoveTo('frigate_camera_name', CONF_FRIGATE_CAMERA_NAME), upgradeMoveTo('frigate_camera_name', 'frigate.camera_name'),
upgradeMoveTo('label', CONF_FRIGATE_LABEL), upgradeMoveTo('label', 'frigate.label'),
upgradeMoveTo('zone', CONF_FRIGATE_ZONE), upgradeMoveTo('zone', 'frigate.zone'),
upgradeMoveTo('view_default', CONF_VIEW_DEFAULT), upgradeMoveTo('view_default', CONF_VIEW_DEFAULT),
upgradeMoveTo('view_timeout', CONF_VIEW_TIMEOUT), upgradeMoveTo('view_timeout', CONF_VIEW_TIMEOUT),
upgradeMoveTo('live_provider', CONF_LIVE_PROVIDER), upgradeMoveTo('live_provider', CONF_LIVE_PROVIDER),
@@ -174,4 +248,7 @@ const UPGRADES = [
// v2.0.0 -> v2.1.0 // v2.0.0 -> v2.1.0
upgradeMoveTo('update_entities', CONF_VIEW_UPDATE_ENTITIES), upgradeMoveTo('update_entities', CONF_VIEW_UPDATE_ENTITIES),
// v2.1.0 -> v3.0.0
upgradeToMultipleCameras(),
]; ];
+7 -6
View File
@@ -2,12 +2,13 @@ export const CARD_VERSION = '2.1.0';
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card'; export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card';
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting`; export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting`;
export const CONF_CAMERA_ENTITY = 'camera_entity'; export const CONF_CAMERAS = 'cameras';
export const CONF_FRIGATE_CAMERA_NAME = 'frigate.camera_name'; export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY = 'cameras.#.camera_entity';
export const CONF_FRIGATE_CLIENT_ID = 'frigate.client_id'; export const CONF_CAMERAS_ARRAY_CAMERA_NAME = 'cameras.#.camera_name';
export const CONF_FRIGATE_LABEL = 'frigate.label'; export const CONF_CAMERAS_ARRAY_CLIENT_ID = 'cameras.#.client_id';
export const CONF_FRIGATE_URL = 'frigate.url'; export const CONF_CAMERAS_ARRAY_LABEL = 'cameras.#.label';
export const CONF_FRIGATE_ZONE = 'frigate.zone'; export const CONF_CAMERAS_ARRAY_URL = 'cameras.#.url';
export const CONF_CAMERAS_ARRAY_ZONE = 'cameras.#.zone';
export const CONF_VIEW_DEFAULT = 'view.default'; export const CONF_VIEW_DEFAULT = 'view.default';
export const CONF_VIEW_TIMEOUT = 'view.timeout'; export const CONF_VIEW_TIMEOUT = 'view.timeout';
+240 -68
View File
@@ -5,20 +5,20 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import { frigateCardConfigDefaults, RawFrigateCardConfig } from './types.js'; import {
frigateCardConfigDefaults,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
} from './types.js';
import frigate_card_editor_style from './scss/editor.scss';
import { import {
copyConfig, CONF_CAMERAS,
deleteConfigValue, CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
getConfigValue, CONF_CAMERAS_ARRAY_CAMERA_NAME,
isConfigUpgradeable, CONF_CAMERAS_ARRAY_CLIENT_ID,
setConfigValue, CONF_CAMERAS_ARRAY_LABEL,
trimConfig, CONF_CAMERAS_ARRAY_URL,
upgradeConfig, CONF_CAMERAS_ARRAY_ZONE,
} from './config-mgmt.js';
import {
CONF_CAMERA_ENTITY,
CONF_DIMENSIONS_ASPECT_RATIO, CONF_DIMENSIONS_ASPECT_RATIO,
CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_DIMENSIONS_ASPECT_RATIO_MODE,
CONF_EVENT_VIEWER_AUTOPLAY_CLIP, CONF_EVENT_VIEWER_AUTOPLAY_CLIP,
@@ -28,11 +28,6 @@ import {
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
CONF_EVENT_VIEWER_DRAGGABLE, CONF_EVENT_VIEWER_DRAGGABLE,
CONF_EVENT_VIEWER_LAZY_LOAD, CONF_EVENT_VIEWER_LAZY_LOAD,
CONF_FRIGATE_CAMERA_NAME,
CONF_FRIGATE_CLIENT_ID,
CONF_FRIGATE_LABEL,
CONF_FRIGATE_URL,
CONF_FRIGATE_ZONE,
CONF_IMAGE_SRC, CONF_IMAGE_SRC,
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
CONF_LIVE_CONTROLS_THUMBNAILS_MODE, CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
@@ -55,6 +50,17 @@ import {
CONF_VIEW_TIMEOUT, CONF_VIEW_TIMEOUT,
CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_FORCE,
} from './const.js'; } from './const.js';
import { arrayMove } from './common.js';
import {
copyConfig,
deleteConfigValue,
getConfigValue,
isConfigUpgradeable,
setConfigValue,
upgradeConfig,
} from './config-mgmt.js';
import frigate_card_editor_style from './scss/editor.scss';
interface EditorOptionsSet { interface EditorOptionsSet {
icon: string; icon: string;
@@ -66,29 +72,27 @@ interface EditorOptions {
[setName: string]: EditorOptionsSet; [setName: string]: EditorOptionsSet;
} }
interface EditorOptionTarget { interface ConfigValueTarget {
configValue: string; configValue: string;
checked?: boolean; checked?: boolean;
value?: string; value?: string;
} }
interface EditorCameraTarget {
cameraIndex: number;
}
interface EditorOptionSetTarget { interface EditorOptionSetTarget {
optionSetName: string; optionSetName: string;
} }
const options: EditorOptions = { const options: EditorOptions = {
basic: { cameras: {
icon: 'cog', icon: 'video',
name: localize('editor.basic'), name: localize('editor.cameras'),
secondary: localize('editor.basic_secondary'), secondary: localize('editor.cameras_secondary'),
show: true, show: true,
}, },
frigate: {
icon: 'alpha-f-box',
name: localize('editor.frigate'),
secondary: localize('editor.frigate_secondary'),
show: false,
},
view: { view: {
icon: 'eye', icon: 'eye',
name: localize('editor.view'), name: localize('editor.view'),
@@ -135,6 +139,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
protected _initialized = false; protected _initialized = false;
protected _configUpgradeable = false; protected _configUpgradeable = false;
@property({ attribute: false })
protected _expandedCameraIndex: number | null = null;
public setConfig(config: RawFrigateCardConfig): void { public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be // Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially // partially or completely invalid. It's more useful to have a partially
@@ -190,6 +197,20 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
`; `;
} }
/**
* Get a localized help label for a given config path.
* @param configPath The config path.
* @returns A localized label.
*/
protected _getLabel(configPath: string): string {
// Strip out single number path components as they are array indicies.
const path = configPath
.split('.')
.filter((e) => isNaN(Number(e)))
.join('.');
return localize(`config.${path}`);
}
/** /**
* Render a dropdown menu. * Render a dropdown menu.
* @param configPath The configuration path to set/read. * @param configPath The configuration path to set/read.
@@ -207,7 +228,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return html` return html`
<paper-dropdown-menu <paper-dropdown-menu
.label=${localize(`config.${configPath}`)} .label=${this._getLabel(configPath)}
@value-changed=${this._valueChangedHandler} @value-changed=${this._valueChangedHandler}
.configValue=${configPath} .configValue=${configPath}
> >
@@ -225,6 +246,160 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
`; `;
} }
/**
* Render a camera header.
* @param cameraIndex The index of the camera to edit/add.
* @param cameraConfig The configuration of the camera in question.
* @param addNewCamera Whether or not this is a header to add a new camera.
* @returns A rendered template.
*/
protected _renderCameraHeader(
cameraIndex: number,
cameraConfig: RawFrigateCardConfig | undefined,
addNewCamera?: boolean,
): TemplateResult {
return html`
<div
class="camera-header"
@click=${this._toggleCameraHandler}
.cameraIndex=${cameraIndex}
>
<div>
<ha-icon .icon=${addNewCamera ? 'mdi:video-plus' : 'mdi:video'}></ha-icon>
<span>
${addNewCamera
? html` <span class="new-camera">
[${localize('editor.add_new_camera')}...]
</span>`
: // Attempt to render a recognizable name for the camera,
// starting with the most likely to be useful and working our
// ways towards the least useful.
html` <span>
Camera:
${cameraConfig?.title ||
cameraConfig?.camera_entity ||
cameraConfig?.card_id ||
[
cameraConfig?.client_id,
cameraConfig?.camera_name,
cameraConfig?.label,
cameraConfig?.zone,
]
.filter(Boolean)
.join(' / ') ||
cameraIndex}
</span>`}
</span>
</div>
</div>
`;
}
/**
* Render a camera section.
* @param cameras The full array of cameras.
* @param cameraIndex The index (in the array) to render.
* @param cameraEntities The full list of camera entities.
* @param addNewCamera Whether or not this is a section to add a new non-existent camera.
* @returns A rendered template.
*/
protected _renderCamera(
cameras: RawFrigateCardConfigArray,
cameraIndex: number,
cameraEntities: string[],
addNewCamera?: boolean,
): TemplateResult | void {
// Get the config path for this camera (taking into account its camera index).
const getArrayPath = (path: string): string => {
return path.replace('#', cameraIndex.toString());
};
// Make a new config and update the editor with changes on it,
const modifyConfig = (func: (config: RawFrigateCardConfig) => boolean): void => {
if (this._config) {
const newConfig = copyConfig(this._config);
if (func(newConfig)) {
this._updateConfig(newConfig);
}
}
};
return html`
${this._renderCameraHeader(cameraIndex, cameras[cameraIndex], addNewCamera)}
${this._expandedCameraIndex === cameraIndex
? html` <div class="values">
<div class="controls">
<ha-icon-button
class="button"
.label=${localize('editor.move_up')}
.disabled=${!this._config ||
!Array.isArray(this._config.cameras) ||
cameraIndex <= 0}
@click=${() =>
modifyConfig((config: RawFrigateCardConfig): boolean => {
if (Array.isArray(config.cameras) && cameraIndex > 0) {
arrayMove(config.cameras, cameraIndex, cameraIndex - 1);
this._expandedCameraIndex = cameraIndex - 1;
return true;
}
return false;
})}
>
<ha-icon icon="mdi:arrow-up"></ha-icon>
</ha-icon-button>
<ha-icon-button
class="button"
.label=${localize('editor.move_down')}
.disabled=${!this._config ||
!Array.isArray(this._config.cameras) ||
cameraIndex >= this._config.cameras.length - 1}
@click=${() =>
modifyConfig((config: RawFrigateCardConfig): boolean => {
if (
Array.isArray(config.cameras) &&
cameraIndex < config.cameras.length - 1
) {
arrayMove(config.cameras, cameraIndex, cameraIndex + 1);
this._expandedCameraIndex = cameraIndex + 1;
return true;
}
return false;
})}
>
<ha-icon icon="mdi:arrow-down"></ha-icon>
</ha-icon-button>
<ha-icon-button
class="button"
.label=${localize('editor.delete')}
.disabled=${addNewCamera}
@click=${() => {
modifyConfig((config: RawFrigateCardConfig): boolean => {
if (Array.isArray(config.cameras)) {
config.cameras.splice(cameraIndex, 1);
this._expandedCameraIndex = null;
return true;
}
return false;
});
}}
>
<ha-icon icon="mdi:delete"></ha-icon>
</ha-icon-button>
</div>
${this._renderDropdown(
getArrayPath(CONF_CAMERAS_ARRAY_CAMERA_ENTITY),
cameraEntities,
)}
${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_CAMERA_NAME))}
${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_URL))}
${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_LABEL))}
${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_ZONE))}
${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_CLIENT_ID))}
</div>`
: ``}
`;
}
/** /**
* Render a string input field. * Render a string input field.
* @param configPath The configuration path to set/read. * @param configPath The configuration path to set/read.
@@ -239,7 +414,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return; return;
} }
return html` <paper-input return html` <paper-input
label=${localize(`config.${configPath}`)} label=${this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')} .value=${getConfigValue(this._config, configPath, '')}
.configValue=${configPath} .configValue=${configPath}
allowed-pattern=${ifDefined(allowedPattern ? allowedPattern : undefined)} allowed-pattern=${ifDefined(allowedPattern ? allowedPattern : undefined)}
@@ -263,7 +438,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
if (!this._config) { if (!this._config) {
return; return;
} }
return html` <ha-formfield .label=${label || localize(`config.${configPath}`)}> return html` <ha-formfield .label=${label || this._getLabel(configPath)}>
<ha-switch <ha-switch
.checked="${getConfigValue(this._config, configPath, valueDefault)}" .checked="${getConfigValue(this._config, configPath, valueDefault)}"
.configValue=${configPath} .configValue=${configPath}
@@ -272,6 +447,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</ha-formfield>`; </ha-formfield>`;
} }
protected _updateConfig(config: RawFrigateCardConfig): void {
this._config = config;
fireEvent(this, 'config-changed', { config: this._config });
}
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this.hass || !this._helpers || !this._config) { if (!this.hass || !this._helpers || !this._config) {
return html``; return html``;
@@ -353,6 +533,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
const getShowButtonLabel = (configPath: string) => const getShowButtonLabel = (configPath: string) =>
localize('editor.show_button') + ': ' + localize(`config.${configPath}`); localize('editor.show_button') + ': ' + localize(`config.${configPath}`);
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
[]) as RawFrigateCardConfigArray;
return html` return html`
${this._configUpgradeable ${this._configUpgradeable
? html` <div class="upgrade"> ? html` <div class="upgrade">
@@ -365,10 +548,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
if (this._config) { if (this._config) {
const upgradedConfig = copyConfig(this._config); const upgradedConfig = copyConfig(this._config);
upgradeConfig(upgradedConfig); upgradeConfig(upgradedConfig);
this._config = upgradedConfig; this._updateConfig(upgradedConfig);
fireEvent(this, 'config-changed', { config: this._config });
this.requestUpdate();
} }
}} }}
> >
@@ -378,25 +558,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<br />` <br />`
: html``} : html``}
<div class="card-config"> <div class="card-config">
${this._renderOptionSetHeader('basic')} ${this._renderOptionSetHeader('cameras')}
${options.basic.show ${options.cameras.show
? html` ? html` <div class="cameras">
<div class="values"> ${cameras.map((_, index) =>
${this._renderDropdown(CONF_CAMERA_ENTITY, cameraEntities)} this._renderCamera(cameras, index, cameraEntities),
</div> )}
` ${this._renderCamera(cameras, cameras.length, cameraEntities, true)}
: ''} </div>`
${this._renderOptionSetHeader('frigate')}
${options.frigate.show
? html`
<div class="values">
${this._renderStringInput(CONF_FRIGATE_CAMERA_NAME)}
${this._renderStringInput(CONF_FRIGATE_URL)}
${this._renderStringInput(CONF_FRIGATE_LABEL)}
${this._renderStringInput(CONF_FRIGATE_ZONE)}
${this._renderStringInput(CONF_FRIGATE_CLIENT_ID)}
</div>
`
: ''} : ''}
${this._renderOptionSetHeader('view')} ${this._renderOptionSetHeader('view')}
${options.view.show ${options.view.show
@@ -404,9 +573,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="values"> <div class="values">
${this._renderDropdown(CONF_VIEW_DEFAULT, viewModes)} ${this._renderDropdown(CONF_VIEW_DEFAULT, viewModes)}
${this._renderStringInput(CONF_VIEW_TIMEOUT, '[0-9]')} ${this._renderStringInput(CONF_VIEW_TIMEOUT, '[0-9]')}
${this._renderSwitch( ${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
CONF_VIEW_UPDATE_FORCE,
defaults.view.update_force)}
</div> </div>
` `
: ''} : ''}
@@ -541,6 +708,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
this._helpers = await (window as any).loadCardHelpers(); this._helpers = await (window as any).loadCardHelpers();
} }
/**
* Display/hide a camera section.
* @param ev The event triggering the change.
*/
protected _toggleCameraHandler(ev: { target: EditorCameraTarget | null }): void {
if (ev && ev.target) {
this._expandedCameraIndex =
this._expandedCameraIndex == ev.target.cameraIndex
? null
: ev.target.cameraIndex;
}
}
/** /**
* Handle a toggled set of options. * Handle a toggled set of options.
* @param ev The event triggering the change. * @param ev The event triggering the change.
@@ -573,7 +753,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
* @param ev Event triggering the change. * @param ev Event triggering the change.
*/ */
protected _valueChangedHandler(ev: { protected _valueChangedHandler(ev: {
target: (EditorOptionTarget & HTMLElement) | null; target: (ConfigValueTarget & HTMLElement) | null;
}): void { }): void {
const target = ev.target; const target = ev.target;
if (!this._config || !this.hass || !target) { if (!this._config || !this.hass || !target) {
@@ -593,19 +773,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
const newConfig = copyConfig(this._config); const newConfig = copyConfig(this._config);
if (value === '' || typeof value === 'undefined') { if (value === '' || typeof value === 'undefined') {
// Don't delete empty properties that are from a dropdown menu. An empty deleteConfigValue(newConfig, key);
// property in that context may just be a user-entered value that is not
// in the valid choices in the dropdown. This probably won't end well for
// the user anyway, but having the whole property deleted the moment they
// press a key is very jarring.
if (target.tagName != 'PAPER-DROPDOWN-MENU') {
deleteConfigValue(newConfig, key);
}
} else { } else {
setConfigValue(newConfig, key, value); setConfigValue(newConfig, key, value);
} }
this._config = newConfig; this._updateConfig(newConfig);
fireEvent(this, 'config-changed', { config: this._config });
} }
/** /**
+10 -8
View File
@@ -9,9 +9,9 @@
"no_clip": "No recent clip" "no_clip": "No recent clip"
}, },
"config": { "config": {
"camera_entity": "Camera Entity", "cameras": {
"frigate": { "camera_entity": "Camera Entity",
"camera_name": "Frigate camera name (Optional, autodetected from entity)", "camera_name": "Frigate camera name (Autodetected from entity)",
"client_id": "Frigate client id (For >1 Frigate server)", "client_id": "Frigate client id (For >1 Frigate server)",
"label": "Frigate label/object filter", "label": "Frigate label/object filter",
"url": "Frigate server URL", "url": "Frigate server URL",
@@ -121,10 +121,8 @@
} }
}, },
"editor": { "editor": {
"basic": "Basic", "cameras": "Cameras",
"basic_secondary": "Options for most users", "cameras_secondary": "What cameras to render on this card",
"frigate": "Frigate",
"frigate_secondary": "Frigate server options",
"view": "View", "view": "View",
"view_secondary": "What the card should show and how to show it", "view_secondary": "What the card should show and how to show it",
"menu": "Menu", "menu": "Menu",
@@ -139,7 +137,11 @@
"dimensions_secondary": "Dimensions & shape options", "dimensions_secondary": "Dimensions & shape options",
"show_button": "Show button", "show_button": "Show button",
"upgrade": "Upgrade", "upgrade": "Upgrade",
"upgrade_available": "An automatic card configuration upgrade is available" "upgrade_available": "An automatic card configuration upgrade is available",
"delete": "Delete",
"move_up": "Move up",
"move_down": "Move down",
"add_new_camera": "Add new camera"
}, },
"error": { "error": {
"empty_response": "Received empty response from Home Assistant for request", "empty_response": "Received empty response from Home Assistant for request",
+31 -2
View File
@@ -1,3 +1,5 @@
@use './button.scss';
.option { .option {
padding: 4px 0px; padding: 4px 0px;
cursor: pointer; cursor: pointer;
@@ -18,8 +20,8 @@
pointer-events: none; pointer-events: none;
} }
.values { .values {
padding-left: 32px; margin-left: 30px;
padding-top: 10px; padding: 10px;
background: var(--secondary-background-color); background: var(--secondary-background-color);
display: grid; display: grid;
} }
@@ -38,3 +40,30 @@ div.upgrade {
div.upgrade span { div.upgrade span {
padding: 10px; padding: 10px;
} }
.camera-header {
margin-top: 4px;
cursor: pointer;
}
.camera-header * {
// Only allow clicks on the outermost header.
pointer-events: none;
}
.camera-header ha-icon {
padding-right: 10px;
}
.camera-header .new-camera {
font-style: italic;
}
.cameras {
margin: 5px 5px 10px 20px;
}
.cameras .controls {
display: inline-block;
margin-left: auto;
margin-right: 0px;
}
.cameras .controls ha-icon-button.button {
--mdc-icon-button-size: 32px;
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
}
+2 -1
View File
@@ -650,7 +650,7 @@ const dimensionsConfigSchema = z
*/ */
export const frigateCardConfigSchema = z.object({ export const frigateCardConfigSchema = z.object({
// Main configuration sections. // Main configuration sections.
camera: cameraConfigDefaultSchema.array().nonempty(), cameras: cameraConfigDefaultSchema.array().nonempty(),
view: viewConfigSchema, view: viewConfigSchema,
menu: menuConfigSchema, menu: menuConfigSchema,
live: liveConfigSchema, live: liveConfigSchema,
@@ -666,6 +666,7 @@ export const frigateCardConfigSchema = z.object({
}); });
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>; export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
export type RawFrigateCardConfig = Record<string, unknown>; export type RawFrigateCardConfig = Record<string, unknown>;
export type RawFrigateCardConfigArray = Record<string, unknown>[];
export const frigateCardConfigDefaults = { export const frigateCardConfigDefaults = {
cameras: cameraConfigDefault, cameras: cameraConfigDefault,