Allow reset of otbr network, thread panel fixes (#15815)

This commit is contained in:
Bram Kragten 2023-03-20 20:06:40 +01:00 committed by GitHub
parent c9d709152a
commit 24c3ddb96b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 216 additions and 20 deletions

View File

@ -9,3 +9,24 @@ export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfo> =>
hass.callWS({ hass.callWS({
type: "otbr/info", type: "otbr/info",
}); });
export const OTBRCreateNetwork = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "otbr/create_network",
});
export const OTBRSetNetwork = (
hass: HomeAssistant,
dataset_id: string
): Promise<void> =>
hass.callWS({
type: "otbr/set_network",
dataset_id,
});
export const OTBRGetExtendedAddress = (
hass: HomeAssistant
): Promise<{ extended_address: string }> =>
hass.callWS({
type: "otbr/get_extended_address",
});

View File

@ -4,6 +4,7 @@ export interface ThreadRouter {
brand: "google" | "apple" | "homeassistant"; brand: "google" | "apple" | "homeassistant";
server: string; server: string;
extended_pan_id: string; extended_pan_id: string;
extended_address: string;
model_name: string | null; model_name: string | null;
network_name: string; network_name: string;
vendor_name: string; vendor_name: string;
@ -87,3 +88,12 @@ export const removeThreadDataSet = (
type: "thread/delete_dataset", type: "thread/delete_dataset",
dataset_id, dataset_id,
}); });
export const setPreferredThreadDataSet = (
hass: HomeAssistant,
dataset_id: string
): Promise<void> =>
hass.callWS({
type: "thread/set_preferred_dataset",
dataset_id,
});

View File

