From 24c3ddb96b50b448ed96966aa71350ee1639fde6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 20 Mar 2023 20:06:40 +0100 Subject: [PATCH] Allow reset of otbr network, thread panel fixes (#15815) --- src/data/otbr.ts | 21 ++ src/data/thread.ts | 10 + .../thread/thread-config-panel.ts | 197 ++++++++++++++++-- src/translations/en.json | 8 + 4 files changed, 216 insertions(+), 20 deletions(-) diff --git a/src/data/otbr.ts b/src/data/otbr.ts index 23bf157102..93e77a1eda 100644 --- a/src/data/otbr.ts +++ b/src/data/otbr.ts @@ -9,3 +9,24 @@ export const getOTBRInfo = (hass: HomeAssistant): Promise => hass.callWS({ type: "otbr/info", }); + +export const OTBRCreateNetwork = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "otbr/create_network", + }); + +export const OTBRSetNetwork = ( + hass: HomeAssistant, + dataset_id: string +): Promise => + hass.callWS({ + type: "otbr/set_network", + dataset_id, + }); + +export const OTBRGetExtendedAddress = ( + hass: HomeAssistant +): Promise<{ extended_address: string }> => + hass.callWS({ + type: "otbr/get_extended_address", + }); diff --git a/src/data/thread.ts b/src/data/thread.ts index 044e44c9a0..cd575fcb04 100644 --- a/src/data/thread.ts +++ b/src/data/thread.ts @@ -4,6 +4,7 @@ export interface ThreadRouter { brand: "google" | "apple" | "homeassistant"; server: string; extended_pan_id: string; + extended_address: string; model_name: string | null; network_name: string; vendor_name: string; @@ -87,3 +88,12 @@ export const removeThreadDataSet = ( type: "thread/delete_dataset", dataset_id, }); + +export const setPreferredThreadDataSet = ( + hass: HomeAssistant, + dataset_id: string +): Promise => + hass.callWS({ + type: "thread/set_preferred_dataset", + dataset_id, + }); diff --git a/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts b/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts index 07f2b58c4b..763f1f08b7 100644 --- a/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { ActionDetail } from "@material/mwc-list"; import { mdiDeleteOutline, mdiDevices, @@ -14,11 +15,18 @@ import { extractSearchParam } from "../../../../../common/url/search-params"; import "../../../../../components/ha-card"; import { getSignedPath } from "../../../../../data/auth"; import { getConfigEntryDiagnosticsDownloadUrl } from "../../../../../data/diagnostics"; -import { getOTBRInfo } from "../../../../../data/otbr"; +import { + getOTBRInfo, + OTBRCreateNetwork, + OTBRGetExtendedAddress, + OTBRInfo, + OTBRSetNetwork, +} from "../../../../../data/otbr"; import { addThreadDataSet, listThreadDataSets, removeThreadDataSet, + setPreferredThreadDataSet, subscribeDiscoverThreadRouters, ThreadDataSet, ThreadRouter, @@ -54,6 +62,8 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { @state() private _datasets: ThreadDataSet[] = []; + @state() private _otbrInfo?: OTBRInfo & { extended_address?: string }; + protected render(): TemplateResult { const networks = this._groupRoutersByNetwork(this._routers, this._datasets); @@ -82,11 +92,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { "ui.panel.config.thread.add_dataset_from_tlv" )} - ${this.hass.localize( - "ui.panel.config.thread.add_open_thread_border_router" - )} + ${!this._otbrInfo + ? html`${this.hass.localize( + "ui.panel.config.thread.add_open_thread_border_router" + )}` + : ""}

${this.hass.localize("ui.panel.config.thread.my_network")}

@@ -150,7 +162,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
${network.routers.map( (router) => - html` + html` ${router.model_name || router.server.replace(".local.", "")} ${router.server} + ${router.extended_address === this._otbrInfo?.extended_address + ? html` + ${this.hass.localize( + "ui.panel.config.thread.reset_border_router" + )}${network.dataset?.preferred + ? "" + : html`${this.hass.localize( + "ui.panel.config.thread.add_to_my_network" + )}`}` + : ""} ` )}` : html`
- ${this.hass.localize("ui.panel.config.thread.no_border_routers")} -
`} + ${network.dataset?.extended_pan_id && + this._otbrInfo?.active_dataset_tlvs?.includes( + network.dataset.extended_pan_id + ) + ? html`${this.hass.localize( + "ui.panel.config.thread.no_routers_otbr_network" + )} + ${this.hass.localize( + "ui.panel.config.thread.reset_border_router" + )}` + : this.hass.localize("ui.panel.config.thread.no_border_routers")} + `} + ${network.dataset && !network.dataset.preferred + ? html`
+ Make preferred network +
` + : ""} `; } private async _showDatasetInfo(ev: Event) { const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet; - if (isComponentLoaded(this.hass, "otbr")) { - const otbrInfo = await getOTBRInfo(this.hass); + if (this._otbrInfo) { if ( dataset.extended_pan_id && - otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id) + this._otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id) ) { showAlertDialog(this, { title: dataset.network_name, @@ -189,8 +254,8 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { Dataset id: ${dataset.dataset_id}
Pan id: ${dataset.pan_id}
Extended Pan id: ${dataset.extended_pan_id}
- OTBR URL: ${otbrInfo.url}
- Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}`, + OTBR URL: ${this._otbrInfo.url}
+ Active dataset TLVs: ${this._otbrInfo.active_dataset_tlvs}`, }); return; } @@ -236,18 +301,21 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { let preferred: ThreadNetwork | undefined; const networks: { [key: string]: ThreadNetwork } = {}; for (const router of routers) { - const network = router.network_name; + const network = router.extended_pan_id; if (network in networks) { networks[network].routers!.push(router); } else { - networks[network] = { name: network, routers: [router] }; + networks[network] = { name: router.network_name, routers: [router] }; } } for (const dataset of datasets) { - const network = dataset.network_name; + const network = dataset.extended_pan_id; + if (!network) { + continue; + } if (dataset.preferred) { preferred = { - name: network, + name: dataset.network_name, dataset: dataset, routers: networks[network]?.routers, }; @@ -257,7 +325,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { if (network in networks) { networks[network].dataset = dataset; } else { - networks[network] = { name: network, dataset: dataset }; + networks[network] = { name: dataset.network_name, dataset: dataset }; } } return { @@ -269,10 +337,24 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { } ); - private _refresh() { + private async _refresh() { listThreadDataSets(this.hass).then((datasets) => { this._datasets = datasets.datasets; }); + if (!isComponentLoaded(this.hass, "otbr")) { + return; + } + try { + const _otbrAddress = OTBRGetExtendedAddress(this.hass); + const _otbrInfo = getOTBRInfo(this.hass); + const [otbrAddress, otbrInfo] = await Promise.all([ + _otbrAddress, + _otbrInfo, + ]); + this._otbrInfo = { ...otbrAddress, ...otbrInfo }; + } catch (err) { + this._otbrInfo = undefined; + } } private async _signUrl(ev) { @@ -295,6 +377,74 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { }); } + private _handleRouterAction(ev: CustomEvent) { + switch (ev.detail.index) { + case 0: + this._resetBorderRouter(); + break; + case 1: + this._setDataset(); + break; + } + } + + private async _resetBorderRouter() { + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.thread.confirm_reset_border_router" + ), + text: this.hass.localize( + "ui.panel.config.thread.confirm_reset_border_router_text" + ), + }); + if (!confirm) { + return; + } + try { + await OTBRCreateNetwork(this.hass); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.thread.otbr_config_failed"), + text: err.message, + }); + } + this._refresh(); + } + + private async _setDataset() { + const networks = this._groupRoutersByNetwork(this._routers, this._datasets); + const preferedDatasetId = networks.preferred?.dataset?.dataset_id; + if (!preferedDatasetId) { + return; + } + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.thread.confirm_set_dataset_border_router" + ), + text: this.hass.localize( + "ui.panel.config.thread.confirm_set_dataset_border_router_text" + ), + }); + if (!confirm) { + return; + } + try { + await OTBRSetNetwork(this.hass, preferedDatasetId); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.thread.otbr_config_failed"), + text: err.message, + }); + } + this._refresh(); + } + + private async _setPreferred(ev) { + const datasetId = ev.target.datasetId; + await setPreferredThreadDataSet(this.hass, datasetId); + this._refresh(); + } + private async _addTLV() { const tlv = await showPromptDialog(this, { title: this.hass.localize("ui.panel.config.thread.add_dataset"), @@ -355,6 +505,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { margin: 0 auto; direction: ltr; } + ha-list-item.router { + --mdc-list-side-padding: 16px; + --mdc-list-item-meta-size: 48px; + cursor: default; + overflow: visible; + } ha-button-menu a { text-decoration: none; } @@ -365,6 +521,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { display: flex; flex-direction: column; align-items: center; + text-align: center; } .no-routers ha-svg-icon { background-color: var(--light-primary-color); diff --git a/src/translations/en.json b/src/translations/en.json index 9675fec300..ca63b724df 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3292,10 +3292,18 @@ "my_network": "My network", "no_preferred_network": "You don't have a preferred network yet.", "add_open_thread_border_router": "Add an OpenThread border router", + "reset_border_router": "Reset border router", + "add_to_my_network": "Add to my network", + "no_routers_otbr_network": "No border routers where found, maybe the border router is not configured correctly. You can try to reset it to the factory settings.", "add_dataset_from_tlv": "Add dataset from TLV", "add_dataset": "Add Thread dataset", "add_dataset_label": "Operational dataset TLV", "add_dataset_button": "Add dataset", + "confirm_reset_border_router": "Reset border router?", + "confirm_reset_border_router_text": "This will reset the Home Assistant border router to its factory defaults and form a new Thread network. The old network may no longer be available, and any devices that were attached to this network may need to be recomissioned.", + "confirm_set_dataset_border_router": "Reconfigure border router?", + "confirm_set_dataset_border_router_text": "This will reconfigure the Home Assistant border router to use a different Thread network. The old network may no longer be available, and any devices that were attached to this network may need to be recomissioned.", + "otbr_config_failed": "Failed to configure the border router", "confirm_delete_dataset": "Delete {name} dataset?", "confirm_delete_dataset_text": "This network will be removed from Home Assistant.", "no_border_routers": "No border routers found",