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:
parent
b700e08d52
commit
b159f4c074
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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}",
|
||||
|
|
Loading…
Reference in New Issue