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:
@@ -70,6 +70,7 @@ export class CardElementManager {
|
||||
this._api.getMediaLoadedInfoManager().initialize();
|
||||
this._api.getMicrophoneManager().initialize();
|
||||
this._api.getPIPManager().initialize();
|
||||
this._api.getProblemManager().initialize();
|
||||
this._api.getKeyboardStateManager().initialize();
|
||||
|
||||
// These initializers are called when the config is updated, but on initial
|
||||
@@ -171,6 +172,7 @@ export class CardElementManager {
|
||||
this._api.getMediaLoadedInfoManager().clear();
|
||||
this._api.getFullscreenManager().disconnect();
|
||||
this._api.getPIPManager().uninitialize();
|
||||
this._api.getProblemManager().uninitialize();
|
||||
this._api.getKeyboardStateManager().uninitialize();
|
||||
this._api.getActionsManager().uninitialize();
|
||||
this._api.getDefaultManager().uninitialize();
|
||||
|
||||
@@ -28,7 +28,6 @@ export class ConfigManager {
|
||||
private _overriddenConfig: AdvancedCameraCardConfig | null = null;
|
||||
private _rawConfig: RawAdvancedCameraCardConfig | null = null;
|
||||
private _cardWideConfig: CardWideConfig | null = null;
|
||||
private _upgradeable = false;
|
||||
private _overridesManager = new OverridesManager(() => this._processOverrideConfig());
|
||||
|
||||
constructor(api: CardConfigAPI) {
|
||||
@@ -55,22 +54,17 @@ export class ConfigManager {
|
||||
return this._rawConfig;
|
||||
}
|
||||
|
||||
public isUpgradeable(): boolean {
|
||||
return this._upgradeable;
|
||||
}
|
||||
|
||||
public setConfig(inputConfig?: RawAdvancedCameraCardConfig): void {
|
||||
if (!inputConfig) {
|
||||
throw new Error(localize('error.invalid_configuration'));
|
||||
}
|
||||
|
||||
const parseResult = advancedCameraCardConfigSchema.safeParse(inputConfig);
|
||||
this._upgradeable = isConfigUpgradeable(inputConfig);
|
||||
|
||||
if (!parseResult.success) {
|
||||
const hint = getParseError(parseResult.error);
|
||||
let upgradeMessage = '';
|
||||
if (this._upgradeable) {
|
||||
if (isConfigUpgradeable(inputConfig)) {
|
||||
upgradeMessage = `${localize('error.upgrade_available')}. `;
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@@ -33,6 +33,7 @@ import { MessageManager } from './message-manager';
|
||||
import { MicrophoneManager } from './microphone-manager';
|
||||
import { NotificationManager } from './notification-manager';
|
||||
import { PIPManager } from './pip-manager';
|
||||
import { ProblemManager } from './problems/manager';
|
||||
import { QueryStringManager } from './query-string-manager';
|
||||
import { StatusBarItemManager } from './status-bar-item-manager';
|
||||
import { StyleManager } from './style-manager';
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
CardMicrophoneAPI,
|
||||
CardNotificationAPI,
|
||||
CardPIPAPI,
|
||||
CardProblemAPI,
|
||||
CardQueryStringAPI,
|
||||
CardStyleAPI,
|
||||
CardTriggersAPI,
|
||||
@@ -83,6 +85,7 @@ export class CardController
|
||||
CardFullscreenAPI,
|
||||
CardHASSAPI,
|
||||
CardPIPAPI,
|
||||
CardProblemAPI,
|
||||
CardInitializerAPI,
|
||||
CardInteractionAPI,
|
||||
CardKeyboardStateAPI,
|
||||
@@ -127,6 +130,7 @@ export class CardController
|
||||
private _microphoneManager = new MicrophoneManager(this);
|
||||
private _notificationManager = new NotificationManager(this);
|
||||
private _pipManager = new PIPManager(this);
|
||||
private _problemManager = new ProblemManager(this);
|
||||
private _queryStringManager = new QueryStringManager(this);
|
||||
private _statusBarItemManager = new StatusBarItemManager(this);
|
||||
private _styleManager = new StyleManager(this);
|
||||
@@ -260,6 +264,10 @@ export class CardController
|
||||
return this._pipManager;
|
||||
}
|
||||
|
||||
public getProblemManager(): ProblemManager {
|
||||
return this._problemManager;
|
||||
}
|
||||
|
||||
public getQueryStringManager(): QueryStringManager {
|
||||
return this._queryStringManager;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PQueue from 'p-queue';
|
||||
import { loadLanguages } from '../localize/localize';
|
||||
import { sideLoadHomeAssistantElements } from '../ha/side-load-ha-elements';
|
||||
import { loadLanguages } from '../localize/localize';
|
||||
import { Initializer } from '../utils/initializer/initializer';
|
||||
import { CardInitializerAPI } from './types';
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum InitializationAspect {
|
||||
SIDE_LOAD_ELEMENTS = 'side-load-elements',
|
||||
CAMERAS = 'cameras',
|
||||
MICROPHONE_CONNECT = 'microphone-connect',
|
||||
PROBLEMS = 'problems',
|
||||
VIEW = 'view',
|
||||
|
||||
// The initial triggering must happen after both the config is set (and
|
||||
@@ -50,6 +51,10 @@ export class InitializationManager {
|
||||
return this._initializer.isInitialized(aspect);
|
||||
}
|
||||
|
||||
public isInitializedBackground(): boolean {
|
||||
return this._initializer.isInitialized(InitializationAspect.PROBLEMS);
|
||||
}
|
||||
|
||||
public isInitializedMandatory(): boolean {
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
if (!config) {
|
||||
@@ -167,6 +172,25 @@ export class InitializationManager {
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
public async initializeBackground(): Promise<void> {
|
||||
await this._initializationQueue.add(() => this._initializeBackground());
|
||||
}
|
||||
|
||||
private async _initializeBackground(): Promise<void> {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
if (!hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._initializer.initializeIfNecessary(
|
||||
InitializationAspect.PROBLEMS,
|
||||
async () => {
|
||||
await this._api.getProblemManager().detectStatic(hass);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public uninitialize(aspect: InitializationAspect): void {
|
||||
this._initializer.uninitialize(aspect);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { ConditionStateChange } from '../../conditions/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { CardProblemAPI } from '../types';
|
||||
import { ConfigUpgradeProblem } from './problems/config-upgrade';
|
||||
import { LegacyResourceProblem } from './problems/legacy-resource';
|
||||
import { StreamNotLoadingProblem } from './problems/stream-not-loading';
|
||||
import {
|
||||
KeyedProblemResult,
|
||||
Problem,
|
||||
ProblemDynamicContext,
|
||||
ProblemKey,
|
||||
ProblemPresence,
|
||||
ProblemTriggerContext,
|
||||
} from './types';
|
||||
|
||||
export class ProblemManager {
|
||||
private _api: CardProblemAPI;
|
||||
private _problems = new Map<ProblemKey, Problem>();
|
||||
|
||||
constructor(api: CardProblemAPI) {
|
||||
this._api = api;
|
||||
|
||||
this._addProblem(
|
||||
new ConfigUpgradeProblem(() => this._api.getConfigManager().getRawConfig()),
|
||||
);
|
||||
this._addProblem(
|
||||
new LegacyResourceProblem(() => this._api.getCardElementManager().update()),
|
||||
);
|
||||
this._addProblem(
|
||||
new StreamNotLoadingProblem(() => this._api.getCardElementManager().update()),
|
||||
);
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this._api.getConditionStateManager().addListener(this._stateChangeHandler);
|
||||
}
|
||||
|
||||
public uninitialize(): void {
|
||||
this._api.getConditionStateManager().removeListener(this._stateChangeHandler);
|
||||
}
|
||||
|
||||
private _addProblem(problem: Problem): void {
|
||||
this._problems.set(problem.key, problem);
|
||||
}
|
||||
|
||||
public async detectStatic(hass: HomeAssistant): Promise<void> {
|
||||
for (const problem of this._problems.values()) {
|
||||
await problem.detectStatic?.(hass);
|
||||
}
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
// Silently trigger a problem by key, updating state without user
|
||||
// interaction. Use this for system-originated events (e.g. provider errors).
|
||||
public trigger(key: ProblemKey, context?: ProblemTriggerContext): void {
|
||||
const problem = this._problems.get(key);
|
||||
if (!problem) {
|
||||
return;
|
||||
}
|
||||
problem.trigger?.(context);
|
||||
|
||||
// Re-evaluate dynamic state so the trigger could take effect immediately.
|
||||
// trigger() only records context (e.g. marking a camera as errored);
|
||||
// detectDynamic() decides whether to activate based on current state (e.g.
|
||||
// whether it is the selected camera with the error).
|
||||
const state = this._api.getConditionStateManager().getState();
|
||||
this._detectAllDynamic({
|
||||
cameraID: state.camera,
|
||||
view: state.view,
|
||||
mediaLoaded: !!state.mediaLoadedInfo,
|
||||
});
|
||||
}
|
||||
|
||||
// Show the notification popup for a problem, regardless of whether or not
|
||||
// that problem has triggered (example usecase: the stream is loading and the
|
||||
// user clicks the blue loading icon).
|
||||
public forceNotify(key: ProblemKey): void {
|
||||
const notification = this._problems.get(key)?.getNotification?.();
|
||||
if (notification) {
|
||||
this._api.getNotificationManager().setNotification(notification);
|
||||
}
|
||||
}
|
||||
|
||||
public getProblemResults(): KeyedProblemResult[] {
|
||||
const results: KeyedProblemResult[] = [];
|
||||
for (const problem of this._problems.values()) {
|
||||
const result = problem.getResult();
|
||||
if (result) {
|
||||
results.push({ key: problem.key, problem: result });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public getProblemPresence(): ProblemPresence {
|
||||
const presence: ProblemPresence = {};
|
||||
for (const problem of this._problems.values()) {
|
||||
presence[problem.key] = problem.hasResult();
|
||||
}
|
||||
return presence;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.uninitialize();
|
||||
for (const problem of this._problems.values()) {
|
||||
problem.destroy?.();
|
||||
}
|
||||
this._problems.clear();
|
||||
}
|
||||
|
||||
private _stateChangeHandler = (change: ConditionStateChange): void => {
|
||||
this._detectAllDynamic({
|
||||
cameraID: change.new.camera,
|
||||
view: change.new.view,
|
||||
mediaLoaded: !!change.new.mediaLoadedInfo,
|
||||
});
|
||||
};
|
||||
|
||||
private _detectAllDynamic(context: ProblemDynamicContext): void {
|
||||
let stateChanged = false;
|
||||
for (const problem of this._problems.values()) {
|
||||
const hadResult = problem.hasResult();
|
||||
problem.detectDynamic?.(context);
|
||||
stateChanged ||= problem.hasResult() !== hadResult;
|
||||
}
|
||||
if (stateChanged) {
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { isConfigUpgradeable } from '../../../config/management.js';
|
||||
import { RawAdvancedCameraCardConfig } from '../../../config/types.js';
|
||||
import { TROUBLESHOOTING_CONFIG_UPGRADE_URL } from '../../../const.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import { Problem, ProblemResult } from '../types';
|
||||
|
||||
export class ConfigUpgradeProblem implements Problem {
|
||||
public readonly key = 'config_upgrade' as const;
|
||||
|
||||
private _upgradeable = false;
|
||||
private _getRawConfig: () => RawAdvancedCameraCardConfig | null;
|
||||
|
||||
constructor(getRawConfig: () => RawAdvancedCameraCardConfig | null) {
|
||||
this._getRawConfig = getRawConfig;
|
||||
}
|
||||
|
||||
public async detectStatic(): Promise<void> {
|
||||
const rawConfig = this._getRawConfig();
|
||||
this._upgradeable = !!rawConfig && isConfigUpgradeable(rawConfig);
|
||||
}
|
||||
|
||||
public hasResult(): boolean {
|
||||
return this._upgradeable;
|
||||
}
|
||||
|
||||
public getResult(): ProblemResult | null {
|
||||
if (!this._upgradeable) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium',
|
||||
notification: {
|
||||
heading: {
|
||||
text: localize('problems.config_upgrade.heading'),
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium',
|
||||
},
|
||||
text: localize('problems.config_upgrade.text'),
|
||||
link: {
|
||||
url: TROUBLESHOOTING_CONFIG_UPGRADE_URL,
|
||||
title: localize('problems.troubleshooting_guide'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { z } from 'zod';
|
||||
import { TROUBLESHOOTING_LEGACY_RESOURCE_URL } from '../../../const.js';
|
||||
import { HomeAssistant } from '../../../ha/types';
|
||||
import { localize } from '../../../localize/localize';
|
||||
import { createInternalCallbackAction } from '../../../utils/action';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { Problem, ProblemResult } from '../types';
|
||||
|
||||
const LEGACY_RESOURCE_FILENAME = 'frigate-hass-card.js';
|
||||
|
||||
const ADVANCED_CAMERA_CARD_PATTERN = 'advanced-camera-card.js';
|
||||
|
||||
const getResourcePath = (url: string, baseURL: string): string => {
|
||||
try {
|
||||
return new URL(url, baseURL).pathname;
|
||||
} catch {
|
||||
// Fallback: strip query string manually.
|
||||
const queryIndex = url.indexOf('?');
|
||||
return queryIndex >= 0 ? url.slice(0, queryIndex) : url;
|
||||
}
|
||||
};
|
||||
|
||||
const resourcesSchema = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
export class LegacyResourceProblem implements Problem {
|
||||
public readonly key = 'legacy_resource' as const;
|
||||
|
||||
private _legacyResourceIDs: string[] = [];
|
||||
private _hasCorrectResource = false;
|
||||
private _checked = false;
|
||||
private _triggerUpdate: () => void;
|
||||
|
||||
constructor(triggerUpdate: () => void) {
|
||||
this._triggerUpdate = triggerUpdate;
|
||||
}
|
||||
|
||||
public async detectStatic(hass: HomeAssistant): Promise<void> {
|
||||
// Only admin users can view/modify dashboard resources.
|
||||
if (!hass.user?.is_admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rawResources = await hass.callWS({
|
||||
type: 'lovelace/resources',
|
||||
});
|
||||
|
||||
const parseResult = resourcesSchema.safeParse(rawResources);
|
||||
if (!parseResult.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._legacyResourceIDs = [];
|
||||
this._hasCorrectResource = false;
|
||||
|
||||
for (const resource of parseResult.data) {
|
||||
const path = getResourcePath(resource.url, hass.hassUrl());
|
||||
|
||||
if (path.endsWith(LEGACY_RESOURCE_FILENAME)) {
|
||||
this._legacyResourceIDs.push(resource.id);
|
||||
}
|
||||
if (path.endsWith(ADVANCED_CAMERA_CARD_PATTERN)) {
|
||||
this._hasCorrectResource = true;
|
||||
}
|
||||
}
|
||||
|
||||
this._checked = true;
|
||||
} catch {
|
||||
// Silently ignore WS failures (e.g. non-admin, connection issues).
|
||||
}
|
||||
}
|
||||
|
||||
public hasResult(): boolean {
|
||||
return this._checked && this._legacyResourceIDs.length > 0;
|
||||
}
|
||||
|
||||
public getResult(): ProblemResult | null {
|
||||
if (!this.hasResult()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = this._hasCorrectResource
|
||||
? localize('problems.legacy_resource.text_both')
|
||||
: localize('problems.legacy_resource.text_only_legacy');
|
||||
|
||||
return {
|
||||
icon: 'mdi:alert',
|
||||
severity: 'high',
|
||||
notification: {
|
||||
heading: {
|
||||
text: localize('problems.legacy_resource.heading'),
|
||||
icon: 'mdi:alert',
|
||||
severity: 'high',
|
||||
},
|
||||
text,
|
||||
link: {
|
||||
url: TROUBLESHOOTING_LEGACY_RESOURCE_URL,
|
||||
title: localize('problems.troubleshooting_guide'),
|
||||
},
|
||||
...(this._hasCorrectResource
|
||||
? {
|
||||
controls: [
|
||||
{
|
||||
tooltip: localize('problems.legacy_resource.remove'),
|
||||
icon: 'mdi:delete',
|
||||
severity: 'high',
|
||||
actions: {
|
||||
tap_action: createInternalCallbackAction(
|
||||
async (api: CardActionsAPI) => {
|
||||
const hass = api.getHASSManager().getHASS();
|
||||
if (hass) {
|
||||
await this.fix(hass);
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
dismiss: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async fix(hass: HomeAssistant): Promise<boolean> {
|
||||
if (
|
||||
!hass.user?.is_admin ||
|
||||
!this._hasCorrectResource ||
|
||||
!this._legacyResourceIDs.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
this._legacyResourceIDs.map((id) =>
|
||||
hass.callWS({
|
||||
type: 'lovelace/resources/delete',
|
||||
resource_id: id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Re-detect to verify removal.
|
||||
this._checked = false;
|
||||
await this.detectStatic(hass);
|
||||
|
||||
const fixed = !this.hasResult();
|
||||
if (fixed) {
|
||||
this._triggerUpdate();
|
||||
}
|
||||
return fixed;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Notification } from '../../../config/schema/actions/types.js';
|
||||
import { TROUBLESHOOTING_STREAM_URL } from '../../../const.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import { Timer } from '../../../utils/timer.js';
|
||||
import {
|
||||
Problem,
|
||||
ProblemDynamicContext,
|
||||
ProblemResult,
|
||||
ProblemTriggerContext,
|
||||
} from '../types.js';
|
||||
|
||||
const STREAM_LOADING_TIMEOUT_SECONDS = 10;
|
||||
|
||||
export class StreamNotLoadingProblem implements Problem {
|
||||
public readonly key = 'stream_not_loading' as const;
|
||||
|
||||
private _problemActive = false;
|
||||
private _cameraIDsWithErrors = new Set<string>();
|
||||
private _timer = new Timer();
|
||||
private _timerCameraID: string | null = null;
|
||||
private _triggerUpdate: () => void;
|
||||
|
||||
constructor(triggerUpdate: () => void) {
|
||||
this._triggerUpdate = triggerUpdate;
|
||||
}
|
||||
|
||||
public trigger(context?: ProblemTriggerContext): void {
|
||||
if (context?.cameraID) {
|
||||
this._cameraIDsWithErrors.add(context.cameraID);
|
||||
}
|
||||
}
|
||||
|
||||
public detectDynamic(context: ProblemDynamicContext): void {
|
||||
if (context.view !== 'live') {
|
||||
this._deactivate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.mediaLoaded) {
|
||||
this._handleStreamLoaded(context.cameraID);
|
||||
} else {
|
||||
this._handleStreamNotLoaded(context.cameraID);
|
||||
}
|
||||
}
|
||||
|
||||
// Stream loaded successfully. Deactivate and clear any prior provider error
|
||||
// for this camera so it won't re-trigger on the next evaluation.
|
||||
private _handleStreamLoaded(cameraID?: string): void {
|
||||
this._deactivate();
|
||||
if (cameraID) {
|
||||
this._cameraIDsWithErrors.delete(cameraID);
|
||||
}
|
||||
}
|
||||
|
||||
// Stream not yet loaded. Activate immediately if this camera has a known
|
||||
// provider error, otherwise start a timeout to detect slow loads.
|
||||
private _handleStreamNotLoaded(cameraID?: string): void {
|
||||
if (this._hasCameraError(cameraID)) {
|
||||
this._activate();
|
||||
} else if (!this._problemActive) {
|
||||
// Restart the timer when the selected camera changes so each camera
|
||||
// gets its own timeout window.
|
||||
if (!this._timer.isRunning() || this._timerCameraID !== (cameraID ?? null)) {
|
||||
this._timerCameraID = cameraID ?? null;
|
||||
this._timer.start(STREAM_LOADING_TIMEOUT_SECONDS, () => {
|
||||
this._activate();
|
||||
this._triggerUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public hasResult(): boolean {
|
||||
return this._problemActive;
|
||||
}
|
||||
|
||||
public getNotification(): Notification {
|
||||
return {
|
||||
heading: {
|
||||
text: localize('problems.stream_not_loading.heading'),
|
||||
icon: 'mdi:cctv-off',
|
||||
severity: 'high',
|
||||
},
|
||||
text: localize('problems.stream_not_loading.text'),
|
||||
link: {
|
||||
url: TROUBLESHOOTING_STREAM_URL,
|
||||
title: localize('problems.troubleshooting_guide'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public getResult(): ProblemResult | null {
|
||||
if (!this._problemActive) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
icon: 'mdi:cctv-off',
|
||||
severity: 'high',
|
||||
notification: this.getNotification(),
|
||||
};
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this._deactivate();
|
||||
this._cameraIDsWithErrors.clear();
|
||||
}
|
||||
|
||||
private _activate(): void {
|
||||
this._timer.stop();
|
||||
this._problemActive = true;
|
||||
}
|
||||
|
||||
private _deactivate(): void {
|
||||
this._timer.stop();
|
||||
this._timerCameraID = null;
|
||||
this._problemActive = false;
|
||||
}
|
||||
|
||||
private _hasCameraError(camera?: string): boolean {
|
||||
return !!camera && this._cameraIDsWithErrors.has(camera);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Notification } from '../../config/schema/actions/types';
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Severity } from '../../severity';
|
||||
|
||||
export type ProblemKey = 'config_upgrade' | 'legacy_resource' | 'stream_not_loading';
|
||||
|
||||
export interface ProblemResult {
|
||||
icon: string;
|
||||
severity: Severity;
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
export interface KeyedProblemResult {
|
||||
key: ProblemKey;
|
||||
problem: ProblemResult;
|
||||
}
|
||||
|
||||
export type ProblemPresence = Partial<Record<ProblemKey, boolean>>;
|
||||
|
||||
export interface ProblemDynamicContext {
|
||||
cameraID?: string;
|
||||
view?: AdvancedCameraCardView;
|
||||
mediaLoaded: boolean;
|
||||
}
|
||||
|
||||
export interface ProblemTriggerContext {
|
||||
cameraID?: string;
|
||||
}
|
||||
|
||||
export type ProblemTriggerEventData = { key: ProblemKey } & ProblemTriggerContext;
|
||||
|
||||
export interface Problem {
|
||||
readonly key: ProblemKey;
|
||||
|
||||
// One-time async detection (WS calls, config checks).
|
||||
detectStatic?(hass?: HomeAssistant): Promise<void>;
|
||||
|
||||
// Ongoing sync evaluation, called on state changes.
|
||||
detectDynamic?(context: ProblemDynamicContext): void;
|
||||
|
||||
// Explicitly trigger this problem.
|
||||
trigger?(context?: ProblemTriggerContext): void;
|
||||
|
||||
hasResult(): boolean;
|
||||
getResult(): ProblemResult | null;
|
||||
|
||||
// Return notification content regardless of active state, for
|
||||
// user-initiated queries (e.g. clicking a loading icon).
|
||||
getNotification?(): Notification | null;
|
||||
|
||||
// Optional automatic fixing.
|
||||
fix?(hass: HomeAssistant): Promise<boolean>;
|
||||
|
||||
// Cleanup.
|
||||
destroy?(): void;
|
||||
}
|
||||
@@ -2,14 +2,18 @@ import { isEqual } from 'lodash-es';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { StatusBarItem } from '../config/schema/actions/types';
|
||||
import { StatusBarConfig } from '../config/schema/status-bar';
|
||||
import { localize } from '../localize/localize';
|
||||
import { MediaLoadedInfo } from '../types';
|
||||
import { createNotificationAction } from '../utils/action';
|
||||
import { View } from '../view/view';
|
||||
import { KeyedProblemResult, ProblemKey } from './problems/types';
|
||||
import { CardStatusBarAPI } from './types';
|
||||
|
||||
const RESOLUTION_TOLERANCE_PCT = 0.01;
|
||||
|
||||
const problemKeyToStatusBarKey = (key: ProblemKey): keyof StatusBarConfig['items'] => {
|
||||
return `problem_${key}`;
|
||||
};
|
||||
|
||||
export class StatusBarItemManager {
|
||||
private _api: CardStatusBarAPI;
|
||||
|
||||
@@ -44,7 +48,7 @@ export class StatusBarItemManager {
|
||||
cameraManager?: CameraManager | null;
|
||||
view?: View | null;
|
||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
isUpgradeable?: boolean;
|
||||
problems?: KeyedProblemResult[] | null;
|
||||
}): StatusBarItem[] {
|
||||
const cameraMetadata = options?.view?.camera
|
||||
? options?.cameraManager?.getCameraMetadata(options.view.camera)
|
||||
@@ -128,26 +132,22 @@ export class StatusBarItemManager {
|
||||
]
|
||||
: []),
|
||||
|
||||
...(options?.isUpgradeable
|
||||
? [
|
||||
{
|
||||
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium' as const,
|
||||
actions: {
|
||||
tap_action: createNotificationAction({
|
||||
heading: {
|
||||
text: localize('notification.upgrade.heading'),
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium',
|
||||
},
|
||||
text: localize('notification.upgrade.text'),
|
||||
}),
|
||||
},
|
||||
...options?.statusConfig?.items.upgrade,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(options?.problems ?? [])
|
||||
.filter(
|
||||
({ key }) =>
|
||||
options?.statusConfig?.items[problemKeyToStatusBarKey(key)]?.enabled !==
|
||||
false,
|
||||
)
|
||||
.map(({ key, problem }) => ({
|
||||
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
||||
icon: problem.icon,
|
||||
severity: problem.severity,
|
||||
title: problem.notification.heading?.text,
|
||||
actions: {
|
||||
tap_action: createNotificationAction(problem.notification),
|
||||
},
|
||||
...options?.statusConfig?.items[problemKeyToStatusBarKey(key)],
|
||||
})),
|
||||
...this._dynamicItems,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { MessageManager } from './message-manager';
|
||||
import type { MicrophoneManager } from './microphone-manager';
|
||||
import type { NotificationManager } from './notification-manager';
|
||||
import type { PIPManager } from './pip-manager';
|
||||
import type { ProblemManager } from './problems/manager';
|
||||
import type { QueryStringManager } from './query-string-manager';
|
||||
import type { StatusBarItemManager } from './status-bar-item-manager';
|
||||
import type { StyleManager } from './style-manager';
|
||||
@@ -116,8 +117,8 @@ export interface CardConfigLoaderAPI {
|
||||
getAutomationsManager(): AutomationsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getFoldersManager(): FoldersManager;
|
||||
getMessageManager(): MessageManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getMessageManager(): MessageManager;
|
||||
}
|
||||
|
||||
export interface CardDefaultManagerAPI {
|
||||
@@ -153,6 +154,7 @@ export interface CardElementAPI {
|
||||
getMediaPlayerManager(): MediaPlayerManager;
|
||||
getMicrophoneManager(): MicrophoneManager;
|
||||
getPIPManager(): PIPManager;
|
||||
getProblemManager(): ProblemManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
@@ -213,6 +215,7 @@ export interface CardInitializerAPI {
|
||||
getHASSManager(): HASSManager;
|
||||
getMediaPlayerManager(): MediaPlayerManager;
|
||||
getMessageManager(): MessageManager;
|
||||
getProblemManager(): ProblemManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
getResolvedMediaCache(): ResolvedMediaCache;
|
||||
getTriggersManager(): TriggersManager;
|
||||
@@ -261,6 +264,13 @@ export interface CardNotificationAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
}
|
||||
|
||||
export interface CardProblemAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getNotificationManager(): NotificationManager;
|
||||
}
|
||||
|
||||
export interface CardMicrophoneAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
|
||||
+15
-2
@@ -7,6 +7,10 @@ import { styleMap } from 'lit/directives/style-map.js';
|
||||
import 'web-dialog';
|
||||
import { actionHandler } from './action-handler-directive.js';
|
||||
import { CardController } from './card-controller/controller';
|
||||
import type {
|
||||
ProblemKey,
|
||||
ProblemTriggerEventData,
|
||||
} from './card-controller/problems/types.js';
|
||||
import { MenuButtonController } from './components-lib/menu-button-controller';
|
||||
import './components/effects/effects';
|
||||
import './components/elements.js';
|
||||
@@ -170,7 +174,9 @@ class AdvancedCameraCard extends LitElement {
|
||||
}
|
||||
|
||||
if (!this._controller.getInitializationManager().isInitializedMandatory()) {
|
||||
this._controller.getInitializationManager().initializeMandatory();
|
||||
/* async */ this._controller.getInitializationManager().initializeMandatory();
|
||||
} else if (!this._controller.getInitializationManager().isInitializedBackground()) {
|
||||
/* async */ this._controller.getInitializationManager().initializeBackground();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -288,7 +294,7 @@ class AdvancedCameraCard extends LitElement {
|
||||
cameraManager: this._controller.getCameraManager(),
|
||||
view: this._controller.getViewManager().getView(),
|
||||
mediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
||||
isUpgradeable: this._controller.getConfigManager().isUpgradeable(),
|
||||
problems: this._controller.getProblemManager().getProblemResults(),
|
||||
})}
|
||||
.config=${this._config.status_bar}
|
||||
></advanced-camera-card-status-bar>
|
||||
@@ -365,6 +371,12 @@ class AdvancedCameraCard extends LitElement {
|
||||
}}
|
||||
@advanced-camera-card:media:unloaded=${() =>
|
||||
this._controller.getMediaLoadedInfoManager().clear()}
|
||||
@advanced-camera-card:problem:notify=${(ev: CustomEvent<ProblemKey>) =>
|
||||
this._controller.getProblemManager().forceNotify(ev.detail)}
|
||||
@advanced-camera-card:problem:trigger=${({
|
||||
detail: { key, ...context },
|
||||
}: CustomEvent<ProblemTriggerEventData>) =>
|
||||
this._controller.getProblemManager().trigger(key, context)}
|
||||
@advanced-camera-card:media:volumechange=${
|
||||
() => this.requestUpdate() /* Refresh mute menu button */
|
||||
}
|
||||
@@ -411,6 +423,7 @@ class AdvancedCameraCard extends LitElement {
|
||||
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
||||
: undefined}
|
||||
.deviceRegistryManager=${this._controller.getDeviceRegistryManager()}
|
||||
.problems=${this._controller.getProblemManager().getProblemPresence()}
|
||||
></advanced-camera-card-views>
|
||||
${this._controller.getMessageManager().hasMessage()
|
||||
? // Keep message rendering to last to show messages that may have been
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import yaml from 'js-yaml';
|
||||
import { Link } from '../../config/schema/common/link.js';
|
||||
import { TROUBLESHOOTING_URL } from '../../const';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import { Message, MessageURL } from '../../types';
|
||||
import { Message } from '../../types';
|
||||
|
||||
export class MessageController {
|
||||
public getMessageString(message: Message): string {
|
||||
@@ -21,11 +22,11 @@ export class MessageController {
|
||||
: 'mdi:information-outline';
|
||||
}
|
||||
|
||||
public getURL(message: Message): MessageURL | null {
|
||||
return message.url
|
||||
? message.url
|
||||
public getLink(message: Message): Link | null {
|
||||
return message.link
|
||||
? message.link
|
||||
: message.type === 'error'
|
||||
? { link: TROUBLESHOOTING_URL, title: localize('error.troubleshooting') }
|
||||
? { url: TROUBLESHOOTING_URL, title: localize('error.troubleshooting') }
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>`
|
||||
: ``}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { KeysState, MicrophoneState } from '../card-controller/types';
|
||||
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
||||
import { ViewDisplayMode } from '../config/schema/common/display';
|
||||
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
@@ -19,7 +20,7 @@ export interface ConditionState {
|
||||
hass?: HomeAssistant;
|
||||
triggered?: Set<string>;
|
||||
userAgent?: string;
|
||||
view?: string;
|
||||
view?: AdvancedCameraCardView;
|
||||
}
|
||||
|
||||
export interface ConditionStateChange {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { linkSchema } from '../common/link';
|
||||
import { severitySchema } from '../common/severity';
|
||||
import { statusBarItemBaseSchema } from '../common/status-bar';
|
||||
import { advancedCameraCardCustomActionsBaseSchema } from './custom/base';
|
||||
@@ -138,6 +139,7 @@ const notificationSchema = z.object({
|
||||
controls: notificationControlSchema.array().optional(),
|
||||
details: notificationDetailSchema.array().optional(),
|
||||
text: z.string().optional(),
|
||||
link: linkSchema.optional(),
|
||||
});
|
||||
export type Notification = z.infer<typeof notificationSchema>;
|
||||
|
||||
@@ -157,6 +159,7 @@ const statusBarItemElementsBaseSchema = statusBarItemBaseSchema.extend({
|
||||
exclusive: z.boolean().default(false).optional(),
|
||||
expand: z.boolean().default(false).optional(),
|
||||
severity: severitySchema.optional(),
|
||||
title: z.string().optional(),
|
||||
actions: actionsBaseSchema.optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const linkSchema = z.object({
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
});
|
||||
export type Link = z.infer<typeof linkSchema>;
|
||||
@@ -26,7 +26,11 @@ export const statusBarConfigDefault = {
|
||||
severity: statusBarItemDefault,
|
||||
technology: statusBarItemDefault,
|
||||
title: statusBarItemDefault,
|
||||
upgrade: statusBarItemDefault,
|
||||
|
||||
// Problems.
|
||||
problem_config_upgrade: statusBarItemDefault,
|
||||
problem_legacy_resource: statusBarItemDefault,
|
||||
problem_stream_not_loading: statusBarItemDefault,
|
||||
},
|
||||
position: 'bottom' as const,
|
||||
style: 'popup' as const,
|
||||
@@ -46,15 +50,25 @@ export const statusBarConfigSchema = z
|
||||
items: z
|
||||
.object({
|
||||
engine: statusBarItemBaseSchema.default(statusBarConfigDefault.items.engine),
|
||||
technology: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.technology,
|
||||
),
|
||||
resolution: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.resolution,
|
||||
),
|
||||
severity: statusBarItemBaseSchema.default(statusBarConfigDefault.items.severity),
|
||||
technology: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.technology,
|
||||
),
|
||||
title: statusBarItemBaseSchema.default(statusBarConfigDefault.items.title),
|
||||
upgrade: statusBarItemBaseSchema.default(statusBarConfigDefault.items.upgrade),
|
||||
|
||||
// Problems.
|
||||
problem_config_upgrade: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.problem_config_upgrade,
|
||||
),
|
||||
problem_legacy_resource: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.problem_legacy_resource,
|
||||
),
|
||||
problem_stream_not_loading: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.problem_stream_not_loading,
|
||||
),
|
||||
})
|
||||
.default(statusBarConfigDefault.items),
|
||||
})
|
||||
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
export const REPO_URL = 'https://github.com/dermotduffy/advanced-camera-card' as const;
|
||||
export const DOCS_URL = 'https://card.camera' as const;
|
||||
export const TROUBLESHOOTING_URL = `${DOCS_URL}/#/troubleshooting` as const;
|
||||
export const STREAM_TROUBLESHOOTING_URL =
|
||||
export const TROUBLESHOOTING_CONFIG_UPGRADE_URL =
|
||||
`${TROUBLESHOOTING_URL}?id=configuration-upgrade-available` as const;
|
||||
export const TROUBLESHOOTING_LEGACY_RESOURCE_URL =
|
||||
`${TROUBLESHOOTING_URL}?id=legacy-dashboard-resource-detected` as const;
|
||||
export const TROUBLESHOOTING_STREAM_URL =
|
||||
`${TROUBLESHOOTING_URL}?id=stream-does-not-load` as const;
|
||||
const CONFIGURATION_URL = `${DOCS_URL}/#/configuration`;
|
||||
export const FOLDERS_CONFIGURATION_URL = `${CONFIGURATION_URL}/folders`;
|
||||
|
||||
+2
-2
@@ -2281,8 +2281,8 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
${renderMessage({
|
||||
message: localize('config.folders.ha.path_info'),
|
||||
icon: 'mdi:information-outline',
|
||||
url: {
|
||||
link: FOLDERS_CONFIGURATION_URL,
|
||||
link: {
|
||||
url: FOLDERS_CONFIGURATION_URL,
|
||||
title: localize('error.configuration'),
|
||||
},
|
||||
})}
|
||||
|
||||
@@ -767,7 +767,6 @@
|
||||
"no_supported_camera": "The selected camera or media does not support this view",
|
||||
"no_supported_cameras": "No cameras support this view",
|
||||
"reconnecting": "Reconnecting",
|
||||
"stream_not_loading": "The video stream has not yet loaded. This is could for any number of reasons. If configured (and by default), there will be an image refreshing every second until the stream loads correctly",
|
||||
"too_many_automations": "Too many nested automation calls, please check your configuration for loops",
|
||||
"troubleshooting": "Check troubleshooting",
|
||||
"unknown": "Unknown error",
|
||||
@@ -823,10 +822,21 @@
|
||||
"media_viewer": {
|
||||
"unseekable": "Seek time not found in media"
|
||||
},
|
||||
"notification": {
|
||||
"upgrade": {
|
||||
"problems": {
|
||||
"troubleshooting_guide": "Troubleshooting guide",
|
||||
"config_upgrade": {
|
||||
"heading": "Configuration upgrade available",
|
||||
"text": "A configuration upgrade is available. To upgrade, edit this card (Dashboard pencil icon \u2192 Three-dot menu \u2192 Edit) and click the 'Automatic Upgrade' button in the card editor."
|
||||
},
|
||||
"legacy_resource": {
|
||||
"heading": "Legacy dashboard resource detected",
|
||||
"text_both": "The legacy 'frigate-hass-card.js' resource is still registered, please either manually remove it or click the delete icon to automatically remove it. It will be removed in a future release.",
|
||||
"text_only_legacy": "The legacy 'frigate-hass-card.js' resource must be replaced with 'advanced-camera-card.js'. It will be removed in a future release.",
|
||||
"remove": "Remove legacy resource"
|
||||
},
|
||||
"stream_not_loading": {
|
||||
"heading": "Live stream not loading",
|
||||
"text": "The video stream has not yet loaded. This could be for any number of reasons. If configured (and by default), there will be an image refreshing every second until the stream loads correctly"
|
||||
}
|
||||
},
|
||||
"thumbnail": {
|
||||
|
||||
@@ -290,3 +290,21 @@
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.url {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
color: var(--advanced-camera-card-notification-text-color);
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import type { EffectOptions } from './card-controller/effects/types';
|
||||
import type { Link } from './config/schema/common/link';
|
||||
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
|
||||
|
||||
// UI-facing media types for galleries and views.
|
||||
@@ -49,10 +50,6 @@ export interface MediaLoadedInfo {
|
||||
}
|
||||
|
||||
export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics';
|
||||
export interface MessageURL {
|
||||
link: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
message: string;
|
||||
@@ -60,7 +57,7 @@ export interface Message {
|
||||
icon?: string;
|
||||
context?: unknown;
|
||||
dotdotdot?: boolean;
|
||||
url?: MessageURL;
|
||||
link?: Link;
|
||||
}
|
||||
|
||||
export type WebkitHTMLVideoElement = HTMLVideoElement & {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pkg from '../../package.json';
|
||||
import { ProblemPresence } from '../card-controller/problems/types';
|
||||
import { RawAdvancedCameraCardConfig } from '../config/types';
|
||||
import { getIntegrationManifest } from '../ha/integration';
|
||||
import { IntegrationManifest } from '../ha/integration/types';
|
||||
@@ -46,6 +47,7 @@ interface Diagnostics {
|
||||
|
||||
ha_version?: string;
|
||||
config?: RawAdvancedCameraCardConfig;
|
||||
problems?: ProblemPresence;
|
||||
|
||||
custom_integrations: {
|
||||
frigate: IntegrationDiagnostics & {
|
||||
@@ -80,6 +82,7 @@ export const getDiagnostics = async (
|
||||
hass?: HomeAssistant,
|
||||
deviceRegistryManager?: DeviceRegistryManager,
|
||||
rawConfig?: RawAdvancedCameraCardConfig,
|
||||
problems?: ProblemPresence,
|
||||
): Promise<Diagnostics> => {
|
||||
// Get the Frigate devices in order to extract the Frigate integration and
|
||||
// server version numbers.
|
||||
@@ -121,6 +124,7 @@ export const getDiagnostics = async (
|
||||
},
|
||||
hass_web_proxy: await getIntegrationDiagnostics(HASS_WEB_PROXY_DOMAIN, hass),
|
||||
},
|
||||
...(problems && { problems }),
|
||||
...(rawConfig && { config: rawConfig }),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user