Files
advanced-camera-card/src/editor.ts
T

3688 lines
131 KiB
TypeScript

import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import './components/icon.js';
import './components/key-assigner.js';
import { renderMessage } from './components/message.js';
import {
copyConfig,
deleteConfigValue,
getArrayConfigPath,
getConfigValue,
isConfigUpgradeable,
setConfigValue,
upgradeConfig,
} from './config/management.js';
import { setProfiles } from './config/profiles/set-profiles.js';
import {
BUTTON_SIZE_MIN,
MENU_PRIORITY_MAX,
STATUS_BAR_PRIORITY_MAX,
} from './config/schema/common/const.js';
import {
THUMBNAIL_WIDTH_MAX,
THUMBNAIL_WIDTH_MIN,
} from './config/schema/common/controls/thumbnails.js';
import { ZOOM_MAX, ZOOM_MIN } from './config/schema/common/zoom.js';
import { profilesSchema } from './config/schema/profiles.js';
import { STATUS_BAR_HEIGHT_MIN } from './config/schema/status-bar.js';
import { AdvancedCameraCardConfig, configDefaults } from './config/schema/types.js';
import { KeyboardShortcut } from './config/schema/view.js';
import {
RawAdvancedCameraCardConfig,
RawAdvancedCameraCardConfigArray,
} from './config/types.js';
import {
CONF_CAMERAS,
CONF_CAMERAS_ARRAY_ALWAYS_ERROR_IF_ENTITY_UNAVAILABLE,
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE,
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT,
CONF_CAMERAS_ARRAY_CAPABILITIES_FORCE,
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH,
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH,
CONF_CAMERAS_ARRAY_CAST_METHOD,
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
CONF_CAMERAS_ARRAY_DIMENSIONS_ASPECT_RATIO,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_PAN_X,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_PAN_Y,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_ZOOM_FACTOR,
CONF_CAMERAS_ARRAY_DIMENSIONS_ROTATION,
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID,
CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
CONF_CAMERAS_ARRAY_FRIGATE_URL,
CONF_CAMERAS_ARRAY_FRIGATE_ZONES,
CONF_CAMERAS_ARRAY_GO2RTC_METADATA_FETCH_TIMEOUT_SECONDS,
CONF_CAMERAS_ARRAY_GO2RTC_MODES,
CONF_CAMERAS_ARRAY_GO2RTC_STREAM,
CONF_CAMERAS_ARRAY_GO2RTC_URL,
CONF_CAMERAS_ARRAY_ICON,
CONF_CAMERAS_ARRAY_ID,
CONF_CAMERAS_ARRAY_IMAGE_ENTITY,
CONF_CAMERAS_ARRAY_IMAGE_ENTITY_PARAMETERS,
CONF_CAMERAS_ARRAY_IMAGE_MODE,
CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
CONF_CAMERAS_ARRAY_IMAGE_URL,
CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
CONF_CAMERAS_ARRAY_MEDIA_EVENTS_TYPE,
CONF_CAMERAS_ARRAY_MEDIA_FOLDERS,
CONF_CAMERAS_ARRAY_MEDIA_REVIEWED,
CONF_CAMERAS_ARRAY_MEDIA_TYPE,
CONF_CAMERAS_ARRAY_MOTIONEYE_IMAGES_DIRECTORY_PATTERN,
CONF_CAMERAS_ARRAY_MOTIONEYE_IMAGES_FILE_PATTERN,
CONF_CAMERAS_ARRAY_MOTIONEYE_MOVIES_DIRECTORY_PATTERN,
CONF_CAMERAS_ARRAY_MOTIONEYE_MOVIES_FILE_PATTERN,
CONF_CAMERAS_ARRAY_MOTIONEYE_URL,
CONF_CAMERAS_ARRAY_PROXY_DYNAMIC,
CONF_CAMERAS_ARRAY_PROXY_LIVE,
CONF_CAMERAS_ARRAY_PROXY_MEDIA,
CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS,
CONF_CAMERAS_ARRAY_PROXY_SSL_VERIFICATION,
CONF_CAMERAS_ARRAY_REOLINK_MEDIA_RESOLUTION,
CONF_CAMERAS_ARRAY_REOLINK_URL,
CONF_CAMERAS_ARRAY_TITLE,
CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES,
CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS,
CONF_CAMERAS_ARRAY_TRIGGERS_MOTION,
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
CONF_CAMERAS_ARRAY_TRIGGERS_REVIEWS_DESCRIPTION,
CONF_CAMERAS_ARRAY_TRIGGERS_REVIEWS_SEVERITIES,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
CONF_DIMENSIONS_ASPECT_RATIO,
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
CONF_DIMENSIONS_HEIGHT,
CONF_FOLDERS,
CONF_FOLDERS_ARRAY_HA_URL,
CONF_FOLDERS_ARRAY_ICON,
CONF_FOLDERS_ARRAY_ID,
CONF_FOLDERS_ARRAY_TITLE,
CONF_FOLDERS_ARRAY_TYPE,
CONF_IMAGE_ENTITY,
CONF_IMAGE_ENTITY_PARAMETERS,
CONF_IMAGE_MODE,
CONF_IMAGE_REFRESH_SECONDS,
CONF_IMAGE_URL,
CONF_LIVE_AUTO_MUTE,
CONF_LIVE_AUTO_PAUSE,
CONF_LIVE_AUTO_PLAY,
CONF_LIVE_AUTO_UNMUTE,
CONF_LIVE_CONTROLS_BUILTIN,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_LIVE_CONTROLS_PTZ_HIDE_HOME,
CONF_LIVE_CONTROLS_PTZ_HIDE_PAN_TILT,
CONF_LIVE_CONTROLS_PTZ_HIDE_ZOOM,
CONF_LIVE_CONTROLS_PTZ_MODE,
CONF_LIVE_CONTROLS_PTZ_ORIENTATION,
CONF_LIVE_CONTROLS_PTZ_POSITION,
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
CONF_LIVE_CONTROLS_TIMELINE_FORMAT_24H,
CONF_LIVE_CONTROLS_TIMELINE_MODE,
CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_LIVE_CONTROLS_TIMELINE_STYLE,
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_LIVE_CONTROLS_WHEEL,
CONF_LIVE_DISPLAY_GRID_COLUMNS,
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION,
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
CONF_LIVE_DISPLAY_MODE,
CONF_LIVE_DRAGGABLE,
CONF_LIVE_LAZY_LOAD,
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED,
CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS,
CONF_LIVE_MICROPHONE_MUTE_AFTER_MICROPHONE_MUTE_SECONDS,
CONF_LIVE_PRELOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
CONF_LIVE_TRANSITION_EFFECT,
CONF_LIVE_ZOOMABLE,
CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SIZE,
CONF_MEDIA_VIEWER_AUTO_MUTE,
CONF_MEDIA_VIEWER_AUTO_PAUSE,
CONF_MEDIA_VIEWER_AUTO_PLAY,
CONF_MEDIA_VIEWER_AUTO_UNMUTE,
CONF_MEDIA_VIEWER_CONTROLS_BUILTIN,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT_24H,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_WHEEL,
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION,
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
CONF_MEDIA_VIEWER_DISPLAY_MODE,
CONF_MEDIA_VIEWER_DRAGGABLE,
CONF_MEDIA_VIEWER_LAZY_LOAD,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MEDIA_VIEWER_ZOOMABLE,
CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTON_SIZE,
CONF_MENU_BUTTONS,
CONF_MENU_POSITION,
CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
CONF_PERFORMANCE_FEATURES_CARD_LOADING_EFFECTS,
CONF_PERFORMANCE_FEATURES_CARD_LOADING_INDICATOR,
CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
CONF_PERFORMANCE_PROFILE,
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_PROFILES,
CONF_REMOTE_CONTROL_ENTITIES_CAMERA,
CONF_STATUS_BAR_HEIGHT,
CONF_STATUS_BAR_ITEMS,
CONF_STATUS_BAR_POPUP_SECONDS,
CONF_STATUS_BAR_POSITION,
CONF_STATUS_BAR_STYLE,
CONF_TIMELINE_CLUSTERING_THRESHOLD,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
CONF_TIMELINE_FORMAT_24H,
CONF_TIMELINE_SHOW_RECORDINGS,
CONF_TIMELINE_STYLE,
CONF_TIMELINE_WINDOW_SECONDS,
CONF_VIEW_CAMERA_SELECT,
CONF_VIEW_DEFAULT,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
CONF_VIEW_DEFAULT_RESET,
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
CONF_VIEW_DEFAULT_RESET_ENTITIES,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_DIM,
CONF_VIEW_INTERACTION_SECONDS,
CONF_VIEW_KEYBOARD_SHORTCUTS,
CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_DOWN,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_HOME,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_LEFT,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_RIGHT,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_UP,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_IN,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT,
CONF_VIEW_THEME_THEMES,
CONF_VIEW_TRIGGERS,
CONF_VIEW_TRIGGERS_ACTIONS,
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS,
DOCS_URL,
FOLDERS_CONFIGURATION_URL,
MEDIA_CHUNK_SIZE_MAX,
} from './const.js';
import { fireHASSEvent } from './ha/fire-hass-event.js';
import { getEntitiesFromHASS } from './ha/get-entities.js';
import { getEntityTitle } from './ha/get-entity-title.js';
import { sideLoadHomeAssistantElements } from './ha/side-load-ha-elements.js';
import { HomeAssistant, LovelaceCardEditor } from './ha/types.js';
import { localize } from './localize/localize.js';
import editorStyle from './scss/editor.scss';
import type { CapabilityKey } from './types.js';
import { arrayMove, prettifyTitle } from './utils/basic.js';
import { getCameraID } from './utils/camera.js';
import { fireAdvancedCameraCardEvent } from './utils/fire-advanced-camera-card-event.js';
import { getFolderID } from './utils/folder.js';
const MENU_CAMERAS = 'cameras';
const MENU_CAMERAS_CAPABILITIES = 'cameras.capabilities';
const MENU_CAMERAS_CAST = 'cameras.cast';
const MENU_CAMERAS_DEPENDENCIES = 'cameras.dependencies';
const MENU_CAMERAS_DIMENSIONS = 'cameras.dimensions';
const MENU_CAMERAS_DIMENSIONS_LAYOUT = 'cameras.dimensions.layout';
const MENU_CAMERAS_ENGINE = 'cameras.engine';
const MENU_CAMERAS_FRIGATE = 'cameras.frigate';
const MENU_CAMERAS_GO2RTC = 'cameras.go2rtc';
const MENU_CAMERAS_IMAGE = 'cameras.image';
const MENU_CAMERAS_LIVE_PROVIDER = 'cameras.live_provider';
const MENU_CAMERAS_MOTIONEYE = 'cameras.motioneye';
const MENU_CAMERAS_PROXY = 'cameras.proxy';
const MENU_CAMERAS_REOLINK = 'cameras.reolink';
const MENU_CAMERAS_TRIGGERS = 'cameras.triggers';
const MENU_CAMERAS_TRIGGERS_REVIEWS = 'cameras.triggers.reviews';
const MENU_CAMERAS_WEBRTC_CARD = 'cameras.webrtc_card';
const MENU_CAMERAS_MEDIA = 'cameras.media';
const MENU_FOLDERS = 'folders';
const MENU_FOLDERS_HA = 'folders.ha';
const MENU_LIVE_CONTROLS = 'live.controls';
const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
const MENU_LIVE_CONTROLS_PTZ = 'live.controls.ptz';
const MENU_LIVE_CONTROLS_THUMBNAILS = 'live.controls.thumbnails';
const MENU_LIVE_CONTROLS_TIMELINE = 'live.controls.timeline';
const MENU_LIVE_CONTROLS_TIMELINE_FORMAT = 'live.controls.timeline.format';
const MENU_LIVE_DISPLAY = 'live.display';
const MENU_LIVE_MICROPHONE = 'live.microphone';
const MENU_MEDIA_GALLERY_CONTROLS_FILTER = 'media_gallery.controls.filter';
const MENU_MEDIA_GALLERY_CONTROLS_THUMBNAILS = 'media_gallery.controls.thumbnails';
const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
const MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS = 'media_viewer.controls.next_previous';
const MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS = 'media_viewer.controls.thumbnails';
const MENU_MEDIA_VIEWER_CONTROLS_TIMELINE = 'media_viewer.controls.timeline';
const MENU_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT =
'media_viewer.controls.timeline.format';
const MENU_MEDIA_VIEWER_DISPLAY = 'media_viewer.display';
const MENU_MENU_BUTTONS = 'menu.buttons';
const MENU_OPTIONS = 'options';
const MENU_PERFORMANCE_FEATURES = 'performance.features';
const MENU_PERFORMANCE_STYLE = 'performance.style';
const MENU_REMOTE_CONTROL_ENTITIES = 'remote_control.entities';
const MENU_STATUS_BAR_ITEMS = 'status_bar.items';
const MENU_TIMELINE_FORMAT = 'timeline.format';
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
const MENU_VIEW_DEFAULT_RESET = 'view.default_reset';
const MENU_VIEW_KEYBOARD_SHORTCUTS = 'view.keyboard_shortcuts';
const MENU_VIEW_TRIGGERS = 'view.triggers';
const MENU_VIEW_TRIGGERS_ACTIONS = 'view.triggers.actions';
const SECTION_DOC_LINKS: Record<string, string> = {
cameras: 'configuration/cameras/README',
dimensions: 'configuration/dimensions',
view: 'configuration/view',
menu: 'configuration/menu',
status_bar: 'configuration/status-bar',
live: 'configuration/live',
folders: 'configuration/folders',
media_viewer: 'configuration/media-viewer',
media_gallery: 'configuration/media-gallery',
image: 'configuration/image',
timeline: 'configuration/timeline',
performance: 'configuration/performance',
profiles: 'configuration/profiles',
remote_control: 'configuration/remote-control',
};
const SUBMENU_DOC_LINKS: Record<string, string> = {
[MENU_VIEW_DEFAULT_RESET]: 'configuration/view?id=default_reset',
[MENU_VIEW_TRIGGERS]: 'configuration/view?id=triggers',
[MENU_VIEW_TRIGGERS_ACTIONS]: 'configuration/view?id=trigger-action-configuration',
[MENU_VIEW_KEYBOARD_SHORTCUTS]: 'configuration/view?id=keyboard_shortcuts',
[MENU_CAMERAS]: 'configuration/cameras/README',
[MENU_CAMERAS_CAPABILITIES]: 'configuration/cameras/README?id=capabilities',
[MENU_CAMERAS_CAST]: 'configuration/cameras/README?id=cast',
[MENU_CAMERAS_DEPENDENCIES]: 'configuration/cameras/README?id=dependencies',
[MENU_CAMERAS_DIMENSIONS]: 'configuration/cameras/README?id=dimensions',
[MENU_CAMERAS_DIMENSIONS_LAYOUT]:
'configuration/cameras/README?id=layout-configuration',
[MENU_CAMERAS_ENGINE]: 'configuration/cameras/engine',
[MENU_CAMERAS_FRIGATE]: 'configuration/cameras/engine?id=frigate',
[MENU_CAMERAS_GO2RTC]: 'configuration/cameras/live-provider?id=go2rtc',
[MENU_CAMERAS_IMAGE]: 'configuration/cameras/live-provider?id=image',
[MENU_CAMERAS_LIVE_PROVIDER]: 'configuration/cameras/live-provider',
[MENU_CAMERAS_MOTIONEYE]: 'configuration/cameras/engine?id=motioneye',
[MENU_CAMERAS_PROXY]: 'configuration/cameras/README?id=proxy',
[MENU_CAMERAS_REOLINK]: 'configuration/cameras/engine?id=reolink',
[MENU_CAMERAS_TRIGGERS]: 'configuration/cameras/README?id=triggers',
[MENU_CAMERAS_TRIGGERS_REVIEWS]: 'configuration/cameras/README?id=reviews',
[MENU_CAMERAS_WEBRTC_CARD]: 'configuration/cameras/live-provider?id=webrtc_card',
[MENU_CAMERAS_MEDIA]: 'configuration/cameras/README?id=media',
[MENU_FOLDERS]: 'configuration/folders',
[MENU_FOLDERS_HA]: 'configuration/folders?id=ha',
[MENU_LIVE_CONTROLS]: 'configuration/live?id=controls',
[MENU_LIVE_CONTROLS_NEXT_PREVIOUS]: 'configuration/live?id=next_previous',
[MENU_LIVE_CONTROLS_PTZ]: 'configuration/live?id=ptz',
[MENU_LIVE_CONTROLS_THUMBNAILS]: 'configuration/live?id=thumbnails',
[MENU_LIVE_CONTROLS_TIMELINE]: 'configuration/live?id=timeline',
[MENU_LIVE_CONTROLS_TIMELINE_FORMAT]: 'configuration/live?id=format',
[MENU_LIVE_DISPLAY]: 'configuration/live?id=display',
[MENU_LIVE_MICROPHONE]: 'configuration/live?id=microphone',
[MENU_MEDIA_GALLERY_CONTROLS_FILTER]: 'configuration/media-gallery?id=filter',
[MENU_MEDIA_GALLERY_CONTROLS_THUMBNAILS]: 'configuration/media-gallery?id=thumbnails',
[MENU_MEDIA_VIEWER_CONTROLS]: 'configuration/media-viewer?id=controls',
[MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS]:
'configuration/media-viewer?id=next_previous',
[MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS]: 'configuration/media-viewer?id=thumbnails',
[MENU_MEDIA_VIEWER_CONTROLS_TIMELINE]: 'configuration/media-viewer?id=timeline',
[MENU_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT]: 'configuration/media-viewer?id=format',
[MENU_MEDIA_VIEWER_DISPLAY]: 'configuration/media-viewer?id=display',
[MENU_MENU_BUTTONS]: 'configuration/menu?id=buttons',
[MENU_OPTIONS]: 'configuration/README',
[MENU_PERFORMANCE_FEATURES]: 'configuration/performance?id=features',
[MENU_PERFORMANCE_STYLE]: 'configuration/performance?id=style',
[MENU_REMOTE_CONTROL_ENTITIES]: 'configuration/remote-control?id=entities',
[MENU_STATUS_BAR_ITEMS]: 'configuration/status-bar?id=items',
[MENU_TIMELINE_FORMAT]: 'configuration/timeline?id=format',
[MENU_TIMELINE_CONTROLS_THUMBNAILS]: 'configuration/timeline?id=thumbnails',
};
interface EditorOptionsSet {
icon: string;
name: string;
secondary: string;
}
interface EditorOptions {
[setName: string]: EditorOptionsSet;
}
interface EditorSelectOption {
value: unknown;
label: string;
}
interface EditorMenuTarget {
domain: string;
key: string | number;
}
const options: EditorOptions = {
cameras: {
icon: 'video',
name: localize('editor.cameras'),
secondary: localize('editor.cameras_secondary'),
},
view: {
icon: 'eye',
name: localize('editor.view'),
secondary: localize('editor.view_secondary'),
},
menu: {
icon: 'menu',
name: localize('editor.menu'),
secondary: localize('editor.menu_secondary'),
},
status_bar: {
icon: 'sign-text',
name: localize('editor.status_bar'),
secondary: localize('editor.status_bar_secondary'),
},
live: {
icon: 'cctv',
name: localize('editor.live'),
secondary: localize('editor.live_secondary'),
},
folders: {
icon: 'folder-multiple',
name: localize('editor.folders'),
secondary: localize('editor.folders'),
},
media_gallery: {
icon: 'play-box-multiple',
name: localize('editor.media_gallery'),
secondary: localize('editor.media_gallery_secondary'),
},
media_viewer: {
icon: 'filmstrip',
name: localize('editor.media_viewer'),
secondary: localize('editor.media_viewer_secondary'),
},
image: {
icon: 'image',
name: localize('editor.image'),
secondary: localize('editor.image_secondary'),
},
timeline: {
icon: 'chart-gantt',
name: localize('editor.timeline'),
secondary: localize('editor.timeline_secondary'),
},
dimensions: {
icon: 'aspect-ratio',
name: localize('editor.dimensions'),
secondary: localize('editor.dimensions_secondary'),
},
performance: {
icon: 'speedometer',
name: localize('editor.performance'),
secondary: localize('editor.performance_secondary'),
},
profiles: {
icon: 'folder-wrench-outline',
name: localize('editor.profiles'),
secondary: localize('editor.profiles_secondary'),
},
remote_control: {
icon: 'remote',
name: localize('editor.remote_control'),
secondary: localize('editor.remote_control_secondary'),
},
overrides: {
icon: 'file-replace',
name: localize('editor.overrides'),
secondary: localize('editor.overrides_secondary'),
},
};
@customElement('advanced-camera-card-editor')
export class AdvancedCameraCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: RawAdvancedCameraCardConfig;
@state() private _defaults = copyConfig(configDefaults);
private _initialized = false;
private _configUpgradeable = false;
@state()
private _expandedMenus: Record<string, string | number> = {};
private _viewModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'auto', label: localize('config.view.views.auto') },
{ value: 'clip', label: localize('config.view.views.clip') },
{ value: 'clips', label: localize('config.view.views.clips') },
{ value: 'folder', label: localize('config.view.views.folder') },
{ value: 'folders', label: localize('config.view.views.folders') },
{ value: 'gallery', label: localize('config.view.views.gallery') },
{ value: 'image', label: localize('config.view.views.image') },
{ value: 'live', label: localize('config.view.views.live') },
{ value: 'media', label: localize('config.view.views.media') },
{ value: 'recording', label: localize('config.view.views.recording') },
{ value: 'recordings', label: localize('config.view.views.recordings') },
{ value: 'review', label: localize('config.view.views.review') },
{ value: 'reviews', label: localize('config.view.views.reviews') },
{ value: 'snapshot', label: localize('config.view.views.snapshot') },
{ value: 'snapshots', label: localize('config.view.views.snapshots') },
{ value: 'timeline', label: localize('config.view.views.timeline') },
];
private _cameraSelectViewModes: EditorSelectOption[] = [
...this._viewModes,
{ value: 'current', label: localize('config.view.views.current') },
];
private _filterModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'none',
label: localize('config.common.controls.filter.modes.none'),
},
{
value: 'left',
label: localize('config.common.controls.filter.modes.left'),
},
{
value: 'right',
label: localize('config.common.controls.filter.modes.right'),
},
];
private _menuStyles: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'none', label: localize('config.menu.styles.none') },
{ value: 'hidden', label: localize('config.menu.styles.hidden') },
{ value: 'overlay', label: localize('config.menu.styles.overlay') },
{ value: 'hover', label: localize('config.menu.styles.hover') },
{ value: 'hover-card', label: localize('config.menu.styles.hover-card') },
{ value: 'outside', label: localize('config.menu.styles.outside') },
];
private _menuPositions: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'left', label: localize('config.menu.positions.left') },
{ value: 'right', label: localize('config.menu.positions.right') },
{ value: 'top', label: localize('config.menu.positions.top') },
{ value: 'bottom', label: localize('config.menu.positions.bottom') },
];
private _menuAlignments: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'left', label: localize('config.menu.alignments.left') },
{ value: 'right', label: localize('config.menu.alignments.right') },
{ value: 'top', label: localize('config.menu.alignments.top') },
{ value: 'bottom', label: localize('config.menu.alignments.bottom') },
];
private _nextPreviousControlStyles: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'chevrons',
label: localize('config.common.controls.next_previous.styles.chevrons'),
},
{
value: 'icons',
label: localize('config.common.controls.next_previous.styles.icons'),
},
{
value: 'none',
label: localize('config.common.controls.next_previous.styles.none'),
},
{
value: 'thumbnails',
label: localize('config.common.controls.next_previous.styles.thumbnails'),
},
];
private _aspectRatioModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'dynamic',
label: localize('config.dimensions.aspect_ratio_modes.dynamic'),
},
{ value: 'static', label: localize('config.dimensions.aspect_ratio_modes.static') },
{
value: 'unconstrained',
label: localize('config.dimensions.aspect_ratio_modes.unconstrained'),
},
];
private _thumbnailModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'none',
label: localize('config.common.controls.thumbnails.modes.none'),
},
{
value: 'above',
label: localize('config.common.controls.thumbnails.modes.above'),
},
{
value: 'below',
label: localize('config.common.controls.thumbnails.modes.below'),
},
{
value: 'left',
label: localize('config.common.controls.thumbnails.modes.left'),
},
{
value: 'right',
label: localize('config.common.controls.thumbnails.modes.right'),
},
];
private _thumbnailMediaTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'auto',
label: localize('config.common.media_types.auto'),
},
{
value: 'events',
label: localize('config.common.media_types.events'),
},
{
value: 'recordings',
label: localize('config.common.media_types.recordings'),
},
{
value: 'reviews',
label: localize('config.common.media_types.reviews'),
},
];
private _timelineThumbnailMediaTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'auto',
label: localize('config.common.media_types.auto'),
},
{
value: 'events',
label: localize('config.common.media_types.events'),
},
{
value: 'reviews',
label: localize('config.common.media_types.reviews'),
},
];
private _thumbnailEventsMediaTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'all',
label: localize('config.common.events_media_types.all'),
},
{
value: 'clips',
label: localize('config.common.events_media_types.clips'),
},
{
value: 'snapshots',
label: localize('config.common.events_media_types.snapshots'),
},
];
private _cameraMediaReviewedOptions: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'unreviewed',
label: localize('config.cameras.media.revieweds.unreviewed'),
},
{
value: 'reviewed',
label: localize('config.cameras.media.revieweds.reviewed'),
},
{
value: 'all',
label: localize('config.cameras.media.revieweds.all'),
},
];
private _transitionEffects: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'none', label: localize('config.media_viewer.transition_effects.none') },
{ value: 'slide', label: localize('config.media_viewer.transition_effects.slide') },
];
private _imageModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'camera', label: localize('config.common.image.modes.camera') },
{ value: 'entity', label: localize('config.common.image.modes.entity') },
{ value: 'screensaver', label: localize('config.common.image.modes.screensaver') },
{ value: 'url', label: localize('config.common.image.modes.url') },
];
private _timelineEventsMediaTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'all', label: localize('config.common.events_media_types.all') },
{
value: 'clips',
label: localize('config.common.events_media_types.clips'),
},
{
value: 'snapshots',
label: localize('config.common.events_media_types.snapshots'),
},
];
private _timelineStyleTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'ribbon', label: localize('config.common.timeline.styles.ribbon') },
{ value: 'stack', label: localize('config.common.timeline.styles.stack') },
];
private _mediaActionNegativeConditions: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'unselected',
label: localize('config.common.media_action_conditions.unselected'),
},
{ value: 'hidden', label: localize('config.common.media_action_conditions.hidden') },
];
private _mediaActionPositiveConditions: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'selected',
label: localize('config.common.media_action_conditions.selected'),
},
{
value: 'visible',
label: localize('config.common.media_action_conditions.visible'),
},
];
private _mediaLiveUnmuteConditions: EditorSelectOption[] = [
...this._mediaActionPositiveConditions,
{
value: 'microphone',
label: localize('config.common.media_action_conditions.microphone_unmute'),
},
];
private _mediaLiveMuteConditions: EditorSelectOption[] = [
...this._mediaActionNegativeConditions,
{
value: 'microphone',
label: localize('config.common.media_action_conditions.microphone_mute'),
},
];
private _layoutFits: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'contain',
label: localize('config.cameras.dimensions.layout.fits.contain'),
},
{ value: 'cover', label: localize('config.cameras.dimensions.layout.fits.cover') },
{ value: 'fill', label: localize('config.cameras.dimensions.layout.fits.fill') },
];
private _miniTimelineModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'none', label: localize('config.common.controls.timeline.modes.none') },
{ value: 'above', label: localize('config.common.controls.timeline.modes.above') },
{ value: 'below', label: localize('config.common.controls.timeline.modes.below') },
];
private _profiles: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'casting', label: localize('config.profiles.casting') },
{ value: 'low-performance', label: localize('config.profiles.low-performance') },
{ value: 'scrubbing', label: localize('config.profiles.scrubbing') },
];
private _go2rtcModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'mse', label: localize('config.cameras.go2rtc.modes.mse') },
{ value: 'webrtc', label: localize('config.cameras.go2rtc.modes.webrtc') },
{ value: 'mp4', label: localize('config.cameras.go2rtc.modes.mp4') },
{ value: 'mjpeg', label: localize('config.cameras.go2rtc.modes.mjpeg') },
];
private _microphoneButtonTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'momentary', label: localize('config.menu.buttons.types.momentary') },
{ value: 'toggle', label: localize('config.menu.buttons.types.toggle') },
];
private _displayModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'single', label: localize('display_modes.single') },
{ value: 'grid', label: localize('display_modes.grid') },
];
private _gridSelectPositions: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'default',
label: localize('config.common.display.grid_selected_positions.default'),
},
{
value: 'first',
label: localize('config.common.display.grid_selected_positions.first'),
},
{
value: 'last',
label: localize('config.common.display.grid_selected_positions.last'),
},
];
private _castMethods: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'standard', label: localize('config.cameras.cast.methods.standard') },
{ value: 'dashboard', label: localize('config.cameras.cast.methods.dashboard') },
];
private _cameraMediaTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'auto', label: localize('config.common.media_types.auto') },
{ value: 'events', label: localize('config.common.media_types.events') },
{ value: 'recordings', label: localize('config.common.media_types.recordings') },
{ value: 'reviews', label: localize('config.common.media_types.reviews') },
{ value: 'folder', label: localize('config.common.media_types.folder') },
];
private _ptzModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'on', label: localize('config.live.controls.ptz.modes.on') },
{ value: 'off', label: localize('config.live.controls.ptz.modes.off') },
];
private _ptzOrientations: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'vertical',
label: localize('config.live.controls.ptz.orientations.vertical'),
},
{
value: 'horizontal',
label: localize('config.live.controls.ptz.orientations.horizontal'),
},
];
private _ptzPositions: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'top-left',
label: localize('config.live.controls.ptz.positions.top-left'),
},
{
value: 'top-right',
label: localize('config.live.controls.ptz.positions.top-right'),
},
{
value: 'bottom-left',
label: localize('config.live.controls.ptz.positions.bottom-left'),
},
{
value: 'bottom-right',
label: localize('config.live.controls.ptz.positions.bottom-right'),
},
];
private _triggersActionsInteractionModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'all',
label: localize('config.view.triggers.actions.interaction_modes.all'),
},
{
value: 'inactive',
label: localize('config.view.triggers.actions.interaction_modes.inactive'),
},
{
value: 'active',
label: localize('config.view.triggers.actions.interaction_modes.active'),
},
];
private _triggersActionsTrigger: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'default',
label: localize('config.view.triggers.actions.triggers.default'),
},
{
value: 'live',
label: localize('config.view.triggers.actions.triggers.live'),
},
{
value: 'media',
label: localize('config.view.triggers.actions.triggers.media'),
},
{
value: 'none',
label: localize('config.view.triggers.actions.triggers.none'),
},
];
private _triggersActionsUntrigger: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'default',
label: localize('config.view.triggers.actions.untriggers.default'),
},
{
value: 'none',
label: localize('config.view.triggers.actions.untriggers.none'),
},
];
private _triggersEvents: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'events',
label: localize('config.cameras.triggers.events.events'),
},
{
value: 'clips',
label: localize('config.cameras.triggers.events.clips'),
},
{
value: 'snapshots',
label: localize('config.cameras.triggers.events.snapshots'),
},
];
private _severities: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'high',
label: localize('common.severities.high'),
},
{
value: 'medium',
label: localize('common.severities.medium'),
},
{
value: 'low',
label: localize('common.severities.low'),
},
];
private _timelinePanModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'pan',
label: localize('config.common.controls.timeline.pan_modes.pan'),
},
{
value: 'seek',
label: localize('config.common.controls.timeline.pan_modes.seek'),
},
{
value: 'seek-in-media',
label: localize('config.common.controls.timeline.pan_modes.seek-in-media'),
},
{
value: 'seek-in-camera',
label: localize('config.common.controls.timeline.pan_modes.seek-in-camera'),
},
];
private _capabilities: EditorSelectOption[] = (() => {
// 'satisfies' enforces all CapabilityKey values are present at compile time.
const labels = {
'2-way-audio': localize('config.cameras.capabilities.capabilities.2-way-audio'),
clips: localize('config.cameras.capabilities.capabilities.clips'),
'favorite-events': localize(
'config.cameras.capabilities.capabilities.favorite-events',
),
'favorite-recordings': localize(
'config.cameras.capabilities.capabilities.favorite-recordings',
),
live: localize('config.cameras.capabilities.capabilities.live'),
menu: localize('config.cameras.capabilities.capabilities.menu'),
ptz: localize('config.cameras.capabilities.capabilities.ptz'),
recordings: localize('config.cameras.capabilities.capabilities.recordings'),
'remote-control-entity': localize(
'config.cameras.capabilities.capabilities.remote-control-entity',
),
reviews: localize('config.cameras.capabilities.capabilities.reviews'),
seek: localize('config.cameras.capabilities.capabilities.seek'),
snapshots: localize('config.cameras.capabilities.capabilities.snapshots'),
substream: localize('config.cameras.capabilities.capabilities.substream'),
trigger: localize('config.cameras.capabilities.capabilities.trigger'),
} satisfies Record<CapabilityKey, string>;
return [
{ value: '', label: '' },
...Object.entries(labels).map(([value, label]) => ({ value, label })),
];
})();
private _forceableCapabilities: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: '2-way-audio',
label: localize('config.cameras.capabilities.capabilities.2-way-audio'),
},
];
private _defaultResetInteractionModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'all',
label: localize('config.view.default_reset.interaction_modes.all'),
},
{
value: 'inactive',
label: localize('config.view.default_reset.interaction_modes.inactive'),
},
{
value: 'active',
label: localize('config.view.default_reset.interaction_modes.active'),
},
];
private _proxyModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'auto',
label: localize('config.cameras.proxy.modes.auto'),
},
{
value: true,
label: localize('config.cameras.proxy.modes.true'),
},
{
value: false,
label: localize('config.cameras.proxy.modes.false'),
},
];
private _proxySSLCiphers: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'auto',
label: localize('config.cameras.proxy.ssl_ciphers.auto'),
},
{
value: 'default',
label: localize('config.cameras.proxy.ssl_ciphers.default'),
},
{
value: 'insecure',
label: localize('config.cameras.proxy.ssl_ciphers.insecure'),
},
{
value: 'intermediate',
label: localize('config.cameras.proxy.ssl_ciphers.intermediate'),
},
{
value: 'modern',
label: localize('config.cameras.proxy.ssl_ciphers.modern'),
},
];
private _proxySSLVerification: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'auto',
label: localize('config.cameras.proxy.ssl_verification.auto'),
},
{
value: true,
label: localize('config.cameras.proxy.ssl_verification.true'),
},
{
value: false,
label: localize('config.cameras.proxy.ssl_verification.false'),
},
];
private _reolinkMediaResolution: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'high',
label: localize('config.cameras.reolink.media_resolution.high'),
},
{
value: 'low',
label: localize('config.cameras.reolink.media_resolution.low'),
},
];
private _statusBarStyles: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'hover', label: localize('config.status_bar.styles.hover') },
{ value: 'hover-card', label: localize('config.status_bar.styles.hover-card') },
{ value: 'none', label: localize('config.status_bar.styles.none') },
{ value: 'outside', label: localize('config.status_bar.styles.outside') },
{ value: 'overlay', label: localize('config.status_bar.styles.overlay') },
{ value: 'popup', label: localize('config.status_bar.styles.popup') },
];
private _statusBarPositions: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'top', label: localize('config.status_bar.positions.top') },
{ value: 'bottom', label: localize('config.status_bar.positions.bottom') },
];
private _themes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'ha', label: localize('config.view.theme.themes.ha') },
{ value: 'dark', label: localize('config.view.theme.themes.dark') },
{ value: 'light', label: localize('config.view.theme.themes.light') },
{ value: 'traditional', label: localize('config.view.theme.themes.traditional') },
];
private _rotations: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 0, label: localize('config.cameras.dimensions.rotations.0') },
{ value: 90, label: localize('config.cameras.dimensions.rotations.90') },
{ value: 180, label: localize('config.cameras.dimensions.rotations.180') },
{ value: 270, label: localize('config.cameras.dimensions.rotations.270') },
];
public setConfig(config: RawAdvancedCameraCardConfig): void {
// Note: This does not use Zod to parse the full configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
// valid configuration here, to allow the user to fix the broken parts. As
// such, RawAdvancedCameraCardConfig is used as the type.
this._config = config;
this._configUpgradeable = isConfigUpgradeable(config);
const profiles = profilesSchema.safeParse(
(this._config as AdvancedCameraCardConfig).profiles,
);
if (profiles.success) {
const defaults = copyConfig(configDefaults);
setProfiles(this._config, defaults, profiles.data);
this._defaults = defaults;
}
}
protected willUpdate(): void {
if (!this._initialized) {
sideLoadHomeAssistantElements().then((success) => {
if (success) {
this._initialized = true;
}
});
}
}
/**
* Render an option set header
* @param optionSetName The name of the EditorOptionsSet.
* @returns A rendered template.
*/
private _renderOptionSetHeader(
optionSetName: string,
titleClass?: string,
): TemplateResult {
const optionSet = options[optionSetName];
return html`
<div
class="option option-${optionSetName}"
@click=${this._toggleMenu}
.domain=${'options'}
.key=${optionSetName}
>
<div class="row">
<advanced-camera-card-icon
.icon=${{ icon: `mdi:${optionSet.icon}` }}
></advanced-camera-card-icon>
<div class="title ${titleClass ?? ''}">${optionSet.name}</div>
</div>
<div class="secondary">${optionSet.secondary}</div>
</div>
`;
}
private _renderDocLinkRow(docPath: string): TemplateResult {
return html`
<a
class="doc-link-row"
href="${DOCS_URL}/#/${docPath}"
target="_blank"
rel="noopener noreferrer"
title=${localize('editor.docs')}
>
<ha-icon icon="mdi:book-open-page-variant"></ha-icon>
<div>${localize('editor.docs')}</div>
<ha-icon icon="mdi:open-in-new"></ha-icon>
</a>
`;
}
/**
* Get a localized help label for a given config path.
* @param configPath The config path.
* @returns A localized label.
*/
private _getLabel(configPath: string): string {
// Strip out array indices from the path.
const path = configPath
.split('.')
.filter((e) => !e.match(/^\[[0-9]+\]$/))
.join('.');
return localize(`config.${path}`);
}
/**
* Render an entity selector.
* @param configPath The configuration path to set/read.
* @param domain Only entities from this domain will be shown.
* @returns A rendered template.
*/
private _renderEntitySelector(
configPath: string,
domain: string,
): TemplateResult | void {
if (!this._config) {
return;
}
return html`
<ha-selector
.hass=${this.hass}
.selector=${{ entity: { domain: domain } }}
.label=${this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
</ha-selector>
`;
}
/**
* Render an option/"select" selector.
* @param configPath The configuration path to set/read.
* @param options The options to show in the selector.
* @param params Option parameters to control the selector.
* @returns A rendered template.
*/
private _renderOptionSelector(
configPath: string,
options: string[] | { value: unknown; label: string }[] = [],
params?: {
multiple?: boolean;
label?: string;
},
): TemplateResult | void {
if (!this._config) {
return;
}
return html`
<ha-selector
.hass=${this.hass}
.selector=${{
select: {
mode: 'dropdown',
multiple: !!params?.multiple,
custom_value: !options.length,
options: options,
},
}}
.label=${params?.label || this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
</ha-selector>
`;
}
/**
* Render an icon selector.
* @param configPath The configuration path to set/read.
* @param params Optional parameters to control the selector.
* @returns A rendered template.
*/
private _renderIconSelector(
configPath: string,
params?: {
label?: string;
},
): TemplateResult | void {
if (!this._config) {
return;
}
return html`
<ha-selector
.hass=${this.hass}
.selector=${{
icon: {},
}}
.label=${params?.label || this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
</ha-selector>
`;
}
/**
* Render a number slider.
* @param configPath Configuration path of the variable.
* @param params Optional parameters to control the selector.
* @returns A rendered template.
*/
private _renderNumberInput(
configPath: string,
params?: {
min?: number;
max?: number;
label?: string;
default?: number;
step?: number;
},
): TemplateResult | void {
if (!this._config) {
return;
}
const value = getConfigValue(this._config, configPath);
const mode = params?.max === undefined ? 'box' : 'slider';
return html`
<ha-selector
.hass=${this.hass}
.selector=${{
number: {
min: params?.min || 0,
max: params?.max,
mode: mode,
step: params?.step,
},
}}
.label=${params?.label || this._getLabel(configPath)}
.value=${value ?? params?.default}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
</ha-selector>
`;
}
/**
* Render a simple text info box.
* @param info The string to display.
* @returns A rendered template.
*/
private _renderInfo(info: string): TemplateResult {
return html` <span class="info">${info}</span>`;
}
/**
* Get an editor title for the camera.
* @param cameraIndex The index of the camera in the cameras array.
* @param cameraConfig The raw camera configuration object.
* @returns A string title.
*/
private _getEditorCameraTitle(
cameraIndex: number,
cameraConfig: RawAdvancedCameraCardConfig,
): string {
// 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.
// This is only used for the editor since the card itself can use the
// cameraManager.
return (
(typeof cameraConfig?.title === 'string' && cameraConfig.title) ||
(typeof cameraConfig?.camera_entity === 'string'
? getEntityTitle(this.hass, cameraConfig.camera_entity)
: '') ||
(typeof cameraConfig?.webrtc_card === 'object' &&
cameraConfig.webrtc_card &&
typeof cameraConfig.webrtc_card['entity'] === 'string' &&
cameraConfig.webrtc_card['entity']) ||
// Usage of engine specific logic here is allowed as an exception, since
// the camera manager cannot be started with an unparsed and unloaded
// config.
(typeof cameraConfig?.frigate === 'object' &&
cameraConfig.frigate &&
typeof cameraConfig?.frigate['camera_name'] === 'string' &&
cameraConfig.frigate['camera_name']
? prettifyTitle(cameraConfig.frigate['camera_name'])
: '') ||
(typeof cameraConfig?.id === 'string' && cameraConfig.id) ||
localize('editor.camera') + ' #' + cameraIndex
);
}
/**
* Get an editor title for the camera.
* @param cameraIndex The index of the camera in the cameras array.
* @param cameraConfig The raw camera configuration object.
* @returns A string title.
*/
private _getEditorFolderTitle(
folderIndex: number,
folderConfig: RawAdvancedCameraCardConfig,
): string {
// 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.
// This is only used for the editor since the card itself can use the
// cameraManager.
return (
(typeof folderConfig?.title === 'string' && folderConfig.title) ||
(typeof folderConfig?.id === 'string' && folderConfig.id) ||
localize('common.folder') + ' #' + folderIndex
);
}
private _renderViewDefaultResetMenu(): TemplateResult {
return this._putInSubmenu(
MENU_VIEW_DEFAULT_RESET,
true,
`config.${CONF_VIEW_DEFAULT_RESET}.editor_label`,
'mdi:restart',
html`
${this._renderSwitch(
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
this._defaults.view.default_reset.after_interaction,
)}
${this._renderNumberInput(CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS)}
${this._renderOptionSelector(
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
this._defaultResetInteractionModes,
{
label: localize('config.view.default_reset.interaction_mode'),
},
)},
${this._renderOptionSelector(
CONF_VIEW_DEFAULT_RESET_ENTITIES,
this.hass ? getEntitiesFromHASS(this.hass) : [],
{
multiple: true,
},
)}
`,
);
}
private _renderViewTriggersMenu(): TemplateResult {
return this._putInSubmenu(
MENU_VIEW_TRIGGERS,
true,
`config.${CONF_VIEW_TRIGGERS}.editor_label`,
'mdi:target-account',
html`
${this._renderSwitch(
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
this._defaults.view.triggers.filter_selected_camera,
{
label: localize(`config.${CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA}`),
},
)}
${this._renderSwitch(
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
this._defaults.view.triggers.show_trigger_status,
{
label: localize(`config.${CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS}`),
},
)}
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS, {
default: this._defaults.view.triggers.untrigger_delay_seconds,
})}
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS, {
default: this._defaults.view.triggers.untrigger_force_seconds,
})}
${this._putInSubmenu(
MENU_VIEW_TRIGGERS_ACTIONS,
true,
`config.${CONF_VIEW_TRIGGERS_ACTIONS}.editor_label`,
'mdi:cogs',
html` ${this._renderOptionSelector(
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
this._triggersActionsTrigger,
{
label: localize('config.view.triggers.actions.trigger'),
},
)}
${this._renderOptionSelector(
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
this._triggersActionsUntrigger,
{
label: localize('config.view.triggers.actions.untrigger'),
},
)}
${this._renderOptionSelector(
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
this._triggersActionsInteractionModes,
{
label: localize('config.view.triggers.actions.interaction_mode'),
},
)}`,
)}
`,
);
}
private _renderKeyAssigner(
configPath: string,
defaultValue: KeyboardShortcut,
): TemplateResult {
return html` <advanced-camera-card-key-assigner
.label=${localize(`config.${configPath}`)}
.value=${this._config
? getConfigValue(this._config, configPath, defaultValue)
: null}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
></advanced-camera-card-key-assigner>`;
}
private _renderViewKeyboardShortcutMenu(): TemplateResult {
return this._putInSubmenu(
MENU_VIEW_KEYBOARD_SHORTCUTS,
true,
`config.${CONF_VIEW_KEYBOARD_SHORTCUTS}.editor_label`,
'mdi:keyboard',
html`
${this._renderSwitch(
CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED,
this._defaults.view.keyboard_shortcuts.enabled,
{
label: localize(`config.${CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED}`),
},
)}
${this._renderKeyAssigner(
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_LEFT,
this._defaults.view.keyboard_shortcuts.ptz_left,
)}
${this._renderKeyAssigner(
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_RIGHT,
this._defaults.view.keyboard_shortcuts.ptz_right,
)}
${this._renderKeyAssigner(
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_UP,
this._defaults.view.keyboard_shortcuts.ptz_up,
)}
${this._renderKeyAssigner(
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_DOWN,
this._defaults.view.keyboard_shortcuts.ptz_down,
)}
${this._renderKeyAssigner(
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_IN,
this._defaults.view.keyboard_shortcuts.ptz_zoom_in,
)}
${this._renderKeyAssigner(
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT,
this._defaults.view.keyboard_shortcuts.ptz_zoom_out,
)}
${this._renderKeyAssigner(
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_HOME,
this._defaults.view.keyboard_shortcuts.ptz_home,
)}
`,
);
}
private _renderStatusBarItem(item: string): TemplateResult {
return html` ${this._putInSubmenu(
MENU_STATUS_BAR_ITEMS,
item,
`config.status_bar.items.${item}`,
'mdi:feature-search',
html`
${this._renderSwitch(
`${CONF_STATUS_BAR_ITEMS}.${item}.enabled`,
this._defaults.status_bar.items[item]?.enabled ?? true,
{
label: localize('config.status_bar.items.enabled'),
},
)}
${this._renderNumberInput(`${CONF_STATUS_BAR_ITEMS}.${item}.priority`, {
max: STATUS_BAR_PRIORITY_MAX,
default: this._defaults.status_bar.items[item]?.priority,
label: localize('config.status_bar.items.priority'),
})}
`,
)}`;
}
private _renderMenuButton(
button: string,
additionalOptions?: TemplateResult,
): TemplateResult {
const menuButtonAlignments: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'matching', label: localize('config.menu.buttons.alignments.matching') },
{ value: 'opposing', label: localize('config.menu.buttons.alignments.opposing') },
];
return html` ${this._putInSubmenu(
MENU_MENU_BUTTONS,
button,
`config.menu.buttons.${button}`,
'mdi:gesture-tap-button',
html`
${this._renderSwitch(
`${CONF_MENU_BUTTONS}.${button}.enabled`,
this._defaults.menu.buttons[button]?.enabled ?? true,
{
label: localize('config.menu.buttons.enabled'),
},
)}
${this._renderOptionSelector(
`${CONF_MENU_BUTTONS}.${button}.alignment`,
menuButtonAlignments,
{
label: localize('config.menu.buttons.alignment'),
},
)}
${this._renderSwitch(
`${CONF_MENU_BUTTONS}.${button}.permanent`,
this._defaults.menu.buttons[button]?.permanent ?? false,
{
label: localize('config.menu.buttons.permanent'),
},
)}
${this._renderNumberInput(`${CONF_MENU_BUTTONS}.${button}.priority`, {
max: MENU_PRIORITY_MAX,
default: this._defaults.menu.buttons[button]?.priority,
label: localize('config.menu.buttons.priority'),
})}
${this._renderIconSelector(`${CONF_MENU_BUTTONS}.${button}.icon`, {
label: localize('config.menu.buttons.icon'),
})}
${additionalOptions}
`,
)}`;
}
/**
* Put a given rendered template into a submenu.
* @param domain The submenu domain.
* @param key The submenu key.
* @param icon The icon for the submenu.
* @param labelPath The path to the label to localize.
* @param template The template to put in the submenu.
* @returns
*/
private _putInSubmenu(
domain: string,
key: unknown,
labelPath: string,
icon: string,
template: TemplateResult,
): TemplateResult {
const selected = this._expandedMenus[domain] === key;
const submenuClasses = {
submenu: true,
selected: selected,
};
const docPath = SUBMENU_DOC_LINKS[domain as string];
return html` <div class="${classMap(submenuClasses)}">
<div
class="submenu-header"
@click=${this._toggleMenu}
.domain=${domain}
.key=${key}
>
<advanced-camera-card-icon .icon=${{ icon: icon }}></advanced-camera-card-icon>
<span>${localize(labelPath)}</span>
</div>
${selected
? html`<div class="values">
${docPath ? this._renderDocLinkRow(docPath) : ''} ${template}
</div>`
: ''}
</div>`;
}
/**
* Render a media layout section.
* @param domain The submenu domain.
* @param labelPath The path to the label.
* @param configPathFit The path to the fit config.
* @param configPathPositionX The path to the position.x config.
* @param configPathPositionY The path to the position.y config.
* @returns A rendered template.
*/
private _renderMediaLayout(
domain: string,
labelPath: string,
configPathFit: string,
configPathPositionX: string,
configPathPositionY: string,
configPathViewBoxTop: string,
configPathViewBoxBottom: string,
configPathViewBoxLeft: string,
configPathViewBoxRight: string,
configPathZoom: string,
configPathPanX: string,
configPathPanY: string,
): TemplateResult | void {
return this._putInSubmenu(
domain,
true,
labelPath,
'mdi:page-layout-body',
html`
${this._renderNumberInput(configPathZoom, {
min: ZOOM_MIN,
max: ZOOM_MAX,
label: localize('config.cameras.dimensions.layout.zoom'),
step: 0.1,
})}
${this._renderNumberInput(configPathPanX, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.pan.x'),
})}
${this._renderNumberInput(configPathPanY, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.pan.y'),
})}
${this._renderOptionSelector(configPathFit, this._layoutFits, {
label: localize('config.cameras.dimensions.layout.fit'),
})}
${this._putInSubmenu(
`${domain}.position`,
true,
'config.cameras.dimensions.layout.position.editor_label',
'mdi:aspect-ratio',
html` ${this._renderNumberInput(configPathPositionX, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.position.x'),
})}
${this._renderNumberInput(configPathPositionY, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.position.y'),
})}`,
)}
${this._putInSubmenu(
`${domain}.view_box`,
true,
'config.cameras.dimensions.layout.view_box.editor_label',
'mdi:crop',
html`
${this._renderNumberInput(configPathViewBoxTop, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.view_box.top'),
})}
${this._renderNumberInput(configPathViewBoxBottom, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.view_box.bottom'),
})}
${this._renderNumberInput(configPathViewBoxLeft, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.view_box.left'),
})}
${this._renderNumberInput(configPathViewBoxRight, {
min: 0,
max: 100,
label: localize('config.cameras.dimensions.layout.view_box.right'),
})}
`,
)}
`,
);
}
/**
* Render the core timeline controls (mini or full timeline),
* @param configPathStyle Timeline style config path.
* @param configPathWindowSeconds Timeline window config path.
* @param configPathClusteringThreshold Clustering threshold config path.
* @param configPathShowRecordings Show recordings config path.
* @param defaultShowRecordings Default value of show_recordings.
* @returns A rendered template.
*/
private _renderTimelineCoreControls(
formatDomain: string,
configPathStyle: string,
configPathWindowSeconds: string,
configPathClusteringThreshold: string,
configPathShowRecordings: string,
configPathFormat24h: string,
defaultShowRecordings: boolean,
defaultFormat24h: boolean,
configPathPanMode?: string,
): TemplateResult {
return html`
${this._renderOptionSelector(configPathStyle, this._timelineStyleTypes, {
label: localize(`config.common.${CONF_TIMELINE_STYLE}`),
})}
${configPathPanMode
? this._renderOptionSelector(configPathPanMode, this._timelinePanModes, {
label: localize(`config.common.controls.timeline.pan_mode`),
})
: ``}
${this._renderNumberInput(configPathWindowSeconds, {
label: localize(`config.common.${CONF_TIMELINE_WINDOW_SECONDS}`),
})}
${this._renderNumberInput(configPathClusteringThreshold, {
label: localize(`config.common.${CONF_TIMELINE_CLUSTERING_THRESHOLD}`),
})}
${this._renderSwitch(configPathShowRecordings, defaultShowRecordings, {
label: localize(`config.common.${CONF_TIMELINE_SHOW_RECORDINGS}`),
})}
${this._putInSubmenu(
formatDomain,
true,
'config.common.controls.timeline.format.editor_label',
'mdi:clock-edit',
html`
${this._renderSwitch(configPathFormat24h, defaultFormat24h, {
label: localize('config.common.controls.timeline.format.24h'),
})}
`,
)}
`;
}
/**
* Render the mini timeline controls.
* @param domain The submenu domain.
* @param configPathWindowSeconds Timeline window config path.
* @param configPathClusteringThreshold Clustering threshold config path.
* @param configPathShowRecordings Show recordings config path.
* @returns A rendered template.
*/
private _renderMiniTimeline(
domain: string,
formatDomain: string,
configPathMode: string,
configPathStyle: string,
configPathWindowSeconds: string,
configPathClusteringThreshold: string,
configPathShowRecordings: string,
configPathFormat24h: string,
defaultShowRecordings: boolean,
defaultFormat24h: boolean,
configPathPanMode: string,
): TemplateResult | void {
return this._putInSubmenu(
domain,
true,
'config.common.controls.timeline.editor_label',
'mdi:chart-gantt',
html` ${this._renderOptionSelector(configPathMode, this._miniTimelineModes, {
label: localize('config.common.controls.timeline.mode'),
})}
${this._renderTimelineCoreControls(
formatDomain,
configPathStyle,
configPathWindowSeconds,
configPathClusteringThreshold,
configPathShowRecordings,
configPathFormat24h,
defaultShowRecordings,
defaultFormat24h,
configPathPanMode,
)}`,
);
}
/**
* Render the next & previous controls.
* @param domain The submenu domain.
* @param configPathStyle Next previous style config path.
* @param configPathSize Next previous size config path.
* @returns A rendered template.
*/
private _renderViewDisplay(
domain: string,
configPathMode: string,
configPathSelectedPosition: string,
configPathSelectedWidthFactor: string,
configPathColumns: string,
configPathMaxColumns: string,
): TemplateResult | void {
return this._putInSubmenu(
domain,
true,
'config.common.display.editor_label',
'mdi:palette-swatch',
html`
${this._renderOptionSelector(configPathMode, this._displayModes, {
label: localize('config.common.display.mode'),
})}
${this._renderOptionSelector(
configPathSelectedPosition,
this._gridSelectPositions,
{
label: localize('config.common.display.grid_selected_position'),
},
)}
${this._renderNumberInput(configPathSelectedWidthFactor, {
min: 0,
label: localize('config.common.display.grid_selected_width_factor'),
})}
${this._renderNumberInput(configPathColumns, {
min: 0,
label: localize('config.common.display.grid_columns'),
})}
${this._renderNumberInput(configPathMaxColumns, {
min: 0,
label: localize('config.common.display.grid_max_columns'),
})}
`,
);
}
/**
* Render the next & previous controls.
* @param domain The submenu domain.
* @param configPathStyle Next previous style config path.
* @param configPathSize Next previous size config path.
* @returns A rendered template.
*/
private _renderNextPreviousControls(
domain: string,
configPathStyle: string,
configPathSize: string,
options?: {
allowIcons?: boolean;
allowThumbnails?: boolean;
},
): TemplateResult | void {
return this._putInSubmenu(
domain,
true,
'config.common.controls.next_previous.editor_label',
'mdi:arrow-right-bold-circle',
html`
${this._renderOptionSelector(
configPathStyle,
this._nextPreviousControlStyles.filter(
(item) =>
(!!options?.allowThumbnails || item.value !== 'thumbnails') &&
(!!options?.allowIcons || item.value !== 'icons'),
),
{
label: localize('config.common.controls.next_previous.style'),
},
)}
${this._renderNumberInput(configPathSize, {
min: BUTTON_SIZE_MIN,
label: localize('config.common.controls.next_previous.size'),
})}
`,
);
}
/**
* Render the thumbnails controls.
* @param domain The submenu domain.
* @param configPathMode Thumbnails mode config path.
* @param configPathSize Thumbnails size config path.
* @param configPathShowDetails Thumbnails show details config path.
* @param configPathShowFavoriteControl Thumbnails show favorite control config path.
* @param configPathShowTimelineControl Thumbnails show timeline control config path,
* @param options An optional config path to media selection and mini-timeline mode.
* @returns A rendered template.
*/
private _renderThumbnailsControls(
domain: string,
configPathSize: string,
configPathShowDetails: string,
configPathShowFavoriteControl: string,
configPathShowTimelineControl: string,
configPathShowDownloadControl: string,
configPathShowReviewControl: string,
configPathShowInfoControl: string,
defaults: {
show_details: boolean;
show_favorite_control: boolean;
show_timeline_control: boolean;
show_download_control: boolean;
show_review_control: boolean;
show_info_control: boolean;
},
options?: {
configPathMode?: string;
},
): TemplateResult | void {
return this._putInSubmenu(
domain,
true,
'config.common.controls.thumbnails.editor_label',
'mdi:image-text',
html`
${options?.configPathMode
? html`${this._renderOptionSelector(
options.configPathMode,
this._thumbnailModes,
{
label: localize('config.common.controls.thumbnails.mode'),
},
)}`
: html``}
${this._renderNumberInput(configPathSize, {
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
label: localize('config.common.controls.thumbnails.size'),
})}
${this._renderSwitch(configPathShowDetails, defaults.show_details, {
label: localize('config.common.controls.thumbnails.show_details'),
})}
${this._renderSwitch(
configPathShowFavoriteControl,
defaults.show_favorite_control,
{
label: localize('config.common.controls.thumbnails.show_favorite_control'),
},
)}
${this._renderSwitch(
configPathShowTimelineControl,
defaults.show_timeline_control,
{
label: localize('config.common.controls.thumbnails.show_timeline_control'),
},
)}
${this._renderSwitch(
configPathShowDownloadControl,
defaults.show_download_control,
{
label: localize('config.common.controls.thumbnails.show_download_control'),
},
)}
${this._renderSwitch(configPathShowReviewControl, defaults.show_review_control, {
label: localize('config.common.controls.thumbnails.show_review_control'),
})}
${this._renderSwitch(configPathShowInfoControl, defaults.show_info_control, {
label: localize('config.common.controls.thumbnails.show_info_control'),
})}
`,
);
}
/**
* Render the thumbnails controls.
* @param domain The submenu domain.
* @param configPathMode Filter mode config path.
* @returns A rendered template.
*/
private _renderFilterControls(
domain: string,
configPathMode: string,
): TemplateResult | void {
return this._putInSubmenu(
domain,
true,
'config.common.controls.filter.editor_label',
'mdi:filter-cog',
html`
${configPathMode
? html`${this._renderOptionSelector(configPathMode, this._filterModes, {
label: localize('config.common.controls.filter.mode'),
})}`
: html``}
`,
);
}
private _renderImageOptions(
configPathMode: string,
configPathUrl: string,
configPathEntity: string,
configPathEntityParameters: string,
configPathRefreshSeconds: string,
): TemplateResult {
return html`
${this._renderOptionSelector(configPathMode, this._imageModes, {
label: localize('config.common.image.mode'),
})}
${this._renderStringInput(configPathUrl, {
label: localize('config.common.image.url'),
})}
${this._renderOptionSelector(
configPathEntity,
this.hass ? getEntitiesFromHASS(this.hass) : [],
{
label: localize('config.common.image.entity'),
},
)}
${this._renderStringInput(configPathEntityParameters, {
label: localize('config.common.image.entity_parameters'),
})}
${this._renderNumberInput(configPathRefreshSeconds, {
label: localize('config.common.image.refresh_seconds'),
})}
`;
}
private _modifyConfig(func: (config: RawAdvancedCameraCardConfig) => boolean): void {
if (this._config) {
const newConfig = copyConfig(this._config);
if (func(newConfig)) {
this._updateConfig(newConfig);
}
}
}
private _renderArrayManagementControls(
configPathArray: string,
index: number,
menu: string,
add?: boolean,
): TemplateResult | void {
const array = this._config ? getConfigValue(this._config, configPathArray) : null;
return html`
<div class="controls">
<ha-icon-button
.label=${localize('editor.move_up')}
.disabled=${add || !this._config || !Array.isArray(array) || index <= 0}
@click=${() =>
!add &&
this._modifyConfig((config: RawAdvancedCameraCardConfig): boolean => {
const array = getConfigValue(config, configPathArray);
if (Array.isArray(array) && index > 0) {
arrayMove(array, index, index - 1);
this._openMenu(menu, index - 1);
return true;
}
return false;
})}
>
<advanced-camera-card-icon
.icon=${{ icon: 'mdi:arrow-up' }}
></advanced-camera-card-icon>
</ha-icon-button>
<ha-icon-button
.label=${localize('editor.move_down')}
.disabled=${add ||
!this._config ||
!Array.isArray(this._config.cameras) ||
index >= this._config.cameras.length - 1}
@click=${() =>
!add &&
this._modifyConfig((config: RawAdvancedCameraCardConfig): boolean => {
const array = getConfigValue(config, configPathArray);
if (Array.isArray(array) && index < array.length - 1) {
arrayMove(array, index, index + 1);
this._openMenu(menu, index + 1);
return true;
}
return false;
})}
>
<advanced-camera-card-icon
.icon=${{ icon: 'mdi:arrow-down' }}
></advanced-camera-card-icon>
</ha-icon-button>
<ha-icon-button
.label=${localize('editor.delete')}
.disabled=${add}
@click=${() => {
this._modifyConfig((config: RawAdvancedCameraCardConfig): boolean => {
const array = getConfigValue(config, configPathArray);
if (Array.isArray(array)) {
array.splice(index, 1);
this._closeMenu(menu);
return true;
}
return false;
});
}}
>
<advanced-camera-card-icon
.icon=${{ icon: 'mdi:delete' }}
></advanced-camera-card-icon>
</ha-icon-button>
</div>
`;
}
private _renderFolder(
folders: RawAdvancedCameraCardConfigArray,
folderIndex: number,
addNewFolder?: boolean,
): TemplateResult | void {
const submenuClasses = {
submenu: true,
selected: this._expandedMenus[MENU_FOLDERS] === folderIndex,
};
const folderTypes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'ha', label: localize('config.folders.types.ha') },
];
return html` <div class="${classMap(submenuClasses)}">
<div
class="submenu-header"
@click=${this._toggleMenu}
.domain=${MENU_FOLDERS}
.key=${folderIndex}
>
<advanced-camera-card-icon
.icon=${{ icon: addNewFolder ? 'mdi:folder-plus' : 'mdi:folder' }}
></advanced-camera-card-icon>
<span>
${addNewFolder
? html` <span class="new"> [${localize('editor.add_new_folder')}...] </span>`
: html`<span
>${this._getEditorFolderTitle(
folderIndex,
folders[folderIndex] || {},
)}</span
>`}
</span>
</div>
${this._expandedMenus[MENU_FOLDERS] === folderIndex
? html` <div class="values">
${this._renderArrayManagementControls(
CONF_FOLDERS,
folderIndex,
MENU_FOLDERS,
addNewFolder,
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_FOLDERS_ARRAY_TYPE, folderIndex),
folderTypes,
)}
${this._renderStringInput(
getArrayConfigPath(CONF_FOLDERS_ARRAY_TITLE, folderIndex),
)}
${this._renderIconSelector(
getArrayConfigPath(CONF_FOLDERS_ARRAY_ICON, folderIndex),
{
label: localize('config.folders.icon'),
},
)}
${this._renderStringInput(
getArrayConfigPath(CONF_FOLDERS_ARRAY_ID, folderIndex),
)}
${this._putInSubmenu(
MENU_FOLDERS_HA,
folderIndex,
'config.folders.ha.editor_label',
'mdi:home-assistant',
html`
${this._renderStringInput(
getArrayConfigPath(CONF_FOLDERS_ARRAY_HA_URL, folderIndex),
)}
${renderMessage({
message: localize('config.folders.ha.path_info'),
icon: 'mdi:information-outline',
url: {
link: FOLDERS_CONFIGURATION_URL,
title: localize('error.configuration'),
},
})}
`,
)}
</div>`
: ''}
</div>`;
}
/**
* Render a camera section.
* @param cameras The full array of cameras.
* @param cameraIndex The index (in the array) to render.
* @param addNewCamera Whether or not this is a section to add a new non-existent camera.
* @returns A rendered template.
*/
private _renderCamera(
cameras: RawAdvancedCameraCardConfigArray,
cameraIndex: number,
entities: string[],
folders: RawAdvancedCameraCardConfigArray,
addNewCamera?: boolean,
): TemplateResult | void {
const liveProviders: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
{
value: 'image',
label: localize('config.cameras.live_providers.image'),
},
{
value: 'jsmpeg',
label: localize('config.cameras.live_providers.jsmpeg'),
},
{
value: 'go2rtc',
label: localize('config.cameras.live_providers.go2rtc'),
},
{
value: 'webrtc-card',
label: localize('config.cameras.live_providers.webrtc-card'),
},
];
const dependentCameras: EditorSelectOption[] = [];
cameras.forEach((camera, index) => {
if (index !== cameraIndex) {
dependentCameras.push({
value: getCameraID(camera),
label: this._getEditorCameraTitle(index, camera),
});
}
});
const folderOptions: EditorSelectOption[] = [];
folders.forEach((folder, index) => {
folderOptions.push({
value: getFolderID(folder, index),
label: this._getEditorFolderTitle(index, folder),
});
});
const submenuClasses = {
submenu: true,
selected: this._expandedMenus[MENU_CAMERAS] === cameraIndex,
};
return html`
<div class="${classMap(submenuClasses)}">
<div
class="submenu-header"
@click=${this._toggleMenu}
.domain=${MENU_CAMERAS}
.key=${cameraIndex}
>
<advanced-camera-card-icon
.icon=${{ icon: addNewCamera ? 'mdi:video-plus' : 'mdi:video' }}
></advanced-camera-card-icon>
<span>
${addNewCamera
? html` <span class="new">
[${localize('editor.add_new_camera')}...]
</span>`
: html`<span
>${this._getEditorCameraTitle(
cameraIndex,
cameras[cameraIndex] || {},
)}</span
>`}
</span>
</div>
${this._expandedMenus[MENU_CAMERAS] === cameraIndex
? html` <div class="values">
${this._renderArrayManagementControls(
CONF_CAMERAS,
cameraIndex,
MENU_CAMERAS,
addNewCamera,
)}
${this._renderEntitySelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_CAMERA_ENTITY, cameraIndex),
'camera',
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_LIVE_PROVIDER, cameraIndex),
liveProviders,
)}
${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_TITLE, cameraIndex),
)}
${this._renderIconSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_ICON, cameraIndex),
{
label: localize('config.cameras.icon'),
},
)}
${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex),
)}
${this._renderSwitch(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_ALWAYS_ERROR_IF_ENTITY_UNAVAILABLE,
cameraIndex,
),
this._defaults.cameras.always_error_if_entity_unavailable,
)}
${this._putInSubmenu(
MENU_CAMERAS_ENGINE,
true,
'config.cameras.engines.editor_label',
'mdi:engine',
html`${this._putInSubmenu(
MENU_CAMERAS_FRIGATE,
cameraIndex,
'config.cameras.frigate.editor_label',
'frigate',
html`
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
cameraIndex,
),
)}
${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_URL, cameraIndex),
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_LABELS, cameraIndex),
[],
{
multiple: true,
label: localize('config.cameras.frigate.labels'),
},
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_FRIGATE_ZONES, cameraIndex),
[],
{
multiple: true,
label: localize('config.cameras.frigate.zones'),
},
)}
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID,
cameraIndex,
),
)}
`,
)}
${this._putInSubmenu(
MENU_CAMERAS_MOTIONEYE,
cameraIndex,
'config.cameras.motioneye.editor_label',
'motioneye',
html` ${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_MOTIONEYE_URL, cameraIndex),
)}
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_MOTIONEYE_IMAGES_DIRECTORY_PATTERN,
cameraIndex,
),
)}
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_MOTIONEYE_IMAGES_FILE_PATTERN,
cameraIndex,
),
)}
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_MOTIONEYE_MOVIES_DIRECTORY_PATTERN,
cameraIndex,
),
)}
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_MOTIONEYE_MOVIES_FILE_PATTERN,
cameraIndex,
),
)}`,
)}
${this._putInSubmenu(
MENU_CAMERAS_REOLINK,
cameraIndex,
'config.cameras.reolink.editor_label',
'reolink',
html` ${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_REOLINK_URL, cameraIndex),
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_REOLINK_MEDIA_RESOLUTION,
cameraIndex,
),
this._reolinkMediaResolution,
{
label: localize(
'config.cameras.reolink.media_resolution.editor_label',
),
},
)}`,
)}`,
)}
${this._putInSubmenu(
MENU_CAMERAS_LIVE_PROVIDER,
true,
'config.cameras.live_provider_options.editor_label',
'mdi:cctv',
html` ${this._putInSubmenu(
MENU_CAMERAS_GO2RTC,
cameraIndex,
'config.cameras.go2rtc.editor_label',
'mdi:alpha-g-circle',
html`${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_GO2RTC_MODES, cameraIndex),
this._go2rtcModes,
{
multiple: true,
label: localize('config.cameras.go2rtc.modes.editor_label'),
},
)}
${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_GO2RTC_STREAM, cameraIndex),
)}
${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_GO2RTC_URL, cameraIndex),
)}
${this._renderNumberInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_GO2RTC_METADATA_FETCH_TIMEOUT_SECONDS,
cameraIndex,
),
{
min: 0,
default:
this._defaults.cameras.go2rtc.metadata_fetch_timeout_seconds,
},
)} `,
)}
${this._putInSubmenu(
MENU_CAMERAS_IMAGE,
true,
'config.cameras.image.editor_label',
'mdi:image',
this._renderImageOptions(
getArrayConfigPath(CONF_CAMERAS_ARRAY_IMAGE_MODE, cameraIndex),
getArrayConfigPath(CONF_CAMERAS_ARRAY_IMAGE_URL, cameraIndex),
getArrayConfigPath(CONF_CAMERAS_ARRAY_IMAGE_ENTITY, cameraIndex),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_IMAGE_ENTITY_PARAMETERS,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
cameraIndex,
),
),
)}
${this._putInSubmenu(
MENU_CAMERAS_WEBRTC_CARD,
cameraIndex,
'config.cameras.webrtc_card.editor_label',
'mdi:webrtc',
html`${this._renderEntitySelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
cameraIndex,
),
'camera',
)}
${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, cameraIndex),
)}`,
)}`,
)}
${this._putInSubmenu(
MENU_CAMERAS_DEPENDENCIES,
cameraIndex,
'config.cameras.dependencies.editor_label',
'mdi:graph',
html` ${this._renderSwitch(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
cameraIndex,
),
this._defaults.cameras.dependencies.all_cameras,
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
cameraIndex,
),
dependentCameras,
{
multiple: true,
},
)}`,
)}
${this._putInSubmenu(
MENU_CAMERAS_TRIGGERS,
cameraIndex,
'config.cameras.triggers.editor_label',
'mdi:magnify-scan',
html`
${this._renderSwitch(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
cameraIndex,
),
this._defaults.cameras.triggers.occupancy,
)}
${this._renderSwitch(
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_MOTION, cameraIndex),
this._defaults.cameras.triggers.motion,
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES,
cameraIndex,
),
entities,
{
multiple: true,
},
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS, cameraIndex),
this._triggersEvents,
{
multiple: true,
label: localize('config.cameras.triggers.events.editor_label'),
},
)}
${this._putInSubmenu(
MENU_CAMERAS_TRIGGERS_REVIEWS,
cameraIndex,
'config.cameras.triggers.reviews.editor_label',
'mdi:check-circle',
html`
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_TRIGGERS_REVIEWS_SEVERITIES,
cameraIndex,
),
this._severities,
{
multiple: true,
label: localize('common.severity'),
},
)}
${this._renderSwitch(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_TRIGGERS_REVIEWS_DESCRIPTION,
cameraIndex,
),
this._defaults.cameras.triggers.reviews.description,
)}
`,
)}
`,
)}
${this._putInSubmenu(
MENU_CAMERAS_MEDIA,
cameraIndex,
'config.cameras.media.editor_label',
'mdi:play-box-outline',
html`
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_MEDIA_TYPE, cameraIndex),
this._cameraMediaTypes,
{
label: localize('config.cameras.media.type'),
},
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_MEDIA_EVENTS_TYPE,
cameraIndex,
),
this._thumbnailEventsMediaTypes,
{
label: localize('config.cameras.media.events_type'),
},
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_MEDIA_REVIEWED, cameraIndex),
this._cameraMediaReviewedOptions,
{
label: localize('config.cameras.media.reviewed'),
},
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_MEDIA_FOLDERS, cameraIndex),
folderOptions,
{
multiple: true,
label: localize('config.cameras.media.folders'),
},
)}
`,
)}
${this._putInSubmenu(
MENU_CAMERAS_CAST,
cameraIndex,
'config.cameras.cast.editor_label',
'mdi:cast',
html`
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_CAST_METHOD, cameraIndex),
this._castMethods,
)}
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH,
cameraIndex,
),
)}
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH,
cameraIndex,
),
)}
`,
)}
${this._putInSubmenu(
MENU_CAMERAS_DIMENSIONS,
cameraIndex,
'config.cameras.dimensions.editor_label',
'mdi:aspect-ratio',
html`
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_ASPECT_RATIO,
cameraIndex,
),
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_ROTATION,
cameraIndex,
),
this._rotations,
)}
${this._renderMediaLayout(
MENU_CAMERAS_DIMENSIONS_LAYOUT,
'config.cameras.dimensions.layout.editor_label',
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_ZOOM_FACTOR,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_PAN_X,
cameraIndex,
),
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_PAN_Y,
cameraIndex,
),
)}
`,
)}
${this._putInSubmenu(
MENU_CAMERAS_CAPABILITIES,
cameraIndex,
'config.cameras.capabilities.editor_label',
'mdi:cog-stop',
html`
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE,
cameraIndex,
),
this._capabilities,
{
multiple: true,
},
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT,
cameraIndex,
),
this._capabilities,
{
multiple: true,
},
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_CAPABILITIES_FORCE,
cameraIndex,
),
this._forceableCapabilities,
{
multiple: true,
},
)}
`,
)}
${this._putInSubmenu(
MENU_CAMERAS_PROXY,
cameraIndex,
'config.cameras.proxy.editor_label',
'mdi:arrow-decision',
html`
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_LIVE, cameraIndex),
this._proxyModes,
{
label: localize('config.cameras.proxy.live'),
},
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_MEDIA, cameraIndex),
this._proxyModes,
{
label: localize('config.cameras.proxy.media'),
},
)}
${this._renderSwitch(
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_DYNAMIC, cameraIndex),
this._defaults.cameras.proxy.dynamic,
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_PROXY_SSL_VERIFICATION,
cameraIndex,
),
this._proxySSLVerification,
{
label: localize(
'config.cameras.proxy.ssl_verification.editor_label',
),
},
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS,
cameraIndex,
),
this._proxySSLCiphers,
{
label: localize('config.cameras.proxy.ssl_ciphers.editor_label'),
},
)}
`,
)}
</div>`
: ``}
</div>
`;
}
/**
* Render a string input field.
* @param configPath The configuration path to set/read.
* @param type The allowable input
* @returns A rendered template.
*/
private _renderStringInput(
configPath: string,
params?: {
label?: string;
type?:
| 'number'
| 'text'
| 'search'
| 'tel'
| 'url'
| 'email'
| 'password'
| 'date'
| 'month'
| 'week'
| 'time'
| 'datetime-local'
| 'color';
},
): TemplateResult | void {
if (!this._config) {
return;
}
return html`
<ha-selector
.hass=${this.hass}
.selector=${{ text: { type: params?.type || 'text' } }}
.label=${params?.label ?? this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
</ha-selector>
`;
}
/**
* Render a boolean selector.
* @param configPath The configuration path to set/read.
* @param valueDefault The default switch value if unset.
* @param params Optional parameters to control the selector.
* @returns A rendered template.
*/
private _renderSwitch(
configPath: string,
valueDefault: boolean,
params?: {
label?: string;
},
): TemplateResult | void {
if (!this._config) {
return;
}
return html`
<ha-selector
.hass=${this.hass}
.selector=${{ boolean: {} }}
.label=${params?.label || this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, valueDefault)}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
</ha-selector>
`;
}
private _updateConfig(config: RawAdvancedCameraCardConfig): void {
this._config = config;
fireHASSEvent(this, 'config-changed', { config: this._config });
}
protected render(): TemplateResult | void {
if (!this.hass || !this._config) {
return html``;
}
const entities = getEntitiesFromHASS(this.hass);
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
[]) as RawAdvancedCameraCardConfigArray;
const folders = (getConfigValue(this._config, CONF_FOLDERS) ||
[]) as RawAdvancedCameraCardConfigArray;
return html`
<div class="card-config">
${this._renderOptionSetHeader('cameras')}
${this._expandedMenus[MENU_OPTIONS] === 'cameras'
? html`
<div class="values">
${SECTION_DOC_LINKS['cameras']
? this._renderDocLinkRow(SECTION_DOC_LINKS['cameras'])
: ''}
${cameras.map((_, index) =>
this._renderCamera(cameras, index, entities, folders),
)}
${this._renderCamera(cameras, cameras.length, entities, folders, true)}
</div>
`
: ''}
${this._renderOptionSetHeader('profiles')}
${this._expandedMenus[MENU_OPTIONS] === 'profiles'
? html` <div class="values">
${this._renderOptionSelector(CONF_PROFILES, this._profiles, {
multiple: true,
label: localize('config.profiles.editor_label'),
})}
</div>`
: ''}
${this._renderOptionSetHeader('view')}
${this._expandedMenus[MENU_OPTIONS] === 'view'
? html`
<div class="values">
${SECTION_DOC_LINKS['view']
? this._renderDocLinkRow(SECTION_DOC_LINKS['view'])
: ''}
${this._renderOptionSelector(CONF_VIEW_DEFAULT, this._viewModes)}
${this._renderOptionSelector(
CONF_VIEW_CAMERA_SELECT,
this._cameraSelectViewModes,
)}
${this._renderSwitch(CONF_VIEW_DIM, this._defaults.view.dim)}
${this._renderNumberInput(CONF_VIEW_INTERACTION_SECONDS)}
${this._renderSwitch(
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
this._defaults.view.default_cycle_camera,
)}
${this._renderViewDefaultResetMenu()} ${this._renderViewTriggersMenu()}
${this._renderViewKeyboardShortcutMenu()}
${this._renderOptionSelector(CONF_VIEW_THEME_THEMES, this._themes, {
label: localize('config.view.theme.themes.editor_label'),
multiple: true,
})}
</div>
`
: ''}
${this._renderOptionSetHeader('menu')}
${this._expandedMenus[MENU_OPTIONS] === 'menu'
? html`
<div class="values">
${SECTION_DOC_LINKS['menu']
? this._renderDocLinkRow(SECTION_DOC_LINKS['menu'])
: ''}
${this._renderOptionSelector(CONF_MENU_STYLE, this._menuStyles)}
${this._renderOptionSelector(CONF_MENU_POSITION, this._menuPositions)}
${this._renderOptionSelector(CONF_MENU_ALIGNMENT, this._menuAlignments)}
${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, {
min: BUTTON_SIZE_MIN,
})}
${this._renderMenuButton('iris') /* */}
${this._renderMenuButton('camera_ui')}
${this._renderMenuButton('cameras') /* */}
${this._renderMenuButton('clips')}
${this._renderMenuButton('display_mode')}
${this._renderMenuButton('download') /* */}
${this._renderMenuButton('expand') /* */}
${this._renderMenuButton('folders')}
${this._renderMenuButton('fullscreen')}
${this._renderMenuButton('gallery')}
${this._renderMenuButton('image') /* */}
${this._renderMenuButton('info') /* */}
${this._renderMenuButton('live')}
${this._renderMenuButton('media_player')}
${this._renderMenuButton(
'microphone',
html`${this._renderOptionSelector(
`${CONF_MENU_BUTTONS}.microphone.type`,
this._microphoneButtonTypes,
{ label: localize('config.menu.buttons.type') },
)}`,
)}
${this._renderMenuButton('mute') /* */}
${this._renderMenuButton('play')}
${this._renderMenuButton('ptz_controls')}
${this._renderMenuButton('ptz_home')}
${this._renderMenuButton('recordings')}
${this._renderMenuButton('reviews')}
${this._renderMenuButton('screenshot')}
${this._renderMenuButton('set_review')}
${this._renderMenuButton('snapshots')}
${this._renderMenuButton('substreams')}
${this._renderMenuButton('timeline')}
</div>
`
: ''}
${this._renderOptionSetHeader('status_bar')}
${this._expandedMenus[MENU_OPTIONS] === 'status_bar'
? html`
<div class="values">
${SECTION_DOC_LINKS['status_bar']
? this._renderDocLinkRow(SECTION_DOC_LINKS['status_bar'])
: ''}
${this._renderOptionSelector(
CONF_STATUS_BAR_STYLE,
this._statusBarStyles,
)}
${this._renderOptionSelector(
CONF_STATUS_BAR_POSITION,
this._statusBarPositions,
)}
${this._renderNumberInput(CONF_STATUS_BAR_HEIGHT, {
min: STATUS_BAR_HEIGHT_MIN,
label: localize('config.status_bar.height'),
})}
${this._renderNumberInput(CONF_STATUS_BAR_POPUP_SECONDS, {
min: 0,
max: 60,
default: this._defaults.status_bar.popup_seconds,
label: localize('config.status_bar.popup_seconds'),
})}
${this._renderStatusBarItem('title') /* */}
${this._renderStatusBarItem('resolution') /* */}
${this._renderStatusBarItem('technology') /* */}
${this._renderStatusBarItem('engine') /* */}
</div>
`
: ''}
${this._renderOptionSetHeader('live')}
${this._expandedMenus[MENU_OPTIONS] === 'live'
? html`
<div class="values">
${SECTION_DOC_LINKS['live']
? this._renderDocLinkRow(SECTION_DOC_LINKS['live'])
: ''}
${this._renderSwitch(CONF_LIVE_PRELOAD, this._defaults.live.preload)}
${this._renderSwitch(CONF_LIVE_DRAGGABLE, this._defaults.live.draggable)}
${this._renderSwitch(CONF_LIVE_ZOOMABLE, this._defaults.live.zoomable)}
${this._renderSwitch(CONF_LIVE_LAZY_LOAD, this._defaults.live.lazy_load)}
${this._renderOptionSelector(
CONF_LIVE_LAZY_UNLOAD,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_PLAY,
this._mediaActionPositiveConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_PAUSE,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_MUTE,
this._mediaLiveMuteConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_UNMUTE,
this._mediaLiveUnmuteConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_TRANSITION_EFFECT,
this._transitionEffects,
)}
${this._renderSwitch(
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
this._defaults.live.show_image_during_load,
)}
${this._renderViewDisplay(
MENU_LIVE_DISPLAY,
CONF_LIVE_DISPLAY_MODE,
CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION,
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
CONF_LIVE_DISPLAY_GRID_COLUMNS,
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
)}
${this._putInSubmenu(
MENU_LIVE_CONTROLS,
true,
'config.live.controls.editor_label',
'mdi:gamepad',
html`
${this._renderSwitch(
CONF_LIVE_CONTROLS_BUILTIN,
this._defaults.live.controls.builtin,
{
label: localize('config.common.controls.builtin'),
},
)}
${this._renderSwitch(
CONF_LIVE_CONTROLS_WHEEL,
this._defaults.live.controls.wheel,
{
label: localize('config.common.controls.wheel'),
},
)}
${this._renderNextPreviousControls(
MENU_LIVE_CONTROLS_NEXT_PREVIOUS,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
{
allowIcons: true,
},
)}
${this._renderThumbnailsControls(
MENU_LIVE_CONTROLS_THUMBNAILS,
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
this._defaults.live.controls.thumbnails,
{
configPathMode: CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
},
)}
${this._renderMiniTimeline(
MENU_LIVE_CONTROLS_TIMELINE,
MENU_LIVE_CONTROLS_TIMELINE_FORMAT,
CONF_LIVE_CONTROLS_TIMELINE_MODE,
CONF_LIVE_CONTROLS_TIMELINE_STYLE,
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_LIVE_CONTROLS_TIMELINE_FORMAT_24H,
this._defaults.live.controls.timeline.show_recordings,
this._defaults.live.controls.timeline.format['24h'],
CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
)}
${this._putInSubmenu(
MENU_LIVE_CONTROLS_PTZ,
true,
'config.live.controls.ptz.editor_label',
'mdi:pan',
html`
${this._renderOptionSelector(
CONF_LIVE_CONTROLS_PTZ_MODE,
this._ptzModes,
)}
${this._renderOptionSelector(
CONF_LIVE_CONTROLS_PTZ_POSITION,
this._ptzPositions,
)}
${this._renderOptionSelector(
CONF_LIVE_CONTROLS_PTZ_ORIENTATION,
this._ptzOrientations,
)}
${this._renderSwitch(
CONF_LIVE_CONTROLS_PTZ_HIDE_PAN_TILT,
this._defaults.live.controls.ptz.hide_pan_tilt,
{
label: localize('config.live.controls.ptz.hide_pan_tilt'),
},
)}
${this._renderSwitch(
CONF_LIVE_CONTROLS_PTZ_HIDE_ZOOM,
this._defaults.live.controls.ptz.hide_pan_tilt,
{
label: localize('config.live.controls.ptz.hide_zoom'),
},
)}
${this._renderSwitch(
CONF_LIVE_CONTROLS_PTZ_HIDE_HOME,
this._defaults.live.controls.ptz.hide_home,
{
label: localize('config.live.controls.ptz.hide_home'),
},
)}
`,
)}
`,
)}
${this._putInSubmenu(
MENU_LIVE_MICROPHONE,
true,
'config.live.microphone.editor_label',
'mdi:microphone',
html`
${this._renderNumberInput(CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS)}
${this._renderSwitch(
CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED,
this._defaults.live.microphone.always_connected,
)}
${this._renderNumberInput(
CONF_LIVE_MICROPHONE_MUTE_AFTER_MICROPHONE_MUTE_SECONDS,
)}
`,
)}
</div>
`
: ''}
${this._renderOptionSetHeader('folders')}
${this._expandedMenus[MENU_OPTIONS] === 'folders'
? html` <div class="values">
${SECTION_DOC_LINKS['folders']
? this._renderDocLinkRow(SECTION_DOC_LINKS['folders'])
: ''}
${folders.map((_, index) => this._renderFolder(folders, index))}
${this._renderFolder(folders, folders.length, true)}
</div>`
: ''}
${this._renderOptionSetHeader('media_gallery')}
${this._expandedMenus[MENU_OPTIONS] === 'media_gallery'
? html` <div class="values">
${SECTION_DOC_LINKS['media_gallery']
? this._renderDocLinkRow(SECTION_DOC_LINKS['media_gallery'])
: ''}
${this._renderThumbnailsControls(
MENU_MEDIA_GALLERY_CONTROLS_THUMBNAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SIZE,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
this._defaults.media_gallery.controls.thumbnails,
)}
${this._renderFilterControls(
MENU_MEDIA_GALLERY_CONTROLS_FILTER,
CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE,
)}
</div>`
: ''}
${this._renderOptionSetHeader('media_viewer')}
${this._expandedMenus[MENU_OPTIONS] === 'media_viewer'
? html` <div class="values">
${SECTION_DOC_LINKS['media_viewer']
? this._renderDocLinkRow(SECTION_DOC_LINKS['media_viewer'])
: ''}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_PLAY,
this._mediaActionPositiveConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_PAUSE,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_MUTE,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_UNMUTE,
this._mediaActionPositiveConditions,
{
multiple: true,
},
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_DRAGGABLE,
this._defaults.media_viewer.draggable,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_ZOOMABLE,
this._defaults.media_viewer.zoomable,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_LAZY_LOAD,
this._defaults.media_viewer.lazy_load,
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
this._transitionEffects,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
this._defaults.media_viewer.snapshot_click_plays_clip,
)}
${this._renderViewDisplay(
MENU_MEDIA_VIEWER_DISPLAY,
CONF_MEDIA_VIEWER_DISPLAY_MODE,
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION,
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
)}
${this._putInSubmenu(
MENU_MEDIA_VIEWER_CONTROLS,
true,
'config.media_viewer.controls.editor_label',
'mdi:gamepad',
html`
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_BUILTIN,
this._defaults.media_viewer.controls.builtin,
{
label: localize('config.common.controls.builtin'),
},
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_WHEEL,
this._defaults.media_viewer.controls.wheel,
{
label: localize('config.common.controls.wheel'),
},
)}
${this._renderNextPreviousControls(
MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
{
allowThumbnails: true,
},
)}
${this._renderThumbnailsControls(
MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
this._defaults.media_viewer.controls.thumbnails,
{
configPathMode: CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
},
)}
${this._renderMiniTimeline(
MENU_MEDIA_VIEWER_CONTROLS_TIMELINE,
MENU_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT_24H,
this._defaults.media_viewer.controls.timeline.show_recordings,
this._defaults.media_viewer.controls.timeline.format['24h'],
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE,
)}
`,
)}
</div>`
: ''}
${this._renderOptionSetHeader('image')}
${this._expandedMenus[MENU_OPTIONS] === 'image'
? html` <div class="values">
${SECTION_DOC_LINKS['image']
? this._renderDocLinkRow(SECTION_DOC_LINKS['image'])
: ''}
${this._renderImageOptions(
CONF_IMAGE_MODE,
CONF_IMAGE_URL,
CONF_IMAGE_ENTITY,
CONF_IMAGE_ENTITY_PARAMETERS,
CONF_IMAGE_REFRESH_SECONDS,
)}
</div>`
: ''}
${this._renderOptionSetHeader('timeline')}
${this._expandedMenus[MENU_OPTIONS] === 'timeline'
? html` <div class="values">
${SECTION_DOC_LINKS['timeline']
? this._renderDocLinkRow(SECTION_DOC_LINKS['timeline'])
: ''}
${this._renderTimelineCoreControls(
MENU_TIMELINE_FORMAT,
CONF_TIMELINE_STYLE,
CONF_TIMELINE_WINDOW_SECONDS,
CONF_TIMELINE_CLUSTERING_THRESHOLD,
CONF_TIMELINE_SHOW_RECORDINGS,
CONF_TIMELINE_FORMAT_24H,
this._defaults.timeline.show_recordings,
this._defaults.timeline.format['24h'],
)}
${this._renderThumbnailsControls(
MENU_TIMELINE_CONTROLS_THUMBNAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_REVIEW_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_INFO_CONTROL,
this._defaults.timeline.controls.thumbnails,
{
configPathMode: CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
},
)}
</div>`
: ''}
${this._renderOptionSetHeader('dimensions')}
${this._expandedMenus[MENU_OPTIONS] === 'dimensions'
? html` <div class="values">
${SECTION_DOC_LINKS['dimensions']
? this._renderDocLinkRow(SECTION_DOC_LINKS['dimensions'])
: ''}
${this._renderOptionSelector(
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
this._aspectRatioModes,
)}
${this._renderStringInput(CONF_DIMENSIONS_ASPECT_RATIO)}
${this._renderStringInput(CONF_DIMENSIONS_HEIGHT)}
</div>`
: ''}
${this._renderOptionSetHeader(
'performance',
getConfigValue(this._config, CONF_PERFORMANCE_PROFILE) === 'low'
? 'warning'
: undefined,
)}
${this._expandedMenus[MENU_OPTIONS] === 'performance'
? html` <div class="values">
${getConfigValue(this._config, CONF_PERFORMANCE_PROFILE) === 'low'
? this._renderInfo(localize('config.performance.warning'))
: html``}
${this._putInSubmenu(
MENU_PERFORMANCE_FEATURES,
true,
'config.performance.features.editor_label',
'mdi:feature-search',
html`
${this._renderSwitch(
CONF_PERFORMANCE_FEATURES_CARD_LOADING_INDICATOR,
this._defaults.performance.features.card_loading_indicator,
)}
${this._renderSwitch(
CONF_PERFORMANCE_FEATURES_CARD_LOADING_EFFECTS,
this._defaults.performance.features.card_loading_effects,
)}
${this._renderSwitch(
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
this._defaults.performance.features.animated_progress_indicator,
)}
${this._renderNumberInput(CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE, {
max: MEDIA_CHUNK_SIZE_MAX,
})}
${this._renderNumberInput(
CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS,
{
min: 1,
},
)}
`,
)}
${this._putInSubmenu(
MENU_PERFORMANCE_STYLE,
true,
'config.performance.style.editor_label',
'mdi:palette-swatch-variant',
html`
${this._renderSwitch(
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
this._defaults.performance.style.border_radius,
)}
${this._renderSwitch(
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
this._defaults.performance.style.box_shadow,
)}
`,
)}
</div>`
: ''}
${this._renderOptionSetHeader('remote_control')}
${this._expandedMenus[MENU_OPTIONS] === 'remote_control'
? html` <div class="values">
${SECTION_DOC_LINKS['remote_control']
? this._renderDocLinkRow(SECTION_DOC_LINKS['remote_control'])
: ''}
${this._putInSubmenu(
MENU_REMOTE_CONTROL_ENTITIES,
true,
'config.remote_control.entities.editor_label',
'mdi:devices',
html`
${this._renderEntitySelector(
CONF_REMOTE_CONTROL_ENTITIES_CAMERA,
'input_select',
)}
`,
)}
</div>`
: ''}
${this._config['overrides'] !== undefined
? html` ${this._renderOptionSetHeader('overrides')}
${this._expandedMenus[MENU_OPTIONS] === 'overrides'
? html` <div class="values">
${this._renderInfo(localize('config.overrides.info'))}
</div>`
: ''}`
: html``}
<div class="action-buttons">
${this._configUpgradeable
? html`<ha-button
appearance="filled"
variant="warning"
title=${localize('editor.upgrade_available')}
aria-label=${localize('editor.upgrade_available')}
@click=${() => {
if (this._config) {
const upgradedConfig = copyConfig(this._config);
upgradeConfig(upgradedConfig);
this._updateConfig(upgradedConfig);
}
}}
>
${localize('editor.upgrade')}
</ha-button>`
: ''}
<ha-button
title=${localize('editor.toggle_diagnostics')}
aria-label=${localize('editor.toggle_diagnostics')}
@click=${() => {
fireAdvancedCameraCardEvent(this, 'editor:diagnostics');
}}
>
${localize('editor.toggle_diagnostics')}
</ha-button>
</div>
</div>
`;
}
/**
* Close the editor menu with the given domain.
* @param targetDomain The menu domain to close.
*/
private _closeMenu(targetDomain: string) {
delete this._expandedMenus[targetDomain];
this.requestUpdate();
}
/**
* Open an editor menu.
* @param targetDomain The menu domain to open.
* @param key The menu object key to open.
*/
private _openMenu(targetDomain: string, key: number | string) {
this._expandedMenus[targetDomain] = key;
this.requestUpdate();
}
/**
* Toggle an editor menu.
* @param ev An event.
*/
private _toggleMenu(ev: { target: EditorMenuTarget | null }): void {
if (ev && ev.target) {
const domain = ev.target.domain;
const key = ev.target.key;
if (this._expandedMenus[domain] === key) {
this._closeMenu(domain);
} else {
this._openMenu(domain, key);
}
}
}
/**
* Handle a changed option value.
* @param ev Event triggering the change.
*/
private _valueChangedHandler(key: string, ev: CustomEvent<{ value: unknown }>): void {
if (!this._config || !this.hass) {
return;
}
let value;
if (ev.detail && ev.detail.value !== undefined) {
value = ev.detail.value;
if (typeof value === 'string') {
value = value.trim();
}
}
if (getConfigValue(this._config, key) === value) {
return;
}
const newConfig = copyConfig(this._config);
if (value === '' || value === undefined) {
deleteConfigValue(newConfig, key);
} else {
setConfigValue(newConfig, key, value);
}
this._updateConfig(newConfig);
}
static get styles(): CSSResultGroup {
return unsafeCSS(editorStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'advanced-camera-card-editor': AdvancedCameraCardEditor;
}
}