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 <mail@bramkragten.nl>
This commit is contained in:
Marcel van der Veldt 2024-01-31 14:16:21 +01:00 committed by GitHub
parent b700e08d52
commit b159f4c074
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1278 additions and 0 deletions

View File

@ -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<MatterNodeDiagnostics> =>
hass.callWS({
type: "matter/node_diagnostics",
device_id,
});
export const pingMatterNode = (
hass: HomeAssistant,
device_id: string
): Promise<MatterPingResult> =>
hass.callWS({
type: "matter/ping_node",
device_id,
});
export const openMatterCommissioningWindow = (
hass: HomeAssistant,
device_id: string
): Promise<MatterCommissioningParameters> =>
hass.callWS({
type: "matter/open_commissioning_window",
device_id,
});
export const removeMatterFabric = (
hass: HomeAssistant,
device_id: string,
fabric_index: number
): Promise<void> =>
hass.callWS({
type: "matter/remove_matter_fabric",
device_id,
fabric_index,
});
export const interviewMatterNode = (
hass: HomeAssistant,
device_id: string
): Promise<void> =>
hass.callWS({
type: "matter/interview_node",
device_id,
});

View File

@ -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<DeviceAction[]> => {
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;
};

View File

@ -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`
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.matter.device_info.device_info"
)}
>
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.node_id"
)}:</span
>
<span class="value">${this._nodeDiagnostics.node_id}</span>
</div>
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.network_type"
)}:</span
>
<span class="value"
>${this.hass.localize(
`ui.panel.config.matter.network_type.${this._nodeDiagnostics.network_type}`
)}</span
>
</div>
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.node_type"
)}:</span
>
<span class="value"
>${this.hass.localize(
`ui.panel.config.matter.node_type.${this._nodeDiagnostics.node_type}`
)}</span
>
</div>
${this._nodeDiagnostics.network_name
? html`
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.network_name"
)}:</span
>
<span class="value">${this._nodeDiagnostics.network_name}</span>
</div>
`
: nothing}
${this._nodeDiagnostics.mac_address
? html`
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.mac_address"
)}:</span
>
<span class="value">${this._nodeDiagnostics.mac_address}</span>
</div>
`
: nothing}
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.ip_adresses"
)}:</span
>
<span class="value"
>${this._nodeDiagnostics.ip_adresses.map(
(ip) => html`${ip}<br />`
)}</span
>
</div>
</ha-expansion-panel>
`;
}
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;
}
}

View File

@ -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 {
></ha-device-info-zwave_js>
`);
}
if (domains.includes("matter")) {
import(
"./device-detail/integration-elements/matter/ha-device-info-matter"
);
deviceInfo.push(html`
<ha-device-info-matter
.hass=${this.hass}
.device=${device}
></ha-device-info-matter>
`);
}
}
private async _showSettings() {

View File

@ -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<void> {
this.device_id = params.device_id;
this._fetchNodeDetails();
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.matter.manage_fabrics.title")
)}
>
<p>
${this.hass.localize("ui.panel.config.matter.manage_fabrics.fabrics")}
</p>
${this._nodeDiagnostics
? html`<mwc-list>
${this._nodeDiagnostics.active_fabrics.map(
(fabric) =>
html`<ha-list-item
noninteractive
.hasMeta=${this._nodeDiagnostics?.available &&
fabric.vendor_id !== NABUCASA_FABRIC}
>${fabric.vendor_name ||
fabric.fabric_label ||
fabric.vendor_id}
<ha-icon-button
@click=${this._removeFabric}
slot="meta"
.fabric=${fabric}
.path=${mdiClose}
></ha-icon-button>
</ha-list-item>`
)}
</mwc-list>`
: html`<div class="center">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`}
</ha-dialog>
`;
}
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;
}
}

View File

@ -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<void> {
this.device_id = params.device_id;
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.title"
)
)}
>
${this._commissionParams
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.sharing_code"
)}: <b>${this._commissionParams.setup_manual_code}</b>
</p>
</div>
</div>
<ha-qr-code
.data=${this._commissionParams.setup_qr_code}
errorCorrectionLevel="quartile"
scale="6"
></ha-qr-code>
<div></div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<div class="status">
<p>
<b>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.in_progress"
)}
</b>
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.failed"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: html`
<p>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.introduction"
)}
</p>
<mwc-button slot="primaryAction" @click=${this._start}>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.start_commissioning"
)}
</mwc-button>
`}
</ha-dialog>
`;
}
private async _start(): Promise<void> {
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;
}
}

View File

@ -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<void> {
this.device_id = params.device_id;
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.matter.ping_node.title")
)}
>
${this._pingResult
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.ping_node.ping_complete"
)}
</p>
</div>
</div>
<div>
<mwc-list>
${Object.entries(this._pingResult).map(
([ip, success]) =>
html`<ha-list-item hasMeta
>${ip}
<ha-icon
slot="meta"
icon=${success ? "mdi:check" : "mdi:close"}
></ha-icon>
</ha-list-item>`
)}
</mwc-list>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<div class="status">
<p>
<b>
${this.hass.localize(
"ui.panel.config.matter.ping_node.in_progress"
)}
</b>
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.ping_node.ping_failed"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: html`
<p>
${this.hass.localize(
"ui.panel.config.matter.ping_node.introduction"
)}
</p>
<p>
<em>
${this.hass.localize(
"ui.panel.config.matter.ping_node.battery_device_warning"
)}
</em>
</p>
<mwc-button slot="primaryAction" @click=${this._startPing}>
${this.hass.localize(
"ui.panel.config.matter.ping_node.start_ping"
)}
</mwc-button>
`}
</ha-dialog>
`;
}
private async _startPing(): Promise<void> {
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;
}
}

View File

@ -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<void> {
this.device_id = params.device_id;
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.matter.reinterview_node.title")
)}
>
${!this._status
? html`
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.introduction"
)}
</p>
<p>
<em>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.battery_device_warning"
)}
</em>
</p>
<mwc-button slot="primaryAction" @click=${this._startReinterview}>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.start_reinterview"
)}
</mwc-button>
`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<div class="status">
<p>
<b>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.in_progress"
)}
</b>
</p>
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.run_in_background"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.interview_failed"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "finished"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.interview_complete"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: nothing}
</ha-dialog>
`;
}
private async _startReinterview(): Promise<void> {
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;
}
}

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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}",