Merge pull request #1283 from dermotduffy/camera-availability
Deal with camera unavailability in a cleaner way
This commit is contained in:
@@ -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": [
|
||||||
|
"{",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -221,7 +221,7 @@ export class ViewManager {
|
|||||||
log(
|
log(
|
||||||
this._api.getConfigManager().getCardWideConfig(),
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
`Frigate Card view change: `,
|
`Frigate Card view change: `,
|
||||||
view.view,
|
view,
|
||||||
);
|
);
|
||||||
this._view = view;
|
this._view = view;
|
||||||
|
|
||||||
|
|||||||
+4
-8
@@ -8,6 +8,9 @@ import { ViewContext } from 'view';
|
|||||||
import 'web-dialog';
|
import 'web-dialog';
|
||||||
import pkg from '../package.json';
|
import pkg from '../package.json';
|
||||||
import { actionHandler } from './action-handler-directive.js';
|
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 './components/elements.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
import './components/menu.js';
|
import './components/menu.js';
|
||||||
@@ -21,15 +24,8 @@ import { FrigateCardConfig, MenuItem, RawFrigateCardConfig } from './config/type
|
|||||||
import { REPO_URL } from './const.js';
|
import { REPO_URL } from './const.js';
|
||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
import cardStyle from './scss/card.scss';
|
import cardStyle from './scss/card.scss';
|
||||||
import {
|
import { ExtendedHomeAssistant, MediaLoadedInfo, Message } from './types.js';
|
||||||
ExtendedHomeAssistant,
|
|
||||||
MediaLoadedInfo,
|
|
||||||
Message,
|
|
||||||
} from './types.js';
|
|
||||||
import { frigateCardHasAction } from './utils/action.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';
|
import { View } from './view/view.js';
|
||||||
|
|
||||||
// ***************************************************************************
|
// ***************************************************************************
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||||
import { Timer } from './utils/timer';
|
import { Timer } from '../utils/timer';
|
||||||
|
|
||||||
export class CachedValueController<T> implements ReactiveController {
|
export class CachedValueController<T> implements ReactiveController {
|
||||||
protected _value?: T;
|
protected _value?: T;
|
||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
dispatchFrigateCardEvent,
|
dispatchFrigateCardEvent,
|
||||||
getChildrenFromElement,
|
getChildrenFromElement,
|
||||||
setOrRemoveAttribute,
|
setOrRemoveAttribute,
|
||||||
} from './basic';
|
} from '../utils/basic';
|
||||||
import {
|
import {
|
||||||
FrigateMediaLoadedEventTarget,
|
FrigateMediaLoadedEventTarget,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
dispatchMediaUnloadedEvent,
|
dispatchMediaUnloadedEvent,
|
||||||
} from './media-info';
|
} from '../utils/media-info';
|
||||||
|
|
||||||
// The default minimum cell width: if the columns are not specified this value
|
// 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
|
// is used to compute the number of columns, always trying to keep each cell as
|
||||||
@@ -13,12 +13,12 @@ import {
|
|||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { createFrigateCardCustomAction } from './action';
|
import { createFrigateCardCustomAction } from '../utils/action';
|
||||||
import { getAllDependentCameras } from './camera';
|
import { getAllDependentCameras } from '../utils/camera';
|
||||||
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
||||||
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||||
import { getEntityIcon, getEntityTitle } from './ha';
|
import { getEntityIcon, getEntityTitle } from '../utils/ha';
|
||||||
import { hasSubstream } from './substream';
|
import { hasSubstream } from '../utils/substream';
|
||||||
export interface MenuButtonControllerOptions {
|
export interface MenuButtonControllerOptions {
|
||||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
showCameraUIButton?: boolean;
|
showCameraUIButton?: boolean;
|
||||||
@@ -12,7 +12,7 @@ import { EventQuery, RecordingQuery, RecordingSegment } from '../camera-manager/
|
|||||||
import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
||||||
import { ClipsOrSnapshotsOrAll } from '../types';
|
import { ClipsOrSnapshotsOrAll } from '../types';
|
||||||
import { ViewMedia } from '../view/media';
|
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
|
// 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).
|
// (caching times in the data-engine may increase the effective delay).
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import Panzoom, { PanzoomEventDetail, PanzoomObject } from '@dermotduffy/panzoom';
|
import Panzoom, { PanzoomEventDetail, PanzoomObject } from '@dermotduffy/panzoom';
|
||||||
import round from 'lodash-es/round';
|
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) {
|
constructor(element: HTMLElement) {
|
||||||
this._element = element;
|
this._element = element;
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
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 { CameraConfig, ImageViewConfig } from '../config/types.js';
|
||||||
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraConfig } from '../../config/types';
|
import { CameraConfig } from '../../config/types';
|
||||||
|
import { localize } from '../../localize/localize';
|
||||||
import '../../patches/ha-camera-stream';
|
import '../../patches/ha-camera-stream';
|
||||||
import '../../patches/ha-hls-player.js';
|
import '../../patches/ha-hls-player.js';
|
||||||
import '../../patches/ha-web-rtc-player.ts';
|
import '../../patches/ha-web-rtc-player.ts';
|
||||||
import liveHAStyle from '../../scss/live-ha.scss';
|
import liveHAStyle from '../../scss/live-ha.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../../types.js';
|
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')
|
@customElement('frigate-card-live-ha')
|
||||||
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
||||||
@@ -68,7 +70,14 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
|||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (stateObj.state === 'unavailable') {
|
||||||
|
return renderMessage({
|
||||||
|
message: localize('error.live_camera_unavailable'),
|
||||||
|
type: 'error',
|
||||||
|
icon: 'mdi:connection',
|
||||||
|
context: this.cameraConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
return html` <frigate-card-ha-camera-stream
|
return html` <frigate-card-ha-camera-stream
|
||||||
${ref(this._playerRef)}
|
${ref(this._playerRef)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CameraConfig } from '../../config/types';
|
|||||||
import basicBlockStyle from '../../scss/basic-block.scss';
|
import basicBlockStyle from '../../scss/basic-block.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../../types.js';
|
import { FrigateCardMediaPlayer } from '../../types.js';
|
||||||
import '../image.js';
|
import '../image.js';
|
||||||
import { getStateObjOrDispatchError } from './live.js';
|
import { getStateObjOrDispatchError } from '../../utils/get-state-obj';
|
||||||
|
|
||||||
@customElement('frigate-card-live-image')
|
@customElement('frigate-card-live-image')
|
||||||
export class FrigateCardLiveImage extends LitElement implements FrigateCardMediaPlayer {
|
export class FrigateCardLiveImage extends LitElement implements FrigateCardMediaPlayer {
|
||||||
|
|||||||
+28
-46
@@ -1,5 +1,3 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import { HassEntity } from 'home-assistant-js-websocket';
|
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
@@ -16,6 +14,11 @@ import { keyed } from 'lit/directives/keyed.js';
|
|||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { CameraConfigs, CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraConfigs, CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
|
import {
|
||||||
|
ConditionsManagerEpoch,
|
||||||
|
getOverriddenConfig,
|
||||||
|
} from '../../card-controller/conditions-manager.js';
|
||||||
|
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
@@ -37,16 +40,11 @@ import {
|
|||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||||
import { contentsChanged } from '../../utils/basic.js';
|
import { contentsChanged } from '../../utils/basic.js';
|
||||||
import {
|
|
||||||
ConditionsManagerEpoch,
|
|
||||||
getOverriddenConfig,
|
|
||||||
} from '../../card-controller/conditions-manager.js';
|
|
||||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||||
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
||||||
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
||||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||||
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
||||||
import { MediaGridSelected } from '../../utils/media-grid-controller.js';
|
|
||||||
import {
|
import {
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
dispatchMediaUnloadedEvent,
|
dispatchMediaUnloadedEvent,
|
||||||
@@ -56,7 +54,7 @@ import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
|||||||
import { Timer } from '../../utils/timer.js';
|
import { Timer } from '../../utils/timer.js';
|
||||||
import { dispatchViewContextChangeEvent, View } from '../../view/view.js';
|
import { dispatchViewContextChangeEvent, View } from '../../view/view.js';
|
||||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||||
import { dispatchErrorMessageEvent, dispatchMessageEvent } from '../message.js';
|
import { renderMessage } from '../message.js';
|
||||||
import '../next-prev-control.js';
|
import '../next-prev-control.js';
|
||||||
import '../surround.js';
|
import '../surround.js';
|
||||||
import '../title-control.js';
|
import '../title-control.js';
|
||||||
@@ -65,6 +63,7 @@ import {
|
|||||||
getDefaultTitleConfigForView,
|
getDefaultTitleConfigForView,
|
||||||
showTitleControlAfterDelay,
|
showTitleControlAfterDelay,
|
||||||
} from '../title-control.js';
|
} from '../title-control.js';
|
||||||
|
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
||||||
|
|
||||||
interface LiveViewContext {
|
interface LiveViewContext {
|
||||||
// A cameraID override (used for dependencies/substreams to force a different
|
// A cameraID override (used for dependencies/substreams to force a different
|
||||||
@@ -85,44 +84,6 @@ interface LastMediaLoadedInfo {
|
|||||||
|
|
||||||
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the state object or dispatch an error. Used in `ha` and `image` live
|
|
||||||
* providers.
|
|
||||||
* @param element HTMLElement to dispatch errors from.
|
|
||||||
* @param hass Home Assistant object.
|
|
||||||
* @param cameraConfig Camera configuration.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getStateObjOrDispatchError = (
|
|
||||||
element: HTMLElement,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
cameraConfig?: CameraConfig,
|
|
||||||
): HassEntity | null => {
|
|
||||||
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')
|
@customElement('frigate-card-live')
|
||||||
export class FrigateCardLive extends LitElement {
|
export class FrigateCardLive extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -977,6 +938,27 @@ export class FrigateCardLiveProvider
|
|||||||
hidden: showImageDuringLoading,
|
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')}${
|
||||||
|
this.label ? `: ${this.label}` : ''
|
||||||
|
}`,
|
||||||
|
type: 'info',
|
||||||
|
icon: 'mdi:cctv-off',
|
||||||
|
dotdotdot: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this._useZoomIfRequired(html`
|
return this._useZoomIfRequired(html`
|
||||||
${showImageDuringLoading || provider === 'image'
|
${showImageDuringLoading || provider === 'image'
|
||||||
? html` <frigate-card-live-image
|
? html` <frigate-card-live-image
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { MediaGridController } from '../components-lib/media-grid-controller.js';
|
||||||
import { ViewDisplayConfig } from '../config/types';
|
import { ViewDisplayConfig } from '../config/types';
|
||||||
import mediaGridStyle from '../scss/media-grid.scss';
|
import mediaGridStyle from '../scss/media-grid.scss';
|
||||||
import { MediaGridController } from '../utils/media-grid-controller.js';
|
|
||||||
|
|
||||||
@customElement('frigate-card-media-grid')
|
@customElement('frigate-card-media-grid')
|
||||||
export class FrigateCardMediaGrid extends LitElement {
|
export class FrigateCardMediaGrid extends LitElement {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class FrigateCardErrorMessage extends LitElement {
|
|||||||
return html` <frigate-card-message
|
return html` <frigate-card-message
|
||||||
.message=${html` ${this.message.message}.
|
.message=${html` ${this.message.message}.
|
||||||
<a href="${TROUBLESHOOTING_URL}"> ${localize('error.troubleshooting')}</a>.`}
|
<a href="${TROUBLESHOOTING_URL}"> ${localize('error.troubleshooting')}</a>.`}
|
||||||
.icon=${'mdi:alert-circle'}
|
.icon=${this.message.icon ?? 'mdi:alert-circle'}
|
||||||
.context=${this.message.context}
|
.context=${this.message.context}
|
||||||
.dotdotdot=${this.message.dotdotdot}
|
.dotdotdot=${this.message.dotdotdot}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import { CameraManager } from '../camera-manager/manager';
|
|||||||
import { rangesOverlap } from '../camera-manager/range';
|
import { rangesOverlap } from '../camera-manager/range';
|
||||||
import { MediaQuery } from '../camera-manager/types';
|
import { MediaQuery } from '../camera-manager/types';
|
||||||
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
||||||
|
import {
|
||||||
|
FrigateCardTimelineItem,
|
||||||
|
TimelineDataSource,
|
||||||
|
} from '../components-lib/timeline-source';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
@@ -54,7 +58,6 @@ import {
|
|||||||
executeMediaQueryForViewWithErrorDispatching,
|
executeMediaQueryForViewWithErrorDispatching,
|
||||||
findBestMediaIndex,
|
findBestMediaIndex,
|
||||||
} from '../utils/media-to-view';
|
} from '../utils/media-to-view';
|
||||||
import { FrigateCardTimelineItem, TimelineDataSource } from '../utils/timeline-source';
|
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { guard } from 'lit/directives/guard.js';
|
|||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
|
import { MediaGridSelected } from '../components-lib/media-grid-controller.js';
|
||||||
import {
|
import {
|
||||||
dispatchMessageEvent,
|
dispatchMessageEvent,
|
||||||
renderMessage,
|
renderMessage,
|
||||||
@@ -47,7 +48,6 @@ import AutoMediaLoadedInfo from '../utils/embla/plugins/auto-media-loaded-info/a
|
|||||||
import AutoSize from '../utils/embla/plugins/auto-size/auto-size.js';
|
import AutoSize from '../utils/embla/plugins/auto-size/auto-size.js';
|
||||||
import { canonicalizeHAURL } from '../utils/ha/index.js';
|
import { canonicalizeHAURL } from '../utils/ha/index.js';
|
||||||
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||||
import { MediaGridSelected } from '../utils/media-grid-controller.js';
|
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, state } from 'lit/decorators.js';
|
import { customElement, state } from 'lit/decorators.js';
|
||||||
|
import { ZoomController } from '../components-lib/zoom-controller.js';
|
||||||
import { setOrRemoveAttribute } from '../utils/basic.js';
|
import { setOrRemoveAttribute } from '../utils/basic.js';
|
||||||
import { Zoom } from '../utils/zoom/zoom.js';
|
|
||||||
|
|
||||||
@customElement('frigate-card-zoomer')
|
@customElement('frigate-card-zoomer')
|
||||||
export class FrigateCardZoomer extends LitElement {
|
export class FrigateCardZoomer extends LitElement {
|
||||||
protected _zoom = new Zoom(this);
|
protected _zoom = new ZoomController(this);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _zoomed = false;
|
protected _zoomed = false;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class EmblaReInitController {
|
|||||||
this._shouldReInitOnScrollStop = false;
|
this._shouldReInitOnScrollStop = false;
|
||||||
this._emblaApi?.reInit();
|
this._emblaApi?.reInit();
|
||||||
},
|
},
|
||||||
200,
|
500,
|
||||||
{ trailing: true },
|
{ trailing: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { HomeAssistant } from "custom-card-helpers";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { CameraConfig } from "../config/types.js";
|
||||||
|
import { dispatchErrorMessageEvent } from "../components/message.js";
|
||||||
|
import { localize } from "../localize/localize.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the state object or dispatch an error. Used in `ha` and `image` live
|
||||||
|
* providers.
|
||||||
|
* @param element HTMLElement to dispatch errors from.
|
||||||
|
* @param hass Home Assistant object.
|
||||||
|
* @param cameraConfig Camera configuration.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getStateObjOrDispatchError = (
|
||||||
|
element: HTMLElement,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig?: CameraConfig,
|
||||||
|
): HassEntity | null => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ReactiveControllerHost } from 'lit';
|
import { ReactiveControllerHost } from 'lit';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
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
|
// @vitest-environment jsdom
|
||||||
describe('CachedValueController', () => {
|
describe('CachedValueController', () => {
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import { MediaLoadedInfo } from '../../src/types';
|
|||||||
import {
|
import {
|
||||||
MediaGridConstructorOptions,
|
MediaGridConstructorOptions,
|
||||||
MediaGridController,
|
MediaGridController,
|
||||||
} from '../../src/utils/media-grid-controller';
|
} from '../../src/components-lib/media-grid-controller';
|
||||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
||||||
import {
|
import {
|
||||||
MutationObserverMock,
|
MutationObserverMock,
|
||||||
@@ -4,15 +4,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { CameraManager } from '../../src/camera-manager/manager';
|
import { CameraManager } from '../../src/camera-manager/manager';
|
||||||
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types';
|
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 { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
||||||
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
||||||
import {
|
import {
|
||||||
MenuButtonController,
|
MenuButtonController,
|
||||||
MenuButtonControllerOptions,
|
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 { ViewMedia } from '../../src/view/media';
|
||||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||||
import { View } from '../../src/view/view';
|
import { View } from '../../src/view/view';
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
import Panzoom, { PanzoomEventDetail, PanzoomObject } from '@dermotduffy/panzoom';
|
||||||
import { Zoom } from '../../src/utils/zoom/zoom';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { PanzoomObject, PanzoomEventDetail } from '@dermotduffy/panzoom';
|
|
||||||
import Panzoom from '@dermotduffy/panzoom';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { ZoomController } from '../../src/components-lib/zoom-controller';
|
||||||
|
|
||||||
vi.mock('@dermotduffy/panzoom');
|
vi.mock('@dermotduffy/panzoom');
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ vi.mock('@dermotduffy/panzoom');
|
|||||||
(window as any).PointerEvent = MouseEvent;
|
(window as any).PointerEvent = MouseEvent;
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('Zoom', () => {
|
describe('ZoomController', () => {
|
||||||
const mediaMediSpy = vi.spyOn(window, 'matchMedia');
|
const mediaMediSpy = vi.spyOn(window, 'matchMedia');
|
||||||
|
|
||||||
const createMockPanZoom = (): PanzoomObject => {
|
const createMockPanZoom = (): PanzoomObject => {
|
||||||
@@ -19,8 +18,8 @@ describe('Zoom', () => {
|
|||||||
return panzoom;
|
return panzoom;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAndRegisterZoom = (element: HTMLElement): Zoom => {
|
const createAndRegisterZoom = (element: HTMLElement): ZoomController => {
|
||||||
const zoom = new Zoom(element);
|
const zoom = new ZoomController(element);
|
||||||
zoom.activate();
|
zoom.activate();
|
||||||
return zoom;
|
return zoom;
|
||||||
};
|
};
|
||||||
@@ -52,7 +51,7 @@ describe('Zoom', () => {
|
|||||||
|
|
||||||
it('should be creatable', () => {
|
it('should be creatable', () => {
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
const zoom = new Zoom(element);
|
const zoom = new ZoomController(element);
|
||||||
expect(zoom).toBeTruthy();
|
expect(zoom).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user