Skip to content

feat(ws-controller): add host_name, ssid and bssid to topology nodes - #993

Merged
Apollon77 merged 6 commits into
matter-js:mainfrom
MindFreeze:topology-border-router-host-name
Aug 19, 2026
Merged

feat(ws-controller): add host_name, ssid and bssid to topology nodes#993
Apollon77 merged 6 commits into
matter-js:mainfrom
MindFreeze:topology-border-router-host-name

Conversation

@MindFreeze

@MindFreeze MindFreeze commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Type of change

  • 🐛 Fix — corrects a defect or wrong behavior
  • Feature — adds new functionality or capability

Description

Part of the Matter network visualization work for OpenHomeFoundation roadmap #210. Follows #832 (the topology service and get_network_topology) and #917 (its Python client counterpart).

Three optional, additive fields that let a consumer label topology nodes with something a person recognises. No schema bump; nothing existing changes value.

Border routers: host_name

Border routers currently reach the wire as vendor_name / model_name only. Several vendors report the same pair for every unit they ship — Apple reports Apple / BorderRouter — so a home with three of them renders three identically-labelled nodes and the graph becomes unreadable. The dashboard already sidesteps this by deriving a label from the mDNS host name it holds locally (stripMdnsHostname(device.hostname) in thread-graph.ts), but that value never reaches the wire, so every other consumer — Home Assistant's network visualization above all — is stuck with the generic pair.

What's here

  • host_name on NetworkTopologyNode — optional, populated for border_router nodes from the passively-discovered Border Router registry's hostname. It is a display label, not an address: the trailing dot and a .local suffix are stripped ("Cuisine.local.""Cuisine"), matching how the rest of the topology model is already normalised (uppercased ext_pan_id, colon-stripped BSSIDs in ap_… ids, friendly routing-role strings). The raw FQDN stays available verbatim through the schema-12 get_thread_border_routers, so nothing is lost.
  • stripMdnsHostname hoisted into ws-client — it was dashboard-local; it now lives beside the rest of the shared topology derivation in topology-utils.ts and is re-exported from the dashboard's network-utils.ts, so existing dashboard imports resolve unchanged. This follows the same move refactor(ws-client): extract shared Thread/WiFi topology derivation #829 made for the derivation pipeline, and it means one implementation of the transform instead of one per consumer.
  • Python client parity in the same PRNetworkTopologyNode gains host_name: str | None = None. This is not cosmetic: dataclass_from_dict constructs only from declared dataclass fields, so shipping the TypeScript half alone would make the value disappear silently at the Python parse boundary, with no error and no failing test. feat(ws-controller): network topology service + get_network_topology API (schema 13) #832/feat(python-client): get_network_topology + network_topology_updated (schema 13) #917 split JS from Python, but that split does not work for a field addition.

Wi-Fi nodes: ssid and bssid

wifi_ap nodes carry the BSSID in network_name — a radio address under a field whose name promises a network name — and Wi-Fi stations carry no network identity at all. Both now get ssid and bssid explicitly.

network_name on wifi_ap is deliberately left at its current value so the dashboard and any existing consumer keep working; it is documented as superseded for Wi-Fi and can be dropped at the next schema bump.

The SSID comes from NetworkCommissioning (0/49/1). Its networkID is only an SSID on Wi-Fi — it is the extended PAN id on Thread and the interface name on Ethernet — so it is read only for Wi-Fi nodes, and a non-text payload is rejected rather than rendered. Some devices report no network at all, so the bssid fallback is permanent. Worth knowing for consumers: one BSSID is one AP radio, so a mesh legitimately reports several wifi_ap nodes sharing an ssid.

Scope and compatibility

All three fields are optional and additive. SCHEMA_VERSION stays 13 (WebSocketControllerHandler.ts), minimum supported stays 11 — the field is additive and no command or event shape changes. Worth flagging explicitly: schema 13 already shipped in 1.4.0, so unlike the earlier in-place additions to this section there is no version signal for this field. Clients must treat it as presence-detectable only and keep a vendor_name / model_name fallback: an absent host_name means either an older server or a border router whose _meshcop SRV host name was never resolved; an absent ssid means an older server or a device that reports no NetworkCommissioning network. That fallback is permanent, not transitional. The schema changelog entry says so, and the field's JSDoc repeats it.

Backing evidence

This is an additive feature rather than a defect fix, so there is no failure log to attach. The behaviour it addresses is visible in the dashboard's own Thread graph, which already prefers the mDNS host name over vendor/model for exactly this reason.

