Merge pull request #42 from dermotduffy/add-arbitrary-entities-to-menu
Add support to add arbitrary entities to menu
This commit is contained in:
@@ -22,7 +22,7 @@ A full-featured Frigate Lovelace card:
|
|||||||
* Clips and snapshot browsing via mini-gallery.
|
* Clips and snapshot browsing via mini-gallery.
|
||||||
* Automatic updating to continually show latest clip / snapshot.
|
* Automatic updating to continually show latest clip / snapshot.
|
||||||
* Support for filtering events by zone and label.
|
* Support for filtering events by zone and label.
|
||||||
* Motion sensor access.
|
* Arbitrary entity access via menu (e.g. motion sensor access).
|
||||||
* Full Lovelace editing support.
|
* Full Lovelace editing support.
|
||||||
* Theme friendly.
|
* Theme friendly.
|
||||||
* **Advanced**: Support for [WebRTC](https://github.com/AlexxIT/WebRTC) live viewing.
|
* **Advanced**: Support for [WebRTC](https://github.com/AlexxIT/WebRTC) live viewing.
|
||||||
@@ -59,15 +59,20 @@ lovelace:
|
|||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ------------- | --------------------------------------------- | - |
|
| ------------- | --------------------------------------------- | - |
|
||||||
| `motion_entity` | | A binary sensor to show in the menu (e.g. a Frigate motion binary sensor) and to use to trigger card updates.|
|
|
||||||
| `frigate_camera_name` | The string after the "camera." in the `camera_entity` option (above). | This parameter allows the camera name heuristic to be overriden for cases where the entity name does not cleanly map to the Frigate camera name (e.g. when the Frigate camera name is capitalized, but the entity name is lower case). This camera name is used for communicating with the Frigate backend, e.g. for fetching events. |
|
| `frigate_camera_name` | The string after the "camera." in the `camera_entity` option (above). | This parameter allows the camera name heuristic to be overriden for cases where the entity name does not cleanly map to the Frigate camera name (e.g. when the Frigate camera name is capitalized, but the entity name is lower case). This camera name is used for communicating with the Frigate backend, e.g. for fetching events. |
|
||||||
| `view_default` | `live` | The view to show by default. See [views](#views) below.|
|
| `view_default` | `live` | The view to show by default. See [views](#views) below.|
|
||||||
| `menu_mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
|
||||||
| `frigate_client_id` | `frigate` | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://blakeblackshear.github.io/frigate/usage/home-assistant/#multiple-instance-support).|
|
| `frigate_client_id` | `frigate` | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://blakeblackshear.github.io/frigate/usage/home-assistant/#multiple-instance-support).|
|
||||||
| `view_timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
| `view_timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
||||||
| `frigate_url` | | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. |
|
| `frigate_url` | | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. |
|
||||||
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
||||||
|
|
||||||
|
### Appearance
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| ------------- | --------------------------------------------- | - |
|
||||||
|
| `menu_mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
||||||
|
| `menu_buttons.{frigate, live, clips, snapshots, frigate_ui}` | `true` | Whether or not to show these builtin actions in the card menu. |
|
||||||
|
|
||||||
### Advanced
|
### Advanced
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
@@ -94,6 +99,35 @@ events/snapshots/UI. A perfect combination!
|
|||||||
|
|
||||||
**Note**: WebRTC must be installed and configured separately (see [details](https://github.com/AlexxIT/WebRTC)) before it can be used with this card.
|
**Note**: WebRTC must be installed and configured separately (see [details](https://github.com/AlexxIT/WebRTC)) before it can be used with this card.
|
||||||
|
|
||||||
|
<a name="entities"></a>
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
|
||||||
|
Additional entities may be configured to trigger updates to the card, and
|
||||||
|
optionally to appear in the menu. An `entities` section may be added to the card
|
||||||
|
configuration containing a list with entries of the following format:
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| ------------- | - | -------------------------------------------- |
|
||||||
|
| `entity` | | Entity ID to use to trigger updates, and optionally appear in the menu. |
|
||||||
|
| `icon` | [default entity icon] | An optional manual override of the icon to use in the menu, e.g. `mdi:car`. |
|
||||||
|
| `show`| `true` | Whether or not to show the entity in the menu. When `false` the entity ID will trigger card updates only, but not appear in the menu. |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
This example allows access to the detection, recordings and snapshots switches
|
||||||
|
from the menu. It also enables a different entity to trigger a card update (but
|
||||||
|
without appearing in the menu).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
entities:
|
||||||
|
- entity: switch.front_door_recordings
|
||||||
|
- entity: switch.front_door_snapshots
|
||||||
|
- entity: switch.front_door_detect
|
||||||
|
- entity: binary_sensor.front_door_person_motion
|
||||||
|
show: false
|
||||||
|
```
|
||||||
|
|
||||||
#### Specifying the WebRTC Camera
|
#### Specifying the WebRTC Camera
|
||||||
|
|
||||||
There are two ways to specify the WebRTC source camera:
|
There are two ways to specify the WebRTC source camera:
|
||||||
@@ -132,12 +166,19 @@ This card supports several different views.
|
|||||||
|
|
||||||
### Automatic updates in the `clip` or `snapshot` view
|
### Automatic updates in the `clip` or `snapshot` view
|
||||||
|
|
||||||
Updates will occur whenever the state of the `camera_entity` or `motion_entity`
|
Updates will occur whenever on every change of the state of the `camera_entity`
|
||||||
changes. In particular, if the desire is to have a live view of the most recent
|
or any entity configured under `entities`. In particular, if the desire is
|
||||||
event, the user should configure `motion_entity` to a Frigate binary sensor
|
to have an auto-refreshing view of the most recent event, the `camera_entity`
|
||||||
associated with that camera in order to trigger updates more regularly (the
|
will not be sufficient alone since the Home Assistant state for Frigate camera
|
||||||
underlying camera entity state does not change often, the motion binary sensors
|
entities does not change often. Instead, use the Frigate binary_sensor for that
|
||||||
do).
|
camera (or any other entity at your discretion) to trigger the update:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
entities:
|
||||||
|
- entity: binary_sensor.office_person_motion
|
||||||
|
```
|
||||||
|
|
||||||
|
See [entities](#entities) above.
|
||||||
|
|
||||||
### Getting from a snapshot to a clip
|
### Getting from a snapshot to a clip
|
||||||
|
|
||||||
@@ -171,7 +212,6 @@ A configuration that uses WebRTC for live:
|
|||||||
- type: 'custom:frigate-card'
|
- type: 'custom:frigate-card'
|
||||||
camera_entity: camera.front_door
|
camera_entity: camera.front_door
|
||||||
frigate_url: http://frigate
|
frigate_url: http://frigate
|
||||||
motion_entity: binary_sensor.front_door_person_motion
|
|
||||||
live_provider: webrtc
|
live_provider: webrtc
|
||||||
webrtc:
|
webrtc:
|
||||||
entity: camera.front_door_rtsp
|
entity: camera.front_door_rtsp
|
||||||
@@ -183,7 +223,6 @@ A configuration that shows the latest clip on load, but does not automatically p
|
|||||||
- type: 'custom:frigate-card'
|
- type: 'custom:frigate-card'
|
||||||
camera_entity: camera.front_door
|
camera_entity: camera.front_door
|
||||||
frigate_url: http://frigate
|
frigate_url: http://frigate
|
||||||
motion_entity: binary_sensor.front_door_person_motion
|
|
||||||
view_default: clip
|
view_default: clip
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-27
@@ -171,22 +171,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
</div>
|
</div>
|
||||||
${options.optional.show
|
${options.optional.show
|
||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
<paper-dropdown-menu
|
|
||||||
label=${localize('editor.motion_entity')}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
.configValue=${'motion_entity'}
|
|
||||||
>
|
|
||||||
<paper-listbox
|
|
||||||
slot="dropdown-content"
|
|
||||||
.selected=${binarySensorEntities.indexOf(
|
|
||||||
this._config?.motion_entity || '',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${binarySensorEntities.map((entity) => {
|
|
||||||
return html` <paper-item>${entity}</paper-item> `;
|
|
||||||
})}
|
|
||||||
</paper-listbox>
|
|
||||||
</paper-dropdown-menu>
|
|
||||||
<paper-input
|
<paper-input
|
||||||
label=${localize('editor.frigate_camera_name')}
|
label=${localize('editor.frigate_camera_name')}
|
||||||
.value=${this._config?.frigate_camera_name || ''}
|
.value=${this._config?.frigate_camera_name || ''}
|
||||||
@@ -323,17 +307,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
@change=${this._valueChanged}
|
@change=${this._valueChanged}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<br />
|
<br />`
|
||||||
<ha-formfield
|
|
||||||
.label=${localize('editor.show_button') + ': ' + localize('menu.motion')}
|
|
||||||
>
|
|
||||||
<ha-switch
|
|
||||||
.checked=${this._config?.menu_buttons?.frigate_ui ?? true}
|
|
||||||
.configValue=${'menu_buttons.motion'}
|
|
||||||
@change=${this._valueChanged}
|
|
||||||
></ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
<br /> `
|
|
||||||
: ''}
|
: ''}
|
||||||
<div class="option" @click=${this._toggleOption} .option=${'advanced'}>
|
<div class="option" @click=${this._toggleOption} .option=${'advanced'}>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
+47
-30
@@ -18,9 +18,10 @@ import { classMap } from 'lit-html/directives/class-map.js';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
fireEvent,
|
|
||||||
LovelaceCardEditor,
|
LovelaceCardEditor,
|
||||||
|
fireEvent,
|
||||||
getLovelace,
|
getLovelace,
|
||||||
|
stateIcon,
|
||||||
} from 'custom-card-helpers';
|
} from 'custom-card-helpers';
|
||||||
|
|
||||||
import './editor';
|
import './editor';
|
||||||
@@ -78,9 +79,9 @@ type FrigateCardMenuCallback = (name: string) => any;
|
|||||||
function shouldUpdateBasedOnHass(
|
function shouldUpdateBasedOnHass(
|
||||||
newHass: HomeAssistant | null,
|
newHass: HomeAssistant | null,
|
||||||
oldHass: HomeAssistant | undefined,
|
oldHass: HomeAssistant | undefined,
|
||||||
entities: (string | null | undefined)[],
|
entities: string[] | null,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!newHass) {
|
if (!newHass || !entities) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!entities.length) {
|
if (!entities.length) {
|
||||||
@@ -284,9 +285,13 @@ export class FrigateCard extends LitElement {
|
|||||||
// Whether or not there is an active clip being played.
|
// Whether or not there is an active clip being played.
|
||||||
protected _clipPlaying = false;
|
protected _clipPlaying = false;
|
||||||
|
|
||||||
@query(FrigateCardMenu.FRIGATE_CARD_MENU_ID)
|
@query(`#${FrigateCardMenu.FRIGATE_CARD_MENU_ID}`)
|
||||||
_menu!: FrigateCardMenu | null;
|
_menu!: FrigateCardMenu | null;
|
||||||
|
|
||||||
|
// A small cache to avoid needing to create a new list of entities every time
|
||||||
|
// a hass update arrives.
|
||||||
|
protected _entitiesToMonitor: string[] | null = null;
|
||||||
|
|
||||||
protected _updateMenu(): void {
|
protected _updateMenu(): void {
|
||||||
// Manually set hass in the menu. This is to allow the menu to update,
|
// Manually set hass in the menu. This is to allow the menu to update,
|
||||||
// without necessarily re-rendering the entire card (re-rendering interrupts
|
// without necessarily re-rendering the entire card (re-rendering interrupts
|
||||||
@@ -301,29 +306,44 @@ export class FrigateCard extends LitElement {
|
|||||||
protected _getMenuButtons(): Map<string, MenuButton> {
|
protected _getMenuButtons(): Map<string, MenuButton> {
|
||||||
const buttons: Map<string, MenuButton> = new Map();
|
const buttons: Map<string, MenuButton> = new Map();
|
||||||
|
|
||||||
if (this.config.menu_buttons?.frigate) {
|
if (this.config.menu_buttons?.frigate ?? true) {
|
||||||
buttons.set('frigate', { description: 'Frigate Menu' });
|
buttons.set('frigate', { description: 'Frigate Menu' });
|
||||||
}
|
}
|
||||||
if (this.config.menu_buttons?.live) {
|
if (this.config.menu_buttons?.live ?? true) {
|
||||||
buttons.set('live', { icon: 'mdi:cctv', description: 'View Live' });
|
buttons.set('live', {
|
||||||
|
icon: 'mdi:cctv',
|
||||||
|
description: 'View Live',
|
||||||
|
emphasize: this._view.is('live'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (this.config.menu_buttons?.clips) {
|
if (this.config.menu_buttons?.clips ?? true) {
|
||||||
buttons.set('clips', { icon: 'mdi:filmstrip', description: 'View Clips' });
|
buttons.set('clips', {
|
||||||
|
icon: 'mdi:filmstrip',
|
||||||
|
description: 'View Clips',
|
||||||
|
emphasize: this._view.is('clips'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (this.config.menu_buttons?.snapshots) {
|
if (this.config.menu_buttons?.snapshots ?? true) {
|
||||||
buttons.set('snapshots', { icon: 'mdi:camera', description: 'View Snapshots' });
|
buttons.set('snapshots', {
|
||||||
|
icon: 'mdi:camera',
|
||||||
|
description: 'View Snapshots',
|
||||||
|
emphasize: this._view.is('snapshots'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (this.config.menu_buttons?.frigate_ui && this.config.frigate_url) {
|
if ((this.config.menu_buttons?.frigate_ui ?? true) && this.config.frigate_url) {
|
||||||
buttons.set('frigate_ui', { icon: 'mdi:web', description: 'View Frigate UI' });
|
buttons.set('frigate_ui', { icon: 'mdi:web', description: 'View Frigate UI' });
|
||||||
}
|
}
|
||||||
if (this.config.menu_buttons?.motion && this._hass && this.config.motion_entity) {
|
const entities = this.config.entities || [];
|
||||||
const on = this._hass.states[this.config.motion_entity]?.state == 'on';
|
for (let i = 0; this._hass && i < entities.length; i++) {
|
||||||
const motionIcon = on ? 'mdi:motion-sensor' : 'mdi:walk';
|
if (!entities[i].show) {
|
||||||
|
continue;
|
||||||
buttons.set('motion', {
|
}
|
||||||
icon: motionIcon,
|
const entity = entities[i].entity;
|
||||||
description: 'View Motion Sensor',
|
const state = this._hass.states[entity];
|
||||||
emphasize: on,
|
buttons.set(entity, {
|
||||||
|
description: state.attributes.friendly_name || entity,
|
||||||
|
emphasize: ['on', 'active', 'home'].includes(state.state),
|
||||||
|
icon: entities[i].icon || stateIcon(state),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return buttons;
|
return buttons;
|
||||||
@@ -374,6 +394,10 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this._entitiesToMonitor = [
|
||||||
|
...(this.config.entities || []).map((entity) => entity.entity),
|
||||||
|
this.config.camera_entity,
|
||||||
|
];
|
||||||
this._changeView();
|
this._changeView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,10 +431,7 @@ export class FrigateCard extends LitElement {
|
|||||||
if (this._interactionTimerID || this._clipPlaying) {
|
if (this._interactionTimerID || this._clipPlaying) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return shouldUpdateBasedOnHass(this._hass, oldHass, [
|
return shouldUpdateBasedOnHass(this._hass, oldHass, this._entitiesToMonitor);
|
||||||
this.config.camera_entity,
|
|
||||||
this.config.motion_entity,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -623,13 +644,9 @@ export class FrigateCard extends LitElement {
|
|||||||
window.open(frigate_url);
|
window.open(frigate_url);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'motion':
|
|
||||||
if (this.config.motion_entity) {
|
|
||||||
fireEvent(this, 'hass-more-info', { entityId: this.config.motion_entity });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
console.warn('Unknown Frigate card menu option.');
|
// If it's unknown, it's assumed to be an entity_id.
|
||||||
|
fireEvent(this, 'hass-more-info', { entityId: name });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
"advanced_secondary": "Advanced options",
|
"advanced_secondary": "Advanced options",
|
||||||
"camera_entity": "Camera Entity (Required)",
|
"camera_entity": "Camera Entity (Required)",
|
||||||
"motion_entity": "Motion Entity (Optional)",
|
|
||||||
"frigate_camera_name": "Frigate camera name (Optional)",
|
"frigate_camera_name": "Frigate camera name (Optional)",
|
||||||
"default_view": "Default view (Optional)",
|
"default_view": "Default view (Optional)",
|
||||||
"frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)",
|
"frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)",
|
||||||
@@ -38,8 +37,7 @@
|
|||||||
"snapshots": "Snapshots Gallery",
|
"snapshots": "Snapshots Gallery",
|
||||||
"clip": "Latest Clip",
|
"clip": "Latest Clip",
|
||||||
"snapshot": "Latest Snapshot",
|
"snapshot": "Latest Snapshot",
|
||||||
"frigate_ui": "Frigate User Interface",
|
"frigate_ui": "Frigate User Interface"
|
||||||
"motion": "Motion Entity"
|
|
||||||
},
|
},
|
||||||
"menu_mode": {
|
"menu_mode": {
|
||||||
"hidden-top": "Hidden Top",
|
"hidden-top": "Hidden Top",
|
||||||
|
|||||||
+5
-9
@@ -41,7 +41,6 @@ export type FrigateMenuMode = typeof FRIGATE_MENU_MODES[number];
|
|||||||
|
|
||||||
export const frigateCardConfigSchema = z.object({
|
export const frigateCardConfigSchema = z.object({
|
||||||
camera_entity: z.string(),
|
camera_entity: z.string(),
|
||||||
motion_entity: z.string().optional(),
|
|
||||||
// No URL validation to allow relative URLs within HA (e.g. addons).
|
// No URL validation to allow relative URLs within HA (e.g. addons).
|
||||||
frigate_url: z.string().optional(),
|
frigate_url: z.string().optional(),
|
||||||
frigate_client_id: z.string().optional().default("frigate"),
|
frigate_client_id: z.string().optional().default("frigate"),
|
||||||
@@ -68,15 +67,12 @@ export const frigateCardConfigSchema = z.object({
|
|||||||
clips: z.boolean().default(true),
|
clips: z.boolean().default(true),
|
||||||
snapshots: z.boolean().default(true),
|
snapshots: z.boolean().default(true),
|
||||||
frigate_ui: z.boolean().default(true),
|
frigate_ui: z.boolean().default(true),
|
||||||
motion: z.boolean().default(true),
|
|
||||||
}).default({
|
|
||||||
frigate: true,
|
|
||||||
live: true,
|
|
||||||
clips: true,
|
|
||||||
snapshots: true,
|
|
||||||
frigate_ui: true,
|
|
||||||
motion: true,
|
|
||||||
}),
|
}),
|
||||||
|
entities: z.object({
|
||||||
|
entity: z.string(),
|
||||||
|
show: z.boolean().default(true),
|
||||||
|
icon: z.string().optional(),
|
||||||
|
}).array().optional(),
|
||||||
|
|
||||||
// Stock lovelace card config.
|
// Stock lovelace card config.
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user