@ -1,4 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list";
import { import {
mdiDeleteOutline, mdiDeleteOutline,
mdiDevices, mdiDevices,
@ -14,11 +15,18 @@ import { extractSearchParam } from "../../../../../common/url/search-params";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import { getSignedPath } from "../../../../../data/auth"; import { getSignedPath } from "../../../../../data/auth";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../../../data/diagnostics"; import { getConfigEntryDiagnosticsDownloadUrl } from "../../../../../data/diagnostics";
import { getOTBRInfo } from "../../../../../data/otbr"; import {
getOTBRInfo,
OTBRCreateNetwork,
OTBRGetExtendedAddress,
OTBRInfo,
OTBRSetNetwork,
} from "../../../../../data/otbr";
import { import {
addThreadDataSet, addThreadDataSet,
listThreadDataSets, listThreadDataSets,
removeThreadDataSet, removeThreadDataSet,
setPreferredThreadDataSet,
subscribeDiscoverThreadRouters, subscribeDiscoverThreadRouters,
ThreadDataSet, ThreadDataSet,
ThreadRouter, ThreadRouter,
@ -54,6 +62,8 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
@state() private _datasets: ThreadDataSet[] = []; @state() private _datasets: ThreadDataSet[] = [];
@state() private _otbrInfo?: OTBRInfo & { extended_address?: string };
protected render(): TemplateResult { protected render(): TemplateResult {
const networks = this._groupRoutersByNetwork(this._routers, this._datasets); 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" "ui.panel.config.thread.add_dataset_from_tlv"
)}</mwc-list-item )}</mwc-list-item
> >
<mwc-list-item @click=${this._addOTBR} ${!this._otbrInfo
? html`<mwc-list-item @click=${this._addOTBR}
>${this.hass.localize( >${this.hass.localize(
"ui.panel.config.thread.add_open_thread_border_router" "ui.panel.config.thread.add_open_thread_border_router"
)}</mwc-list-item )}</mwc-list-item
> >`
: ""}
</ha-button-menu> </ha-button-menu>
<div class="content"> <div class="content">
<h1>${this.hass.localize("ui.panel.config.thread.my_network")}</h1> <h1>${this.hass.localize("ui.panel.config.thread.my_network")}</h1>
@ -150,7 +162,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
</div> </div>
${network.routers.map( ${network.routers.map(
(router) => (router) =>
html`<ha-list-item noninteractive twoline graphic="avatar"> html`<ha-list-item
class="router"
twoline
graphic="avatar"
.hasMeta=${router.extended_address ===
this._otbrInfo?.extended_address}
>
<img <img
slot="graphic" slot="graphic"
.src=${brandsUrl({ .src=${brandsUrl({
@ -166,22 +184,69 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
/> />
${router.model_name || router.server.replace(".local.", "")} ${router.model_name || router.server.replace(".local.", "")}
<span slot="secondary">${router.server}</span> <span slot="secondary">${router.server}</span>
${router.extended_address === this._otbrInfo?.extended_address
? html`<ha-button-menu
corner="BOTTOM_START"
slot="meta"
@action=${this._handleRouterAction}
>
<ha-icon-button
.label=${this.hass.localize(
"ui.common.overflow_menu"
)}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button
><ha-list-item
>${this.hass.localize(
"ui.panel.config.thread.reset_border_router"
)}</ha-list-item
>${network.dataset?.preferred
? ""
: html`<ha-list-item
>${this.hass.localize(
"ui.panel.config.thread.add_to_my_network"
)}</ha-list-item
></ha-button-menu
>`}</ha-button-menu
>`
: ""}
</ha-list-item>` </ha-list-item>`
)}` )}`
: html`<div class="card-content no-routers"> : html`<div class="card-content no-routers">
<ha-svg-icon .path=${mdiDevices}></ha-svg-icon> <ha-svg-icon .path=${mdiDevices}></ha-svg-icon>
${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"
)}
<mwc-button @click=${this._resetBorderRouter}
>${this.hass.localize(
"ui.panel.config.thread.reset_border_router"
)}</mwc-button
>`
: this.hass.localize("ui.panel.config.thread.no_border_routers")}
</div> `} </div> `}
${network.dataset && !network.dataset.preferred
? html`<div class="card-actions">
<mwc-button
.datasetId=${network.dataset.dataset_id}
@click=${this._setPreferred}
>Make preferred network</mwc-button
>
</div>`
: ""}
</ha-card>`; </ha-card>`;
} }
private async _showDatasetInfo(ev: Event) { private async _showDatasetInfo(ev: Event) {
const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet; const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
if (isComponentLoaded(this.hass, "otbr")) { if (this._otbrInfo) {
const otbrInfo = await getOTBRInfo(this.hass);
if ( if (
dataset.extended_pan_id && 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, { showAlertDialog(this, {
title: dataset.network_name, title: dataset.network_name,
@ -189,8 +254,8 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
Dataset id: ${dataset.dataset_id}<br /> Dataset id: ${dataset.dataset_id}<br />
Pan id: ${dataset.pan_id}<br /> Pan id: ${dataset.pan_id}<br />
Extended Pan id: ${dataset.extended_pan_id}<br /> Extended Pan id: ${dataset.extended_pan_id}<br />
OTBR URL: ${otbrInfo.url}<br /> OTBR URL: ${this._otbrInfo.url}<br />
Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}`, Active dataset TLVs: ${this._otbrInfo.active_dataset_tlvs}`,
}); });
return; return;
} }
@ -236,18 +301,21 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
let preferred: ThreadNetwork | undefined; let preferred: ThreadNetwork | undefined;
const networks: { [key: string]: ThreadNetwork } = {}; const networks: { [key: string]: ThreadNetwork } = {};
for (const router of routers) { for (const router of routers) {
const network = router.network_name; const network = router.extended_pan_id;
if (network in networks) { if (network in networks) {
networks[network].routers!.push(router); networks[network].routers!.push(router);
} else { } else {
networks[network] = { name: network, routers: [router] }; networks[network] = { name: router.network_name, routers: [router] };
} }
} }
for (const dataset of datasets) { for (const dataset of datasets) {
const network = dataset.network_name; const network = dataset.extended_pan_id;
if (!network) {
continue;
}
if (dataset.preferred) { if (dataset.preferred) {
preferred = { preferred = {
name: network, name: dataset.network_name,
dataset: dataset, dataset: dataset,
routers: networks[network]?.routers, routers: networks[network]?.routers,
}; };
@ -257,7 +325,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
if (network in networks) { if (network in networks) {
networks[network].dataset = dataset; networks[network].dataset = dataset;
} else { } else {
networks[network] = { name: network, dataset: dataset }; networks[network] = { name: dataset.network_name, dataset: dataset };
} }
} }
return { return {
@ -269,10 +337,24 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
} }
); );
private _refresh() { private async _refresh() {
listThreadDataSets(this.hass).then((datasets) => { listThreadDataSets(this.hass).then((datasets) => {
this._datasets = datasets.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) { private async _signUrl(ev) {
@ -295,6 +377,74 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
}); });
} }
private _handleRouterAction(ev: CustomEvent<ActionDetail>) {
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() { private async _addTLV() {
const tlv = await showPromptDialog(this, { const tlv = await showPromptDialog(this, {
title: this.hass.localize("ui.panel.config.thread.add_dataset"), title: this.hass.localize("ui.panel.config.thread.add_dataset"),
@ -355,6 +505,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
margin: 0 auto; margin: 0 auto;
direction: ltr; 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 { ha-button-menu a {
text-decoration: none; text-decoration: none;
} }
@ -365,6 +521,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center;
} }
.no-routers ha-svg-icon { .no-routers ha-svg-icon {
background-color: var(--light-primary-color); background-color: var(--light-primary-color);

View File

@ -3292,10 +3292,18 @@
"my_network": "My network", "my_network": "My network",
"no_preferred_network": "You don't have a preferred network yet.", "no_preferred_network": "You don't have a preferred network yet.",
"add_open_thread_border_router": "Add an OpenThread border router", "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_from_tlv": "Add dataset from TLV",
"add_dataset": "Add Thread dataset", "add_dataset": "Add Thread dataset",
"add_dataset_label": "Operational dataset TLV", "add_dataset_label": "Operational dataset TLV",
"add_dataset_button": "Add dataset", "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": "Delete {name} dataset?",
"confirm_delete_dataset_text": "This network will be removed from Home Assistant.", "confirm_delete_dataset_text": "This network will be removed from Home Assistant.",
"no_border_routers": "No border routers found", "no_border_routers": "No border routers found",