From b10b0e607213190fa81f7a5d66e00275acb21127 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 16 Aug 2022 21:27:10 -0700 Subject: [PATCH 1/4] Add native PTZ element. --- README.md | 83 ++++++++++++++++++- src/components/elements.ts | 144 ++++++++++++++++++++++++++++++--- src/localize/languages/en.json | 11 +++ src/scss/elements-ptz.scss | 135 +++++++++++++++++++++++++++++++ src/types.ts | 42 ++++++++++ 5 files changed, 401 insertions(+), 14 deletions(-) create mode 100644 src/scss/elements-ptz.scss diff --git a/README.md b/README.md index 71a81f71..ddb828e2 100644 --- a/README.md +++ b/README.md @@ -803,6 +803,7 @@ elements to add special Frigate card functionality. | `custom:frigate-card-menu-submenu` | Add a configurable submenu dropdown. See [configuration below](#frigate-card-menu-submenu).| | `custom:frigate-card-menu-submenu-select` | Add a submenu based on a `select` or `input_select`. See [configuration below](#frigate-card-submenu-select).| | `custom:frigate-card-conditional` | Restrict a set of elements to only render when the card is showing particular a particular [view](#views). See [configuration below](#frigate-card-conditional).| +| `custom:frigate-card-ptz` | Add a PTZ (Pan Tilt Zoom) controller overlay. See [configuration below](#frigate-card-ptz).| **Note**: ℹ️ Manual positioning of custom menu icons or submenus via the `style` @@ -864,6 +865,19 @@ Parameters for the `custom:frigate-card-conditional` element: `elements` | The elements to render. Can be any supported element, include additional condition or custom elements. | | `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. See [Frigate Card Conditions](#frigate-card-conditions). | +#### `custom:frigate-card-ptz` + +Parameters for the `custom:frigate-card-ptz` element: + +| Parameter | Default | Description | +| ------------- | - | -------------------------------------------- | +| `type` | | Must be `custom:frigate-card-ptz`. | +| `style` | `translate(-50%, -50%)` | Position and style the element using CSS. See [Picture Element styling](https://www.home-assistant.io/dashboards/picture-elements/#how-to-use-the-style-object). | +| `mode` | `vertical` | Whether to show a `vertical` or `horizontal` PTZ control. | +| `actions_left`, `actions_right`, `actions_up`, `actions_down`, `actions_zoom_in`, `actions_zoom_out`, `actions_home` | The [Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) to call when this icon is interacted with. | +| `data_left`, `data_right`, `data_up`, `data_down`, `data_zoom_in`, `data_zoom_out`, `data_home` | Shorthand for a `tap_action` that calls the `service` with the data provided in this argument. Internally, this is just translated into the longer-form `actions_[button]`. If both `actions_X` and `data_X` are specified, `actions_X` takes priority. This is compatible with [AlexxIT's WebRTC Card PTZ configuration](https://github.com/AlexxIT/WebRTC/wiki/PTZ-Config-Examples). | +| `service` | | An optional Home Assistant service to call when the `data_` parameters are used. | + ### Special Actions #### `custom:frigate-card-action` @@ -1663,6 +1677,31 @@ elements: state: on state_not: off mediaLoaded: true + # Full form PTZ actions (only left button shown). + - type: custom:frigate-card-ptz + mode: vertical + style: + transform: none + right: 20px + top: 180px + actions_left: + tap_action: + action: call-service + service: sonoff.send_command + service_data: + device: '048123' + cmd: left + # Equivalent short form PTZ actions (only left button shown) + - type: custom:frigate-card-ptz + mode: vertical + style: + transform: none + right: 20px + top: 180px + service: sonoff.send_command + data_left: + device: '048123' + cmd: left ``` @@ -2147,7 +2186,7 @@ You can restrict icons to only show for certain [views](#views) using a `custom:frigate-card-conditional` element (e.g. PTZ controls)
- Expand: View-based conditions (e.g. PTZ controls) + Expand: View-based conditions (e.g. custom PTZ controls) This example shows PTZ icons that call a PTZ service, but only in the `live` view. @@ -2491,7 +2530,6 @@ overrides: ```
- ### Refreshing a static image
@@ -2532,6 +2570,47 @@ elements: ```
+### Using a PTZ picture element + +The card supports a custom PTZ element (`custom:frigate-card-ptz`) to conveniently control pan, tilt and zoom for cameras. + +
+ Expand: Using the native PTZ picture element + +This example shows the native PTZ element when the `live` or `image` view is displayed and the stream (media) has loaded. + +```yaml +[...] +elements: + - type: custom:frigate-card-conditional + conditions: + mediaLoaded: true + view: + - live + - image + elements: + - type: custom:frigate-card-ptz + mode: horizontal + style: + transform: none + right: 20px + top: 180px + service: sonoff.send_command + data_left: + device: '048123' + cmd: left + data_right: + device: '048123' + cmd: right + data_up: + device: '048123' + cmd: up + data_down: + device: '048123' + cmd: down +``` +
+ ### Using `card-mod` to style the card diff --git a/src/components/elements.ts b/src/components/elements.ts index e09c48ef..6061e181 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -1,28 +1,39 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers'; import { CSSResultGroup, html, LitElement, PropertyValues, TemplateResult, - unsafeCSS + unsafeCSS, } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { ConditionState, fetchStateAndEvaluateCondition } from '../card-condition.js'; import { localize } from '../localize/localize.js'; import elementsStyle from '../scss/elements.scss'; +import ptzStyle from '../scss/elements-ptz.scss'; import { + Actions, + ActionsConfig, FrigateCardError, + FrigateCardPTZConfig, FrigateConditional, MenuButton, MenuIcon, MenuStateIcon, MenuSubmenu, MenuSubmenuSelect, - PictureElements + PictureElements, } from '../types.js'; import { dispatchFrigateCardEvent } from '../utils/basic.js'; import { dispatchFrigateCardErrorEvent } from './message.js'; +import { actionHandler } from '../action-handler-directive.js'; +import { + frigateCardHandleActionConfig, + frigateCardHasAction, + getActionConfigGivenAction, +} from '../utils/action.js'; +import { classMap } from 'lit/directives/class-map.js'; /* A note on picture element rendering: * @@ -343,14 +354,123 @@ export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuI @customElement('frigate-card-menu-submenu-select') export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon {} +@customElement('frigate-card-ptz') +export class FrigateCardPTZ extends LitElement { + @property({ attribute: false }) + public hass?: HomeAssistant; + + @state() + protected _config: FrigateCardPTZConfig | null = null; + + /** + * Set the card config. + * @param config The configuration. + */ + public setConfig(config: FrigateCardPTZConfig): void { + this._config = config; + } + + /** + * Called before each update. + */ + protected willUpdate(changedProps: PropertyValues): void { + if (changedProps.has('_config')) { + this.setAttribute('data-orientation', this._config?.orientation ?? 'vertical'); + } + } + + /** + * Handle a PTZ action. + * @param ev The actionHandler event. + * @param config The action configuration. + */ + protected _actionHandler( + ev: HASSDomEvent<{ action: string }>, + config?: ActionsConfig, + ): void { + // Nothing else has the configuration for this action, so don't let it + // propagate further. + ev.stopPropagation(); + + const interaction: string = ev.detail.action; + const action = getActionConfigGivenAction(interaction, config); + if (config && action && this.hass) { + frigateCardHandleActionConfig(this, this.hass, config, interaction, action); + } + } + + /** + * Render the elements. + * @returns A rendered template or void. + */ + protected render(): TemplateResult | void { + if (!this._config) { + return; + } + const renderIcon = ( + name: string, + icon: string, + actions?: Actions, + ): TemplateResult => { + const hasHold = frigateCardHasAction(actions?.hold_action); + const hasDoubleClick = frigateCardHasAction(actions?.double_tap_action); + const classes = { + [name]: true, + disabled: !actions, + }; + + return html` this._actionHandler(ev, actions)} + >`; + }; + + return html`
+
+ ${renderIcon('right', 'mdi:arrow-right', this._config.actions_right)} + ${renderIcon('left', 'mdi:arrow-left', this._config.actions_left)} + ${renderIcon('up', 'mdi:arrow-up', this._config.actions_up)} + ${renderIcon('down', 'mdi:arrow-down', this._config.actions_down)} +
+ ${this._config.actions_zoom_in || this._config.actions_zoom_out + ? html`
+ ${renderIcon('zoom_in', 'mdi:plus', this._config.actions_zoom_in)} + ${renderIcon('zoom_out', 'mdi:minus', this._config.actions_zoom_out)} +
` + : html``} + ${this._config.actions_home + ? html` +
+ ${renderIcon('home', 'mdi:home', this._config.actions_home)} +
+ ` + : html``} +
`; + } + + /** + * Return compiled CSS styles. + */ + static get styles(): CSSResultGroup { + return unsafeCSS(ptzStyle); + } +} + declare global { - interface HTMLElementTagNameMap { - "frigate-card-conditional": FrigateCardElementsConditional - "frigate-card-elements": FrigateCardElements - "frigate-card-menu-submenu-select": FrigateCardElementsMenuSubmenuSelect - "frigate-card-menu-submenu": FrigateCardElementsMenuSubmenu - "frigate-card-menu-state-icon": FrigateCardElementsMenuStateIcon - "frigate-card-menu-icon": FrigateCardElementsMenuIcon - "frigate-card-elements-core": FrigateCardElementsCore - } + interface HTMLElementTagNameMap { + 'frigate-card-conditional': FrigateCardElementsConditional; + 'frigate-card-elements': FrigateCardElements; + 'frigate-card-menu-submenu-select': FrigateCardElementsMenuSubmenuSelect; + 'frigate-card-menu-submenu': FrigateCardElementsMenuSubmenu; + 'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon; + 'frigate-card-menu-icon': FrigateCardElementsMenuIcon; + 'frigate-card-elements-core': FrigateCardElementsCore; + 'frigate-card-ptz': FrigateCardPTZ; + } } diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index edd0f093..fb13f480 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -372,5 +372,16 @@ "no_thumbnail": "No thumbnail available", "retain_indefinitely": "Event will be indefinitely retained", "timeline": "See event in timeline" + }, + "elements": { + "ptz": { + "up": "Up", + "down": "Down", + "left": "Left", + "right": "Right", + "zoom_in": "Zoom In", + "zoom_out": "Zoom Out", + "home": "Home" + } } } diff --git a/src/scss/elements-ptz.scss b/src/scss/elements-ptz.scss new file mode 100644 index 00000000..34bfbb4a --- /dev/null +++ b/src/scss/elements-ptz.scss @@ -0,0 +1,135 @@ +// Modified from / inspired by: +// https://github.com/AlexxIT/WebRTC/blob/master/custom_components/webrtc/www/webrtc-camera.js +:host { + position: relative; + + width: fit-content; + height: fit-content; + + --frigate-card-ptz-icon-size: 24px; +} + +/***************** + * Main Containers + *****************/ +.ptz { + display: flex; + gap: 10px; + + color: var(--light-primary-color); + opacity: 0.4; + + transition: opacity 0.3s ease-in-out; +} + +:host([data-orientation='vertical']) .ptz { + flex-direction: column; +} +:host([data-orientation='horizontal']) .ptz { + flex-direction: row; +} + +.ptz:hover { + opacity: 1; +} + +:host([data-orientation='vertical']) .ptz div { + width: calc(var(--frigate-card-ptz-icon-size) * 3); +} +:host([data-orientation='horizontal']) .ptz div { + height: calc(var(--frigate-card-ptz-icon-size) * 3); +} + +.ptz-move, +.ptz-zoom, +.ptz-home { + position: relative; + background-color: rgba(0, 0, 0, 0.3); +} + +.ptz-move { + height: calc(var(--frigate-card-ptz-icon-size) * 3); + width: calc(var(--frigate-card-ptz-icon-size) * 3); + + border-radius: 50%; +} + +:host([data-orientation='horizontal']) .ptz .ptz-zoom, +:host([data-orientation='horizontal']) .ptz .ptz-home { + width: calc(var(--frigate-card-ptz-icon-size) * 1.5); +} +:host([data-orientation='vertical']) .ptz .ptz-zoom, +:host([data-orientation='vertical']) .ptz .ptz-home { + height: calc(var(--frigate-card-ptz-icon-size) * 1.5); +} + +.ptz-zoom, +.ptz-home { + border-radius: var(--ha-card-border-radius, 4px); +} + +/*********** + * PTZ Icons + ***********/ +ha-icon { + position: absolute; + --mdc-icon-size: var(--frigate-card-ptz-icon-size); +} +ha-icon:not(.disabled) { + cursor: pointer; +} +.disabled { + color: var(--disabled-text-color); +} +.up { + top: 5px; + left: 50%; + transform: translateX(-50%); +} +.down { + bottom: 5px; + left: 50%; + transform: translateX(-50%); +} +.left { + left: 5px; + top: 50%; + transform: translateY(-50%); +} +.right { + right: 5px; + top: 50%; + transform: translateY(-50%); +} + +:host([data-orientation='vertical']) .zoom_in { + right: 5px; + top: 50%; +} +:host([data-orientation='vertical']) .zoom_out { + left: 5px; + top: 50%; +} +:host([data-orientation='horizontal']) .zoom_in { + left: 50%; + top: 5px; +} +:host([data-orientation='horizontal']) .zoom_out { + left: 50%; + bottom: 5px; +} + +:host([data-orientation='vertical']) .zoom_in, +:host([data-orientation='vertical']) .zoom_out { + transform: translateY(-50%); +} +:host([data-orientation='horizontal']) .zoom_in, +:host([data-orientation='horizontal']) .zoom_out { + transform: translateX(-50%); +} + +.home { + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); +} diff --git a/src/types.ts b/src/types.ts index a7d4046c..9749f550 100644 --- a/src/types.ts +++ b/src/types.ts @@ -499,6 +499,47 @@ const frigateConditionalSchema = z.object({ }); export type FrigateConditional = z.infer; +const frigateCardPTZSchema = z.preprocess( + // To avoid lots of YAML duplication, provide an easy way to just specify the + // service data as actions for each PTZ icon, and it will be preprocessed into + // the full form. This also provides compatability with the AlexIT/WebRTC PTZ + // configuration. + (data) => { + if (!data || typeof data !== 'object' || !data['service']) { + return data; + } + const out = { ...data }; + ['left', 'right', 'up', 'down', 'zoom_in', 'zoom_out', 'home'].forEach((name) => { + if (`data_${name}` in data && !(`actions_${name}` in data)) { + out[`actions_${name}`] = { + tap_action: { + action: 'call-service', + service: data['service'], + service_data: data[`data_${name}`], + }, + }; + delete out[`data_${name}`]; + } + }); + return out; + }, + z.object({ + type: z.literal('custom:frigate-card-ptz'), + style: z.object({}).passthrough().optional(), + orientation: z.enum(['vertical', 'horizontal']).default('vertical').optional(), + service: z.string().optional(), + actions_left: actionsBaseSchema.optional(), + actions_right: actionsBaseSchema.optional(), + actions_up: actionsBaseSchema.optional(), + actions_down: actionsBaseSchema.optional(), + actions_zoom_in: actionsBaseSchema.optional(), + actions_zoom_out: actionsBaseSchema.optional(), + actions_home: actionsBaseSchema.optional(), + }), +); + +export type FrigateCardPTZConfig = z.infer; + // Cannot use discriminatedUnion since customSchema uses a superRefine, which // causes false rejections. const pictureElementSchema = z.union([ @@ -507,6 +548,7 @@ const pictureElementSchema = z.union([ menuSubmenuSchema, menuSubmenuSelectSchema, frigateConditionalSchema, + frigateCardPTZSchema, stateBadgeIconSchema, stateIconSchema, stateLabelSchema, From 0e679afbfa16c32a14f00ab719b7f3a1f27ab924 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 16 Aug 2022 21:35:19 -0700 Subject: [PATCH 2/4] Fix README. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ddb828e2..6189a4f2 100644 --- a/README.md +++ b/README.md @@ -873,7 +873,7 @@ Parameters for the `custom:frigate-card-ptz` element: | ------------- | - | -------------------------------------------- | | `type` | | Must be `custom:frigate-card-ptz`. | | `style` | `translate(-50%, -50%)` | Position and style the element using CSS. See [Picture Element styling](https://www.home-assistant.io/dashboards/picture-elements/#how-to-use-the-style-object). | -| `mode` | `vertical` | Whether to show a `vertical` or `horizontal` PTZ control. | +| `orientation` | `vertical` | Whether to show a `vertical` or `horizontal` PTZ control. | | `actions_left`, `actions_right`, `actions_up`, `actions_down`, `actions_zoom_in`, `actions_zoom_out`, `actions_home` | The [Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) to call when this icon is interacted with. | | `data_left`, `data_right`, `data_up`, `data_down`, `data_zoom_in`, `data_zoom_out`, `data_home` | Shorthand for a `tap_action` that calls the `service` with the data provided in this argument. Internally, this is just translated into the longer-form `actions_[button]`. If both `actions_X` and `data_X` are specified, `actions_X` takes priority. This is compatible with [AlexxIT's WebRTC Card PTZ configuration](https://github.com/AlexxIT/WebRTC/wiki/PTZ-Config-Examples). | | `service` | | An optional Home Assistant service to call when the `data_` parameters are used. | @@ -1679,7 +1679,7 @@ elements: mediaLoaded: true # Full form PTZ actions (only left button shown). - type: custom:frigate-card-ptz - mode: vertical + orientation: vertical style: transform: none right: 20px @@ -1693,7 +1693,7 @@ elements: cmd: left # Equivalent short form PTZ actions (only left button shown) - type: custom:frigate-card-ptz - mode: vertical + orientation: vertical style: transform: none right: 20px @@ -2590,7 +2590,7 @@ elements: - image elements: - type: custom:frigate-card-ptz - mode: horizontal + orientation: horizontal style: transform: none right: 20px From 429ea80db1a0e4191bc683f88542321287db09d3 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 3 Sep 2022 08:46:00 -0700 Subject: [PATCH 3/4] README fixes. --- README.md | 53 +++++++---------------------------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 6189a4f2..bc83f75e 100644 --- a/README.md +++ b/README.md @@ -746,7 +746,7 @@ All variables listed are under a `conditions:` section. | `state` | A list of state conditions to compare with Home Assistant state. See below. | | `mediaLoaded` | If `true` the condition is satisfied if there is media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. Note that if `true` this condition will never be satisfied for views that do not themselves load media directly (e.g. gallery).| -See the [PTZ example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used. +See the [example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used. ### State Conditions @@ -1682,8 +1682,8 @@ elements: orientation: vertical style: transform: none - right: 20px - top: 180px + right: 5% + top: 50% actions_left: tap_action: action: call-service @@ -2183,12 +2183,12 @@ elements: ### Restricting Icons To Certain Views You can restrict icons to only show for certain [views](#views) using a -`custom:frigate-card-conditional` element (e.g. PTZ controls) +`custom:frigate-card-conditional` element
- Expand: View-based conditions (e.g. custom PTZ controls) + Expand: View-based conditions -This example shows PTZ icons that call a PTZ service, but only in the `live` view. +This example shows a car icon that calls a service but only in the `live` view. ```yaml [...] @@ -2199,7 +2199,7 @@ elements: - live elements: - type: icon - icon: mdi:arrow-up + icon: mdi:car style: background: rgba(255, 255, 255, 0.25) border-radius: 5px @@ -2211,45 +2211,6 @@ elements: service_data: entity_id: camera.kitchen movement: up - - type: icon - icon: mdi:arrow-down - style: - background: rgba(255, 255, 255, 0.25) - border-radius: 5px - right: 25px - bottom: 0px - tap_action: - action: call-service - service: amcrest.ptz_control - service_data: - entity_id: camera.kitchen - movement: down - - type: icon - icon: mdi:arrow-left - style: - background: rgba(255, 255, 255, 0.25) - border-radius: 5px - right: 50px - bottom: 25px - tap_action: - action: call-service - service: amcrest.ptz_control - service_data: - entity_id: camera.kitchen - movement: left - - type: icon - icon: mdi:arrow-right - style: - background: rgba(255, 255, 255, 0.25) - border-radius: 5px - right: 0px - bottom: 25px - tap_action: - action: call-service - service: amcrest.ptz_control - service_data: - entity_id: camera.kitchen - movement: right ```
From 24e59dfe4dece9593d2a0bc9f41cac84ccf863ae Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 3 Sep 2022 15:30:38 -0700 Subject: [PATCH 4/4] Correctly record unloaded media. --- src/cached-value-controller.ts | 1 + src/card.ts | 16 ++++++++++++++++ src/components/image.ts | 8 ++++++-- src/components/live.ts | 16 ++++++++++++---- src/components/media-carousel.ts | 1 + 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/cached-value-controller.ts b/src/cached-value-controller.ts index 1022c46d..9e39e06d 100644 --- a/src/cached-value-controller.ts +++ b/src/cached-value-controller.ts @@ -17,6 +17,7 @@ export class CachedValueController implements ReactiveController { * Remove the controller for the host. */ public removeController(): void { + this.stopTimer(); this._host.removeController(this); } diff --git a/src/card.ts b/src/card.ts index 1776ed2e..c623130b 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1602,6 +1602,10 @@ export class FrigateCard extends LitElement { if (!this._message || newPriority >= currentPriority) { this._message = message; + + // When a message is displayed it is effectively unloading the media. + this._mediaUnloadedHandler(); + if (!skipUpdate) { this.requestUpdate(); this._resetMainScroll(); @@ -1646,6 +1650,14 @@ export class FrigateCard extends LitElement { this.requestUpdate(); } + /** + * Unload a media item. + */ + protected _mediaUnloadedHandler(): void { + this._currentMediaLoadedInfo = null; + this._generateConditionState(); + } + /** * Handler called when fullscreen is toggled. */ @@ -1672,6 +1684,9 @@ export class FrigateCard extends LitElement { * Component disconnected callback. */ disconnectedCallback(): void { + // When the dashboard 'tab' is changed, the media is effectively unloaded. + this._mediaUnloadedHandler(); + if (screenfull.isEnabled) { screenfull.off('change', this._fullscreenHandler.bind(this)); } @@ -1797,6 +1812,7 @@ export class FrigateCard extends LitElement { @frigate-card:view:change=${this._changeViewHandler.bind(this)} @frigate-card:view:change-context=${this._addViewContextHandler.bind(this)} @frigate-card:media:loaded=${this._mediaLoadedHandler.bind(this)} + @frigate-card:media:unloaded=${this._mediaUnloadedHandler.bind(this)} @frigate-card:render=${() => this.requestUpdate()} > ${renderMenuAbove ? this._renderMenu() : ''} diff --git a/src/components/image.ts b/src/components/image.ts index b1ffb6f8..a8f5a743 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -21,8 +21,9 @@ import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.j import { dispatchMediaLoadedEvent } from '../utils/media-info.js'; import { View } from '../view.js'; import { dispatchErrorMessageEvent } from './message.js'; +import { contentsChanged } from '../utils/basic.js'; -// See: https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py#L101 +// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py . const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000; @customElement('frigate-card-image') @@ -36,7 +37,10 @@ export class FrigateCardImage extends LitElement { @property({ attribute: false }) public cameraConfig?: CameraConfig; - @property({ attribute: false }) + // Using contentsChanged to ensure overridden configs (e.g. when the + // 'show_image_during_load' option is true for live views, an overridden + // config may be used here). + @property({ attribute: false, hasChanged: contentsChanged }) public imageConfig?: ImageViewConfig; protected _refImage: Ref = createRef(); diff --git a/src/components/live.ts b/src/components/live.ts index bb8fbbb5..9b67c980 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -106,7 +106,7 @@ export class FrigateCardLive extends LitElement { // MediaLoadedInfo object and message from the underlying live object. In the // case of pre-loading these may be propagated upwards later. - protected _savedMediaLoadedInfo: MediaLoadedInfo | null = null; + protected _backgroundMediaLoadedInfo: MediaLoadedInfo | null = null; protected _messageReceivedPostRender = false; protected _renderKey = 0; @@ -127,11 +127,12 @@ export class FrigateCardLive extends LitElement { if ( !this._inBackground && !this._messageReceivedPostRender && - this._savedMediaLoadedInfo + this._backgroundMediaLoadedInfo ) { // If this isn't being rendered in the background, the last render did not // generate a message and there's a saved MediaInfo, dispatch it upwards. - dispatchExistingMediaLoadedInfoAsEvent(this, this._savedMediaLoadedInfo); + dispatchExistingMediaLoadedInfoAsEvent(this, this._backgroundMediaLoadedInfo); + this._backgroundMediaLoadedInfo = null; } // Trigger a re-render which may be necessary if the prior render resulted @@ -223,8 +224,8 @@ export class FrigateCardLive extends LitElement { } }} @frigate-card:media:loaded=${(ev: CustomEvent) => { - this._savedMediaLoadedInfo = ev.detail; if (this._inBackground) { + this._backgroundMediaLoadedInfo = ev.detail; ev.stopPropagation(); } }} @@ -735,6 +736,13 @@ export class FrigateCardLiveProvider extends LitElement { ); } + /** + * Component disconnected callback. + */ + disconnectedCallback(): void { + this._isVideoMediaLoaded = false; + } + /** * Record that video media is being shown. */ diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index d4192897..8f7c7280 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -293,6 +293,7 @@ export class FrigateCardMediaCarousel extends LitElement { this._resizeObserver.disconnect(); this._intersectionObserver.disconnect(); + this._mediaLoadedInfo = {}; super.disconnectedCallback(); }