feat: add problem detection framework for common problems (#2412)
Introduces a ProblemManager that detects and surfaces actionable issues (stale config, legacy frigate-hass-card resources, slow/failed streams) via status bar indicators and notification popups with fix actions.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { ProblemPresence } from '../card-controller/problems/types';
|
||||
import { RawAdvancedCameraCardConfig } from '../config/types';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
@@ -20,11 +21,15 @@ export class AdvancedCameraCardDiagnostics extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public rawConfig?: RawAdvancedCameraCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public problems?: ProblemPresence;
|
||||
|
||||
private async _renderDiagnostics(): Promise<TemplateResult> {
|
||||
const diagnostics = await getDiagnostics(
|
||||
this.hass,
|
||||
this.deviceRegistryManager,
|
||||
this.rawConfig,
|
||||
this.problems,
|
||||
);
|
||||
|
||||
return renderMessage({
|
||||
|
||||
@@ -18,7 +18,6 @@ import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch
|
||||
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
||||
import { LiveConfig } from '../../config/schema/live.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
MediaPlayerController,
|
||||
MediaPlayerElement,
|
||||
} from '../../types.js';
|
||||
import { fireAdvancedCameraCardEvent } from '../../utils/fire-advanced-camera-card-event.js';
|
||||
import { getResolvedLiveProvider } from '../../utils/live-provider.js';
|
||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||
import '../icon.js';
|
||||
@@ -73,9 +73,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
@state()
|
||||
private _hasProviderError = false;
|
||||
|
||||
@state()
|
||||
private _showStreamTroubleshooting = false;
|
||||
|
||||
private _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||
|
||||
private _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
||||
@@ -118,8 +115,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
!!this.camera?.getConfig()?.camera_entity &&
|
||||
!!this.hass &&
|
||||
!!this.liveConfig?.show_image_during_load &&
|
||||
!this._showStreamTroubleshooting &&
|
||||
// Do not continue to show image during loading if an error has occurred.
|
||||
!this._hasProviderError
|
||||
);
|
||||
}
|
||||
@@ -131,11 +126,19 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
|
||||
private _videoMediaShowHandler(): void {
|
||||
this._isVideoMediaLoaded = true;
|
||||
this._showStreamTroubleshooting = false;
|
||||
}
|
||||
|
||||
private _providerErrorHandler(): void {
|
||||
private _providerErrorHandler(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this._hasProviderError = true;
|
||||
|
||||
const cameraID = this.camera?.getID();
|
||||
if (cameraID) {
|
||||
fireAdvancedCameraCardEvent(this, 'problem:trigger', {
|
||||
key: 'stream_not_loading' as const,
|
||||
cameraID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
@@ -305,7 +308,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
// so it should not be hidden.
|
||||
hidden: false,
|
||||
})}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
@advanced-camera-card:live:error=${(ev: Event) =>
|
||||
this._providerErrorHandler(ev)}
|
||||
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||
ev.detail.placeholder = provider !== 'image';
|
||||
}}
|
||||
@@ -319,7 +323,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${cameraConfig}
|
||||
?controls=${this._getEffectiveBuiltinControls()}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
@advanced-camera-card:live:error=${(ev: Event) =>
|
||||
this._providerErrorHandler(ev)}
|
||||
>
|
||||
</advanced-camera-card-live-ha>`
|
||||
: provider === 'go2rtc'
|
||||
@@ -332,7 +337,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
.microphoneState=${this.microphoneState}
|
||||
.microphoneConfig=${this.liveConfig.microphone}
|
||||
?controls=${this._getEffectiveBuiltinControls()}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
@advanced-camera-card:live:error=${(ev: Event) =>
|
||||
this._providerErrorHandler(ev)}
|
||||
>
|
||||
</advanced-camera-card-live-go2rtc>`
|
||||
: provider === 'webrtc-card'
|
||||
@@ -344,7 +350,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
?controls=${this._getEffectiveBuiltinControls()}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
@advanced-camera-card:live:error=${(ev: Event) =>
|
||||
this._providerErrorHandler(ev)}
|
||||
>
|
||||
</advanced-camera-card-live-webrtc-card>`
|
||||
: provider === 'jsmpeg'
|
||||
@@ -355,7 +362,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
.cameraConfig=${cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
@advanced-camera-card:live:error=${(ev: Event) =>
|
||||
this._providerErrorHandler(ev)}
|
||||
>
|
||||
</advanced-camera-card-live-jsmpeg>`
|
||||
: html``}
|
||||
@@ -364,24 +372,9 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
? html`<advanced-camera-card-icon
|
||||
title=${localize('error.awaiting_live')}
|
||||
.icon=${{ icon: 'mdi:progress-helper' }}
|
||||
@click=${() => {
|
||||
this._showStreamTroubleshooting = !this._showStreamTroubleshooting;
|
||||
}}
|
||||
@click=${() =>
|
||||
fireAdvancedCameraCardEvent(this, 'problem:notify', 'stream_not_loading')}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
${this._showStreamTroubleshooting
|
||||
? renderMessage(
|
||||
{
|
||||
type: 'error',
|
||||
icon: 'mdi:camera-off',
|
||||
message: localize('error.stream_not_loading'),
|
||||
url: {
|
||||
link: STREAM_TROUBLESHOOTING_URL,
|
||||
title: localize('error.troubleshooting'),
|
||||
},
|
||||
},
|
||||
{ overlay: true },
|
||||
)
|
||||
: ''}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ export class AdvancedCameraCardMessage extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = this._controller.getURL(this.message);
|
||||
const link = this._controller.getLink(this.message);
|
||||
const messageTemplate = html`
|
||||
${this._controller.getMessageString(this.message)}
|
||||
${url ? html`. <a href="${url.link}">${url.title}</a>` : ''}
|
||||
${link ? html`. <a href="${link.url}">${link.title}</a>` : ''}
|
||||
`;
|
||||
|
||||
const icon = this._controller.getIcon(this.message);
|
||||
|
||||
@@ -61,6 +61,17 @@ export class AdvancedCameraCardNotification extends LitElement {
|
||||
${heading ? this._renderDetail(heading, true) : ''}
|
||||
${details.map((detail) => this._renderDetail(detail))}
|
||||
${text ? html`<div class="description">${text}</div>` : ''}
|
||||
${this.notification.link
|
||||
? html`<div class="url">
|
||||
<a
|
||||
href=${this.notification.link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click=${stopEventFromActivatingCardWideActions}
|
||||
>${this.notification.link.title}</a
|
||||
>
|
||||
</div>`
|
||||
: ''}
|
||||
</div>
|
||||
${controls.length
|
||||
? html`<div class="controls">
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
html,
|
||||
nothing,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
@@ -101,6 +102,7 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
||||
return html`<div
|
||||
.actionHandler=${handler}
|
||||
class="${classes}"
|
||||
title=${item.title ?? nothing}
|
||||
data-severity=${item.severity ?? ''}
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
>
|
||||
@@ -111,6 +113,7 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
||||
.actionHandler=${handler}
|
||||
.icon=${{ icon: item.icon }}
|
||||
class="${classes}"
|
||||
title=${item.title ?? nothing}
|
||||
data-severity=${item.severity ?? ''}
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
></advanced-camera-card-icon>`;
|
||||
@@ -118,6 +121,7 @@ export class AdvancedCameraCardStatusBar extends LitElement {
|
||||
return html`<img
|
||||
.actionHandler=${handler}
|
||||
class="${classes}"
|
||||
title=${item.title ?? nothing}
|
||||
src="${item.image}"
|
||||
data-severity=${item.severity ?? ''}
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { FoldersManager } from '../card-controller/folders/manager.js';
|
||||
import { ProblemPresence } from '../card-controller/problems/types.js';
|
||||
import { MicrophoneState } from '../card-controller/types.js';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
@@ -68,6 +69,9 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public deviceRegistryManager?: DeviceRegistryManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public problems?: ProblemPresence;
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionStateManager?: ConditionStateManagerReadonlyInterface;
|
||||
|
||||
@@ -211,6 +215,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.rawConfig=${this.rawConfig}
|
||||
.deviceRegistryManager=${this.deviceRegistryManager}
|
||||
.problems=${this.problems}
|
||||
>
|
||||
</advanced-camera-card-diagnostics>`
|
||||
: ``}
|
||||
|
||||
Reference in New Issue
Block a user