diff --git a/README.md b/README.md index ad4d500c..31aa0327 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,7 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras) | `webrtc_card` | | :heavy_multiplication_x: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. | | `id` | `camera_entity`, `webrtc_card.entity` or `camera_name` if set (in that preference order). | :heavy_multiplication_x: | An optional identifier to use throughout the card configuration to refer unambiguously to this camera. See [camera IDs](#camera-ids). | | `dependencies` | | :heavy_multiplication_x: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependency-configuration) below. | -| `trigger_by_motion` | `false` | :heavy_multiplication_x: | Whether to not to trigger the camera (used to trigger [scan mode](#scan-mode) or reseting the default view) by automatically detecting and using the motion `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the motion `binary_sensor` entity has been enabled in Home Assistant.| -| `trigger_by_occupancy` | `true` | :heavy_multiplication_x: | Whether to not to trigger the camera (used to trigger [scan mode](#scan-mode) or reseting the default view) by automatically detecting and using the occupancy `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant.| -| `trigger_by_entities` | | :heavy_multiplication_x: | Whether to not to trigger the camera (used to trigger [scan mode](#scan-mode) or reseting the default view) when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). This works for Frigate or non-Frigate cameras.| +| `triggers` | | :heavy_multiplication_x: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. | @@ -162,6 +160,23 @@ cameras: | `cameras` | | :heavy_multiplication_x: | An optional array of other camera identifiers (see [camera IDs](#camera-ids)). If specified the card will fetch events for this camera and *also* recursively events for the named cameras. All dependent cameras must themselves be a configured camera in the card. This can be useful to group events for cameras that are close together, to always have clips/snapshots show fully merged events across all cameras or to show events for the `birdseye` camera that otherwise would not have events itself.| | `all_cameras` | `false` | :heavy_multiplication_x: | Shortcut to specify all other cameras as dependent cameras.| + + +#### Camera Trigger Configuration + +The `triggers` block configures what triggers a camera. Triggering can be used to either reset to the default view / update the card, or active the camera in [scan mode](#scan-mode). + +```yaml +cameras: + - triggers: +``` + +| Option | Default | Overridable | Description | +| - | - | - | - | +| `motion` | `false` | :heavy_multiplication_x: | Whether to not to trigger the camera by automatically detecting and using the motion `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the motion `binary_sensor` entity has been enabled in Home Assistant.| +| `occupancy` | `true` | :heavy_multiplication_x: | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant.| +| `entities` | | :heavy_multiplication_x: | Whether to not to trigger the camera when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). This works for Frigate or non-Frigate cameras.| + #### Camera IDs: Refering to cameras in card configuration @@ -190,7 +205,7 @@ See the [fully expanded view configuration example](#config-expanded-view) for h | `timeout_seconds` | `300` | :white_check_mark: | A numbers of seconds of inactivity after user interaction, after which the card will reset to the default configured view (i.e. 'screensaver' functionality). Inactivity is defined as lack of mouse/touch interaction with the Frigate card. If the default view occurs sooner (e.g. via `update_seconds` or manually) the timer will be stopped. `0` means disable this functionality. | | `update_seconds` | `0` | :white_check_mark: | A number of seconds after which to automatically update/refresh the default view. See [card updates](#card-updates) below for behavior and usecases. If the default view occurs sooner (e.g. manually) the timer will start over. `0` disables this functionality.| | `update_force` | `false` | :white_check_mark: | Whether automated card updates/refreshes should ignore user interaction. See [card updates](#card-updates) below for behavior and usecases.| -| `update_entities` | | :white_check_mark: | **YAML only**: A card-wide list of entities that should cause the view to reset to the default (if the entity only pertains to a particular camera use `trigger_by_entities` for the selected camera instead) See [card updates](#card-updates) below for behavior and usecases.| +| `update_entities` | | :white_check_mark: | **YAML only**: A card-wide list of entities that should cause the view to reset to the default (if the entity only pertains to a particular camera use `triggers` for the selected camera instead, see [Trigger Configuration](#camera-triggers)). See [card updates](#card-updates) below for behavior and usecases.| | `update_cycle_camera` | `false` | :white_check_mark: | When set to `true` the selected camera is cycled on each default view change. | | `render_entities` | | :white_check_mark: | **YAML only**: A list of entity ids that should cause the card to re-render 'in-place'. The view/camera is not changed. `update_*` flags do not pertain/relate to the behavior of this flag. This should **very** rarely be needed, but could be useful if the card is both setting and changing HA state of the same object as could be the case for some complex `card_mod` scenarios ([example](https://github.com/dermotduffy/frigate-hass-card/issues/343)). | | `scan` | | :white_check_mark: | Configuration for [scan mode](#scan-mode). | @@ -207,7 +222,7 @@ view: scan: ``` -Scan mode allows the card to automatically "follow the action". In this mode the card will automatically select a camera in the `live` view when an entity changes to an active state (specifically `on` or `open`). The entities considered are defined by your camera configuration (see `trigger_by_motion`, `trigger_by_occupancy` and `trigger_by_entities` parameters). An untrigger is defined as the state for all the configured entities returning to inactive (i.e. not `on` or `open`), with an optional number of seconds to wait prior to the untriggering (see `untrigger_seconds`). +Scan mode allows the card to automatically "follow the action". In this mode the card will automatically select a camera in the `live` view when an entity changes to an active state (specifically `on` or `open`). The entities considered are defined by your camera configuration (see `triggers` parameters). An untrigger is defined as the state for all the configured entities returning to inactive (i.e. not `on` or `open`), with an optional number of seconds to wait prior to the untriggering (see `untrigger_seconds`). When the camera untriggers, the view will either remain as-is (if `untrigger_reset` is `false`) and the card return to normal operation, or reset to the default view (if `untrigger_reset` is `true` -- the default). @@ -1036,10 +1051,11 @@ cameras: all_cameras: false cameras: - camera-2 - trigger_by_motion: false - trigger_by_occupancy: true - trigger_by_entities: - - binary_sensor.front_door_sensor + triggers: + motion: false + occupancy: true + entities: + - binary_sensor.front_door_sensor - frigate_url: http://my-other.frigate.local client_id: frigate-other camera_name: entrance @@ -1054,10 +1070,11 @@ cameras: webrtc_card: entity: camera.entrance_rtsp url: 'rtsp://username:password@camera:554/av_stream/ch0' - trigger_by_motion: false - trigger_by_occupancy: true - trigger_by_entities: - - binary_sensor.entrance_sensor + triggers: + motion: false + occupancy: true + entities: + - binary_sensor.entrance_sensor dependencies: all_cameras: false ``` @@ -2365,7 +2382,8 @@ the card to trigger a card update based on that entity -- which causes it to use the new overriden default immediately. Alternatives to trigger the card to change view but without `update_entities` would just be having an `update_seconds` parameter which reloads the default view that many seconds -after user interaction stops or through the use of the `trigger_by_entities` option for a given camera. +after user interaction stops or through the use of the `triggers` option for a +given camera (see [Trigger Configuration](#camera-triggers)). ```yaml view: @@ -2526,10 +2544,11 @@ cameras: - camera_entity: camera.back_yard # This camera will automatically trigger by occupancy. - camera_entity: camera.front_door - trigger_by_occupancy: false - trigger_by_motion: true - trigger_by_entities: - - binary_sensor.door_opened + triggers: + occupancy: false + motion: true + entities: + - binary_sensor.door_opened view: scan: enabled: true @@ -2550,7 +2569,7 @@ The following table describes the behavior these flags have. Note that no (other) automated updates are permitted when [scan mode](#scan-mode) is being triggered. -In the below "Trigger Entities" refers to the combination of `view.update_entities` and the `trigger_by_entities` for the currently selected camera (which in turn will also include the occupancy and motion sensor entities for Frigate cameras if `trigger_by_occupancy` and `trigger_by_motion` options are enabled). +In the below "Trigger Entities" refers to the combination of `view.update_entities` and the `triggers.entities` for the currently selected camera (which in turn will also include the occupancy and motion sensor entities for Frigate cameras if `triggers.occupancy` and `triggers.motion` options are enabled, see [Trigger Configuration](#camera-triggers)). | `view . update_seconds` | `view . timeout_seconds` | `view . update_force` | Trigger Entities | Behavior | | :-: | :-: | :-: | :-: | - | @@ -2580,7 +2599,8 @@ view: ```yaml cameras: - entity: camera.office - trigger_by_motion: true + triggers: + motion: true ``` * Cycle the live view of the camera every 60 seconds ```yaml diff --git a/src/card.ts b/src/card.ts index 33252401..e3d8c484 100644 --- a/src/card.ts +++ b/src/card.ts @@ -702,21 +702,21 @@ export class FrigateCard extends LitElement { console.error(e, (e as Error).stack); } - if (config.trigger_by_motion) { + if (config.triggers.motion) { const motionEntity = this._getMotionSensor(cache, config); if (motionEntity) { - config.trigger_by_entities.push(motionEntity); + config.triggers.entities.push(motionEntity); } } - if (config.trigger_by_occupancy) { + if (config.triggers.occupancy) { const occupancyEntity = this._getOccupancySensor(cache, config); if (occupancyEntity) { - config.trigger_by_entities.push(occupancyEntity); + config.triggers.entities.push(occupancyEntity); } } } - config.trigger_by_entities = [...new Set(config.trigger_by_entities)]; + config.triggers.entities = [...new Set(config.triggers.entities)]; const id = getCameraID(config); if (!id) { @@ -996,7 +996,7 @@ export class FrigateCard extends LitElement { let triggerChanges = false; for (const [camera, config] of this._cameras?.entries() ?? []) { - const triggerEntities = config?.trigger_by_entities ?? []; + const triggerEntities = config.triggers.entities ?? []; const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, { stateOnly: true, }); @@ -1096,7 +1096,7 @@ export class FrigateCard extends LitElement { this._isAutomatedViewUpdateAllowed() && isHassDifferent(this._hass, oldHass, [ ...(this._getConfig().view.update_entities || []), - ...(selectedCamera?.trigger_by_entities || []), + ...(selectedCamera?.triggers.entities || []), ]) ) { // If entities being monitored have changed then reset the view to the diff --git a/src/const.ts b/src/const.ts index ca6d8e9d..790e04ce 100644 --- a/src/const.ts +++ b/src/const.ts @@ -25,12 +25,12 @@ export const CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS = `${CONF_CAMERAS}.#.dependencies.cameras` as const; export const CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS = `${CONF_CAMERAS}.#.dependencies.all_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_CAMERAS_ARRAY_TRIGGERS_MOTION = + `${CONF_CAMERAS}.#.triggers.motion` as const; +export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY = + `${CONF_CAMERAS}.#.triggers.occupancy` as const; +export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES = + `${CONF_CAMERAS}.#.triggers.entities` as const; export const CONF_VIEW = 'view' as const; export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const; diff --git a/src/editor.ts b/src/editor.ts index 7fe3457f..b7798e3e 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -23,9 +23,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_TRIGGERS_ENTITIES, + CONF_CAMERAS_ARRAY_TRIGGERS_MOTION, + CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, CONF_CAMERAS_ARRAY_URL, CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY, CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, @@ -114,6 +114,9 @@ import { getEntitiesFromHASS, sideLoadHomeAssistantElements } from './utils/ha'; const MENU_BUTTONS = 'buttons'; const MENU_CAMERAS = 'cameras'; +const MENU_CAMERAS_DEPENDENCIES = 'cameras.dependencies'; +const MENU_CAMERAS_TRIGGERS = 'cameras.triggers'; +const MENU_CAMERAS_WEBRTC = 'cameras.webrtc'; const MENU_OPTIONS = 'options'; const MENU_VIEW_SCAN = 'scan'; @@ -706,6 +709,46 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor `; } + /** + * 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 + */ + protected _putInSubmenu( + domain: string, + key: unknown, + icon: string, + labelPath: string, + template: TemplateResult, + ): TemplateResult { + const submenuClasses = { + submenu: true, + selected: this._expandedMenus[domain] === key, + }; + + return html`
+ `; + } + /** * Render a camera section. * @param cameras The full array of cameras. @@ -876,41 +919,62 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex), )} - ${this._renderEntitySelector( - getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY, cameraIndex), - 'camera', + ${this._putInSubmenu( + MENU_CAMERAS_WEBRTC, + cameraIndex, + 'mdi:webrtc', + 'config.cameras.webrtc_card.options', + html`${this._renderEntitySelector( + getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY, cameraIndex), + 'camera', + )} + ${this._renderStringInput( + getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, cameraIndex), + )}`, )} - ${this._renderStringInput( - getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, cameraIndex), + ${this._putInSubmenu( + MENU_CAMERAS_DEPENDENCIES, + cameraIndex, + 'mdi:graph', + 'config.cameras.dependencies.options', + html` ${this._renderSwitch( + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS, + cameraIndex, + ), + frigateCardConfigDefaults.cameras.dependencies.all_cameras, + )} + ${this._renderOptionSelector( + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS, + cameraIndex, + ), + dependentCameras, + { + multiple: true, + }, + )}`, )} - ${this._renderSwitch( - getArrayConfigPath( - CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS, - cameraIndex, - ), - frigateCardConfigDefaults.cameras.dependencies.all_cameras, - )} - ${this._renderOptionSelector( - getArrayConfigPath(CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS, cameraIndex), - dependentCameras, - { - 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, - }, + ${this._putInSubmenu( + MENU_CAMERAS_TRIGGERS, + cameraIndex, + 'mdi:magnify-scan', + 'config.cameras.triggers.options', + html` ${this._renderSwitch( + getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, cameraIndex), + frigateCardConfigDefaults.cameras.triggers.occupancy, + )} + ${this._renderSwitch( + getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_MOTION, cameraIndex), + frigateCardConfigDefaults.cameras.triggers.motion, + )} + ${this._renderOptionSelector( + getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES, cameraIndex), + entities, + { + multiple: true, + }, + )}`, )} ` : ``} diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 6d261706..1534c875 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -16,6 +16,7 @@ "camera_name": "Frigate camera name (Autodetected from entity)", "client_id": "Frigate client id (For >1 Frigate server)", "dependencies": { + "options": "Dependency Options", "cameras": "Show events for specific cameras with this camera", "all_cameras": "Show events for all cameras with this camera" }, @@ -31,10 +32,14 @@ "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", + "triggers": { + "options": "Trigger Options", + "entities": "Trigger from other entities", + "motion": "Trigger by auto-detecting the motion sensor", + "occupancy": "Trigger by auto-detecting the occupancy sensor" + }, "webrtc_card": { + "options": "WebRTC Card Options", "entity": "WebRTC Card Camera Entity (Not a Frigate camera)", "url": "WebRTC Card Camera URL" }, diff --git a/src/scss/editor.scss b/src/scss/editor.scss index 590a6d79..f7384a63 100644 --- a/src/scss/editor.scss +++ b/src/scss/editor.scss @@ -44,7 +44,7 @@ div.upgrade span { cursor: pointer; } -.submenu.selected .submenu-header { +.submenu.selected > .submenu-header { background-color: var(--primary-color); color: var(--primary-text-color); } @@ -58,7 +58,7 @@ div.upgrade span { .submenu-header .new-camera { font-style: italic; } -.submenu:not(.selected) .submenu-header .new-camera { +.submenu:not(.selected) > .submenu-header .new-camera { color: var(--secondary-text-color, 'black'); } @@ -73,6 +73,17 @@ div.upgrade span { border: 1px solid var(--primary-color); } +// Nested menus should shrink width. +.submenu .submenu { + width: calc(100% - 10px * 2); + margin: auto; + margin-bottom: 10px; + +} +.submenu :not(.submenu.selected) { + border: none; +} + .submenu .controls { display: inline-block; margin-left: auto; diff --git a/src/types.ts b/src/types.ts index 0443c8b9..e1d93d3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -372,13 +372,15 @@ 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: [], dependencies: { all_cameras: false, cameras: [], }, + triggers: { + motion: false, + occupancy: true, + entities: [], + }, }; const webrtcCardCameraConfigSchema = z.object({ entity: z.string().optional(), @@ -414,12 +416,14 @@ const cameraConfigSchema = z }) .default(cameraConfigDefault.dependencies), - 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), + triggers: z.object({ + motion: z.boolean().default(cameraConfigDefault.triggers.motion), + occupancy: z.boolean().default(cameraConfigDefault.triggers.occupancy), + entities: z + .string() + .array() + .default(cameraConfigDefault.triggers.entities), + }).default(cameraConfigDefault.triggers), }) .default(cameraConfigDefault); export type CameraConfig = z.infer