From b159f4c0741c62f57534d2e7dc2936767a0564aa Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 31 Jan 2024 14:16:21 +0100 Subject: [PATCH] Add matter device info and actions (#19578) * add matter device info panel (WIP) * actually enable card on device page * fix remove fabric * add some translation labels * add dialog to interview node * do not show info for bridged devices * first device action * add ping node action and dialog * ping should be always available * update model for MatterCommissioningParameters * add basic support for open commissioning window * move fabric management to dialog * review * Add link to thread panel --------- Co-authored-by: Bram Kragten --- src/data/matter.ts | 91 ++++++++ .../matter/device-actions.ts | 88 ++++++++ .../matter/ha-device-info-matter.ts | 174 +++++++++++++++ .../config/devices/ha-config-device-page.ts | 22 ++ .../matter/dialog-matter-manage-fabrics.ts | 169 +++++++++++++++ ...dialog-matter-open-commissioning-window.ts | 200 ++++++++++++++++++ .../matter/dialog-matter-ping-node.ts | 199 +++++++++++++++++ .../matter/dialog-matter-reinterview-node.ts | 193 +++++++++++++++++ .../show-dialog-matter-manage-fabrics.ts | 19 ++ ...dialog-matter-open-commissioning-window.ts | 19 ++ .../matter/show-dialog-matter-ping-node.ts | 18 ++ .../show-dialog-matter-reinterview-node.ts | 19 ++ src/translations/en.json | 67 ++++++ 13 files changed, 1278 insertions(+) create mode 100644 src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts create mode 100644 src/panels/config/devices/device-detail/integration-elements/matter/ha-device-info-matter.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/dialog-matter-ping-node.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/dialog-matter-reinterview-node.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/show-dialog-matter-manage-fabrics.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/show-dialog-matter-ping-node.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/show-dialog-matter-reinterview-node.ts diff --git a/src/data/matter.ts b/src/data/matter.ts index 11686a7cb2..5b4009178d 100644 --- a/src/data/matter.ts +++ b/src/data/matter.ts @@ -3,6 +3,50 @@ import { navigate } from "../common/navigate"; import { HomeAssistant } from "../types"; import { subscribeDeviceRegistry } from "./device_registry"; +export enum NetworkType { + THREAD = "thread", + WIFI = "wifi", + ETHERNET = "ethernet", + UNKNOWN = "unknown", +} + +export enum NodeType { + END_DEVICE = "end_device", + SLEEPY_END_DEVICE = "sleepy_end_device", + ROUTING_END_DEVICE = "routing_end_device", + BRIDGE = "bridge", + UNKNOWN = "unknown", +} + +export interface MatterFabricData { + fabric_id: number; + vendor_id: number; + fabric_index: number; + fabric_label?: string; + vendor_name?: string; +} + +export interface MatterNodeDiagnostics { + node_id: number; + network_type: NetworkType; + node_type: NodeType; + network_name?: string; + ip_adresses: string[]; + mac_address?: string; + available: boolean; + active_fabrics: MatterFabricData[]; +} + +export interface MatterPingResult { + [ip_address: string]: boolean; +} + +export interface MatterCommissioningParameters { + setup_pin_code: number; + setup_manual_code: string; + setup_qr_code: string; +} + export const canCommissionMatterExternal = (hass: HomeAssistant) => hass.auth.external?.config.canCommissionMatter; @@ -86,3 +130,50 @@ export const matterSetThread = ( type: "matter/set_thread", thread_operation_dataset, }); + +export const getMatterNodeDiagnostics = ( + hass: HomeAssistant, + device_id: string +): Promise => + hass.callWS({ + type: "matter/node_diagnostics", + device_id, + }); + +export const pingMatterNode = ( + hass: HomeAssistant, + device_id: string +): Promise => + hass.callWS({ + type: "matter/ping_node", + device_id, + }); + +export const openMatterCommissioningWindow = ( + hass: HomeAssistant, + device_id: string +): Promise => + hass.callWS({ + type: "matter/open_commissioning_window", + device_id, + }); + +export const removeMatterFabric = ( + hass: HomeAssistant, + device_id: string, + fabric_index: number +): Promise => + hass.callWS({ + type: "matter/remove_matter_fabric", + device_id, + fabric_index, + }); + +export const interviewMatterNode = ( + hass: HomeAssistant, + device_id: string +): Promise => + hass.callWS({ + type: "matter/interview_node", + device_id, + }); diff --git a/src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts new file mode 100644 index 0000000000..6fad700a32 --- /dev/null +++ b/src/panels/config/devices/device-detail/integration-elements/matter/device-actions.ts @@ -0,0 +1,88 @@ +import { + mdiAccessPoint, + mdiChatProcessing, + mdiChatQuestion, + mdiExportVariant, +} from "@mdi/js"; +import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import { + NetworkType, + getMatterNodeDiagnostics, +} from "../../../../../../data/matter"; +import type { HomeAssistant } from "../../../../../../types"; +import { showMatterReinterviewNodeDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-reinterview-node"; +import { showMatterPingNodeDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-ping-node"; +import { showMatterOpenCommissioningWindowDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window"; +import type { DeviceAction } from "../../../ha-config-device-page"; +import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics"; +import { navigate } from "../../../../../../common/navigate"; + +export const getMatterDeviceActions = async ( + el: HTMLElement, + hass: HomeAssistant, + device: DeviceRegistryEntry +): Promise => { + if (device.via_device_id !== null) { + // only show device actions for top level nodes (so not bridged) + return []; + } + + const nodeDiagnostics = await getMatterNodeDiagnostics(hass, device.id); + + const actions: DeviceAction[] = []; + + if (nodeDiagnostics.available) { + // actions that can only be performed if the device is alive + actions.push({ + label: hass.localize( + "ui.panel.config.matter.device_actions.open_commissioning_window" + ), + icon: mdiExportVariant, + action: () => + showMatterOpenCommissioningWindowDialog(el, { + device_id: device.id, + }), + }); + actions.push({ + label: hass.localize( + "ui.panel.config.matter.device_actions.manage_fabrics" + ), + icon: mdiExportVariant, + action: () => + showMatterManageFabricsDialog(el, { + device_id: device.id, + }), + }); + actions.push({ + label: hass.localize( + "ui.panel.config.matter.device_actions.reinterview_device" + ), + icon: mdiChatProcessing, + action: () => + showMatterReinterviewNodeDialog(el, { + device_id: device.id, + }), + }); + } + + if (nodeDiagnostics.network_type === NetworkType.THREAD) { + actions.push({ + label: hass.localize( + "ui.panel.config.matter.device_actions.view_thread_network" + ), + icon: mdiAccessPoint, + action: () => navigate("/config/thread"), + }); + } + + actions.push({ + label: hass.localize("ui.panel.config.matter.device_actions.ping_device"), + icon: mdiChatQuestion, + action: () => + showMatterPingNodeDialog(el, { + device_id: device.id, + }), + }); + + return actions; +}; diff --git a/src/panels/config/devices/device-detail/integration-elements/matter/ha-device-info-matter.ts b/src/panels/config/devices/device-detail/integration-elements/matter/ha-device-info-matter.ts new file mode 100644 index 0000000000..a2263207bf --- /dev/null +++ b/src/panels/config/devices/device-detail/integration-elements/matter/ha-device-info-matter.ts @@ -0,0 +1,174 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + nothing, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../../../../components/ha-expansion-panel"; +import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import { + getMatterNodeDiagnostics, + MatterNodeDiagnostics, +} from "../../../../../../data/matter"; +import "@material/mwc-list"; +import "../../../../../../components/ha-list-item"; +import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin"; +import { haStyle } from "../../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../../types"; + +@customElement("ha-device-info-matter") +export class HaDeviceInfoMatter extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public device!: DeviceRegistryEntry; + + @state() private _nodeDiagnostics?: MatterNodeDiagnostics; + + public willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (changedProperties.has("device")) { + this._fetchNodeDetails(); + } + } + + private async _fetchNodeDetails() { + if (!this.device) { + return; + } + + if (this.device.via_device_id !== null) { + // only show device details for top level nodes (so not bridged) + return; + } + + try { + this._nodeDiagnostics = await getMatterNodeDiagnostics( + this.hass, + this.device.id + ); + } catch (err: any) { + this._nodeDiagnostics = undefined; + } + } + + protected render() { + if (!this._nodeDiagnostics) { + return nothing; + } + return html` + +
+ ${this.hass.localize( + "ui.panel.config.matter.device_info.node_id" + )}: + ${this._nodeDiagnostics.node_id} +
+
+ ${this.hass.localize( + "ui.panel.config.matter.device_info.network_type" + )}: + ${this.hass.localize( + `ui.panel.config.matter.network_type.${this._nodeDiagnostics.network_type}` + )} +
+
+ ${this.hass.localize( + "ui.panel.config.matter.device_info.node_type" + )}: + ${this.hass.localize( + `ui.panel.config.matter.node_type.${this._nodeDiagnostics.node_type}` + )} +
+ ${this._nodeDiagnostics.network_name + ? html` +
+ ${this.hass.localize( + "ui.panel.config.matter.device_info.network_name" + )}: + ${this._nodeDiagnostics.network_name} +
+ ` + : nothing} + ${this._nodeDiagnostics.mac_address + ? html` +
+ ${this.hass.localize( + "ui.panel.config.matter.device_info.mac_address" + )}: + ${this._nodeDiagnostics.mac_address} +
+ ` + : nothing} + +
+ ${this.hass.localize( + "ui.panel.config.matter.device_info.ip_adresses" + )}: + ${this._nodeDiagnostics.ip_adresses.map( + (ip) => html`${ip}
` + )}
+
+
+ `; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + h4 { + margin-bottom: 4px; + } + div { + word-break: break-all; + margin-top: 2px; + } + .row { + display: flex; + justify-content: space-between; + padding-bottom: 4px; + } + .value { + text-align: right; + } + ha-expansion-panel { + margin: 8px -16px 0; + --expansion-panel-summary-padding: 0 16px; + --expansion-panel-content-padding: 0 16px; + --ha-card-border-radius: 0px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-device-info-matter": HaDeviceInfoMatter; + } +} diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 69858621f1..cff45a30a1 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -1099,6 +1099,17 @@ export class HaConfigDevicePage extends LitElement { ); deviceActions.push(...actions); } + if (domains.includes("matter")) { + const matter = await import( + "./device-detail/integration-elements/matter/device-actions" + ); + const actions = await matter.getMatterDeviceActions( + this, + this.hass, + device + ); + deviceActions.push(...actions); + } this._deviceActions = deviceActions; } @@ -1204,6 +1215,17 @@ export class HaConfigDevicePage extends LitElement { > `); } + if (domains.includes("matter")) { + import( + "./device-detail/integration-elements/matter/ha-device-info-matter" + ); + deviceInfo.push(html` + + `); + } } private async _showSettings() { diff --git a/src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts b/src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts new file mode 100644 index 0000000000..5f1a3ef13f --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts @@ -0,0 +1,169 @@ +import "@material/mwc-button/mwc-button"; +import { mdiClose } from "@mdi/js"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-circular-progress"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import "../../../../../components/ha-qr-code"; +import { + MatterFabricData, + MatterNodeDiagnostics, + getMatterNodeDiagnostics, + removeMatterFabric, +} from "../../../../../data/matter"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../../dialogs/generic/show-dialog-box"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { MatterManageFabricsDialogParams } from "./show-dialog-matter-manage-fabrics"; + +const NABUCASA_FABRIC = 4939; + +@customElement("dialog-matter-manage-fabrics") +class DialogMatterManageFabrics extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private device_id?: string; + + @state() private _nodeDiagnostics?: MatterNodeDiagnostics; + + public async showDialog( + params: MatterManageFabricsDialogParams + ): Promise { + this.device_id = params.device_id; + this._fetchNodeDetails(); + } + + protected render() { + if (!this.device_id) { + return nothing; + } + + return html` + +

+ ${this.hass.localize("ui.panel.config.matter.manage_fabrics.fabrics")} +

+ ${this._nodeDiagnostics + ? html` + ${this._nodeDiagnostics.active_fabrics.map( + (fabric) => + html`${fabric.vendor_name || + fabric.fabric_label || + fabric.vendor_id} + + ` + )} + ` + : html`
+ +
`} +
+ `; + } + + private async _fetchNodeDetails() { + if (!this.device_id) { + return; + } + + try { + this._nodeDiagnostics = await getMatterNodeDiagnostics( + this.hass, + this.device_id + ); + } catch (err: any) { + this._nodeDiagnostics = undefined; + } + } + + private async _removeFabric(ev) { + const fabric: MatterFabricData = ev.target.fabric; + const fabricName = + fabric.vendor_name || fabric.fabric_label || fabric.vendor_id.toString(); + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.matter.manage_fabrics.remove_fabric_confirm_header", + { fabric: fabricName } + ), + text: this.hass.localize( + "ui.panel.config.matter.manage_fabrics.remove_fabric_confirm_text", + { fabric: fabricName } + ), + warning: true, + }); + + if (!confirm) { + return; + } + + try { + await removeMatterFabric(this.hass, this.device_id!, fabric.fabric_index); + this._fetchNodeDetails(); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.matter.manage_fabrics.remove_fabric_failed_header", + { fabric: fabricName } + ), + text: this.hass.localize( + "ui.panel.config.matter.manage_fabrics.remove_fabric_failed_text" + ), + }); + } + } + + public closeDialog(): void { + this.device_id = undefined; + this._nodeDiagnostics = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + --mdc-list-side-padding: 24px; + --mdc-list-side-padding-right: 16px; + --mdc-list-item-meta-size: 48px; + } + p { + margin: 8px 24px; + } + .center { + display: flex; + align-items: center; + justify-content: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-matter-manage-fabrics": DialogMatterManageFabrics; + } +} diff --git a/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts b/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts new file mode 100644 index 0000000000..54d74a039e --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts @@ -0,0 +1,200 @@ +import "@material/mwc-button/mwc-button"; +import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-circular-progress"; +import "../../../../../components/ha-qr-code"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { + openMatterCommissioningWindow, + MatterCommissioningParameters, +} from "../../../../../data/matter"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { MatterOpenCommissioningWindowDialogParams } from "./show-dialog-matter-open-commissioning-window"; + +@customElement("dialog-matter-open-commissioning-window") +class DialogMatterOpenCommissioningWindow extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private device_id?: string; + + @state() private _status?: string; + + @state() private _commissionParams?: MatterCommissioningParameters; + + public async showDialog( + params: MatterOpenCommissioningWindowDialogParams + ): Promise { + this.device_id = params.device_id; + } + + protected render() { + if (!this.device_id) { + return nothing; + } + + return html` + + ${this._commissionParams + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.matter.open_commissioning_window.sharing_code" + )}: ${this._commissionParams.setup_manual_code} +

+
+
+ +
+ + ${this.hass.localize("ui.common.close")} + + ` + : this._status === "started" + ? html` +
+ +
+

+ + ${this.hass.localize( + "ui.panel.config.matter.open_commissioning_window.in_progress" + )} + +

+
+
+ + ${this.hass.localize("ui.common.close")} + + ` + : this._status === "failed" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.matter.open_commissioning_window.failed" + )} +

+
+
+ + ${this.hass.localize("ui.common.close")} + + ` + : html` +

+ ${this.hass.localize( + "ui.panel.config.matter.open_commissioning_window.introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.matter.open_commissioning_window.start_commissioning" + )} + + `} +
+ `; + } + + private async _start(): Promise { + if (!this.hass) { + return; + } + this._status = "started"; + this._commissionParams = undefined; + try { + this._commissionParams = await openMatterCommissioningWindow( + this.hass, + this.device_id! + ); + } catch (e) { + this._status = "failed"; + } + } + + public closeDialog(): void { + this.device_id = undefined; + this._status = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + .success { + color: var(--success-color); + } + + .failed { + color: var(--error-color); + } + + .flex-container { + display: flex; + align-items: center; + } + + .stages { + margin-top: 16px; + } + + .stage ha-svg-icon { + width: 16px; + height: 16px; + } + .stage { + padding: 8px; + } + + ha-svg-icon { + width: 68px; + height: 48px; + } + + ha-qr-code { + text-align: center; + } + + .flex-container ha-circular-progress, + .flex-container ha-svg-icon { + margin-right: 20px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-matter-open-commissioning-window": DialogMatterOpenCommissioningWindow; + } +} diff --git a/src/panels/config/integrations/integration-panels/matter/dialog-matter-ping-node.ts b/src/panels/config/integrations/integration-panels/matter/dialog-matter-ping-node.ts new file mode 100644 index 0000000000..738523d16d --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/dialog-matter-ping-node.ts @@ -0,0 +1,199 @@ +import "@material/mwc-button/mwc-button"; +import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-circular-progress"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { pingMatterNode, MatterPingResult } from "../../../../../data/matter"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { MatterPingNodeDialogParams } from "./show-dialog-matter-ping-node"; + +@customElement("dialog-matter-ping-node") +class DialogMatterPingNode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private device_id?: string; + + @state() private _status?: string; + + @state() private _pingResult?: MatterPingResult; + + public async showDialog(params: MatterPingNodeDialogParams): Promise { + this.device_id = params.device_id; + } + + protected render() { + if (!this.device_id) { + return nothing; + } + + return html` + + ${this._pingResult + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.matter.ping_node.ping_complete" + )} +

+
+
+
+ + ${Object.entries(this._pingResult).map( + ([ip, success]) => + html`${ip} + + ` + )} + +
+ + ${this.hass.localize("ui.common.close")} + + ` + : this._status === "started" + ? html` +
+ +
+

+ + ${this.hass.localize( + "ui.panel.config.matter.ping_node.in_progress" + )} + +

+
+
+ + ${this.hass.localize("ui.common.close")} + + ` + : this._status === "failed" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.matter.ping_node.ping_failed" + )} +

+
+
+ + ${this.hass.localize("ui.common.close")} + + ` + : html` +

+ ${this.hass.localize( + "ui.panel.config.matter.ping_node.introduction" + )} +

+

+ + ${this.hass.localize( + "ui.panel.config.matter.ping_node.battery_device_warning" + )} + +

+ + ${this.hass.localize( + "ui.panel.config.matter.ping_node.start_ping" + )} + + `} +
+ `; + } + + private async _startPing(): Promise { + if (!this.hass) { + return; + } + this._status = "started"; + try { + this._pingResult = await pingMatterNode(this.hass, this.device_id!); + } catch (err) { + this._status = "failed"; + } + } + + public closeDialog(): void { + this.device_id = undefined; + this._status = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + .success { + color: var(--success-color); + } + + .failed { + color: var(--error-color); + } + + .flex-container { + display: flex; + align-items: center; + } + + .stages { + margin-top: 16px; + } + + .stage ha-svg-icon { + width: 16px; + height: 16px; + } + .stage { + padding: 8px; + } + + ha-svg-icon { + width: 68px; + height: 48px; + } + + .flex-container ha-circular-progress, + .flex-container ha-svg-icon { + margin-right: 20px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-matter-ping-node": DialogMatterPingNode; + } +} diff --git a/src/panels/config/integrations/integration-panels/matter/dialog-matter-reinterview-node.ts b/src/panels/config/integrations/integration-panels/matter/dialog-matter-reinterview-node.ts new file mode 100644 index 0000000000..e6a8391924 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/dialog-matter-reinterview-node.ts @@ -0,0 +1,193 @@ +import "@material/mwc-button/mwc-button"; +import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-circular-progress"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { interviewMatterNode } from "../../../../../data/matter"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { MatterReinterviewNodeDialogParams } from "./show-dialog-matter-reinterview-node"; + +@customElement("dialog-matter-reinterview-node") +class DialogMatterReinterviewNode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private device_id?: string; + + @state() private _status?: string; + + public async showDialog( + params: MatterReinterviewNodeDialogParams + ): Promise { + this.device_id = params.device_id; + } + + protected render() { + if (!this.device_id) { + return nothing; + } + + return html` + + ${!this._status + ? html` +

+ ${this.hass.localize( + "ui.panel.config.matter.reinterview_node.introduction" + )} +

+

+ + ${this.hass.localize( + "ui.panel.config.matter.reinterview_node.battery_device_warning" + )} + +

+ + ${this.hass.localize( + "ui.panel.config.matter.reinterview_node.start_reinterview" + )} + + ` + : this._status === "started" + ? html` +
+ +
+

+ + ${this.hass.localize( + "ui.panel.config.matter.reinterview_node.in_progress" + )} + +

+

+ ${this.hass.localize( + "ui.panel.config.matter.reinterview_node.run_in_background" + )} +

+
+
+ + ${this.hass.localize("ui.common.close")} + + ` + : this._status === "failed" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.matter.reinterview_node.interview_failed" + )} +

+
+
+ + ${this.hass.localize("ui.common.close")} + + ` + : this._status === "finished" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.matter.reinterview_node.interview_complete" + )} +

+
+
+ + ${this.hass.localize("ui.common.close")} + + ` + : nothing} +
+ `; + } + + private async _startReinterview(): Promise { + if (!this.hass) { + return; + } + this._status = "started"; + try { + await interviewMatterNode(this.hass, this.device_id!); + this._status = "finished"; + } catch (err) { + this._status = "failed"; + } + } + + public closeDialog(): void { + this.device_id = undefined; + this._status = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + .success { + color: var(--success-color); + } + + .failed { + color: var(--error-color); + } + + .flex-container { + display: flex; + align-items: center; + } + + .stages { + margin-top: 16px; + } + + .stage ha-svg-icon { + width: 16px; + height: 16px; + } + .stage { + padding: 8px; + } + + ha-svg-icon { + width: 68px; + height: 48px; + } + + .flex-container ha-circular-progress, + .flex-container ha-svg-icon { + margin-right: 20px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-matter-reinterview-node": DialogMatterReinterviewNode; + } +} diff --git a/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-manage-fabrics.ts b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-manage-fabrics.ts new file mode 100644 index 0000000000..658ff91aed --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-manage-fabrics.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface MatterManageFabricsDialogParams { + device_id: string; +} + +export const loadManageFabricsDialog = () => + import("./dialog-matter-manage-fabrics"); + +export const showMatterManageFabricsDialog = ( + element: HTMLElement, + dialogParams: MatterManageFabricsDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-matter-manage-fabrics", + dialogImport: loadManageFabricsDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window.ts b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window.ts new file mode 100644 index 0000000000..44aaf39103 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface MatterOpenCommissioningWindowDialogParams { + device_id: string; +} + +export const loadOpenCommissioningWindowDialog = () => + import("./dialog-matter-open-commissioning-window"); + +export const showMatterOpenCommissioningWindowDialog = ( + element: HTMLElement, + dialogParams: MatterOpenCommissioningWindowDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-matter-open-commissioning-window", + dialogImport: loadOpenCommissioningWindowDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-ping-node.ts b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-ping-node.ts new file mode 100644 index 0000000000..d5a0c4e293 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-ping-node.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface MatterPingNodeDialogParams { + device_id: string; +} + +export const loadPingNodeDialog = () => import("./dialog-matter-ping-node"); + +export const showMatterPingNodeDialog = ( + element: HTMLElement, + pingNodeDialogParams: MatterPingNodeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-matter-ping-node", + dialogImport: loadPingNodeDialog, + dialogParams: pingNodeDialogParams, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-reinterview-node.ts b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-reinterview-node.ts new file mode 100644 index 0000000000..ce5b0e82a8 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/show-dialog-matter-reinterview-node.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface MatterReinterviewNodeDialogParams { + device_id: string; +} + +export const loadReinterviewNodeDialog = () => + import("./dialog-matter-reinterview-node"); + +export const showMatterReinterviewNodeDialog = ( + element: HTMLElement, + reinterviewNodeDialogParams: MatterReinterviewNodeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-matter-reinterview-node", + dialogImport: loadReinterviewNodeDialog, + dialogParams: reinterviewNodeDialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 7f6ad8445a..75cc2c4b02 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4597,6 +4597,73 @@ "download_logs": "Download logs" } }, + "matter": { + "network_type": { + "thread": "Thread", + "wifi": "Wi-Fi", + "ethernet": "Ethernet", + "unknown": "Unknown" + }, + "node_type": { + "end_device": "End-device", + "sleepy_end_device": "Sleepy end device", + "routing_end_device": "Routing end device", + "bridge": "Bridge", + "unknown": "Unknown" + }, + "device_info": { + "device_info": "Device info", + "node_id": "Node ID", + "network_type": "Network Type", + "node_type": "Device type", + "network_name": "Network name", + "ip_adresses": "IP Address(es)", + "mac_address": "MAC address", + "available": "Available?" + }, + "device_actions": { + "reinterview_device": "Re-interview device", + "ping_device": "Ping device", + "open_commissioning_window": "Enable commisisioning mode", + "manage_fabrics": "Manage fabrics", + "view_thread_network": "View Thread network" + }, + "manage_fabrics": { + "title": "Connected fabrics", + "fabrics": "Manage the fabrics that have access to this device.", + "remove_fabric_confirm_header": "Remove {fabric} fabric from device", + "remove_fabric_confirm_text": "Are you sure you want to remove the {fabric} from the device? You will not be able to control/access the device from that ecosystem/fabric after this action!", + "remove_fabric_failed_header": "Remove {fabric} fabric failed", + "remove_fabric_failed_text": "The action did not succeed, check the logs for more information." + }, + "reinterview_node": { + "title": "Re-interview a Matter device", + "introduction": "Perform a full re-interview of a Matter device. Use this feature only if your device has missing or incorrect functionality.", + "battery_device_warning": "You will need to wake battery powered devices before starting the re-interview. Refer to your device's manual for instructions on how to wake the device.", + "run_in_background": "You can close this dialog and the interview will continue in the background.", + "start_reinterview": "Start re-interview", + "in_progress": "The device is being interviewed. This may take some time.", + "interview_failed": "The device interview failed. Additional information may be available in the logs.", + "interview_complete": "Device interview complete." + }, + "ping_node": { + "title": "Ping a Matter device", + "introduction": "Perform a (server-side) ping on your Matter device on all its (known) IP-addresses.", + "battery_device_warning": "Note that especially for battery powered devices this can take a a while. You may need to up powered devices before starting the pinging to speed up the process. Refer to your device's manual for instructions on how to wake the device.", + "start_ping": "Start ping", + "in_progress": "The device is being pinged. This may take some time.", + "ping_failed": "The device ping failed. Additional information may be available in the logs.", + "ping_complete": "Ping device complete." + }, + "open_commissioning_window": { + "title": "Enable commissioning mode", + "introduction": "Enable commissioning mode on the device to pair it to another Matter controller.", + "start_commissioning": "Enable commissioning mode", + "in_progress": "We're communicating with the device. This may take some time.", + "failed": "The command failed. Additional information may be available in the logs.", + "sharing_code": "Sharing code" + } + }, "tips": { "tip": "Tip!", "join": "Join the community on our {forums}, {twitter}, {discord}, {blog} or {newsletter}",