From 1616911ba96f41d0e3ba346071d38679f502cebb Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 23 Aug 2022 10:24:38 -0400 Subject: [PATCH] Display ZHA network settings and allow downloading backups (#13415) --- src/data/zha.ts | 80 +++++++++++++ .../zha/zha-config-dashboard.ts | 108 +++++++++++++++++- src/translations/en.json | 4 +- 3 files changed, 190 insertions(+), 2 deletions(-) diff --git a/src/data/zha.ts b/src/data/zha.ts index 247f1f1827..fb0e2b0f31 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -128,6 +128,54 @@ export interface ZHAConfiguration { schemas: Record; } +export interface ZHANetworkBackupNodeInfo { + nwk: string; + ieee: string; + logical_type: "coordinator" | "router" | "end_device"; +} + +export interface ZHANetworkBackupKey { + key: string; + tx_counter: number; + rx_counter: number; + seq: number; + partner_ieee: string; +} + +export interface ZHANetworkBackupNetworkInfo { + extended_pan_id: string; + pan_id: string; + nwk_update_id: number; + nwk_manager_id: string; + channel: number; + channel_mask: number[]; + security_level: number; + network_key: ZHANetworkBackupKey; + tc_link_key: ZHANetworkBackupKey; + key_table: ZHANetworkBackupKey[]; + children: string[]; + nwk_addresses: Record; + stack_specific?: Record; + metadata: Record; + source: string; +} + +export interface ZHANetworkBackup { + backup_time: string; + network_info: ZHANetworkBackupNetworkInfo; + node_info: ZHANetworkBackupNodeInfo; +} + +export interface ZHANetworkSettings { + settings: ZHANetworkBackup; + radio_type: "ezsp" | "znp" | "deconz" | "zigate" | "xbee"; +} + +export interface ZHANetworkBackupAndMetadata { + backup: ZHANetworkBackup; + is_complete: boolean; +} + export interface ZHAGroupMember { ieee: string; endpoint_id: string; @@ -349,6 +397,38 @@ export const updateZHAConfiguration = ( data: data, }); +export const fetchZHANetworkSettings = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "zha/network/settings", + }); + +export const createZHANetworkBackup = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "zha/network/backups/create", + }); + +export const restoreZHANetworkBackup = ( + hass: HomeAssistant, + backup: ZHANetworkBackup, + ezspForceWriteEUI64 = false +): Promise => + hass.callWS({ + type: "zha/network/backups/restore", + backup: backup, + ezsp_force_write_eui64: ezspForceWriteEUI64, + }); + +export const listZHANetworkBackups = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "zha/network/backups/list", + }); + export const INITIALIZED = "INITIALIZED"; export const INTERVIEW_COMPLETE = "INTERVIEW_COMPLETE"; export const CONFIGURED = "CONFIGURED"; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts index 77213cbae1..e3f3125224 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts @@ -8,10 +8,11 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { computeRTL } from "../../../../../common/util/compute_rtl"; import "../../../../../components/ha-card"; import "../../../../../components/ha-fab"; +import { fileDownload } from "../../../../../util/file_download"; import "../../../../../components/ha-icon-next"; import "../../../../../layouts/hass-tabs-subpage"; import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; @@ -19,11 +20,17 @@ import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, Route } from "../../../../../types"; import "../../../ha-config-section"; import "../../../../../components/ha-form/ha-form"; +import "../../../../../components/buttons/ha-progress-button"; import { fetchZHAConfiguration, updateZHAConfiguration, ZHAConfiguration, + fetchZHANetworkSettings, + createZHANetworkBackup, + ZHANetworkSettings, + ZHANetworkBackupAndMetadata, } from "../../../../../data/zha"; +import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; export const zhaTabs: PageNavigation[] = [ { @@ -57,11 +64,16 @@ class ZHAConfigDashboard extends LitElement { @property() private _configuration?: ZHAConfiguration; + @property() private _networkSettings?: ZHANetworkSettings; + + @state() private _generatingBackup = false; + protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); if (this.hass) { this.hass.loadBackendTranslation("config_panel", "zha", false); this._fetchConfiguration(); + this._fetchSettings(); } } @@ -102,6 +114,51 @@ class ZHAConfigDashboard extends LitElement { ` : ""} + ${this._networkSettings + ? html` +
+
+ PAN ID: + ${this._networkSettings.settings.network_info.pan_id} +
+
+ Extended PAN ID: + ${this._networkSettings.settings.network_info.extended_pan_id} +
+
+ Channel: + ${this._networkSettings.settings.network_info.channel} +
+
+ Coordinator IEEE: + ${this._networkSettings.settings.node_info.ieee} +
+
+ Network key: + ${this._networkSettings.settings.network_info.network_key.key} +
+
+ Radio type: + ${this._networkSettings.radio_type} +
+
+
+ + ${this.hass.localize( + "ui.panel.config.zha.configuration_page.download_backup" + )} + +
+
` + : ""} ${this._configuration ? Object.entries(this._configuration.schemas).map( ([section, schema]) => html` { + this._networkSettings = await fetchZHANetworkSettings(this.hass!); + } + + private async _createAndDownloadBackup(): Promise { + let backup_and_metadata: ZHANetworkBackupAndMetadata; + + this._generatingBackup = true; + + try { + backup_and_metadata = await createZHANetworkBackup(this.hass!); + } catch (err: any) { + showAlertDialog(this, { + title: "Failed to create backup", + text: err.message, + warning: true, + }); + return; + } finally { + this._generatingBackup = false; + } + + if (!backup_and_metadata.is_complete) { + await showAlertDialog(this, { + title: "Backup is incomplete", + text: "A backup has been created but it is incomplete and cannot be restored. This is a coordinator firmware limitation.", + }); + } + + const backupJSON: string = + "data:text/plain;charset=utf-8," + + encodeURIComponent(JSON.stringify(backup_and_metadata.backup, null, 4)); + const backupTime: Date = new Date( + Date.parse(backup_and_metadata.backup.backup_time) + ); + let basename = `ZHA backup ${backupTime.toISOString().replace(/:/g, "-")}`; + + if (!backup_and_metadata.is_complete) { + basename = `Incomplete ${basename}`; + } + + fileDownload(backupJSON, `${basename}.json`); + } + private _dataChanged(ev) { this._configuration!.data[ev.currentTarget!.section] = ev.detail.value; } @@ -176,6 +277,11 @@ class ZHAConfigDashboard extends LitElement { margin-top: 16px; max-width: 500px; } + + .network-settings > div { + word-break: break-all; + margin-top: 2px; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 76ce515eb3..cc604a5cf0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2998,7 +2998,9 @@ }, "configuration_page": { "shortcuts_title": "Shortcuts", - "update_button": "Update Configuration" + "update_button": "Update Configuration", + "download_backup": "Download Network Backup", + "network_settings_title": "Network Settings" }, "add_device_page": { "spinner": "Searching for Zigbee devices…",