-
- ${this._cameras === undefined
- ? until(
- (async () => {
- await this._loadCameras();
- // Don't reset messages as errors may have been generated
- // during the camera load.
- this._changeView({ resetMessage: false });
- return this._render();
- })(),
- renderProgressIndicator(),
- )
- : // Always want to call render even if there's a message, to
- // ensure live preload is always present (even if not displayed).
- this._render()}
- ${this._getConfig().elements
- ? // Always show elements to allow for custom menu items (etc.) to
- // be present even if a particular view has an error. Elements
- // need to render after the main views so it can render 'on top'.
- html` {
- this._addDynamicMenuButton(e.detail);
- }}
- @frigate-card:menu-remove=${(e) => {
- this._removeDynamicMenuButton(e.detail);
- }}
- @frigate-card:condition-state-request=${(ev) => {
- conditionStateRequestHandler(ev, this._conditionState);
- }}
- >
- `
- : ``}
- ${
- // Keep message rendering to last to show messages that may have
- // been generated during the render.
- this._message ? renderMessage(this._message) : ''
- }
-
+
+ ${this._cameras === undefined
+ ? until(
+ (async () => {
+ await this._loadCameras();
+ // Don't reset messages as errors may have been generated
+ // during the camera load.
+ this._changeView({ resetMessage: false });
+ return this._render();
+ })(),
+ renderProgressIndicator(),
+ )
+ : // Always want to call render even if there's a message, to
+ // ensure live preload is always present (even if not displayed).
+ this._render()}
+ ${this._getConfig().elements
+ ? // Always show elements to allow for custom menu items (etc.) to
+ // be present even if a particular view has an error. Elements
+ // need to render after the main views so it can render 'on top'.
+ html` {
+ this._addDynamicMenuButton(e.detail);
+ }}
+ @frigate-card:menu-remove=${(e) => {
+ this._removeDynamicMenuButton(e.detail);
+ }}
+ @frigate-card:condition-state-request=${(ev) => {
+ conditionStateRequestHandler(ev, this._conditionState);
+ }}
+ >
+ `
+ : ``}
+ ${
+ // Keep message rendering to last to show messages that may have
+ // been generated during the render.
+ this._message ? renderMessage(this._message) : ''
+ }
${!renderMenuAbove ? this._renderMenu() : ''}
`;
diff --git a/src/components/image.ts b/src/components/image.ts
index 0ba8f042..98e6e10b 100644
--- a/src/components/image.ts
+++ b/src/components/image.ts
@@ -1,11 +1,11 @@
import { HomeAssistant } from 'custom-card-helpers';
import {
- CSSResultGroup,
- html,
- LitElement,
- PropertyValues,
- TemplateResult,
- unsafeCSS
+ CSSResultGroup,
+ html,
+ LitElement,
+ PropertyValues,
+ TemplateResult,
+ unsafeCSS
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
@@ -14,7 +14,7 @@ import defaultImage from '../images/frigate-bird-in-sky.jpg';
import { localize } from '../localize/localize.js';
import imageStyle from '../scss/image.scss';
import { CameraConfig, ImageViewConfig } from '../types.js';
-import { shouldUpdateBasedOnHass } from '../utils/ha';
+import { isHassDifferent } from '../utils/ha';
import { dispatchMediaShowEvent } from '../utils/media-info.js';
import { View } from '../view.js';
import { dispatchErrorMessageEvent } from './message.js';
@@ -96,7 +96,7 @@ export class FrigateCardImage extends LitElement {
this._imageConfig?.mode === 'camera' &&
cameraEntity
) {
- if (shouldUpdateBasedOnHass(this.hass, changedProps.get('hass'), [cameraEntity])) {
+ if (isHassDifferent(this.hass, changedProps.get('hass'), [cameraEntity])) {
// If the state of the camera entity has changed, remove the cached
// value (will be re-calculated in willUpdate). This is important to
// ensure a changed access token is immediately used.
diff --git a/src/components/live.ts b/src/components/live.ts
index 83ad8627..8cf3c6f0 100644
--- a/src/components/live.ts
+++ b/src/components/live.ts
@@ -1090,7 +1090,8 @@ export class FrigateCardLiveJSMPEG extends LitElement {
if (!this.cameraConfig?.camera_name) {
return dispatchErrorMessageEvent(
this,
- localize('error.no_camera_name') + `: ${JSON.stringify(this.cameraConfig)}`,
+ localize('error.no_camera_name'),
+ this.cameraConfig,
);
}
diff --git a/src/components/submenu.ts b/src/components/submenu.ts
index f7a77562..270bfb76 100644
--- a/src/components/submenu.ts
+++ b/src/components/submenu.ts
@@ -1,12 +1,12 @@
import type { Corner } from '@material/mwc-menu';
import { HomeAssistant } from 'custom-card-helpers';
import {
- CSSResultGroup,
- html,
- LitElement,
- PropertyValues,
- TemplateResult,
- unsafeCSS
+ CSSResultGroup,
+ html,
+ LitElement,
+ PropertyValues,
+ TemplateResult,
+ unsafeCSS
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
@@ -15,10 +15,10 @@ import { actionHandler } from '../action-handler-directive.js';
import submenuStyle from '../scss/submenu.scss';
import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../types.js';
import {
- frigateCardHasAction,
- stopEventFromActivatingCardWideActions
+ frigateCardHasAction,
+ stopEventFromActivatingCardWideActions
} from '../utils/action.js';
-import { refreshDynamicStateParameters, shouldUpdateBasedOnHass } from '../utils/ha';
+import { isHassDifferent, refreshDynamicStateParameters } from '../utils/ha';
import { domainIcon } from '../utils/icons/domain-icon.js';
@customElement('frigate-card-submenu')
@@ -139,7 +139,7 @@ export class FrigateCardSubmenuSelect extends LitElement {
changedProps.size != 1 ||
!this.submenuSelect ||
(!!oldHass &&
- shouldUpdateBasedOnHass(this.hass, oldHass, [this.submenuSelect.entity]))
+ isHassDifferent(this.hass, oldHass, [this.submenuSelect.entity]))
);
}
diff --git a/src/components/viewer.ts b/src/components/viewer.ts
index 0819b960..f48975c6 100644
--- a/src/components/viewer.ts
+++ b/src/components/viewer.ts
@@ -37,7 +37,7 @@ import {
overrideMultiBrowseMediaQueryParameters
} from '../utils/ha/browse-media.js';
import { createMediaShowInfo } from '../utils/media-info.js';
-import { ResolvedMediaCache, resolveMedia } from '../utils/resolved-media.js';
+import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
import { View } from '../view.js';
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
diff --git a/src/const.ts b/src/const.ts
index e61138ac..717e82f4 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -23,6 +23,12 @@ export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER =
`${CONF_CAMERAS}.#.live_provider` as const;
export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS =
`${CONF_CAMERAS}.#.dependent_cameras` as const;
+export const CONF_CAMERAS_ARRAY_TRIGGER_BY_MOTION =
+ `${CONF_CAMERAS}.#.trigger_by_motion` as const;
+export const CONF_CAMERAS_ARRAY_TRIGGER_BY_OCCUPANCY =
+ `${CONF_CAMERAS}.#.trigger_by_occupancy` as const;
+export const CONF_CAMERAS_ARRAY_TRIGGER_BY_ENTITIES =
+ `${CONF_CAMERAS}.#.trigger_by_entities` as const;
export const CONF_VIEW = 'view' as const;
export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const;
@@ -33,6 +39,10 @@ export const CONF_VIEW_UPDATE_CYCLE_CAMERA = `${CONF_VIEW}.update_cycle_camera`
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const;
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
+export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const;
+export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const;
+export const CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS =
+ `${CONF_VIEW_SCAN}.show_trigger_status` as const;
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS =
@@ -142,4 +152,4 @@ export const CONF_DIMENSIONS_ASPECT_RATIO_MODE =
export const CONF_OVERRIDES = 'overrides' as const;
// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93
-export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
\ No newline at end of file
+export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
diff --git a/src/editor.ts b/src/editor.ts
index df36a05b..f0bf24b7 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -21,6 +21,9 @@ import {
CONF_CAMERAS_ARRAY_LABEL,
CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
CONF_CAMERAS_ARRAY_TITLE,
+ CONF_CAMERAS_ARRAY_TRIGGER_BY_ENTITIES,
+ CONF_CAMERAS_ARRAY_TRIGGER_BY_MOTION,
+ CONF_CAMERAS_ARRAY_TRIGGER_BY_OCCUPANCY,
CONF_CAMERAS_ARRAY_URL,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
@@ -81,6 +84,9 @@ import {
CONF_VIEW_CAMERA_SELECT,
CONF_VIEW_DARK_MODE,
CONF_VIEW_DEFAULT,
+ CONF_VIEW_SCAN,
+ CONF_VIEW_SCAN_ENABLED,
+ CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
CONF_VIEW_TIMEOUT_SECONDS,
CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE,
@@ -99,11 +105,12 @@ import {
} from './types.js';
import { arrayMove } from './utils/basic.js';
import { getCameraID, getCameraTitle } from './utils/camera.js';
-import { sideLoadHomeAssistantElements } from './utils/ha';
+import { getEntitiesFromHASS, sideLoadHomeAssistantElements } from './utils/ha';
const MENU_BUTTONS = 'buttons';
const MENU_CAMERAS = 'cameras';
const MENU_OPTIONS = 'options';
+const MENU_VIEW_SCAN = 'scan';
interface EditorOptionsSet {
icon: string;
@@ -398,20 +405,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
}
}
- protected _getEntities(domain: string): string[] {
- if (!this.hass) {
- return [];
- }
- const entities = Object.keys(this.hass.states).filter(
- (eid) => eid.substr(0, eid.indexOf('.')) === domain,
- );
- entities.sort();
-
- // Add a blank entry to unset a selection.
- entities.unshift('');
- return entities;
- }
-
/**
* Render an option set header
* @param optionSetName The name of the EditorOptionsSet.
@@ -601,6 +594,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
);
}
+ protected _renderViewScanMenu(): TemplateResult {
+ return html`
+
+ ${this._expandedMenus[MENU_VIEW_SCAN]
+ ? html`
+ ${this._renderSwitch(
+ CONF_VIEW_SCAN_ENABLED,
+ frigateCardConfigDefaults.view.scan.enabled ?? true,
+ {
+ label: localize(`config.${CONF_VIEW_SCAN_ENABLED}`),
+ },
+ )}
+ ${this._renderSwitch(
+ CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
+ frigateCardConfigDefaults.view.scan.show_trigger_status ?? true,
+ {
+ label: localize(`config.${CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS}`),
+ },
+ )}
+
`
+ : ''}
+ `;
+ }
+
/**
* Render an editor menu for the card menu buttons.
* @param button The name of the button.
@@ -700,6 +725,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
protected _renderCamera(
cameras: RawFrigateCardConfigArray,
cameraIndex: number,
+ entities: string[],
addNewCamera?: boolean,
): TemplateResult | void {
const liveProviders: EditorSelectOption[] = [
@@ -848,6 +874,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
multiple: true,
},
)}
+ ${this._renderSwitch(
+ getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGER_BY_OCCUPANCY, cameraIndex),
+ frigateCardConfigDefaults.cameras.trigger_by_occupancy,
+ )}
+ ${this._renderSwitch(
+ getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGER_BY_MOTION, cameraIndex),
+ frigateCardConfigDefaults.cameras.trigger_by_motion,
+ )}
+ ${this._renderOptionSelector(
+ getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGER_BY_ENTITIES, cameraIndex),
+ entities,
+ {
+ multiple: true,
+ },
+ )}
`
: ``}
`;
@@ -935,7 +976,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
}
const defaults = frigateCardConfigDefaults;
-
+ const entities = getEntitiesFromHASS(this.hass);
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
[]) as RawFrigateCardConfigArray;
@@ -964,8 +1005,8 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSetHeader('cameras')}
${this._expandedMenus[MENU_OPTIONS] === 'cameras'
? html` `
: ''}
${this._renderOptionSetHeader('view')}
@@ -985,6 +1026,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_VIEW_UPDATE_CYCLE_CAMERA,
defaults.view.update_cycle_camera,
)}
+ ${this._renderViewScanMenu()}
`
: ''}
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index 20d1b7bd..4f7dd1a5 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -27,6 +27,9 @@
"frigate-jsmpeg": "Frigate JSMpeg",
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
},
+ "trigger_by_entities": "Trigger from other entities",
+ "trigger_by_motion": "Trigger by auto-detecting the motion sensor",
+ "trigger_by_occupancy": "Trigger by auto-detecting the occupancy sensor",
"webrtc_card": {
"entity": "WebRTC Card Camera Entity (Not a Frigate camera)",
"url": "WebRTC Card Camera URL"
@@ -65,7 +68,12 @@
"timeout_seconds": "Reset to default view X seconds after user action (0=never)",
"update_cycle_camera": "Cycle through cameras when default view updates",
"update_force": "Force card updates (ignore user interaction)",
- "update_seconds": "Refresh default view every X seconds (0=never)"
+ "update_seconds": "Refresh default view every X seconds (0=never)",
+ "scan": {
+ "scan_mode": "Scan mode",
+ "enabled": "Scan mode enabled",
+ "show_trigger_status": "Show pulsing border when triggered"
+ }
},
"event_gallery": {
"controls": {
diff --git a/src/scss/card.scss b/src/scss/card.scss
index 955b25c0..f3ccbbdd 100644
--- a/src/scss/card.scss
+++ b/src/scss/card.scss
@@ -9,48 +9,48 @@
filter: brightness(50%);
}
-.container {
+div.main {
position: relative;
overflow: auto;
width: 100%;
height: 100%;
margin: auto;
display: flex;
+ align-items: center;
justify-content: center;
// Need to apply the border radius on the container level, as the ha-card has
// overflow visible in order to allow a submenu to extend beyond the card
// boundary.
border-radius: var(--ha-card-border-radius, 4px);
-}
-.frigate-card-contents {
- width: inherit;
- height: inherit;
- margin: auto;
- overflow: auto;
- display: flex;
- align-items: center;
- justify-content: center;
+ // Include the borders in the sizing (to keep the triggered warning pulse
+ // visible in fullscreen).
+ box-sizing: border-box;
// Hide scrollbar: Firefox
scrollbar-width: none;
// Hide scrollbar: IE and Edge
-ms-overflow-style: none;
}
-
-/* Hide scrollbar for Chrome, Safari and Opera */
-.frigate-card-contents::-webkit-scrollbar {
- display: none;
+div.main.triggered {
+ @keyframes warning-pulse {
+ 0% {
+ border: solid 1px rgba(0,0,0,0);
+ }
+ 50% {
+ border: solid 1px var(--warning-color);
+ }
+ 100% {
+ border: solid 1px rgba(0,0,0,0);
+ }
+ }
+ animation: warning-pulse 5s infinite;
}
-/* When forcing aspect ratio absolute positioning is required */
-.frigate-card-contents.absolute {
- position: absolute;
- top: 0px;
- right: 0px;
- bottom: 0px;
- left: 0px;
+/* Hide scrollbar for Chrome, Safari and Opera */
+div.main::-webkit-scrollbar {
+ display: none;
}
/* The 'hover' menu mode is styling applied outside of the menu itself */
diff --git a/src/scss/editor.scss b/src/scss/editor.scss
index c965f9a9..5ddfa307 100644
--- a/src/scss/editor.scss
+++ b/src/scss/editor.scss
@@ -42,7 +42,7 @@ div.upgrade span {
.submenu-header {
display: flex;
- margin-top: 4px;
+ margin-top: 10px;
cursor: pointer;
}
diff --git a/src/types.ts b/src/types.ts
index 704b0e44..d01a00e1 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -364,6 +364,9 @@ const customSchema = z
export const cameraConfigDefault = {
client_id: 'frigate' as const,
live_provider: 'auto' as const,
+ trigger_by_motion: false,
+ trigger_by_occupancy: true,
+ trigger_by_entities: [],
};
const webrtcCardCameraConfigSchema = z.object({
entity: z.string().optional(),
@@ -394,6 +397,10 @@ const cameraConfigSchema = z
// Set of cameras IDs upon which this camera depends.
dependent_cameras: z.string().array().optional(),
+
+ trigger_by_motion: z.boolean().default(cameraConfigDefault.trigger_by_motion),
+ trigger_by_occupancy: z.boolean().default(cameraConfigDefault.trigger_by_occupancy),
+ trigger_by_entities: z.string().array().default(cameraConfigDefault.trigger_by_entities),
})
.default(cameraConfigDefault);
export type CameraConfig = z.infer