Testing

  • npm run format / format-verify, npm run build-clean and npm run lint (type-aware) clean from the repo root. npm test -w @matter-server/ws-client (89/89) and npm test -w @matter-server/ws-controller (389/389) green. The full root npm test was not used as the gate here: its matter-server integration suite needs real mDNS on this machine and fails for unrelated reasons on both this branch and main.
  • NetworkTopologyServiceTest covers both directions: a registry entry with hostname: "Cuisine.local." reaches the wire as host_name: "Cuisine", and an entry without a host name omits the field entirely rather than emitting an empty string.
  • Python: npm run python:lint, npm run python:typecheck and npm run python:test (76/76) clean. The wire fixture gained the new key; the existing forward-compat test (unknown keys and enum-like values) still passes, and the assertion that a node without host_name parses to None guards the dataclass default. test_integration.py is excluded by that script and its two network-dependent cases fail identically on main here.
  • getWiFiSsid is unit-tested directly for the connected-network preference, the named/tag field spellings, an empty list, and both binary-rejection paths (invalid UTF-8, and low bytes that decode as valid UTF-8 control characters).
  • Every new assertion was checked against a mutated tree: dropping host_name or ssid from the service, dropping the SSID control-character guard, dropping the connected-network preference, or dropping the field from the Python dataclass each turns a test red.
  • Dashboard behaviour is unchanged by construction — stripMdnsHostname moved but is re-exported from the same module path, and its two call sites are untouched.

Checklist

  • I understand the code I am submitting and can explain how it works (AI policy)
  • Tests added or updated to cover the change
  • npm test passes — per workspace (ws-client 89/89, ws-controller 389/389); see Testing above for the root-suite caveat
  • npm run format-verify and npm run lint pass
  • CHANGELOG updated (if applicable) — docs/websocket-api-schema-changelog.md, schema 13 section

Border routers reached the wire with only vendor_name/model_name, which
for some vendors are identical on every unit, so a multi-BR network
rendered several identically labelled nodes. Send the mDNS host name as
a display label instead, normalized with the helper the dashboard
already used, now hoisted into ws-client so both consumers share it.

The field is optional and additive, so it needs no schema bump.
@MindFreeze
MindFreeze marked this pull request as ready for review August 18, 2026 11:34
Copilot AI lite review requested due to automatic review settings August 18, 2026 11:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional host_name display label to border_router nodes in the network-topology wire model, sourced from the Border Router mDNS hostname and normalized (strip trailing . and .local), so downstream consumers (e.g., Home Assistant) can render multi-BR networks with distinct labels rather than repeated vendor/model strings.

Changes:

  • Extend the NetworkTopologyNode wire model (TS + Python) with optional host_name and populate it for border_router nodes in NetworkTopologyService.
  • Hoist stripMdnsHostname() into @matter-server/ws-client (shared topology utilities) and re-export it from the dashboard to keep existing imports stable.
  • Add/extend unit tests and update the WebSocket schema changelog entry to document presence-detectable semantics (no schema bump).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
python_client/tests/test_network_topology.py Extends wire fixture + asserts host_name parses and defaults to None when absent.
python_client/matter_server/common/models.py Adds host_name: str | None to NetworkTopologyNode dataclass for parity and forward-compat parsing.
packages/ws-controller/test/NetworkTopologyServiceTest.ts Verifies BR hostname normalization and omission behavior when no hostname is available.
packages/ws-controller/src/controller/NetworkTopologyService.ts Populates host_name for border_router nodes using shared stripMdnsHostname().
packages/ws-client/src/topology/topology-utils.ts Introduces shared stripMdnsHostname() utility.
packages/ws-client/src/models/model.ts Documents new optional host_name field on NetworkTopologyNode.
packages/dashboard/src/pages/network/network-utils.ts Re-exports stripMdnsHostname from @matter-server/ws-client and removes the dashboard-local copy.
docs/websocket-api-schema-changelog.md Documents host_name addition (presence-detectable only; no version signal).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

wifi_ap nodes carried the BSSID in network_name, which is a radio
address rather than a network name, and Wi-Fi stations carried no
network identity at all. Add both fields explicitly, on the AP and on
the stations joined to it, sourcing the SSID from NetworkCommissioning.

network_name keeps its existing value on wifi_ap so current consumers
are unaffected. The SSID is best effort: some devices report no network,
and networkID only means an SSID on Wi-Fi, so a bssid fallback stays
necessary.
@MindFreeze MindFreeze changed the title feat(ws-controller): add host_name to topology border router nodes feat(ws-controller): add host_name, ssid and bssid to topology nodes Aug 18, 2026
…n SSID

