From bfa293ae3a72e2cd0eac652fbd823a966cf5bbdf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 16 May 2024 10:01:40 +0200 Subject: [PATCH] Allow storing thread credentials in phone keychain (#20743) * Allow storing thread credentials in phone keychain * Update dialog-thread-dataset.ts * use preferred of dataset if available --- src/external_app/external_messaging.ts | 13 ++- .../thread/dialog-thread-dataset.ts | 90 +++++++++++++++++++ .../thread/show-dialog-thread-dataset.ts | 19 ++++ .../thread/thread-config-panel.ts | 34 ++----- 4 files changed, 126 insertions(+), 30 deletions(-) create mode 100644 src/panels/config/integrations/integration-panels/thread/dialog-thread-dataset.ts create mode 100644 src/panels/config/integrations/integration-panels/thread/show-dialog-thread-dataset.ts diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts index e533a453cb..c4d56f8aac 100644 --- a/src/external_app/external_messaging.ts +++ b/src/external_app/external_messaging.ts @@ -129,6 +129,15 @@ interface EMOutgoingMessageAssistShow extends EMMessage { }; } +interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage { + type: "thread/store_in_platform_keychain"; + payload: { + mac_extended_address: string; + border_agent_id: string | null; + active_operational_dataset: string; + }; +} + type EMOutgoingMessageWithoutAnswer = | EMMessageResultError | EMMessageResultSuccess @@ -146,7 +155,8 @@ type EMOutgoingMessageWithoutAnswer = | EMOutgoingMessageMatterCommission | EMOutgoingMessageSidebarShow | EMOutgoingMessageTagWrite - | EMOutgoingMessageThemeUpdate; + | EMOutgoingMessageThemeUpdate + | EMOutgoingMessageThreadStoreInPlatformKeychain; interface EMIncomingMessageRestart { id: number; @@ -239,6 +249,7 @@ export interface ExternalConfig { hasExoPlayer: boolean; canCommissionMatter: boolean; canImportThreadCredentials: boolean; + canTransferThreadCredentialsToKeychain: boolean; hasAssist: boolean; hasBarCodeScanner: number; } diff --git a/src/panels/config/integrations/integration-panels/thread/dialog-thread-dataset.ts b/src/panels/config/integrations/integration-panels/thread/dialog-thread-dataset.ts new file mode 100644 index 0000000000..191049ecf6 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/thread/dialog-thread-dataset.ts @@ -0,0 +1,90 @@ +import { LitElement, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { HassDialog } from "../../../../../dialogs/make-dialog-manager"; +import { HomeAssistant } from "../../../../../types"; +import { DialogThreadDatasetParams } from "./show-dialog-thread-dataset"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; + +@customElement("ha-dialog-thread-dataset") +class DialogThreadDataset extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: DialogThreadDatasetParams; + + public async showDialog( + params: DialogThreadDatasetParams + ): Promise> { + this._params = params; + } + + public closeDialog(): void { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params) { + return nothing; + } + const network = this._params.network; + const dataset = network.dataset!; + const otbrInfo = this._params.otbrInfo; + + const hasOTBR = + otbrInfo && + dataset.extended_pan_id && + otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id); + + const canImportKeychain = + hasOTBR && + !this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain && + network.routers?.length; + + return html` +
+ Network name: ${dataset.network_name}
+ Channel: ${dataset.channel}
+ Dataset id: ${dataset.dataset_id}
+ Pan id: ${dataset.pan_id}
+ Extended Pan id: ${dataset.extended_pan_id}
+ + ${hasOTBR + ? html`OTBR URL: ${otbrInfo.url}
+ Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}` + : nothing} +
+ ${canImportKeychain + ? html`Send credentials to phone` + : nothing} +
`; + } + + private _sendCredentials() { + this.hass.auth.external!.fireMessage({ + type: "thread/store_in_platform_keychain", + payload: { + mac_extended_address: + this._params?.network.dataset?.preferred_extended_address || + this._params!.network.routers![0]!.extended_address, + border_agent_id: + this._params?.network.dataset?.preferred_border_agent_id || + this._params!.network.routers![0]!.border_agent_id, + active_operational_dataset: this._params!.otbrInfo!.active_dataset_tlvs, + }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-thread-dataset": DialogThreadDataset; + } +} diff --git a/src/panels/config/integrations/integration-panels/thread/show-dialog-thread-dataset.ts b/src/panels/config/integrations/integration-panels/thread/show-dialog-thread-dataset.ts new file mode 100644 index 0000000000..c2be304107 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/thread/show-dialog-thread-dataset.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { OTBRInfo } from "../../../../../data/otbr"; +import { ThreadNetwork } from "./thread-config-panel"; + +export interface DialogThreadDatasetParams { + network: ThreadNetwork; + otbrInfo?: OTBRInfo; +} + +export const showThreadDatasetDialog = ( + element: HTMLElement, + dialogParams: DialogThreadDatasetParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-thread-dataset", + dialogImport: () => import("./dialog-thread-dataset"), + dialogParams, + }); +}; 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 7a5375d8b2..daf1661803 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 @@ -55,8 +55,9 @@ import { HomeAssistant } from "../../../../../types"; import { brandsUrl } from "../../../../../util/brands-url"; import { fileDownload } from "../../../../../util/file_download"; import { documentationUrl } from "../../../../../util/documentation-url"; +import { showThreadDatasetDialog } from "./show-dialog-thread-dataset"; -interface ThreadNetwork { +export interface ThreadNetwork { name: string; dataset?: ThreadDataSet; routers?: ThreadRouter[]; @@ -164,7 +165,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { ${network.name}${network.dataset ? html`
- Channel: ${dataset.channel}
- Dataset id: ${dataset.dataset_id}
- Pan id: ${dataset.pan_id}
- Extended Pan id: ${dataset.extended_pan_id}
- OTBR URL: ${this._otbrInfo.url}
- Active dataset TLVs: ${this._otbrInfo.active_dataset_tlvs}`, - }); - return; - } - } - showAlertDialog(this, { - title: dataset.network_name, - text: html`Network name: ${dataset.network_name}
- Channel: ${dataset.channel}
- Dataset id: ${dataset.dataset_id}
- Pan id: ${dataset.pan_id}
- Extended Pan id: ${dataset.extended_pan_id}`, - }); + const network = (ev.currentTarget as any).network as ThreadNetwork; + showThreadDatasetDialog(this, { network, otbrInfo: this._otbrInfo }); } private _importExternalThreadCredentials() {