/** * @license * Copyright 2025-2026 Open Home Foundation * SPDX-License-Identifier: Apache-2.0 */ import "@material/web/button/outlined-button"; import "@material/web/divider/divider"; import "@material/web/iconbutton/icon-button"; import "@material/web/list/list"; import "@material/web/list/list-item"; import { consume } from "@lit/context"; import { isTestNodeId, MatterClient, MatterNode } from "@matter-server/ws-client"; import { mdiAlertCircleOutline, mdiChevronRight, mdiGraphOutline } from "@mdi/js"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import { guard } from "lit/directives/guard.js"; import { clientContext, tickContext } from "../client/client-context.js"; import "../components/ha-svg-icon"; import { getDeviceIcon, getEndpointIcon } from "../util/device-icons.js"; import { getEndpointDeviceTypes } from "../util/endpoints.js"; import { formatNodeAddress, getEffectiveFabricIndex } from "../util/format_hex.js"; import "./components/header"; import "./components/node-details"; import { notFoundStyles, reducedMotionStyles } from "../util/shared-styles.js"; import { getNetworkType } from "./network/network-utils.js"; declare global { interface HTMLElementTagNameMap { "matter-node-view": MatterNodeView; } } function getUniqueEndpoints(node: MatterNode) { // extract unique endpoints from the node attributes, as (sorted) array return Array.from(new Set(Object.keys(node.attributes).map(key => Number(key.split("/")[0])))).sort((a, b) => { return a - b; }); } @customElement("matter-node-view") class MatterNodeView extends LitElement { @consume({ context: clientContext }) public client!: MatterClient; @consume({ context: tickContext, subscribe: true }) protected _tick = 0; @property() public node?: MatterNode; override render() { if (!this.node) { return html`

Node not found

Back
`; } const networkType = getNetworkType(this.node); // Show graph button for Thread, WiFi, and Ethernet (Ethernet devices are shown in WiFi graph) const showGraphButton = networkType === "thread" || networkType === "wifi" || networkType === "ethernet"; // Ethernet devices go to WiFi graph since they're displayed there const graphViewType = networkType === "ethernet" ? "wifi" : networkType; const graphUrl = showGraphButton ? `#${graphViewType}/${this.node.node_id}` : null; // Format node address for hex display const fabricIndex = getEffectiveFabricIndex( this.client.serverInfo.fabric_index, isTestNodeId(this.node.node_id), ); const nodeHex = formatNodeAddress(fabricIndex, this.node.node_id); return html`

Node ${this.node.node_id} ${nodeHex}

${showGraphButton ? html` Show in graph ` : ""}
Endpoints
${guard([this.node, this.node?.attributes], () => getUniqueEndpoints(this.node!).map(endPointId => { return html`
Endpoint ${endPointId}
Device Type(s): ${getEndpointDeviceTypes(this.node!, endPointId) .map(deviceType => { return deviceType.label; }) .join(" / ")}
`; }), )}
`; } private _goBack() { history.back(); } static override styles = [ notFoundStyles, reducedMotionStyles, css` :host { display: flex; background-color: var(--md-sys-color-background); box-sizing: border-box; flex-direction: column; min-height: 100vh; } .container { padding: 16px; max-width: 95%; margin: 0 auto; width: 100%; } @media (max-width: 600px) { .container { padding: 16px 0; } } .status { color: var(--danger-color); font-weight: bold; font-size: 0.8em; } .node-title-bar { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; } .node-icon { --icon-primary-color: var(--md-sys-color-on-surface-variant, #666); --mdc-icon-size: 28px; } .endpoint-icon { --icon-primary-color: var(--md-sys-color-on-surface-variant, #666); } .node-title-bar h2 { margin: 0; font-size: 1.25rem; font-weight: 500; color: var(--md-sys-color-on-background, #333); } .node-id-hex { font-size: 0.75em; font-weight: 400; color: var(--md-sys-color-on-surface-variant, #666); font-family: var(--monospace-font, monospace); } .show-in-graph-button { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); text-decoration: none; border-radius: 4px; font-size: 0.8rem; font-weight: 500; transition: opacity 0.2s; white-space: nowrap; } .show-in-graph-button:hover { opacity: 0.9; } .show-in-graph-button ha-svg-icon { --icon-primary-color: var(--md-sys-color-on-primary); width: 16px; height: 16px; } @media (max-width: 768px) { .show-in-graph-button { display: none; } } @media (max-width: 480px) { .show-in-graph-button .button-text { display: none; } .show-in-graph-button { padding: 6px; } } `, ]; }