The AP pseudo-node took its `ssid` from whichever station happened to be
iterated first for a BSSID, and the dedup guard then froze it. A first
station that reports no NetworkCommissioning network — the case the `bssid`
fallback exists for — left the AP unnamed even when every other station on
the same radio carried a good SSID, and the outcome flipped with node
iteration order. Keep the AP node and backfill `ssid` from any station
joined to it.

Wi-Fi diagnostics and the SSID are now parsed once per station and carried
to the AP loop, which iterates those records instead of re-walking every
node: one base64 decode per station instead of two, and no dependency on
node ids being unique across the source list.

A border router whose SRV target strips to nothing ("`.local.`") reached the
wire as `host_name: ""`, a worse label than absence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Apollon77
Apollon77 requested a balanced review from Copilot August 19, 2026 08:29
@Apollon77

Copy link
Copy Markdown
Collaborator

Pushed one commit to this branch — reviewing the new fields turned up two issues:

The AP's ssid was decided by iteration order. The wifi_ap node took its SSID from whichever station happened to be iterated first for that BSSID, and the seenAps guard then froze it. A first station that reports no NetworkCommissioning network — exactly the case the bssid fallback exists for — left the AP unnamed even when every other station on the same radio carried a good SSID. Fixed by keeping the AP node and backfilling ssid from any station joined to it. Wi-Fi diagnostics and the SSID are now parsed once per station and handed to the AP loop, so it no longer re-walks every node: one base64 decode per station instead of two, and no dependency on node ids being unique across the source list.

host_name: "" could reach the wire. mapExternal guarded on hostname === undefined, but an SRV target that strips to nothing (".local.") yields an empty string — a worse label than absence. Now || undefined. Theoretical rather than field-observed: stripMdnsHostname("local.") returns "local", so only degenerate targets hit it.

Both are covered by tests that fail against the unfixed tree. The new Wi-Fi test also pins the AP dedup itself (node count + both edges), which nothing asserted before — the existing tests routed nodes through a Map keyed by id, so a duplicate wifi_ap node would have gone unnoticed.

One follow-up deliberately left out: the same strip-to-empty case is worse on the dashboard side. thread-graph.ts does hostname ?? device.networkName ?? "Border Router", and ?? doesn't catch "", so a blank label would also skip the network-name fallback; network-details.ts has the same shape. If we want that closed, the guard belongs inside stripMdnsHostname so all three call sites inherit it — didn't want to change dashboard behaviour inside your feature PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (2)

packages/ws-client/src/topology/topology-utils.ts:973

  • When the list is non-empty but every entry is explicitly connected: false, this fallback labels the current BSSID/AP with the first saved, disconnected network. connected is a required field, and the existing Python path likewise skips disconnected entries (python_client/matter_server/client/client.py:548-555), so return null when no connected entry exists instead of selecting entries[0].
    const chosen = entries.find(entry => (entry["1"] ?? entry.connected) === true) ?? entries[0];

packages/ws-controller/src/controller/NetworkTopologyService.ts:343

  • refresh() still reads only WIFI_REFRESH_PATHS, which omits 0/49/1. Consequently, get_network_topology({ refresh: true }) cannot populate a missing SSID or replace a stale one after the device changes networks; it rebuilds from the old cached Networks value. Add this attribute to the Wi-Fi refresh paths and cover it in the refresh-path assertion.
                const ssid = getWiFiSsid(node) ?? undefined;

MindFreeze and others added 3 commits August 19, 2026 11:36
A device can list saved networks it is not currently on, and naming the
radio after one of those is worse than leaving it to the BSSID. Skip
disconnected entries instead of falling back to the first one, matching
how the python client already reads the same attribute.

Also add the network list to the Wi-Fi refresh paths, so an explicit
refresh can fill in an SSID the cache never had or replace one left over
from a previous network. The path is now asserted by name, because
comparing the read against the constant would accept its absence.
`stripMdnsHostname` returned "" for a target that is nothing but the
suffix, and each of its three call sites handled that differently: the
topology service guarded only against an undefined host name, the graph
label used `?? device.networkName`, which "" does not trigger, so a border
router lost both its label and its network-name fallback, and the details
label guarded the input rather than the result. Return no label instead,
and let the existing fallbacks carry it at every call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Matter network visualization in Matter panel and aim for a consistent graph experience across protocols

3 participants