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:
Dermot Duffy
2026-03-13 20:00:59 -07:00
committed by GitHub
parent 95faddd9a0
commit ab683df5e8
46 changed files with 2147 additions and 166 deletions
+5
View File
@@ -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({
+23 -30
View File
@@ -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 },
)
: ''}`;
}
+2 -2
View File
@@ -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);
+11
View File
@@ -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
View File
@@ -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)}
+5
View File
@@ -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>`
: ``}