diff --git a/src/data/integration.ts b/src/data/integration.ts index 81c28b5945..7b2e2869a6 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -1,5 +1,7 @@ +import { Connection, createCollection } from "home-assistant-js-websocket"; import { LocalizeFunc } from "../common/translations/localize"; import { HomeAssistant } from "../types"; +import { debounce } from "../common/util/debounce"; export type IntegrationType = | "device" @@ -23,6 +25,7 @@ export interface IntegrationManifest { zeroconf?: string[]; homekit?: { models: string[] }; integration_type?: IntegrationType; + loggers?: string[]; quality_scale?: "gold" | "internal" | "platinum" | "silver"; iot_class: | "assumed_state" @@ -36,6 +39,24 @@ export interface IntegrationSetup { seconds?: number; } +export interface IntegrationLogInfo { + domain: string; + level?: number; +} + +export enum LogSeverity { + CRITICAL = 50, + FATAL = 50, + ERROR = 40, + WARNING = 30, + WARN = 30, + INFO = 20, + DEBUG = 10, + NOTSET = 0, +} + +export type IntegrationLogPersistance = "none" | "once" | "permanent"; + export const integrationIssuesUrl = ( domain: string, manifest: IntegrationManifest @@ -69,3 +90,46 @@ export const fetchIntegrationManifest = ( export const fetchIntegrationSetups = (hass: HomeAssistant) => hass.callWS({ type: "integration/setup_info" }); + +export const fetchIntegrationLogInfo = (conn) => + conn.sendMessagePromise({ + type: "logger/log_info", + }); + +export const setIntegrationLogLevel = ( + hass: HomeAssistant, + integration: string, + level: string, + persistence: IntegrationLogPersistance +) => + hass.callWS({ + type: "logger/integration_log_level", + integration, + level, + persistence, + }); + +const subscribeLogInfoUpdates = (conn, store) => + conn.subscribeEvents( + debounce( + () => + fetchIntegrationLogInfo(conn).then((log_infos) => + store.setState(log_infos, true) + ), + 200, + true + ), + "logging_changed" + ); + +export const subscribeLogInfo = ( + conn: Connection, + onChange: (devices: IntegrationLogInfo[]) => void +) => + createCollection( + "_integration_log_info", + fetchIntegrationLogInfo, + subscribeLogInfoUpdates, + conn, + onChange + ); diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 7d70d05222..0ea91c848b 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -51,6 +51,8 @@ import { fetchIntegrationManifest, fetchIntegrationManifests, IntegrationManifest, + IntegrationLogInfo, + subscribeLogInfo, } from "../../../data/integration"; import { getIntegrationDescriptions, @@ -154,6 +156,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { @state() private _diagnosticHandlers?: Record; + @state() private _logInfos?: { + [integration: string]: IntegrationLogInfo; + }; + public hassSubscribe(): Array> { return [ subscribeEntityRegistry(this.hass.connection, (entries) => { @@ -230,6 +236,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { }, { type: ["device", "hub", "service"] } ), + subscribeLogInfo(this.hass.connection, (log_infos) => { + const logInfoLookup: { [integration: string]: IntegrationLogInfo } = {}; + for (const log_info of log_infos) { + logInfoLookup[log_info.domain] = log_info; + } + this._logInfos = logInfoLookup; + }), ]; } @@ -514,6 +527,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { .supportsDiagnostics=${this._diagnosticHandlers ? this._diagnosticHandlers[domain] : false} + .logInfo=${this._logInfos ? this._logInfos[domain] : null} >` ) : this._filter && diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index d6fa797b2e..9a145617ba 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -5,6 +5,8 @@ import { mdiAlertCircle, mdiBookshelf, mdiBug, + mdiBugPlay, + mdiBugStop, mdiChevronLeft, mdiCog, mdiDelete, @@ -47,11 +49,17 @@ import { ERROR_STATES, RECOVERABLE_STATES, } from "../../../data/config_entries"; +import { getErrorLogDownloadUrl } from "../../../data/error_log"; import type { DeviceRegistryEntry } from "../../../data/device_registry"; import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics"; import type { EntityRegistryEntry } from "../../../data/entity_registry"; import type { IntegrationManifest } from "../../../data/integration"; -import { integrationIssuesUrl } from "../../../data/integration"; +import { + integrationIssuesUrl, + IntegrationLogInfo, + LogSeverity, + setIntegrationLogLevel, +} from "../../../data/integration"; import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { @@ -95,6 +103,8 @@ export class HaIntegrationCard extends LitElement { @property({ type: Boolean }) public supportsDiagnostics = false; + @property() public logInfo?: IntegrationLogInfo; + protected render(): TemplateResult { let item = this._selectededConfigEntry; @@ -137,6 +147,8 @@ export class HaIntegrationCard extends LitElement { .localizedDomainName=${item ? item.localized_domain_name : undefined} .manifest=${this.manifest} .configEntry=${item} + .debugLoggingEnabled=${this.logInfo && + this.logInfo.level === LogSeverity.DEBUG} > ${this.items.length > 1 ? html` @@ -398,6 +410,28 @@ export class HaIntegrationCard extends LitElement { ` : ""} + ${this.logInfo + ? html` + ${this.logInfo.level === LogSeverity.DEBUG + ? this.hass.localize( + "ui.panel.config.integrations.config_entry.disable_debug_logging" + ) + : this.hass.localize( + "ui.panel.config.integrations.config_entry.enable_debug_logging" + )} + + ` + : ""} ${this.manifest && (this.manifest.is_built_in || this.manifest.issue_tracker || @@ -501,6 +535,34 @@ export class HaIntegrationCard extends LitElement { `; } + private async _handleEnableDebugLogging(ev: MouseEvent) { + const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any) + .configEntry; + const integration = configEntry.domain; + await setIntegrationLogLevel( + this.hass, + integration, + LogSeverity[LogSeverity.DEBUG], + "once" + ); + } + + private async _handleDisableDebugLogging(ev: MouseEvent) { + const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any) + .configEntry; + const integration = configEntry.domain; + await setIntegrationLogLevel( + this.hass, + integration, + LogSeverity[LogSeverity.NOTSET], + "once" + ); + const timeString = new Date().toISOString().replace(/:/g, "-"); + const logFileName = `home-assistant_${integration}_${timeString}.log`; + const signedUrl = await getSignedPath(this.hass, getErrorLogDownloadUrl); + fileDownload(signedUrl.path, logFileName); + } + private get _selectededConfigEntry(): ConfigEntryExtended | undefined { return this.items.length === 1 ? this.items[0] diff --git a/src/panels/config/integrations/ha-integration-header.ts b/src/panels/config/integrations/ha-integration-header.ts index 1682f5a813..fcc4980c72 100644 --- a/src/panels/config/integrations/ha-integration-header.ts +++ b/src/panels/config/integrations/ha-integration-header.ts @@ -1,4 +1,4 @@ -import { mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js"; +import { mdiBugPlay, mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; @@ -24,6 +24,8 @@ export class HaIntegrationHeader extends LitElement { @property({ attribute: false }) public configEntry?: ConfigEntry; + @property({ attribute: false }) public debugLoggingEnabled?: boolean; + protected render(): TemplateResult { let primary: string; let secondary: string | undefined; @@ -76,6 +78,15 @@ export class HaIntegrationHeader extends LitElement { } } + if (this.debugLoggingEnabled) { + icons.push([ + mdiBugPlay, + this.hass.localize( + "ui.panel.config.integrations.config_entry.debug_logging_enabled" + ), + ]); + } + return html` ${!this.banner ? "" : html``} diff --git a/src/translations/en.json b/src/translations/en.json index d557cfbafb..5091416f17 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3011,10 +3011,12 @@ "system_options": "System options", "documentation": "Documentation", "download_diagnostics": "Download diagnostics", + "disable_debug_logging": "Disable debug logging", "known_issues": "Known issues", "delete": "Delete", "delete_confirm_title": "Delete {title}?", "delete_confirm_text": "Its devices and entities will be permanently deleted.", + "enable_debug_logging": "Enable debug logging", "reload": "Reload", "restart_confirm": "Restart Home Assistant to finish removing this integration", "reload_confirm": "The integration was reloaded", @@ -3049,6 +3051,7 @@ "depends_on_cloud": "Depends on the cloud", "yaml_only": "Needs manual configuration", "disabled_polling": "Automatic polling for updated data disabled", + "debug_logging_enabled": "Debug logging enabled", "state": { "loaded": "Loaded", "setup_error": "Failed to set up",