From d895c43e678bd5d6a679b12a0a0579a0cf16fd65 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 5 Oct 2023 21:18:03 -0700 Subject: [PATCH 1/2] Treat camera unavailability in a more friendly way. Also creates a `components-lib` directory for major webcomponent support code. --- project.inlang.json | 24 +++++++ src/card-controller/view-manager.ts | 2 +- src/card.ts | 12 ++-- .../cached-value-controller.ts | 2 +- .../media-grid-controller.ts | 4 +- .../menu-controller.ts | 8 +-- .../timeline-source.ts | 2 +- .../zoom-controller.ts} | 4 +- src/components/image.ts | 2 +- src/components/live/live-ha.ts | 13 +++- src/components/live/live-image.ts | 2 +- src/components/live/live.ts | 71 +++++++----------- src/components/media-grid.ts | 2 +- src/components/timeline-core.ts | 5 +- src/components/viewer.ts | 2 +- src/components/zoomer.ts | 4 +- src/utils/embla/reinit-controller.ts | 2 +- src/utils/get-state-obj.ts | 36 ++++++++++ tests/cached-value-controller.test.ts | 2 +- .../media-grid-controller.test.ts | 2 +- .../menu-controller.test.ts | 8 +-- .../zoom-controller.test.ts} | 15 ++-- tests/utils/get-state-obj.ts | 72 +++++++++++++++++++ 23 files changed, 207 insertions(+), 89 deletions(-) create mode 100644 project.inlang.json rename src/{ => components-lib}/cached-value-controller.ts (98%) rename src/{utils => components-lib}/media-grid-controller.ts (99%) rename src/{utils => components-lib}/menu-controller.ts (98%) rename src/{utils => components-lib}/timeline-source.ts (99%) rename src/{utils/zoom/zoom.ts => components-lib/zoom-controller.ts} (99%) create mode 100644 src/utils/get-state-obj.ts rename tests/{utils => components-lib}/media-grid-controller.test.ts (99%) rename tests/{utils => components-lib}/menu-controller.test.ts (99%) rename tests/{utils/zoom.test.ts => components-lib/zoom-controller.test.ts} (95%) create mode 100644 tests/utils/get-state-obj.ts diff --git a/project.inlang.json b/project.inlang.json new file mode 100644 index 00000000..9f3af7ab --- /dev/null +++ b/project.inlang.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "sourceLanguageTag": "en", + "languageTags": [ + "en", + "it", + "pt-BR", + "pt-PT" + ], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-json@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js" + ], + "plugin.inlang.json": { + "pathPattern": "./src/localize/languages/{languageTag}.json", + "variableReferencePattern": [ + "{", + "}" + ] + } +} diff --git a/src/card-controller/view-manager.ts b/src/card-controller/view-manager.ts index 3465f710..39c95702 100644 --- a/src/card-controller/view-manager.ts +++ b/src/card-controller/view-manager.ts @@ -221,7 +221,7 @@ export class ViewManager { log( this._api.getConfigManager().getCardWideConfig(), `Frigate Card view change: `, - view.view, + view, ); this._view = view; diff --git a/src/card.ts b/src/card.ts index f7eca6c1..52ee0d1b 100644 --- a/src/card.ts +++ b/src/card.ts @@ -8,6 +8,9 @@ import { ViewContext } from 'view'; import 'web-dialog'; import pkg from '../package.json'; import { actionHandler } from './action-handler-directive.js'; +import { ConditionEvaluateRequestEvent } from './card-controller/conditions-manager.js'; +import { CardController } from './card-controller/controller'; +import { MenuButtonController } from './components-lib/menu-controller'; import './components/elements.js'; import { FrigateCardElements } from './components/elements.js'; import './components/menu.js'; @@ -21,15 +24,8 @@ import { FrigateCardConfig, MenuItem, RawFrigateCardConfig } from './config/type import { REPO_URL } from './const.js'; import { localize } from './localize/localize.js'; import cardStyle from './scss/card.scss'; -import { - ExtendedHomeAssistant, - MediaLoadedInfo, - Message, -} from './types.js'; +import { ExtendedHomeAssistant, MediaLoadedInfo, Message } from './types.js'; import { frigateCardHasAction } from './utils/action.js'; -import { ConditionEvaluateRequestEvent } from './card-controller/conditions-manager.js'; -import { CardController } from './card-controller/controller'; -import { MenuButtonController } from './utils/menu-controller'; import { View } from './view/view.js'; // *************************************************************************** diff --git a/src/cached-value-controller.ts b/src/components-lib/cached-value-controller.ts similarity index 98% rename from src/cached-value-controller.ts rename to src/components-lib/cached-value-controller.ts index 894d15c7..b1a87158 100644 --- a/src/cached-value-controller.ts +++ b/src/components-lib/cached-value-controller.ts @@ -1,5 +1,5 @@ import { ReactiveController, ReactiveControllerHost } from 'lit'; -import { Timer } from './utils/timer'; +import { Timer } from '../utils/timer'; export class CachedValueController implements ReactiveController { protected _value?: T; diff --git a/src/utils/media-grid-controller.ts b/src/components-lib/media-grid-controller.ts similarity index 99% rename from src/utils/media-grid-controller.ts rename to src/components-lib/media-grid-controller.ts index cdf0bba3..89a02b61 100644 --- a/src/utils/media-grid-controller.ts +++ b/src/components-lib/media-grid-controller.ts @@ -7,12 +7,12 @@ import { dispatchFrigateCardEvent, getChildrenFromElement, setOrRemoveAttribute, -} from './basic'; +} from '../utils/basic'; import { FrigateMediaLoadedEventTarget, dispatchExistingMediaLoadedInfoAsEvent, dispatchMediaUnloadedEvent, -} from './media-info'; +} from '../utils/media-info'; // The default minimum cell width: if the columns are not specified this value // is used to compute the number of columns, always trying to keep each cell as diff --git a/src/utils/menu-controller.ts b/src/components-lib/menu-controller.ts similarity index 98% rename from src/utils/menu-controller.ts rename to src/components-lib/menu-controller.ts index 23d43cb2..79efe5e6 100644 --- a/src/utils/menu-controller.ts +++ b/src/components-lib/menu-controller.ts @@ -13,12 +13,12 @@ import { MediaLoadedInfo, } from '../types'; import { View } from '../view/view'; -import { createFrigateCardCustomAction } from './action'; -import { getAllDependentCameras } from './camera'; +import { createFrigateCardCustomAction } from '../utils/action'; +import { getAllDependentCameras } from '../utils/camera'; import { MediaPlayerManager } from '../card-controller/media-player-manager'; import { MicrophoneManager } from '../card-controller/microphone-manager'; -import { getEntityIcon, getEntityTitle } from './ha'; -import { hasSubstream } from './substream'; +import { getEntityIcon, getEntityTitle } from '../utils/ha'; +import { hasSubstream } from '../utils/substream'; export interface MenuButtonControllerOptions { currentMediaLoadedInfo?: MediaLoadedInfo | null; showCameraUIButton?: boolean; diff --git a/src/utils/timeline-source.ts b/src/components-lib/timeline-source.ts similarity index 99% rename from src/utils/timeline-source.ts rename to src/components-lib/timeline-source.ts index 714356be..7e9d10c9 100644 --- a/src/utils/timeline-source.ts +++ b/src/components-lib/timeline-source.ts @@ -12,7 +12,7 @@ import { EventQuery, RecordingQuery, RecordingSegment } from '../camera-manager/ import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/util'; import { ClipsOrSnapshotsOrAll } from '../types'; import { ViewMedia } from '../view/media'; -import { ModifyInterface, errorToConsole } from './basic.js'; +import { ModifyInterface, errorToConsole } from '../utils/basic.js'; // Allow timeline freshness to be at least this number of seconds out of date // (caching times in the data-engine may increase the effective delay). diff --git a/src/utils/zoom/zoom.ts b/src/components-lib/zoom-controller.ts similarity index 99% rename from src/utils/zoom/zoom.ts rename to src/components-lib/zoom-controller.ts index 08f1ce06..07557136 100644 --- a/src/utils/zoom/zoom.ts +++ b/src/components-lib/zoom-controller.ts @@ -1,8 +1,8 @@ import Panzoom, { PanzoomEventDetail, PanzoomObject } from '@dermotduffy/panzoom'; import round from 'lodash-es/round'; -import { dispatchFrigateCardEvent, isHoverableDevice } from '../basic'; +import { dispatchFrigateCardEvent, isHoverableDevice } from '../utils/basic'; -export class Zoom { +export class ZoomController { constructor(element: HTMLElement) { this._element = element; } diff --git a/src/components/image.ts b/src/components/image.ts index e54b91e3..a222972e 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -12,7 +12,7 @@ import { customElement, property } from 'lit/decorators.js'; import { live } from 'lit/directives/live.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js'; import isEqual from 'lodash-es/isEqual'; -import { CachedValueController } from '../cached-value-controller.js'; +import { CachedValueController } from '../components-lib/cached-value-controller.js'; import { CameraConfig, ImageViewConfig } from '../config/types.js'; import defaultImage from '../images/frigate-bird-in-sky.jpg'; import { localize } from '../localize/localize.js'; diff --git a/src/components/live/live-ha.ts b/src/components/live/live-ha.ts index 0ce54b49..603cc22c 100644 --- a/src/components/live/live-ha.ts +++ b/src/components/live/live-ha.ts @@ -3,12 +3,14 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit import { customElement, property } from 'lit/decorators.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { CameraConfig } from '../../config/types'; +import { localize } from '../../localize/localize'; import '../../patches/ha-camera-stream'; import '../../patches/ha-hls-player.js'; import '../../patches/ha-web-rtc-player.ts'; import liveHAStyle from '../../scss/live-ha.scss'; import { FrigateCardMediaPlayer } from '../../types.js'; -import { getStateObjOrDispatchError } from './live.js'; +import { renderMessage } from '../message'; +import { getStateObjOrDispatchError } from '../../utils/get-state-obj'; @customElement('frigate-card-live-ha') export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer { @@ -68,7 +70,14 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla if (!stateObj) { return; } - + if (stateObj.state === 'unavailable') { + return renderMessage({ + message: localize('error.live_camera_unavailable'), + type: 'error', + icon: 'mdi:connection', + context: this.cameraConfig, + }); + } return html` { - if (!cameraConfig?.camera_entity) { - dispatchErrorMessageEvent(element, localize('error.no_live_camera'), { - context: cameraConfig, - }); - return null; - } - - const stateObj = hass.states[cameraConfig.camera_entity]; - if (!stateObj) { - dispatchErrorMessageEvent(element, localize('error.live_camera_not_found'), { - context: cameraConfig, - }); - return null; - } - - if (stateObj.state === 'unavailable') { - dispatchMessageEvent(element, localize('error.live_camera_unavailable'), 'info', { - icon: 'mdi:connection', - context: cameraConfig, - }); - return null; - } - return stateObj; -}; - @customElement('frigate-card-live') export class FrigateCardLive extends LitElement { @property({ attribute: false }) @@ -977,6 +938,24 @@ export class FrigateCardLiveProvider hidden: showImageDuringLoading, }; + if (provider === 'ha' || provider === 'image') { + const stateObj = getStateObjOrDispatchError(this, this.hass, this.cameraConfig); + if (!stateObj) { + return; + } + if (stateObj.state === 'unavailable') { + // An unavailable camera gets a message rendered in place vs dispatched, + // as this may be a common occurrence (e.g. Frigate cameras that stop + // receiving frames). Otherwise a single temporarily unavailable camera + // would render a whole carousel inoperable. + return renderMessage({ + message: localize('error.live_camera_unavailable'), + type: 'error', + context: this.cameraConfig, + }); + } + } + return this._useZoomIfRequired(html` ${showImageDuringLoading || provider === 'image' ? html` { + if (!cameraConfig?.camera_entity) { + dispatchErrorMessageEvent(element, localize('error.no_live_camera'), { + context: cameraConfig, + }); + return null; + } + + const stateObj = hass.states[cameraConfig.camera_entity]; + if (!stateObj) { + dispatchErrorMessageEvent(element, localize('error.live_camera_not_found'), { + context: cameraConfig, + }); + return null; + } + + return stateObj; +}; diff --git a/tests/cached-value-controller.test.ts b/tests/cached-value-controller.test.ts index 29714a7b..f3694cb1 100644 --- a/tests/cached-value-controller.test.ts +++ b/tests/cached-value-controller.test.ts @@ -1,7 +1,7 @@ import { ReactiveControllerHost } from 'lit'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; -import { CachedValueController } from '../src/cached-value-controller'; +import { CachedValueController } from '../src/components-lib/cached-value-controller'; // @vitest-environment jsdom describe('CachedValueController', () => { diff --git a/tests/utils/media-grid-controller.test.ts b/tests/components-lib/media-grid-controller.test.ts similarity index 99% rename from tests/utils/media-grid-controller.test.ts rename to tests/components-lib/media-grid-controller.test.ts index 4f5b0092..bc760878 100644 --- a/tests/utils/media-grid-controller.test.ts +++ b/tests/components-lib/media-grid-controller.test.ts @@ -5,7 +5,7 @@ import { MediaLoadedInfo } from '../../src/types'; import { MediaGridConstructorOptions, MediaGridController, -} from '../../src/utils/media-grid-controller'; +} from '../../src/components-lib/media-grid-controller'; import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info'; import { MutationObserverMock, diff --git a/tests/utils/menu-controller.test.ts b/tests/components-lib/menu-controller.test.ts similarity index 99% rename from tests/utils/menu-controller.test.ts rename to tests/components-lib/menu-controller.test.ts index 1799c611..1711d4ec 100644 --- a/tests/utils/menu-controller.test.ts +++ b/tests/components-lib/menu-controller.test.ts @@ -4,15 +4,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { CameraManager } from '../../src/camera-manager/manager'; import { CameraManagerCameraMetadata } from '../../src/camera-manager/types'; -import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types'; -import { FrigateCardMediaPlayer } from '../../src/types'; -import { createFrigateCardCustomAction } from '../../src/utils/action'; import { MediaPlayerManager } from '../../src/card-controller/media-player-manager'; import { MicrophoneManager } from '../../src/card-controller/microphone-manager'; import { MenuButtonController, MenuButtonControllerOptions, -} from '../../src/utils/menu-controller'; +} from '../../src/components-lib/menu-controller'; +import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types'; +import { FrigateCardMediaPlayer } from '../../src/types'; +import { createFrigateCardCustomAction } from '../../src/utils/action'; import { ViewMedia } from '../../src/view/media'; import { MediaQueriesResults } from '../../src/view/media-queries-results'; import { View } from '../../src/view/view'; diff --git a/tests/utils/zoom.test.ts b/tests/components-lib/zoom-controller.test.ts similarity index 95% rename from tests/utils/zoom.test.ts rename to tests/components-lib/zoom-controller.test.ts index f8817e2c..e040e4d1 100644 --- a/tests/utils/zoom.test.ts +++ b/tests/components-lib/zoom-controller.test.ts @@ -1,8 +1,7 @@ -import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; -import { Zoom } from '../../src/utils/zoom/zoom'; -import { PanzoomObject, PanzoomEventDetail } from '@dermotduffy/panzoom'; -import Panzoom from '@dermotduffy/panzoom'; +import Panzoom, { PanzoomEventDetail, PanzoomObject } from '@dermotduffy/panzoom'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; +import { ZoomController } from '../../src/components-lib/zoom-controller'; vi.mock('@dermotduffy/panzoom'); @@ -10,7 +9,7 @@ vi.mock('@dermotduffy/panzoom'); (window as any).PointerEvent = MouseEvent; // @vitest-environment jsdom -describe('Zoom', () => { +describe('ZoomController', () => { const mediaMediSpy = vi.spyOn(window, 'matchMedia'); const createMockPanZoom = (): PanzoomObject => { @@ -19,8 +18,8 @@ describe('Zoom', () => { return panzoom; }; - const createAndRegisterZoom = (element: HTMLElement): Zoom => { - const zoom = new Zoom(element); + const createAndRegisterZoom = (element: HTMLElement): ZoomController => { + const zoom = new ZoomController(element); zoom.activate(); return zoom; }; @@ -52,7 +51,7 @@ describe('Zoom', () => { it('should be creatable', () => { const element = document.createElement('div'); - const zoom = new Zoom(element); + const zoom = new ZoomController(element); expect(zoom).toBeTruthy(); }); diff --git a/tests/utils/get-state-obj.ts b/tests/utils/get-state-obj.ts new file mode 100644 index 00000000..08b792f1 --- /dev/null +++ b/tests/utils/get-state-obj.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createCameraConfig, createHASS, createStateEntity } from '../test-utils'; +import { getStateObjOrDispatchError } from '../../src/utils/get-state-obj'; + +// @vitest-environment jsdom +describe('getStateObjOrDispatchError', () => { + it('should retrieve valid state object', () => { + const messageHandler = vi.fn(); + const element = document.createElement('div'); + element.addEventListener('frigate-card:message', messageHandler); + const state = createStateEntity(); + + expect( + getStateObjOrDispatchError( + element, + createHASS({ + 'camera.test': state, + }), + createCameraConfig({ + camera_entity: 'camera.test', + }), + ), + ).toBe(state); + + expect(messageHandler).not.toBeCalled(); + }); + + it('should dispatch unspecified entity', () => { + const messageHandler = vi.fn(); + const element = document.createElement('div'); + element.addEventListener('frigate-card:message', messageHandler); + + expect( + getStateObjOrDispatchError(element, createHASS(), createCameraConfig()), + ).toBeNull(); + + expect(messageHandler).toBeCalledWith( + expect.objectContaining({ + detail: expect.objectContaining({ + message: + 'The camera_entity parameter must be set and valid for this live provider', + type: 'error', + }), + }), + ); + }); + + it('should dispatch not found state', () => { + const messageHandler = vi.fn(); + const element = document.createElement('div'); + element.addEventListener('frigate-card:message', messageHandler); + + expect( + getStateObjOrDispatchError( + element, + createHASS(), + createCameraConfig({ + camera_entity: 'camera.will-not-be-found', + }), + ), + ).toBeNull(); + + expect(messageHandler).toBeCalledWith( + expect.objectContaining({ + detail: expect.objectContaining({ + message: 'The configured camera_entity was not found', + type: 'error', + }), + }), + ); + }); +}); From 63d90a8eb5710773ca4fb8c3dbd08c1f576ca350 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 5 Oct 2023 21:36:53 -0700 Subject: [PATCH 2/2] Clean up message content. --- src/components/live/live.ts | 9 ++++++--- src/components/message.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/live/live.ts b/src/components/live/live.ts index 5e1d3ce0..283e1969 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -949,9 +949,12 @@ export class FrigateCardLiveProvider // receiving frames). Otherwise a single temporarily unavailable camera // would render a whole carousel inoperable. return renderMessage({ - message: localize('error.live_camera_unavailable'), - type: 'error', - context: this.cameraConfig, + message: `${localize('error.live_camera_unavailable')}${ + this.label ? `: ${this.label}` : '' + }`, + type: 'info', + icon: 'mdi:cctv-off', + dotdotdot: true, }); } } diff --git a/src/components/message.ts b/src/components/message.ts index a070fd7c..fd0106b8 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -67,7 +67,7 @@ export class FrigateCardErrorMessage extends LitElement { return html` ${localize('error.troubleshooting')}.`} - .icon=${'mdi:alert-circle'} + .icon=${this.message.icon ?? 'mdi:alert-circle'} .context=${this.message.context} .dotdotdot=${this.message.dotdotdot} >