diff --git a/README.md b/README.md index 069ca18..401938a 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,9 @@ node --version # confirm v22.x Leave **Node bin directory** blank in the plugin config — auto-detect finds Homebrew first. (nvm works too; pin `nodeBinDir` explicitly if you use it.) -**2. Install matter-server:** **Plugins ▸ Matter ▸ Install/update matter-server**. This -installs the pinned version with the *same* Node it will run the server with, and pins +**2. Install the Matter controller:** **Plugins ▸ Matter ▸ Install/update the Matter +controller (matter-server)**. This installs the pinned version with the *same* Node it +will run the server with, and pins that Node — which avoids the most common failure, installing with one Node and running with another whose native modules won't load. @@ -144,7 +145,7 @@ manual mode, fabric backups, upgrading and uninstalling, is covered in the [full install guide](https://simons-plugins.github.io/indigo-matter/INSTALL.html). To **export** Indigo devices as well, add a fourth step — **Plugins ▸ Matter ▸ -Install/update the Matter export bridge** — and then pick your devices in *Manage Matter +Install/update the Matter bridge** — and then pick your devices in *Manage Matter Exports…*. The bridge starts itself once something is exported. Full walkthrough in the install guide. @@ -188,7 +189,7 @@ Developer reference, in the repo — not intended as user documentation: - [`docs/API.md`](https://github.com/simons-plugins/indigo-matter/blob/main/docs/API.md) — the Domio ↔ plugin HTTP contract (v1.3). - [`docs/BRIDGE_PROTOCOL.md`](https://github.com/simons-plugins/indigo-matter/blob/main/docs/BRIDGE_PROTOCOL.md) — the plugin ⇄ bridge-node local protocol (export side). - [`docs/PRD-indigo-matter-plugin.md`](https://github.com/simons-plugins/indigo-matter/blob/main/docs/PRD-indigo-matter-plugin.md) — product requirements and milestones (inbound). -- [`docs/PRD-indigo-matter-export.md`](https://github.com/simons-plugins/indigo-matter/blob/main/docs/PRD-indigo-matter-export.md) — the same for the export bridge. +- [`docs/PRD-indigo-matter-export.md`](https://github.com/simons-plugins/indigo-matter/blob/main/docs/PRD-indigo-matter-export.md) — the same for the Matter bridge. - [`bridge-node/`](https://github.com/simons-plugins/indigo-matter/tree/main/bridge-node) — the bridge node itself (TypeScript; the only place matter.js is imported). `npm run build && npm test`. - [`CLAUDE.md`](https://github.com/simons-plugins/indigo-matter/blob/main/CLAUDE.md) — architecture and workspace conventions. @@ -217,7 +218,7 @@ What that means in practice: attestation so uncertified devices (development boards, Homebridge/Matterbridge bridges) can be commissioned. It is off by default, warns on every start, and should be turned off once the device is paired. -- **The export bridge is uncertified in the other direction, and visibly so.** It +- **The Matter bridge is uncertified in the other direction, and visibly so.** It advertises with the specification's test vendor ID, so every ecosystem shows an "uncertified accessory" warning when you add it and you choose *Add Anyway*. That is the normal state for this class of software — Homebridge, matterbridge and Home Assistant's diff --git a/bridge-node/README.md b/bridge-node/README.md index 489027c..9299176 100644 --- a/bridge-node/README.md +++ b/bridge-node/README.md @@ -12,7 +12,7 @@ Home and SmartThings are untested and unclaimed. > no discovery, and no idea what an Indigo device is. It is one half of a pair: > the plugin decides *what* to export and the node makes it a Matter accessory. > Install the Indigo plugin; it installs and manages this package for you, via -> its own **Plugins ▸ Matter ▸ Install/update the Matter export bridge** menu +> its own **Plugins ▸ Matter ▸ Install/update the Matter bridge** menu > item and a launchd LaunchAgent it writes. ## Why it is a separate process diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 4ff103c..f5ab1a3 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -83,7 +83,7 @@ bump both to `0.8.0` together, `npm publish` (Simon's npm login), **jarvis needs a one-time hand edit** — its stale entry predates the marker, so the fix cannot clear it retroactively. With the bridge node stopped (*Stop -the Matter export bridge…*), edit +the Matter bridge…*), edit `~/Library/Application Support/com.simons-plugins.indigo-matter/bridge-node/endpoint-map.json`: set `indigo-459564566`'s number to the live `2`, and resolve the duplicate number 5 (two entries record it — the reset's fingerprint; the non-live one @@ -572,9 +572,9 @@ batch does instead is say so, prominently, on the page itself and in the menu's log line. See `docs/INSTALL.md` → "Before you pair the export bridge". **Two new bridge menu items** close the gap where the controller had recovery -exits and the bridge had none: "Reinstall the Matter export bridge (clean)…" +exits and the bridge had none: "Reinstall the Matter bridge (clean)…" (safe only because `remove_package` became per-package in E7) and "Stop the -Matter export bridge…" — for the user who disables the plugin and is otherwise +Matter bridge…" — for the user who disables the plugin and is otherwise left with a running node and no UI at all, since the allow-list lever needs the plugin to be running. @@ -831,7 +831,7 @@ bundle ships **no JavaScript**; `bridge-node/` stays a top-level source directory in the repo and is published from there. **`indigo-matter-bridge` is not on the registry yet.** Until Simon publishes it, -"Install/update the Matter export bridge" cannot resolve the pin and E7 cannot +"Install/update the Matter bridge" cannot resolve the pin and E7 cannot be exercised end to end on jarvis. #### Publishing the bridge node @@ -872,7 +872,7 @@ menu will still try the registry spec. **So the managed LaunchAgent is not, strictly, blocked on `npm publish`.** It is blocked on the package being *installed*, and the local-install recipe above installs it. What the publish unblocks is the shipped route — the -"Install/update the Matter export bridge" menu item — which is what a user would +"Install/update the Matter bridge" menu item — which is what a user would use and is therefore what "end to end" means. Do not restate the blocker as "npm" when the distinction matters. @@ -980,7 +980,7 @@ gates the next: "Enable Matter export" is ticked, and that the readout line appears. If the readout is blank, Indigo dispatched neither callback spelling — say so, do not guess. -4. **Plugins ▸ Matter ▸ Install/update the Matter export bridge.** Watch for +4. **Plugins ▸ Matter ▸ Install/update the Matter bridge.** Watch for "Matter export bridge installed"; it will refuse to start anything while nothing is exported, which is correct (XG5). 5. **Export one relay** in "Manage Matter Exports…". This is XAC2: the agent @@ -1167,7 +1167,7 @@ Highs, 5 Mediums and 9 coverage gaps. The five that would have hurt a real user: caller anywhere in the plugin, while three user-facing strings told the user to confirm the rebuild "in the plugin". Two menu items now exist: **"Rebuild Matter Endpoint Map…"** (§3.11, one confirm + the duplication - warning) and **"Reset Matter Export Pairings…"** (§3.10 `preserve=true`, two + warning) and **"Reset Matter Bridge Pairings…"** (§3.10 `preserve=true`, two confirms). Both gate on `client.connected`, not `attached` — §1.1 holds the socket open un-attached and that is the only state a rebuild is needed in. `remove_fabric` still has no UI; it needs E6's fabric readout to pick an @@ -1838,7 +1838,7 @@ All merged to `main`, all deployed to jarvis. Plugin **2026.0.1 → 2026.1.1** o - Two `/review-pr` passes found + fixed real safety holes (ignored stop/start bools → success-over-dead-server; swallowed rollback; unvalidated backup output; empty-archive wipe). **#31** fixed a live-caught menu bug: Indigo passes ConfigUI menu callbacks `(self, valuesDict, menuId)` — handlers were 2-arg. - **Live on jarvis:** Export → valid 915-member zip; Restore → server stop → swap (original → `matter-server.pre-restore-…`) → start → reconnect → **5 nodes reconciled** → device toggles. The two `Matter Warning: connection lost` lines during restore are the **expected ~6s reconnect window**, not errors. -**Known polish (not filed):** during an *intentional* matter-server restart (restore / "Restart matter-server"), the reconnect loop logs `connection lost` WARNINGs for ~6s. Consider debouncing — log first attempts at debug, escalate to WARNING only past the normal restart window (`matter_client.py`). Small change + test. +**Known polish (not filed):** during an *intentional* matter-server restart (restore / "Restart the Matter controller"), the reconnect loop logs `connection lost` WARNINGs for ~6s. Consider debouncing — log first attempts at debug, escalate to WARNING only past the normal restart window (`matter_client.py`). Small change + test. **Next milestone — device-class gaps:** umbrella **#15**; priority order #3 Power Source/battery (promised in PRD, never wired), #4 Generic Switch (buttons — needs `node_event` plumbing through protocol.py + device_sync), #5 Window Covering, #6 Door Lock, #7 bridges-done-properly, #8 standalone fans (a fan-only endpoint currently creates NO device), #9 energy, #10 smoke/CO, #11 air quality, #12 valve, #13 pressure/flow, #14 RVC (icebox). All follow the handler-registry pattern (one `matter_handlers/*.py` + `Devices.xml` type + one line in `registry.default_handlers()`; `thermostat.py` is the precedent for attribute writes). diff --git a/docs/INSTALL.md b/docs/INSTALL.md index e1c5aab..a02fc8d 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -3,14 +3,13 @@ The plugin has two halves, and each one is a **separate Node process with its own npm package**. This guide covers both: -- **matter-server** — the inbound controller. The plugin drives it so that Matter - devices in your house become Indigo devices. **Steps 1–4**, and this is what a - normal install needs. These steps are derived from the live +- **The Matter controller** (`matter-server`) — the inbound half. The plugin drives it so + that Matter devices in your house become Indigo devices. **Steps 1–4**, and this is what + a normal install needs. These steps are derived from the live managed-LaunchAgent bring-up on the reference Indigo server. -- **the Matter export bridge** — the optional outbound half, which publishes - selected *Indigo* devices to Apple Home as Matter accessories. **Steps E1–E4**. - Its npm package (`indigo-matter-bridge`) is on the registry and installs from - its own menu item — see +- **The Matter bridge** (`indigo-matter-bridge`) — the optional outbound half, which + publishes selected *Indigo* devices to Apple Home as Matter accessories. **Steps E1–E4**. + Its npm package is on the registry and installs from its own menu item — see [Exporting Indigo devices](#exporting-indigo-devices-indigo-as-a-matter-bridge). If you just installed the plugin and Indigo is logging @@ -92,7 +91,7 @@ A few things to know before you start: - **There is a second, optional half.** The plugin can also work the other way round and publish selected *Indigo* devices to Apple Home as Matter accessories, from a separate bridge process with its own npm package (`indigo-matter-bridge`), installed from - **Plugins ▸ Matter ▸ Install/update the Matter export bridge**. Nothing is exported + **Plugins ▸ Matter ▸ Install/update the Matter bridge**. Nothing is exported and no bridge process runs until you add a device to the export list, and it needs Node (Step 1) and a storage path (Step 3) — *not* a working matter-server. See [Exporting Indigo devices](#exporting-indigo-devices-indigo-as-a-matter-bridge). @@ -108,9 +107,9 @@ did not mean to touch. |---|---|---| | 1 | *Commission device by setup code (advanced)…*, *Decommission Matter device…* | Matter devices Indigo **controls** — the everyday actions | | 2 | *Manage Matter Exports…*, *Pair Matter Bridge…*, *Unpair an Ecosystem…* | Indigo devices Indigo **publishes** — the everyday actions | -| 3 | *Install/update matter-server*, *Restart…*, *Reinstall (clean)…*, *Open matter-server log…* | The inbound controller's own plumbing | -| 4 | *Install/update the Matter export bridge*, *Reinstall… (clean)…*, *Stop…* | The outbound bridge node's own plumbing | -| 5 | *Back up the Matter fabric…*, *Restore a fabric backup…*, *Rebuild Matter Endpoint Map…*, *Reset Matter Export Pairings…* | Backup and recovery. The last two are destructive — see [the two destructive recovery actions](#the-two-destructive-recovery-actions) | +| 3 | *Install/update the Matter controller (matter-server)*, *Restart…*, *Reinstall (clean)…*, *Open the Matter controller log…* | The inbound controller's own plumbing | +| 4 | *Install/update the Matter bridge*, *Reinstall… (clean)…*, *Stop…* | The outbound bridge node's own plumbing | +| 5 | *Back up the Matter fabric…*, *Restore a fabric backup…*, *Rebuild Matter Endpoint Map…*, *Reset Matter Bridge Pairings…* | Backup and recovery. The last two are destructive — see [the two destructive recovery actions](#the-two-destructive-recovery-actions) | --- @@ -172,7 +171,7 @@ The package lives at `~/indigo-matter/node_modules/matter-server`, where the plu managed LaunchAgent expects it. **Easiest (recommended): let the plugin do it.** In local/managed mode, use -**Plugins ▸ Matter ▸ Install/update matter-server**. The plugin installs the package +**Plugins ▸ Matter ▸ Install/update the Matter controller (matter-server)**. The plugin installs the package with the *same* node it will run the server with, and pins that node in `nodeBinDir` — which is the thing that prevents the most common failure (installing with one node and running with another, whose native modules won't load). No Terminal needed. Skip to @@ -262,7 +261,7 @@ node ~/indigo-matter/node_modules/matter-server/dist/esm/MatterServer.js \ | **Manage matter-server LaunchAgent automatically** | off | On = plugin supervises matter-server (Mode A); off = you run it (Mode B). | | **Primary network interface** | `en0` | macOS interface matter-server binds to. Managed mode only. | | **matter-server listen address** | `127.0.0.1` | IP the managed server binds its (unauthenticated) control API to. Keep loopback. Managed mode only. See Security. | -| **Allow test/development device certificates** | off | Passes `--enable-test-net-dcl` to the managed matter-server, trusting the Matter test-net DCL alongside the production one (production verification is unchanged). It **also** allows test-net OTA firmware to be offered to commissioned devices. Turn on only if commissioning fails on device attestation because the device uses a test certificate (Homebridge and other dev bridges do). This is a global setting that stays on until you turn it off — untick it once the device is paired. Managed mode only; takes effect on plugin reload or via Plugins ▸ Matter ▸ Restart matter-server. While it is on, the plugin logs a warning on every startup. | +| **Allow test/development device certificates** | off | Passes `--enable-test-net-dcl` to the managed matter-server, trusting the Matter test-net DCL alongside the production one (production verification is unchanged). It **also** allows test-net OTA firmware to be offered to commissioned devices. Turn on only if commissioning fails on device attestation because the device uses a test certificate (Homebridge and other dev bridges do). This is a global setting that stays on until you turn it off — untick it once the device is paired. Managed mode only; takes effect on plugin reload or via Plugins ▸ Matter ▸ Restart the Matter controller. While it is on, the plugin logs a warning on every startup. | | **Verbose Matter logging** | off | Extra plugin logging. | | **Trace matter-server WebSocket messages** | off | Logs raw WS frames (debug). | @@ -300,7 +299,7 @@ Commission device by setup code…** — and it will appear as a native Indigo d > **You cannot install this yet.** The export bridge needs a second npm package, > `indigo-matter-bridge`, and it **has not been published to the npm registry**. -> Until it is, **Install/update the Matter export bridge** cannot resolve the +> Until it is, **Install/update the Matter bridge** cannot resolve the > version the plugin pins, and the install fails. Nothing else in the plugin is > affected — the inbound controller, your commissioned devices and every Indigo > device carry on as normal — but export cannot be brought up. Everything below @@ -371,13 +370,13 @@ noise in that configuration, not an export fault. ### Step E1 — Install the export bridge -**Plugins ▸ Matter ▸ Install/update the Matter export bridge.** +**Plugins ▸ Matter ▸ Install/update the Matter bridge.** It installs into the same `~/indigo-matter` npm root as matter-server, with the same Node, and takes a minute; watch the Event Log. On success you will see: ``` -Matter export bridge installed. It is NOT being started: nothing is exported yet, +Matter bridge installed. It is NOT being started: nothing is exported yet, and the bridge only runs while the export list is non-empty. ``` @@ -416,8 +415,8 @@ plugin writes the bridge node's LaunchAgent, starts it, and connects. The log should show: ``` -Matter export: bridge node LaunchAgent is running (protocol port 5581, Matter port 5540) -Matter export: bridge node attached — 1 endpoint(s) live, not yet paired +Matter bridge: bridge node LaunchAgent is running (protocol port 5581, Matter port 5540) +Matter bridge: bridge node attached — 1 endpoint(s) live, not yet paired ``` + - Install/update matter-server + Install/update the Matter controller (matter-server) menuInstallMatterServer - Restart matter-server + Restart the Matter controller menuRestartMatterServer - Reinstall matter-server (clean)… + Reinstall the Matter controller (clean)… menuReinstallMatterServerClean - + @@ -244,14 +244,14 @@ If you unpair the LAST ecosystem the bridge resets itself completely and starts - Open matter-server log… + Open the Matter controller log… menuShowMatterServerLogs - Install/update the Matter export bridge + Install/update the Matter bridge menuInstallBridgeNode - Reinstall the Matter export bridge (clean)… + Reinstall the Matter bridge (clean)… menuReinstallBridgeNodeClean +Use this if the Matter bridge will not start after an upgrade and a plain "Install/update" has not fixed it. @@ -282,11 +282,11 @@ Use this if the bridge node will not start after an upgrade and a plain "Install - Stop the Matter export bridge… + Stop the Matter bridge… menuStopBridgeNode - - Reset Matter Export Pairings… + Reset Matter Bridge Pairings… menuResetBridgePairings - - + diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/PluginConfig.xml b/indigo-matter.indigoPlugin/Contents/Server Plugin/PluginConfig.xml index e2d0fe1..6e65648 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/PluginConfig.xml +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/PluginConfig.xml @@ -4,11 +4,11 @@ (local) view and the taller "On another computer" / advanced sections overflow. --> - + - + @@ -45,7 +45,7 @@ defaultValue="~/Library/Application Support/com.simons-plugins.indigo-matter/matter-server" visibleBindingId="showAdvanced" visibleBindingValue="true" alwaysUseInDialogHeightCalc="true"> - + + the Matter controller. @@ -73,7 +73,7 @@ - + - + - + - + diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/bridge_agent.py b/indigo-matter.indigoPlugin/Contents/Server Plugin/bridge_agent.py index 5a9f0ec..046788f 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/bridge_agent.py +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/bridge_agent.py @@ -1,4 +1,4 @@ -"""The Matter **export bridge node** LaunchAgent (PRD-indigo-matter-export §4.2). +"""The Matter **bridge node** LaunchAgent (PRD-indigo-matter-export §4.2). The second of the plugin's two launchd-managed node processes, and the reason :mod:`launch_agent` exists at all: the controller's hard-won recovery machinery @@ -79,7 +79,7 @@ #: that does not exist. It fires on the first-run path (no package, so no plist, #: so the preflight error), where a name the user cannot find in the menu is the #: difference between a fixable state and giving up. -BRIDGE_INSTALL_MENU = "Install/update the Matter export bridge" +BRIDGE_INSTALL_MENU = "Install/update the Matter bridge" #: Matter UDP port the node binds (PRD §4.4). 5540 is Matter's default and the #: one matter.js's ECOSYSTEMS.md records as Alexa's hard requirement; the pref is @@ -159,7 +159,7 @@ def bridge_arguments(agent: "BridgeProcess") -> list[str]: class BridgeProcess(LaunchAgent): - """Install / control the Matter export bridge node's LaunchAgent. + """Install / control the Matter bridge node's LaunchAgent. The bridge specialisation of :class:`~launch_agent.LaunchAgent`: it reads the export prefs, builds its :class:`~launch_agent.AgentSpec`, and adds nothing diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/export_bridge.py b/indigo-matter.indigoPlugin/Contents/Server Plugin/export_bridge.py index 3b10d40..55e81f5 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/export_bridge.py +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/export_bridge.py @@ -267,7 +267,7 @@ def start(self) -> None: if self._un_exporting: self._start_after_un_export = True self._logger.debug( - "Matter export: start deferred until the in-flight un-export finishes") + "Matter bridge: start deferred until the in-flight un-export finishes") return if self.client is not None: return @@ -276,7 +276,7 @@ def start(self) -> None: # Configure…, and every caller of start() is a path that would # ordinarily bring it up. Saying so once per attempt at debug is # enough — the config dialog's own readout is where this belongs. - self._logger.debug("Matter export: export is switched off in Configure…; " + self._logger.debug("Matter bridge: export is switched off in Configure…; " "not connecting to the bridge node") return # The agent BEFORE the client, and deliberately not conditional on it @@ -307,7 +307,7 @@ def start(self) -> None: self._fire(self.client.run(), "bridge client run loop", lost="nothing will be exported until the plugin is reloaded") self._logger.info( - "Matter export: connecting to the bridge node (%d device(s) exported)", + "Matter bridge: connecting to the bridge node (%d device(s) exported)", len(self._store)) def stop(self, timeout: float = 4.0) -> None: @@ -323,7 +323,7 @@ def stop(self, timeout: float = 4.0) -> None: # is the count of tiles now showing something the house is not # doing — and nothing retries a §5 command. self._logger.warning( - "Matter export: shutting down with %d ecosystem command(s) still queued or " + "Matter bridge: shutting down with %d ecosystem command(s) still queued or " "in flight — they will NOT be applied, and paired ecosystems already show " "them as done.", dropped) # Not `wait=True`: a dispatch blocked on a wedged IndigoServer would @@ -397,7 +397,7 @@ def exports_changed(self) -> None: # "delete them from every ecosystem". Drop the socket and stop the # agent; the endpoints, the pairings and the allow-list all stand. if self.client is not None: - self._logger.info("Matter export: export is switched off — disconnecting from the " + self._logger.info("Matter bridge: export is switched off — disconnecting from the " "bridge node. Exported accessories are LEFT paired and will show " "as unavailable until export is switched back on.") self.stop() @@ -417,7 +417,7 @@ def exports_changed(self) -> None: # landed. XG5 says no client while nothing is exported; this is the # one exception, and it lasts exactly one successful attach. self._logger.info( - "Matter export: reconnecting to finish an un-export that did not complete " + "Matter bridge: reconnecting to finish an un-export that did not complete " "earlier (%d accessory record(s) still owed removal)", self._pending_replace_all()) self.start() @@ -468,7 +468,7 @@ def _ensure_agent(self) -> None: self._agent_start() except Exception as exc: # pylint: disable=broad-except self._logger.error( - "Matter export: could not start the bridge node's LaunchAgent (%s). Indigo " + "Matter bridge: could not start the bridge node's LaunchAgent (%s). Indigo " "devices and inbound Matter control are unaffected; exported accessories will " "not be reachable until this is fixed.", exc) self._logger.exception(exc) @@ -492,7 +492,7 @@ def _stop_agent(self) -> None: self._agent_stop() except Exception as exc: # pylint: disable=broad-except self._logger.warning( - "Matter export: could not stop the bridge node's LaunchAgent (%s). It will keep " + "Matter bridge: could not stop the bridge node's LaunchAgent (%s). It will keep " "running with nothing to export, which is harmless — pairings are untouched.", exc) async def _stop_agent_off_loop(self) -> None: @@ -556,7 +556,7 @@ def _pending_replace_all(self) -> int: return int(raw or 0) except (TypeError, ValueError) as exc: self._logger.warning( - "Matter export: the outstanding un-export count in prefs is unreadable (%r: %s) — " + "Matter bridge: the outstanding un-export count in prefs is unreadable (%r: %s) — " "treating it as nothing owed. If accessories from a previous session are still " "showing in a paired ecosystem, re-export one device and remove it again to " "re-record the removal.", raw, exc) @@ -602,7 +602,7 @@ def _record_pending_replace_all(self, removing: int) -> None: self._save_prefs() except Exception as exc: # pylint: disable=broad-except self._logger.warning( - "Matter export: could not record that the un-export is outstanding (%s). " + "Matter bridge: could not record that the un-export is outstanding (%s). " "If it does not complete now, exported accessories may linger.", exc) def _clear_pending_replace_all(self) -> None: @@ -623,7 +623,7 @@ def _clear_pending_replace_all(self) -> None: # it has been paid, so the next attach will carry `replace_all` # again for an un-export that already happened. self._logger.warning( - "Matter export: the un-export completed but the outstanding-work flag could " + "Matter bridge: the un-export completed but the outstanding-work flag could " "not be cleared (%s). A later reconnect may repeat the removal request; " "nothing extra is removed by it.", exc) @@ -658,7 +658,7 @@ def _replace_all_then_stop(self, removing: int) -> None: # not land include the plugin being reloaded and the Mac losing power # mid-attach, and neither of those reaches an `except`. self._record_pending_replace_all(removing) - self._logger.info("Matter export: allow-list is now empty — removing every " + self._logger.info("Matter bridge: allow-list is now empty — removing every " "exported accessory (pairings are kept)") async def _un_export() -> None: @@ -670,7 +670,7 @@ async def _un_export() -> None: self._clear_pending_replace_all() except Exception as exc: # pylint: disable=broad-except self._logger.warning( - "Matter export: could not tell the bridge node the export list is empty " + "Matter bridge: could not tell the bridge node the export list is empty " "(%s). Accessories will LINGER in paired ecosystems until this is finished. " "It is recorded and will be finished automatically the next time the plugin " "connects to the node — which is when you next export a device, or when the " @@ -765,7 +765,7 @@ def _warn_if_wholly_unbridgeable(self, declared: int, specs: list) -> None: return self._wholly_unbridgeable = reasons self._logger.warning( - "Matter export: NONE of the %d device(s) in the export list can be bridged right " + "Matter bridge: NONE of the %d device(s) in the export list can be bridged right " "now — %s. Every exported accessory is being removed from paired ecosystems until " "this is fixed; their endpoint numbers are kept, so putting the devices right " "brings the same accessories back rather than new ones.", @@ -841,7 +841,7 @@ def _skip(self, device_id: int, why: str, detail: str = "") -> None: if self._skipped.get(device_id) != why: self._skipped[device_id] = why self._logger.warning( - "Matter export: device %s is in the export list but will NOT be bridged — %s%s.", + "Matter bridge: device %s is in the export list but will NOT be bridged — %s%s.", device_id, why, f" ({detail})" if detail else "") # ------------------------------------------------------------------ @@ -895,7 +895,7 @@ def device_updated(self, orig_dev: Any, new_dev: Any) -> None: if new_dev.id not in self._update_failed: self._update_failed.add(new_dev.id) self._logger.error( - "Matter export: could not work out what changed about %s (id %s, exported " + "Matter bridge: could not work out what changed about %s (id %s, exported " "as %s) — %s. Its accessory will show stale state until this clears.", getattr(new_dev, "name", ""), new_dev.id, entry.role, exc) self._logger.exception(exc) @@ -949,7 +949,7 @@ def _report_stopped_keys(self, dev: Any, role: str, stopped: frozenset) -> None: return self._stopped_keys[dev.id] = stopped self._logger.warning( - "Matter export: device %s (id %s, exported as %s) stopped reporting %s — paired " + "Matter bridge: device %s (id %s, exported as %s) stopped reporting %s — paired " "ecosystems will keep showing the last known value for it until it reports again.", getattr(dev, "name", ""), dev.id, role, ", ".join(sorted(stopped))) @@ -977,30 +977,30 @@ def _live_client(self, what: str, device_id: int) -> Optional[BridgeClient]: client = self.client if client is None: self._logger.debug( - "Matter export: no bridge client; dropping %s for device %s", what, device_id) + "Matter bridge: no bridge client; dropping %s for device %s", what, device_id) return None if client.attached: return client if client.halted: - self._logger.debug("Matter export: bridge client halted; dropping %s for device %s", + self._logger.debug("Matter bridge: bridge client halted; dropping %s for device %s", what, device_id) if not self._halted_reported: self._halted_reported = True self._logger.warning( - "Matter export: the bridge client is HALTED (%s) — device %s and everything " + "Matter bridge: the bridge client is HALTED (%s) — device %s and everything " "after it is NOT reaching any ecosystem, and nothing will retry on its own.", client.halted_reason or "no reason recorded", device_id) elif client.recovery: - self._logger.debug("Matter export: bridge in recovery; dropping %s for device %s", + self._logger.debug("Matter bridge: bridge in recovery; dropping %s for device %s", what, device_id) if not self._recovery_reported: self._recovery_reported = True self._logger.warning( - "Matter export: the bridge node is awaiting an endpoint-map rebuild — " + "Matter bridge: the bridge node is awaiting an endpoint-map rebuild — " "device %s and everything after it is NOT reaching any ecosystem.", device_id) else: self._logger.debug( - "Matter export: bridge node not attached; dropping %s for device %s " + "Matter bridge: bridge node not attached; dropping %s for device %s " "(the next attach reconciles it)", what, device_id) return None @@ -1083,14 +1083,14 @@ def on_command(self, command: bridge_protocol.BridgeCommand) -> None: if entry is None: # PRD §7 race row: the endpoint outlived the allow-list entry. self._logger.warning( - "Matter export: the bridge node sent %r for Indigo device %s, which is not " + "Matter bridge: the bridge node sent %r for Indigo device %s, which is not " "exported — ignoring. The accessory should disappear at the next reconnect.", command.command, device_id) return handler = export_handlers.handler_for(entry.role) if handler is None: self._logger.warning( - "Matter export: %r arrived for device %s exported as %s, a role this version " + "Matter bridge: %r arrived for device %s exported as %s, a role this version " "cannot bridge — ignoring.", command.command, device_id, entry.role) return self._fire(self._dispatch_off_loop(command, entry, handler), @@ -1139,7 +1139,7 @@ async def _dispatch_off_loop(self, command, entry, handler) -> None: await asyncio.wait_for(asyncio.shield(future), timeout=COMMAND_TIMEOUT) except asyncio.TimeoutError: self._logger.error( - "Matter export: %r for device %s (%s) has not returned after %.0fs. The command " + "Matter bridge: %r for device %s (%s) has not returned after %.0fs. The command " "worker is single-threaded, so every §5 command after it is queued behind this " "one — check whether that Indigo device or its plugin is responding. The " "ecosystem that sent it already shows it as done and NOTHING will correct that " @@ -1153,7 +1153,7 @@ def _note_command_done(self, future) -> None: return exc = future.exception() if exc is not None: - self._logger.warning("Matter export: a §5 command dispatch failed — %s", exc) + self._logger.warning("Matter bridge: a §5 command dispatch failed — %s", exc) def _command_worker(self): """The single thread §5 commands are applied on. Built on first use. @@ -1171,7 +1171,7 @@ def _command_worker(self): # A coroutine that was already queued on the loop when `stop()` ran # would otherwise build a brand-new worker thread underneath a # bridge that has shut down, and nothing would ever join it. - self._logger.debug("Matter export: command worker requested after stop; ignoring") + self._logger.debug("Matter bridge: command worker requested after stop; ignoring") return None if self._executor is None: self._executor = self._executor_factory() @@ -1183,14 +1183,14 @@ def _apply_command(self, command: bridge_protocol.BridgeCommand, entry, handler) dev = self._device_getter(device_id) if dev is None: self._logger.warning( - "Matter export: %r arrived for device %s, which no longer exists in Indigo — " + "Matter bridge: %r arrived for device %s, which no longer exists in Indigo — " "ignoring.", command.command, device_id) return try: outcome = handler.dispatch(command.command, command.args, dev, entry.options) except Exception as exc: # pylint: disable=broad-except self._logger.error( - "Matter export: %r failed for device %s (%s) with args %r — %s. The ecosystem " + "Matter bridge: %r failed for device %s (%s) with args %r — %s. The ecosystem " "still shows the state it asked for; pushing the real one back.", command.command, device_id, entry.role, command.args, exc) self._logger.exception(exc) @@ -1198,7 +1198,7 @@ def _apply_command(self, command: bridge_protocol.BridgeCommand, entry, handler) return if outcome is False: self._logger.warning( - "Matter export: the bridge node sent %r for device %s (%s), which that role " + "Matter bridge: the bridge node sent %r for device %s (%s), which that role " "does not define — ignoring.", command.command, device_id, entry.role) elif isinstance(outcome, str): self._report_no_op(command.command, device_id, entry.role, outcome) @@ -1223,7 +1223,7 @@ def _report_no_op(self, command: str, device_id: int, role: str, reason: str) -> return self._no_op_reported[device_id] = reason self._logger.warning( - "Matter export: %r reached device %s (exported as %s) but changed nothing — %s.", + "Matter bridge: %r reached device %s (exported as %s) but changed nothing — %s.", command, device_id, role, reason) def _correct(self, handler, dev: Any, device_id: int, @@ -1254,7 +1254,7 @@ def _correct(self, handler, dev: Any, device_id: int, states = handler.states_for(dev, options) except Exception as exc: # pylint: disable=broad-except self._logger.warning( - "Matter export: could not read device %s back to correct the ecosystem (%s) — " + "Matter bridge: could not read device %s back to correct the ecosystem (%s) — " "it will show the failed command's state until the next attach.", device_id, exc) return if not states: @@ -1263,7 +1263,7 @@ def _correct(self, handler, dev: Any, device_id: int, # return. It is the same absence `diff_with_gaps` reports: a lock # whose `onState` is None has no truth to tell. self._logger.warning( - "Matter export: cannot push truth for device %s — it reports no readable state " + "Matter bridge: cannot push truth for device %s — it reports no readable state " "at all, so the ecosystem keeps showing the command that failed.", device_id) return self._note_pushed(device_id, states) @@ -1303,7 +1303,7 @@ def _on_attached(self, status, carried_replace_all: bool = False) -> None: # both sides are present and the node can simply be asked. Fire-and- # forget: a readout is not worth blocking a handshake for. self._fire(self._refresh_pairing_window(), "reading the bridge node's pairing window") - self._logger.info("Matter export: bridge node attached — %d endpoint(s) live, %s", + self._logger.info("Matter bridge: bridge node attached — %d endpoint(s) live, %s", status.endpoint_count, "commissioned" if status.commissioned else "not yet paired") self._report_node_warnings(status) @@ -1311,7 +1311,7 @@ def _on_attached(self, status, carried_replace_all: bool = False) -> None: owed = self._pending_replace_all() self._clear_pending_replace_all() self._logger.info( - "Matter export: the outstanding un-export completed — %d accessory record(s) " + "Matter bridge: the outstanding un-export completed — %d accessory record(s) " "removed from the bridge node; paired ecosystems will drop them.", owed) if len(self._store) == 0: # XG5 again: nothing is exported, so nothing needs a socket — @@ -1344,7 +1344,7 @@ def _report_node_warnings(self, status) -> None: return self._node_warnings = warnings for warning in sorted(warnings): - self._logger.warning("Matter export: the bridge node reports — %s", warning) + self._logger.warning("Matter bridge: the bridge node reports — %s", warning) def _on_attach_refused(self, code: str, details: str) -> None: """Surface a refusal with its remedy. The client has already triaged it. @@ -1365,7 +1365,7 @@ def _on_attach_refused(self, code: str, details: str) -> None: # on the way. if bridge_protocol.REFUSE_IDENTITY_UNREADABLE in details: self._logger.error( - "Matter export: the bridge node is serving NOTHING because its identity file " + "Matter bridge: the bridge node is serving NOTHING because its identity file " "is unreadable (%s). Rebuilding the endpoint map will NOT fix this and the " "node refuses to try — the unusable file was moved aside as " "identity.json.unreadable- in the bridge storage folder. Restore " @@ -1374,7 +1374,7 @@ def _on_attach_refused(self, code: str, details: str) -> None: details) return self._logger.error( - "Matter export: the bridge node is serving NOTHING because its endpoint-number " + "Matter bridge: the bridge node is serving NOTHING because its endpoint-number " "map is unreadable (%s). Nothing will be exported until it is rebuilt (Plugins " "▸ Matter ▸ Rebuild Matter Endpoint Map…). The rebuild renumbers nothing: if " "only the map file was damaged no paired ecosystem will see any change, and if " @@ -1385,12 +1385,12 @@ def _on_attach_refused(self, code: str, details: str) -> None: if self._refusal_reported == code: return self._refusal_reported = code - self._logger.error("Matter export: the bridge node refused the connection (%s: %s). " + self._logger.error("Matter bridge: the bridge node refused the connection (%s: %s). " "Nothing is being exported.", code, details) def _on_version_skew(self, hello) -> None: self._logger.error( - "Matter export: the bridge node speaks protocol version %s, this plugin speaks %s " + "Matter bridge: the bridge node speaks protocol version %s, this plugin speaks %s " "(node %s). Export is STOPPED and pairings are untouched — restart the bridge agent " "so it picks up the node that ships with this plugin.", hello.protocol_version, bridge_protocol.PROTOCOL_VERSION, hello.bridge_version) @@ -1421,7 +1421,7 @@ def _on_drift_detected(self, drift: list) -> None: return self._drift_reported = seen self._logger.error( - "Matter export: endpoint-number DRIFT detected — %s. Exported accessories may have " + "Matter bridge: endpoint-number DRIFT detected — %s. Exported accessories may have " "swapped identities in paired ecosystems. Bridge nodes 0.8.0 and newer adopt a " "factory reset's own renumbering automatically, so on a current node persistent " "drift means the bridge's storage changed OUTSIDE any reset — treat it as a real " @@ -1444,13 +1444,13 @@ def _on_fabrics_changed(self, fabrics: list, change: str) -> None: self.fabrics = list(fabrics) described = ", ".join(_describe_fabric(fabric) for fabric in fabrics) or "none" self._logger.info( - "Matter export: the bridge node's paired ecosystems changed (%s) — now paired with: %s", + "Matter bridge: the bridge node's paired ecosystems changed (%s) — now paired with: %s", change or "changed", described) def _on_commissioned(self) -> None: """First fabric (§5 ``commissioned``) — a transition, not a repeat.""" self._logger.info( - "Matter export: the Matter bridge has been PAIRED for the first time. Exported " + "Matter bridge: the Matter bridge has been PAIRED for the first time. Exported " "accessories should now appear in that ecosystem. To add a second ecosystem, use " "Plugins ▸ Matter ▸ Pair Matter Bridge… — the original pairing code no longer works.") @@ -1465,7 +1465,7 @@ def _on_decommissioned(self) -> None: self.fabrics = [] self.window_expires_at = None self._logger.warning( - "Matter export: the Matter bridge is no longer paired with ANY ecosystem. Every " + "Matter bridge: the Matter bridge is no longer paired with ANY ecosystem. Every " "exported accessory has gone with the last fabric. Indigo devices are unaffected; " "use Plugins ▸ Matter ▸ Pair Matter Bridge… to pair it again.") @@ -1473,11 +1473,11 @@ def _on_window_closed(self, reason: str) -> None: """The commissioning window ended (§5 ``window_closed``).""" self.window_expires_at = None if reason == "commissioned": - self._logger.info("Matter export: the pairing window closed — an ecosystem completed " + self._logger.info("Matter bridge: the pairing window closed — an ecosystem completed " "commissioning.") return self._logger.info( - "Matter export: the pairing window has expired without an ecosystem completing " + "Matter bridge: the pairing window has expired without an ecosystem completing " "commissioning. Open a new one with Plugins ▸ Matter ▸ Pair Matter Bridge… — the " "code it showed is now dead.") @@ -1499,7 +1499,7 @@ async def _refresh_pairing_window(self) -> None: try: pairing = await client.get_pairing() except Exception as exc: # pylint: disable=broad-except - self._logger.debug("Matter export: could not read the pairing window (%s)", exc) + self._logger.debug("Matter bridge: could not read the pairing window (%s)", exc) return self.window_expires_at = pairing.window_expires_at if pairing.window_open else None @@ -1527,7 +1527,7 @@ def _on_unreachable(self, attempts: int) -> None: return self._unreachable_reported = True self._logger.warning( - "Matter export: the bridge node is not responding after %d attempts on port %s. " + "Matter bridge: the bridge node is not responding after %d attempts on port %s. " "Indigo devices and inbound Matter control are unaffected; exported accessories " "will show as unavailable.%s", attempts, @@ -1542,7 +1542,7 @@ def _agent_diagnosis(self) -> str: try: detail = self._agent_diagnose() except Exception as exc: # pylint: disable=broad-except - self._logger.debug("Matter export: the bridge agent diagnostic failed (%s)", exc) + self._logger.debug("Matter bridge: the bridge agent diagnostic failed (%s)", exc) return "" return f" {detail}" if detail else "" @@ -1589,14 +1589,14 @@ def health_tick(self) -> None: if not self._halted_reported: self._halted_reported = True self._logger.warning( - "Matter export: the bridge client is HALTED (%s) — nothing is being exported " + "Matter bridge: the bridge client is HALTED (%s) — nothing is being exported " "and it will not retry on its own.", client.halted_reason or "no reason recorded") return if client.recovery: if not self._recovery_reported: self._recovery_reported = True - self._logger.warning("Matter export: the bridge node is awaiting an endpoint-map " + self._logger.warning("Matter bridge: the bridge node is awaiting an endpoint-map " "rebuild; nothing is being exported.") return if client.attached: @@ -1605,10 +1605,10 @@ def health_tick(self) -> None: return self._disconnect_ticks += 1 if self._disconnect_ticks == DISCONNECT_WARN_TICKS: - self._logger.warning("Matter export: still not attached to the bridge node after " + self._logger.warning("Matter bridge: still not attached to the bridge node after " "~1 min") else: - self._logger.debug("Matter export: bridge node not currently attached") + self._logger.debug("Matter bridge: bridge node not currently attached") def _poll_node_status(self, client) -> None: """Ask the node how it is, and say what it answers (§3.6 → §4.3). @@ -1628,7 +1628,7 @@ async def _poll() -> None: try: status = await client.get_status() except Exception as exc: # pylint: disable=broad-except - self._logger.debug("Matter export: status poll failed (%s)", exc) + self._logger.debug("Matter bridge: status poll failed (%s)", exc) return self._report_node_warnings(status) @@ -1650,7 +1650,7 @@ def _check_command_queue(self) -> None: return self._queue_warned = True self._logger.warning( - "Matter export: %d ecosystem command(s) are queued on the command worker and not " + "Matter bridge: %d ecosystem command(s) are queued on the command worker and not " "completing. They run one at a time, so something at the front is not returning — " "the timeout line above names it.", outstanding) @@ -1680,10 +1680,10 @@ def _fire(self, coro, what: str, lost: str = "") -> bool: except Exception as exc: # pylint: disable=broad-except coro.close() if lost: - self._logger.warning("Matter export: could not schedule %s (%s) — %s.", + self._logger.warning("Matter bridge: could not schedule %s (%s) — %s.", what, exc, lost) else: - self._logger.debug("Matter export: could not schedule %s (%s)", what, exc) + self._logger.debug("Matter bridge: could not schedule %s (%s)", what, exc) return False future.add_done_callback(lambda fut: self._log_future(fut, what)) return True @@ -1693,7 +1693,7 @@ def _log_future(self, future, what: str) -> None: return exc = future.exception() if exc is not None: - self._logger.warning("Matter export: %s failed — %s", what, exc) + self._logger.warning("Matter bridge: %s failed — %s", what, exc) #: Matter vendor IDs whose ecosystems a user is likely to recognise. @@ -1798,7 +1798,7 @@ def _indigo_device(device_id: int, logger: Any = None) -> Any: except Exception as exc: # pylint: disable=broad-except if logger is not None: logger.warning( - "Matter export: could not read Indigo device %s (%s: %s). This is NOT the device " + "Matter bridge: could not read Indigo device %s (%s: %s). This is NOT the device " "having been deleted — it is Indigo failing to answer.", device_id, type(exc).__name__, exc) return None diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py b/indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py index f8fdfb5..f5a485e 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py @@ -236,7 +236,7 @@ def classify(dev, plugin_id: str = DEFAULT_PLUGIN_ID) -> Verdict: return Excluded(REASON_NO_ROLE) return handler(dev) except Exception as exc: # pylint: disable=broad-except - _LOG.error("Matter export: could not classify a device for export — %s", exc, + _LOG.error("Matter bridge: could not classify a device for export — %s", exc, exc_info=True) return Excluded(REASON_DEVICE_ERROR) diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/fabric_backup.py b/indigo-matter.indigoPlugin/Contents/Server Plugin/fabric_backup.py index 9b87d00..61ac9ca 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/fabric_backup.py +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/fabric_backup.py @@ -147,7 +147,7 @@ def create_backup(storage_path: str, *, now: datetime, logger: Optional[Any] = N # which is exactly the archive they will reach for after # losing those two files. log.warning( - "Fabric backup: no Matter export bridge storage at %s, so this backup " + "Fabric backup: no Matter bridge storage at %s, so this backup " "does NOT contain identity.json or endpoint-map.json. That is expected " "if you export nothing; if you do export devices, check where the " "bridge node's --storage-path actually points.", bridge_storage_path) @@ -178,7 +178,7 @@ def create_backup(storage_path: str, *, now: datetime, logger: Optional[Any] = N # rather than configured, so "which directory did this actually cover?" # is a question the log should answer without anybody having to guess. log.info("Fabric backup written: %s (%d member(s), including %d from the Matter " - "export bridge node at %s)", archive_path, members_written, bridge_members, + "bridge node at %s)", archive_path, members_written, bridge_members, bridge_storage_path) else: log.info("Fabric backup written: %s (%d member(s))", archive_path, members_written) @@ -328,7 +328,7 @@ def restore_backup( skipped = bridge_members_in(archive_path) if skipped: log.warning( - "This backup also contains %d file(s) from the Matter export bridge node. They are " + "This backup also contains %d file(s) from the Matter bridge node. They are " "NOT being restored: the bridge node has to be stopped first and this plugin cannot " "stop it yet. If you need them, stop the bridge node and extract the '%s' entries of " "%s over %s by hand. Restoring the controller fabric only.", diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/launch_agent.py b/indigo-matter.indigoPlugin/Contents/Server Plugin/launch_agent.py index 163dd11..5106e35 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/launch_agent.py +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/launch_agent.py @@ -122,7 +122,7 @@ class AgentSpec: item. Every message that tells a user to run it interpolated :attr:`package` instead, producing "Plugins ▸ Matter ▸ Install/update indigo-matter-bridge" — a menu that does not exist (the real one is - "Install/update the Matter export bridge"). It fires on the first-run + "Install/update the Matter bridge"). It fires on the first-run path, where the user is already stuck, so a menu name they cannot find is the difference between a fixable state and a support thread. Blank falls back to the old wording via :attr:`install_menu_name`, which is @@ -1187,7 +1187,7 @@ def _warn_on_argument_drift(self, running_args: list[str]) -> None: "the running %s was started with different arguments than the " "current settings would use — it is serving STALE configuration. Running: " "%s. Expected: %s. Reload the plugin (or Plugins ▸ Matter ▸ Restart " - "matter-server) to apply the current settings.", + "the Matter controller) to apply the current settings.", self.spec.package, " ".join(running_args), " ".join(desired), ) diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py b/indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py index f5a3178..3555366 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py @@ -480,7 +480,7 @@ def _issue_device_subscription(self) -> bool: try: indigo.devices.subscribeToChanges() except Exception as exc: # noqa: BLE001 - self.logger.error("Matter export: could not subscribe to Indigo device changes — " + self.logger.error("Matter bridge: could not subscribe to Indigo device changes — " "exported accessories will not follow Indigo state. %s", exc) self.logger.exception(exc) return False @@ -525,7 +525,7 @@ def _resubscribe_tick(self) -> None: if not self._resubscribe_gave_up: self._resubscribe_gave_up = True self.logger.warning( - "Matter export: no Indigo device update has arrived since exporting, after " + "Matter bridge: no Indigo device update has arrived since exporting, after " "re-issuing subscribeToChanges %d times. Unless the house is simply idle, " "exported accessories are NOT following Indigo state and nothing further will " "retry on its own — reload the plugin.", MAX_RESUBSCRIBE_ATTEMPTS) @@ -542,7 +542,7 @@ def _resubscribe_tick(self) -> None: # subscription that never registered — and after that the evidence is # indistinguishable from nothing having happened. self.logger.debug( - "Matter export: no device updates since subscribing; re-issuing " + "Matter bridge: no device updates since subscribing; re-issuing " "subscribeToChanges (attempt %d of %d)", self._resubscribe_attempts, MAX_RESUBSCRIBE_ATTEMPTS) self._issue_device_subscription() @@ -576,7 +576,7 @@ def deviceUpdated(self, origDev, newDev): # noqa: N802 if newDev.id not in self._export_callback_failed: self._export_callback_failed.add(newDev.id) self.logger.error( - "Matter export: the update of %s (id %s) could not be handed to the bridge " + "Matter bridge: the update of %s (id %s) could not be handed to the bridge " "— %s. Its accessory will show stale state until this clears.", getattr(newDev, "name", ""), newDev.id, exc) self.logger.exception(exc) @@ -590,13 +590,13 @@ def deviceDeleted(self, dev): # noqa: N802 return try: self.exports.remove(dev.id) - self.logger.info("Removed Matter export: %s (id %s) — the Indigo device was deleted", + self.logger.info("Removed Matter export for %s (id %s) — the Indigo device was deleted", getattr(dev, "name", ""), dev.id) except Exception as exc: # noqa: BLE001 # The store rolled back, so the entry survives; the endpoint removal # below is still right (the device is gone either way) and the # startup sweep will report the orphan. - self.logger.error("Matter export: removing the deleted device %s from the export " + self.logger.error("Matter bridge: removing the deleted device %s from the export " "list FAILED — %s", dev.id, exc) self.logger.exception(exc) self._export_callback_failed.discard(dev.id) @@ -720,8 +720,8 @@ def _on_server_unreachable(self, attempts: int) -> None: # storage lock. The plugin now reaps such strays on start/restart; point # the user at that in case a reap couldn't run (e.g. ps unavailable). hint = ("\nAnother matter-server appears to be holding the storage lock. " - "Use Plugins ▸ Matter ▸ Restart matter-server (it stops stray " - "servers), or reboot the Mac if it persists.") + "Use Plugins ▸ Matter ▸ Restart the Matter controller (it stops " + "stray servers), or reboot the Mac if it persists.") self.logger.error( "matter-server is not responding after %d attempts and appears to be " "crashing. Recent matter-server errors:\n%s%s", attempts, tail, hint, @@ -730,7 +730,8 @@ def _on_server_unreachable(self, attempts: int) -> None: self.logger.error( "matter-server is not responding after %d attempts and its error log is " "empty — it may not be installed (checked %s). Use Plugins ▸ Matter ▸ " - "Install/update matter-server, then restart the plugin.", + "Install/update the Matter controller (matter-server), then restart the " + "plugin.", attempts, sp.project_dir, ) @@ -811,7 +812,7 @@ def get_prefs_config_ui_values(self): return self.getPrefsConfigUiValues() def _export_readout(self) -> str: - """One line describing the export bridge for the config dialog (PRD §5.5). + """One line describing the Matter bridge for the config dialog (PRD §5.5). What the PRD asks for is *which* ecosystems hold a fabric and whether a window is open — not slot arithmetic. matter.js allows 254 fabrics, so @@ -890,7 +891,7 @@ def closedPrefsConfigUi(self, valuesDict, userCancelled): # noqa: N802 # snapshot, so a changed location/host only takes effect on reload. self.logger.info( "matter-server settings saved — reload the plugin (or Plugins ▸ Matter ▸ " - "Restart matter-server) to apply them" + "Restart the Matter controller) to apply them" ) # Export is the exception: its switch and its ports are read on every # connect, and the ONE change a user expects to act immediately is @@ -904,7 +905,7 @@ def closedPrefsConfigUi(self, valuesDict, userCancelled): # noqa: N802 # (⊗ `test_saving_config_applies_the_export_switch_immediately`). if self.export_bridge is None: self.logger.debug( - "Matter export: the export engine is not running, so the export switch will take " + "Matter bridge: the export engine is not running, so the export switch will take " "effect when the plugin next starts.") return try: @@ -913,7 +914,7 @@ def closedPrefsConfigUi(self, valuesDict, userCancelled): # noqa: N802 # A bare traceback here reads as a crash in "save settings". Say what # did not happen and what to do instead — the prefs ARE saved. self.logger.error( - "Matter export: your settings were saved, but applying the export switch " + "Matter bridge: your settings were saved, but applying the export switch " "immediately FAILED (%s). Reload the plugin to apply it.", exc) self.logger.exception(exc) @@ -1351,8 +1352,9 @@ def _install_matter_server(self, clean: bool = False) -> None: return if not sp.install(): self.logger.error( - "Install/update matter-server did not complete — see the error " - "above. The server was not (re)installed; retry when resolved." + "Install/update of the Matter controller (matter-server) did not " + "complete — see the error above. The server was not (re)installed; " + "retry when resolved." ) return if self._stopping: # plugin is tearing down — don't mutate its state @@ -1374,8 +1376,8 @@ def _install_matter_server(self, clean: bool = False) -> None: self.logger.error( "matter-server was installed and pinned to node at %s, but the " "restart onto the new version FAILED — the old version may still be " - "running. Use Plugins ▸ Matter ▸ Restart matter-server, or reload " - "the plugin.", sp.resolved_bin_dir, + "running. Use Plugins ▸ Matter ▸ Restart the Matter controller, or " + "reload the plugin.", sp.resolved_bin_dir, ) return self.logger.info( @@ -1390,7 +1392,7 @@ def _install_matter_server(self, clean: bool = False) -> None: "matter-server install did not complete after the npm step — the " "package may be installed but the node was not pinned and the server " "was not (re)started. See the trace above, then retry Plugins ▸ Matter " - "▸ Install/update matter-server." + "▸ Install/update the Matter controller (matter-server)." ) def menuReinstallMatterServerClean(self, valuesDict, menuId=""): # noqa: N802, ARG002 @@ -1819,7 +1821,7 @@ def _log_row_failure(self, exc, first: bool) -> None: if first: self.logger.exception(exc) else: - self.logger.error("Matter export: another device could not be read — %s", exc) + self.logger.error("Matter bridge: another device could not be read — %s", exc) @staticmethod def _candidate_row(dev, name: str, plugin_id: str, exported) -> Optional[tuple[str, str]]: @@ -2110,12 +2112,12 @@ def exportAddOrUpdate(self, valuesDict, typeId="", devId=0): except Exception as exc: # pylint: disable=broad-except # The store rolled back, so nothing was saved — say so rather than # reporting the success the old code reported unconditionally. - self.logger.error("Matter export: saving the export list FAILED — %s", exc) + self.logger.error("Matter bridge: saving the export list FAILED — %s", exc) self.logger.exception(exc) values["exportStatus"] = "FAILED to save the export list — see Event Log" return values verb = "Updated" if existed else "Added" - self.logger.info("%s Matter export: %s (id %s) as %s%s", + self.logger.info("%s Matter export for %s (id %s) as %s%s", verb, dev.name, device_id, role, f' named "{name_override}"' if name_override else "") self._nudge_export(device_id, role_changed=role_changed) @@ -2168,7 +2170,7 @@ def exportRemove(self, valuesDict, typeId="", devId=0): try: removed = self.exports.remove(device_id) except Exception as exc: # pylint: disable=broad-except - self.logger.error("Matter export: saving the export list FAILED — %s", exc) + self.logger.error("Matter bridge: saving the export list FAILED — %s", exc) self.logger.exception(exc) values["exportStatus"] = "FAILED to save the export list — see Event Log" return values @@ -2177,7 +2179,7 @@ def exportRemove(self, valuesDict, typeId="", devId=0): return values dev = self._indigo_device(device_id) name = str(getattr(dev, "name", "") or "") if dev is not None else f"device {device_id}" - self.logger.info("Removed Matter export: %s (id %s)", name, device_id) + self.logger.info("Removed Matter export for %s (id %s)", name, device_id) # XAC7: the accessory has to leave every paired ecosystem, not just the # allow-list. Order matters — remove the endpoint BEFORE the empty # allow-list stops the client out from under it. @@ -2208,7 +2210,7 @@ def _recovery_client(self, errors, field: str): bridge = self.export_bridge client = bridge.client if bridge is not None else None if client is None or not client.connected: - msg = ("Not connected to the Matter export bridge node. Start it (it is launched by " + msg = ("Not connected to the Matter bridge node. Start it (it is launched by " "hand in this build), export at least one device so the plugin connects, then " "try again.") self.logger.warning(msg) @@ -2244,7 +2246,7 @@ def menuRebuildEndpointMap(self, valuesDict, menuId=""): # noqa: N802, ARG002 # every export that is not currently live — §3.3 keeps them exactly # so that re-adding a device restores its accessory — and there is # nothing to recover from in the first place. - msg = ("Matter export: the bridge node is NOT refusing to serve endpoints, so there " + msg = ("Matter bridge: the bridge node is NOT refusing to serve endpoints, so there " "is nothing to rebuild. Rebuilding anyway would discard the retained endpoint " "numbers of every device that is not currently exported, and those are what " "make re-adding one restore the same accessory.") @@ -2263,31 +2265,31 @@ def menuRebuildEndpointMap(self, valuesDict, menuId=""): # noqa: N802, ARG002 # Never report success over a rebuild that did not persist: the node # answers with an error rather than a StatusReport when the new map # could not be written, and the refusal is still in force. - self.logger.error("Matter export: rebuilding the endpoint map FAILED — %s. The bridge " + self.logger.error("Matter bridge: rebuilding the endpoint map FAILED — %s. The bridge " "node is unchanged and still refusing to export.", exc) self.logger.exception(exc) errors["confirm"] = "Rebuild failed — see the log. Nothing was changed." return (False, valuesDict, errors) self.logger.warning( - "Matter export: endpoint map REBUILT — the bridge node has stopped refusing and is " + "Matter bridge: endpoint map REBUILT — the bridge node has stopped refusing and is " "serving %d endpoint(s). Nothing was renumbered. If only the map file was damaged, " "no paired ecosystem will see any change; if the bridge's Matter storage was lost, " "your ecosystems already hold dead accessories under the old numbers — delete those " "by hand. Do NOT run this again for the same fault.", status.endpoint_count) for warning in status.warnings: - self.logger.warning("Matter export: the bridge node reports — %s", warning) + self.logger.warning("Matter bridge: the bridge node reports — %s", warning) if not client.attached: # The rebuild stands; only the connection step after it did not. The # client's own triage has already named the reason at error level. self.logger.warning( - "Matter export: the rebuild succeeded but re-attaching to the bridge node did " + "Matter bridge: the rebuild succeeded but re-attaching to the bridge node did " "not. The rebuild does NOT need repeating — exports resume when the connection " "does, and the reason is logged above.") return (True, valuesDict) def menuResetBridgePairings(self, valuesDict, menuId=""): # noqa: N802, ARG002 - """§3.10 — wipe the export bridge's commissioning and re-advertise.""" + """§3.10 — wipe the Matter bridge's commissioning and re-advertise.""" errors = indigo.Dict() # Two boxes, deliberately. This is the only plugin action that destroys # every ecosystem pairing at once, and it is irreversible without @@ -2306,19 +2308,19 @@ def menuResetBridgePairings(self, valuesDict, menuId=""): # noqa: N802, ARG002 # "the map itself is corrupt" path is the rebuild above. self.runtime.submit(client.factory_reset(True)).result(timeout=FACTORY_RESET_TIMEOUT) except Exception as exc: # noqa: BLE001 - self.logger.error("Matter export: resetting the bridge pairings FAILED — %s. " + self.logger.error("Matter bridge: resetting the bridge pairings FAILED — %s. " "Pairings are unchanged.", exc) self.logger.exception(exc) errors["confirmAgain"] = "Reset failed — see the log. Pairings were not changed." return (False, valuesDict, errors) self.logger.warning( - "Matter export: the bridge node's pairings have been RESET. It is advertising for " + "Matter bridge: the bridge node's pairings have been RESET. It is advertising for " "commissioning again — pair it from each ecosystem, and remove the now-dead Indigo " "bridge from any ecosystem that still lists it.") return (True, valuesDict) # ------------------------------------------------------------------ - # The export bridge node's LaunchAgent (E7 — PRD §4.2, XG5, XAC1) + # The Matter bridge node's LaunchAgent (E7 — PRD §4.2, XG5, XAC1) # ------------------------------------------------------------------ def _start_bridge_agent(self) -> None: """Install (if needed) and start the bridge node's LaunchAgent. @@ -2349,18 +2351,18 @@ def _start_bridge_agent(self) -> None: # was reported here as "bridge node LaunchAgent is running". Ask launchd. state = agent.run_state() if state == agent.RUNNING: - self.logger.info("Matter export: bridge node LaunchAgent is running (protocol port %s, " + self.logger.info("Matter bridge: bridge node LaunchAgent is running (protocol port %s, " "Matter port %s)", agent.ws_port, agent.matter_port) elif state == agent.UNKNOWN: # A pid line we could not parse. It may well be serving; what we must # not do is assert either way. self.logger.info( - "Matter export: the bridge node's LaunchAgent is loaded (protocol port %s, Matter " + "Matter bridge: the bridge node's LaunchAgent is loaded (protocol port %s, Matter " "port %s); launchd did not report a readable pid, so whether the process is up " "will show as the plugin connects — or fails to.", agent.ws_port, agent.matter_port) else: self.logger.error( - "Matter export: the bridge node's LaunchAgent %s. Exported accessories will be " + "Matter bridge: the bridge node's LaunchAgent %s. Exported accessories will be " "unavailable until it does. %s", "did not start" if state == agent.LOADED_NOT_RUNNING else "could not be loaded by launchd", @@ -2397,7 +2399,7 @@ def _stop_bridge_agent(self) -> None: # retried: the node kept serving every paired ecosystem with the log # asserting the opposite by omission. self.logger.warning( - "Matter export: nothing is exported, but the bridge node's LaunchAgent could not " + "Matter bridge: nothing is exported, but the bridge node's LaunchAgent could not " "be %s (%s). It keeps running and serving every paired ecosystem, and NOTHING " "retries this on its own — reload the plugin, or run 'launchctl bootout " "gui/$(id -u)/%s' and delete that file by hand.", @@ -2406,14 +2408,14 @@ def _stop_bridge_agent(self) -> None: return if was_loaded: self.logger.info( - "Matter export: nothing is exported — the bridge node has been stopped and its " + "Matter bridge: nothing is exported — the bridge node has been stopped and its " "LaunchAgent removed, so a restart of this Mac cannot bring it back. Its pairings " "are kept.") else: # The two Falses `stop()` conflated: this one is "there was no job", # which is not a failure and must not be reported as one. self.logger.debug( - "Matter export: nothing is exported and no bridge node LaunchAgent was loaded; " + "Matter bridge: nothing is exported and no bridge node LaunchAgent was loaded; " "any plist has been removed. Pairings are kept.") def _bridge_agent_diagnosis(self) -> Optional[str]: @@ -2495,7 +2497,7 @@ def _run_bridge_install(self, *, clean: bool) -> bool: self.logger.warning("An npm install is already in progress — wait for it to finish.") return False self.logger.info( - "%s the Matter export bridge node in the background — watch the log for progress; " + "%s the Matter bridge node in the background — watch the log for progress; " "this can take a minute.", "Removing and reinstalling" if clean else "Installing") self._install_thread = threading.Thread( @@ -2507,7 +2509,7 @@ def _run_bridge_install(self, *, clean: bool) -> bool: def menuStopBridgeNode(self, valuesDict, menuId=""): # noqa: N802, ARG002 """Stop the bridge node and remove its LaunchAgent, by hand. - The controller has "Restart matter-server"; the bridge had nothing at + The controller has "Restart the Matter controller"; the bridge had nothing at all, because it is started and stopped by the allow-list. That leaves one state with no UI: a user who disables the plugin (or whose plugin dies mid-session) has a node still running and still serving every paired @@ -2558,12 +2560,12 @@ def _install_bridge_node(self, clean: bool = False) -> None: # remove_package has said what is still there. Installing over a # wedged install is what the clean variant exists to avoid. self.logger.error( - "Clean reinstall of the Matter export bridge ABANDONED — the old package " + "Clean reinstall of the Matter bridge ABANDONED — the old package " "could not be removed, so nothing was reinstalled over it.") return if not agent.install(): self.logger.error( - "Install/update of the Matter export bridge did not complete — see the error " + "Install/update of the Matter bridge did not complete — see the error " "above. Nothing was changed; retry when resolved.") return if self._stopping: # plugin is tearing down — don't mutate its state @@ -2571,7 +2573,7 @@ def _install_bridge_node(self, clean: bool = False) -> None: self.bridge_process = bridge_agent.BridgeProcess(dict(self.pluginPrefs), self.logger) if self.exports is None or not len(self.exports): self.logger.info( - "Matter export bridge installed. It is NOT being started: nothing is exported " + "Matter bridge installed. It is NOT being started: nothing is exported " "yet, and the bridge only runs while the export list is non-empty. Add a " "device in 'Manage Matter Exports…' and it will start itself.") return @@ -2583,7 +2585,7 @@ def _install_bridge_node(self, clean: bool = False) -> None: applied = self.bridge_process.ensure_installed() if applied is None: self.logger.error( - "The Matter export bridge was installed, but its LaunchAgent could not be " + "The Matter bridge was installed, but its LaunchAgent could not be " "written — see the reason above. The package is on disk; fix that and reload " "the plugin.") return @@ -2591,7 +2593,7 @@ def _install_bridge_node(self, clean: bool = False) -> None: # left alone by ensure_installed is still executing the OLD version. if applied is False and not self.bridge_process.restart(): self.logger.error( - "The Matter export bridge was installed but the restart onto the new version " + "The Matter bridge was installed but the restart onto the new version " "FAILED — the old version may still be running. Check %s.", os.path.join(self.bridge_process.log_dir, bridge_agent.BRIDGE_ERR_LOG)) return @@ -2603,17 +2605,17 @@ def _install_bridge_node(self, clean: bool = False) -> None: # reached a run loop, so the log must not claim it is reconnecting. poked = self.export_bridge is not None and self.export_bridge.retry_now() if poked: - self.logger.info("Matter export bridge installed and restarted onto the new " + self.logger.info("Matter bridge installed and restarted onto the new " "version — reconnecting now.") else: - self.logger.info("Matter export bridge installed and restarted onto the new " + self.logger.info("Matter bridge installed and restarted onto the new " "version — reload the plugin to reconnect.") except Exception as exc: # noqa: BLE001 self.logger.exception(exc) self.logger.error( - "Install of the Matter export bridge did not complete after the npm step — the " + "Install of the Matter bridge did not complete after the npm step — the " "package may be installed but the agent was not restarted. See the trace above, " - "then retry Plugins ▸ Matter ▸ Install/update the Matter export bridge.") + "then retry Plugins ▸ Matter ▸ Install/update the Matter bridge.") # ------------------------------------------------------------------ # Pairing and fabric management (PRD §6, BRIDGE_PROTOCOL §3.7-§3.9) @@ -2639,7 +2641,7 @@ def _pairing_client(self, errors, field: str): why = ("Export at least one device in 'Manage Matter Exports…' first — the bridge only " "runs while something is exported." if not exported else "The bridge node is not answering; see the log for what its own error log says.") - self.logger.warning("Matter export: cannot reach the bridge node for pairing. %s", why) + self.logger.warning("Matter bridge: cannot reach the bridge node for pairing. %s", why) errors[field] = "Not connected to the bridge node — see the log." return None return client @@ -2670,7 +2672,7 @@ def menuPairMatterBridge(self, valuesDict, menuId=""): # noqa: N802, ARG002 try: pairing = self.runtime.submit(client.get_pairing()).result(timeout=PAIRING_READ_TIMEOUT) except Exception as exc: # noqa: BLE001 - self.logger.error("Matter export: could not read the bridge node's pairing state — %s. " + self.logger.error("Matter bridge: could not read the bridge node's pairing state — %s. " "No pairing window was opened.", exc) self.logger.exception(exc) errors["duration"] = "Could not reach the bridge node — see the log." @@ -2690,7 +2692,7 @@ def menuPairMatterBridge(self, valuesDict, menuId=""): # noqa: N802, ARG002 client.open_commissioning_window(duration)).result(timeout=WINDOW_OPEN_TIMEOUT) except Exception as exc: # noqa: BLE001 self.logger.error( - "Matter export: opening a pairing window FAILED — %s. Nothing was changed and " + "Matter bridge: opening a pairing window FAILED — %s. Nothing was changed and " "existing pairings are untouched. If the bridge says a window is already open, " "wait for it to expire (up to 15 minutes) and try again.", exc) self.logger.exception(exc) @@ -2828,7 +2830,7 @@ def menuUnpairEcosystem(self, valuesDict, menuId=""): # noqa: N802, ARG002 removal = self.runtime.submit( client.remove_fabric(fabric_index)).result(timeout=UNPAIR_TIMEOUT) except Exception as exc: # noqa: BLE001 - self.logger.error("Matter export: unpairing ecosystem %s FAILED — %s. Pairings are " + self.logger.error("Matter bridge: unpairing ecosystem %s FAILED — %s. Pairings are " "unchanged.", fabric_index, exc) self.logger.exception(exc) errors["confirmAgain"] = "Unpair failed — see the log. Nothing was changed." @@ -2843,7 +2845,7 @@ def menuUnpairEcosystem(self, valuesDict, menuId=""): # noqa: N802, ARG002 # answered `{}` either way and the menu logged "has been unpaired. # Every accessory has been removed" over a node-side no-op. self.logger.warning( - "Matter export: ecosystem %s was ALREADY gone from the bridge node — nothing was " + "Matter bridge: ecosystem %s was ALREADY gone from the bridge node — nothing was " "removed by this action, because there was nothing there to remove. It had most " "likely unpaired itself since this dialog was opened. The ecosystem list has been " "refreshed%s.", fabric_index, @@ -2858,13 +2860,13 @@ def menuUnpairEcosystem(self, valuesDict, menuId=""): # noqa: N802, ARG002 # that actually means rather than reporting a routine removal, because # the user has just reset the whole bridge without using the reset menu. self.logger.warning( - "Matter export: ecosystem %s was the LAST one paired, so the bridge node has " + "Matter bridge: ecosystem %s was the LAST one paired, so the bridge node has " "reset itself and is advertising for commissioning again — exactly as 'Reset " - "Matter Export Pairings…' would have done. Nothing in Indigo changed. Use " + "Matter Bridge Pairings…' would have done. Nothing in Indigo changed. Use " "'Pair Matter Bridge…' to pair it again.", fabric_index) else: self.logger.warning( - "Matter export: ecosystem %s has been unpaired. Every accessory Indigo exports " + "Matter bridge: ecosystem %s has been unpaired. Every accessory Indigo exports " "has been removed from it; remove any leftover 'Indigo' bridge entry in that " "ecosystem's app by hand.", fabric_index) return (True, valuesDict) @@ -2888,7 +2890,7 @@ def _refresh_fabric_cache(self, client) -> None: except Exception as exc: # noqa: BLE001 # The removal itself already succeeded or was already true; failing # to re-read the list afterwards is not worth reporting as a failure. - self.logger.debug("Matter export: could not refresh the ecosystem list (%s)", exc) + self.logger.debug("Matter bridge: could not refresh the ecosystem list (%s)", exc) def _is_last_fabric(self, fabric_index: int) -> bool: """Whether removing ``fabric_index`` empties the fabric set. diff --git a/indigo-matter.indigoPlugin/Contents/Server Plugin/server_process.py b/indigo-matter.indigoPlugin/Contents/Server Plugin/server_process.py index 2519357..0eb0062 100644 --- a/indigo-matter.indigoPlugin/Contents/Server Plugin/server_process.py +++ b/indigo-matter.indigoPlugin/Contents/Server Plugin/server_process.py @@ -53,7 +53,7 @@ #: The EXACT wording of ``MenuItems.xml``'s ``installMatterServer`` item, for the #: messages that tell a user to run it. See ``AgentSpec.install_menu``. -INSTALL_MENU = "Install/update matter-server" +INSTALL_MENU = "Install/update the Matter controller (matter-server)" # Records the sha256 of the plist launchd was last told to load (bootstrap). launchd # caches a job's ProgramArguments at bootstrap time — rewriting the plist FILE does not # touch an already-loaded job — so we compare against this to tell "current definition diff --git a/tests/test_plugin_module.py b/tests/test_plugin_module.py index 4f4fb30..0d683f6 100644 --- a/tests/test_plugin_module.py +++ b/tests/test_plugin_module.py @@ -118,20 +118,20 @@ def test_dynamic_list_methods_exist_on_plugin(plugin_cls): ["Manage Matter Exports…", "Pair Matter Bridge…", "Unpair an Ecosystem…"], - # 3 · matter-server - ["Install/update matter-server", - "Restart matter-server", - "Reinstall matter-server (clean)…", - "Open matter-server log…"], - # 4 · the export bridge node - ["Install/update the Matter export bridge", - "Reinstall the Matter export bridge (clean)…", - "Stop the Matter export bridge…"], + # 3 · the Matter controller (matter-server) + ["Install/update the Matter controller (matter-server)", + "Restart the Matter controller", + "Reinstall the Matter controller (clean)…", + "Open the Matter controller log…"], + # 4 · the Matter bridge node + ["Install/update the Matter bridge", + "Reinstall the Matter bridge (clean)…", + "Stop the Matter bridge…"], # 5 · backup and recovery ["Back up the Matter fabric…", "Restore a fabric backup…", "Rebuild Matter Endpoint Map…", - "Reset Matter Export Pairings…"], + "Reset Matter Bridge Pairings…"], ] @@ -166,15 +166,16 @@ def test_menu_is_grouped_into_the_documented_sections(): def test_the_two_install_items_are_separated(): - """#134: 'Install/update matter-server' and 'Install/update the Matter - export bridge' sat eleven apart in one flat list and were clicked for one - another live — reinstalling the inbound controller when the outbound - bridge was meant. They are separately versioned, separately installed npm - packages, so they must not share a section.""" + """#134: 'Install/update the Matter controller (matter-server)' and + 'Install/update the Matter bridge' sat eleven apart in one flat list and + were clicked for one another live — reinstalling the inbound controller + when the outbound bridge was meant. They are separately versioned, + separately installed npm packages, so they must not share a section.""" sections = _menu_sections() - controller = next(i for i, s in enumerate(sections) if "Install/update matter-server" in s) + controller = next(i for i, s in enumerate(sections) + if "Install/update the Matter controller (matter-server)" in s) bridge = next(i for i, s in enumerate(sections) - if "Install/update the Matter export bridge" in s) + if "Install/update the Matter bridge" in s) assert controller != bridge @@ -514,3 +515,65 @@ def test_ipv6_literal_preserved(self, plugin_module): def test_blank_stays_blank(self, plugin_module): assert plugin_module.sanitize_host("") == "" + + +# --------------------------------------------------------------------------- +# Issue #147 — retired menu names must not survive anywhere a user reads +# --------------------------------------------------------------------------- + +#: Menu names retired by the #147 terminology sweep. A message or doc that +#: still uses one sends the user to a menu item that no longer exists — and +#: the #147 review found exactly that, twice, in strings WRAPPED across +#: source lines, which no grep of the source text can see. Python strings are +#: therefore collected from the AST (adjacent literals concatenate) and docs +#: are scanned with newlines collapsed. +RETIRED_MENU_NAMES = [ + "Install/update matter-server", + "Restart matter-server", + "Reinstall matter-server (clean)…", + "Open matter-server log…", + "Install/update the Matter export bridge", + "Reinstall the Matter export bridge (clean)…", + "Stop the Matter export bridge…", + "Reset Matter Export Pairings…", +] + +#: Docs a user (or the next session) follows today. The PRDs are deliberately +#: absent: they are frozen planning records whose milestone tables name the +#: menus as they were when each milestone landed. +SCANNED_DOCS = [ + Path(__file__).parent.parent / "README.md", + Path(__file__).parent.parent / "bridge-node" / "README.md", + Path(__file__).parent.parent / "docs" / "INSTALL.md", + Path(__file__).parent.parent / "docs" / "MATTER.md", + Path(__file__).parent.parent / "docs" / "HANDOVER.md", +] + + +def _py_string_constants(path): + import ast + tree = ast.parse(path.read_text(encoding="utf-8")) + return [n.value for n in ast.walk(tree) + if isinstance(n, ast.Constant) and isinstance(n.value, str)] + + +@pytest.mark.parametrize("retired", RETIRED_MENU_NAMES) +def test_no_python_string_names_a_retired_menu_item(retired): + offenders = [] + for py in sorted(SERVER_PLUGIN.glob("*.py")): + for value in _py_string_constants(py): + if retired in value: + offenders.append(py.name) + break + assert not offenders, ( + f"{offenders} still reference the retired menu name {retired!r} — " + "the menu was renamed in #147; users cannot find the old name") + + +@pytest.mark.parametrize("doc", SCANNED_DOCS, ids=lambda p: p.name) +def test_no_current_doc_names_a_retired_menu_item(doc): + # Newlines collapsed so a name wrapped across lines cannot hide — the + # exact way both #147 review criticals escaped the original sweep. + text = " ".join(doc.read_text(encoding="utf-8").split()) + hits = [name for name in RETIRED_MENU_NAMES if name in text] + assert not hits, f"{doc.name} still references retired menu name(s): {hits}"