indigo-matter)HISTORICAL (2026-06-10). This PRD is the original build spec, preserved as-written; it is no longer maintained. Everything in it shipped, and so did most of its own §14 “v2+ candidates” (door lock, window covering, smoke/CO, air quality, energy metering, fabric backup/restore — all live). A real Wi-Fi energy plug has been validated end-to-end via Domio.
Where this document disagrees with reality, the living docs win:
API.mdfor the wire contract (the §5.1 endpoint shapes here are stale — e.g. decommission is nowPOST …?nodeId=, notDELETE),MATTER.mdfor architecture/landscape (incl. the corrected Thread position),INSTALL.mdfor setup, andHANDOVER.mdfor current state. The architecture decision (the share model) is summarised inMATTER.md, not the 0001 path below.Still genuinely pending from this PRD: M11 — Plugin Store submission, and OQ4’s “Indigo admin removed externally” detection beyond basic availability tracking.
Status: Historical — shipped (see banner)
Owner: Simon
Related ADR: maintained privately; the decision is summarised in MATTER.md
Companion PRD: Domio Matter Commissioning — also shipped
Last updated: 2026-05-15 (content); 2026-06-10 (status banner)
A new Indigo plugin that brings Matter device support to the Indigo Domotics smart home server. The plugin manages a matterjs-server instance, holds the Indigo fabric’s identity, exposes an HTTP endpoint for Domio to deliver newly commissioned devices, and translates between Matter clusters and Indigo device types so Matter devices behave as first-class Indigo devices for triggers, schedules, action groups, control pages, and external API consumers.
This is the bulk of the Matter work. Domio handles commissioning UX (one-shot per device); this plugin handles everything else for the lifetime of every Matter device on the system.
matterjs-server crashes, Mac sleep/wake cycles, and LAN outages without losing devices or fabric identity./matter/commission endpoint that takes a setup code, but no user-facing UI.)matterjs-server storage backup only.┌─────────────────────────────────────────────────────────────────┐
│ Indigo Server (macOS) │
│ │
│ ┌──────────────────┐ │
│ │ indigo-matter │ WebSocket ┌─────────────────────┐ │
│ │ plugin (Python) │ ◄────────────► │ matterjs-server │ │
│ │ │ (JSON-RPC) │ (Node.js) │ │
│ │ - HTTP server │ │ │ │
│ │ (for Domio) │ │ - Indigo fabric CA │ │
│ │ - WS client │ │ - mDNS discovery │ │
│ │ - Cluster mapper │ │ - CASE sessions │ │
│ │ - Indigo dev │ │ - Storage @ │ │
│ │ state sync │ │ ~/Library/Application│ │
│ └────────┬─────────┘ │ Support/.../matter │ │
│ │ └──────────┬───────────┘ │
│ │ indigo.server (Python API) │ IPv6 / mDNS │
│ ▼ ▼ │
│ Indigo Database LAN ─► Apple TV (TBR) │
│ │ │
└─────────────────────────────────────────────────┼────────────────┘
▼
Thread mesh / Wi-Fi devices
The plugin and matterjs-server are two processes that must stay in sync. Three plausible approaches:
matterjs-server as a child process. Pro: single lifecycle, simple deploy. Con: plugin reloads kill the server unnecessarily; Node failures harder to debug.matterjs-server runs as a separate launchd LaunchAgent. Pro: independent lifecycle, survives plugin reloads, standard macOS pattern. Con: installation more involved (plist deploy + load); plugin can’t directly tail Node logs.launchd for the long-running server, plugin uses launchctl to start/stop/reload during plugin lifecycle events.Decision: make this a focused implementation-phase decision and document as a follow-up ADR. Recommended starting point is PM-B because it survives Indigo plugin reloads (frequent during development) without restarting the Matter server (slow to start, holds device sessions). If the launchd installation friction proves too high, fall back to PM-A.
matterjs-server owns its own storage directory (containing fabric private keys, certificates, node operational data). Path: ~/Library/Application Support/com.simons-plugins.indigo-matter/matter-server/.(nodeId, endpointId, clusterId) → indigoDeviceId. Stored in Indigo’s normal plugin storage; backed up with Indigo’s database.Served by the Indigo Web Server (IWS) as hidden-action handlers (not a standalone
server — aiohttp is absent from the Indigo framework Python; IWS is the idiomatic
mechanism and rides the Reflector for remote access). Authentication is Indigo’s existing
Reflector/API-key auth, enforced before the handler runs. See API.md v1.1 (authoritative
for the wire shape) and IMPLEMENTATION.md §4.
Endpoints (logical; actual paths are …/message/com.simons-plugins.indigo-matter/{handler} — see API.md):
GET /matter/status
→ 200 { ready: bool, controllerVersion: string, fabricId: string,
nodeCount: int, matterServerReachable: bool }
POST /matter/commission
Body: {
setupCode: string, // 11-digit numeric or "MT:..." QR payload
discriminator: int,
suggestedName: string,
suggestedRoom: string?,
domioNodeId: string? // for logging/correlation
}
→ 202 { jobId: string }
→ 409 { error: "duplicate", existingJobId: string } if same setupCode in-flight
GET /matter/commission/{jobId}
→ 200 {
status: "pending"|"commissioning"|"reading_descriptors"|
"creating_devices"|"success"|"failed",
progress: float, // 0.0–1.0
result?: {
nodeId: string,
indigoDeviceIds: int[],
primaryDeviceId: int // the one to navigate to in Domio
},
error?: { code: string, message: string }
}
DELETE /matter/devices/{nodeId}
→ 204 decommission and remove from Indigo fabric, delete Indigo devices
GET /matter/devices/{nodeId}/diagnostics
→ 200 { reachable, lastSeen, vendorId, productId, swVersion,
fabrics: [...], thread?: {...}, wifi?: {...} }
Uses the documented matterjs-server / python-matter-server WebSocket API (JSON-RPC-ish). Responsibilities:
attribute_updated, node_added, node_removed, node_unreachable).commission_with_code, interview_node, invoke_command, remove_node, open_commissioning_window requests.The most important and most testable component. A registry of cluster handlers, each implementing:
class ClusterHandler(ABC):
cluster_id: int
cluster_name: str
@abstractmethod
def create_indigo_devices(self, node, endpoint) -> list[IndigoDeviceSpec]:
"""Build Indigo device(s) for an endpoint exposing this cluster."""
@abstractmethod
def attributes_to_subscribe(self) -> list[int]:
"""Attribute IDs to subscribe to on this cluster."""
@abstractmethod
def on_attribute_update(self, indigo_dev, attribute_id, value) -> dict:
"""Translate a Matter attribute change to Indigo state updates."""
@abstractmethod
def handle_indigo_action(self, indigo_dev, action) -> MatterCommand | None:
"""Translate an Indigo device action to a Matter command."""
This isolates Matter-spec knowledge into one file per cluster, making future cluster additions a contained change. Composition handles devices exposing multiple clusters on one endpoint (e.g. OnOff + LevelControl + ColorControl → one Indigo dimmer with color states).
| Matter cluster(s) on endpoint | Indigo device type | Indigo states |
|---|---|---|
| OnOff only | Relay | onState |
| OnOff + LevelControl | Dimmer | onState, brightnessLevel |
| OnOff + LevelControl + ColorControl | Dimmer (color) | + hue, saturation, colorTemperature, colorMode |
| TemperatureMeasurement | Sensor (temperature) | sensorValue (°C), batteryLevel if present |
| RelativeHumidityMeasurement | Sensor (humidity) | sensorValue (%RH) |
| OccupancySensing | Sensor (motion / occupancy) | onOffState (boolean) |
| ContactSensor / BooleanState | Sensor (contact) | onOffState (boolean), contactSensorClosed |
| IlluminanceMeasurement | Sensor (illuminance) | sensorValue (lux) |
| Thermostat | Thermostat | hvacMode, hvacFanMode, temperatureInputs, setpoints |
| FanControl (on thermostat endpoint) | merged into Thermostat | fanMode, fanSpeedSetpoint |
| Multi-cluster endpoints not matching above | “Unknown Matter device” placeholder relay | onOffState if OnOff present, otherwise stub |
Bridged devices: each bridged endpoint advertised by a Matter bridge node becomes its own Indigo device, applying the above mapping recursively.
startup — start (or attach to) matterjs-server, open WebSocket, sync devices.shutdown — close WebSocket cleanly. Whether to stop matterjs-server depends on PM-A/B/C decision.deviceStartComm / deviceStopComm — track which Indigo devices are active, gate state updates to active devices only.actionControlDevice, actionControlSensor, actionControlThermostat — dispatch to cluster mapper.validateDeviceConfigUi, validatePrefsConfigUi — config validation.runConcurrentThread — periodic health check, reconnect if WebSocket has dropped, prune stale subscriptions.Plugin config dialog exposes:
Per-device config dialog exposes:
ready.matterjs-server WS push → plugin parses → look up Indigo device by
(nodeId, endpointId) → dispatch to cluster handler → handler returns
{state: value, ...} dict → indigo.dev.updateStatesOnServer([...])
Idempotent: pushing the same value twice is a no-op.
Indigo trigger / Action Group / Domio → plugin.actionControlDevice(action, dev)
→ look up cluster handler from dev.deviceTypeId → handler builds MatterCommand
→ send invoke_command over WS → await ack (5s timeout) → update Indigo state
on confirmation, or revert on failure.
Optimistic update optional per cluster: on/off optimistic is safe, brightness probably not.
errorState = "unreachable".When POST /matter/commission arrives:
commission_with_code on matterjs-server.
b. Update job state through phases (commissioning → reading_descriptors → creating_devices).
c. On matterjs-server completion, run cluster mapper to create Indigo devices.
d. Apply suggestedName: Indigo device name = suggestedName, or suggestedName + “ (endpoint N)” for multi-endpoint devices.
e. Set room if Indigo has rooms configured.
f. Persist (nodeId → indigoDeviceIds) mapping in pluginProps.
g. Mark job success, return result on next poll.failed with structured error code.| Failure | Detection | Behaviour | User-visible state |
|---|---|---|---|
| matterjs-server not running at startup | Connect timeout | Retry with backoff. Log clearly. | Plugin status: “Matter server not running” |
| matterjs-server crashes mid-run | WS disconnect | Reconnect (PM-A: respawn; PM-B: rely on launchd KeepAlive). | Devices briefly marked unreachable. |
| Apple TV (TBR) offline | Thread devices stop reporting | Mark affected devices unreachable after timeout. | Sensor states show “unreachable”. |
| Mac sleeps | OS event | On wake, force WS reconnect and resubscribe. | Brief unreachability, auto-recovers. |
| LAN IP change | Device pings fail | matterjs-server’s mDNS resolves new address. Plugin retries. | Brief unreachability. |
| Indigo plugin reloaded | shutdown → startup | If PM-B: matter-server keeps running, plugin reconnects without losing fabric. | No user-visible state loss. |
| Fabric corruption | matter-server fails to start | Plugin surfaces critical error. Manual restore from backup required. | All Matter devices unreachable until fixed. |
~/Library/Logs/indigo-matter/matter.log, rotated daily, retain 7 days.GET /matter/devices/{nodeId}/diagnostics returns a JSON snapshot for Domio’s future device-detail diagnostics view.POST /matter/commission returning.DELETE /matter/devices/{nodeId} removes the node from the fabric and deletes Indigo devices cleanly. The device can be re-commissioned afterwards.| # | Milestone | Gating criterion |
|---|---|---|
| M0 | Repo + plugin skeleton | Empty plugin loads in Indigo, version visible in plugin list |
| M1 | matterjs-server running standalone | Server starts on Mac, accepts WS connections, persists data |
| M2 | WS client + fabric init | Plugin creates Indigo fabric, status endpoint returns ready=true |
| M3 | HTTP server + commission endpoint (manual code) | Can paste a setup code into a curl call and commission a real device |
| M4 | OnOff cluster end-to-end | Tapo plug controllable from Indigo Action Group (AC4) |
| M5 | LevelControl + ColorControl | Color bulb works (AC5) |
| M6 | Sensors cluster pack (Thread-dependent — see IMPLEMENTATION.md §2.7. Use Wi-Fi sensor variants for v1 if Thread blocked in matter-server.) | TemperatureMeasurement, RelativeHumidityMeasurement, OccupancySensing, ContactSensor, IlluminanceMeasurement (AC6, AC7) |
| M7 | Thermostat cluster (Wi-Fi thermostats only for v1 unless Thread support lands.) | Setpoint/mode round-trip (AC8) |
| M8 | Failure recovery & lifecycle | AC9, AC10, AC11 |
| M9 | Domio integration test | Full path Domio → commission → control (gates Domio M5) |
| M10 | Process management ADR + launchd integration | PM-B installed cleanly, ADR written |
| M11 | Plugin Store submission | Documented install, license, README, screenshots |
M1–M4 form the architectural validation loop from the ADR (Confirmation section). Until M4 lands, the architecture is unproven; once it lands, the rest is mechanical.
matter-server, GitHub matter-js/matterjs-server), Alpha status (v0.6.2 as of latest). Pin to a specific version. Thread commissioning currently non-functional; Wi-Fi unaffected. The plugin’s WebSocket client should be designed to be portable to python-matter-server as a Thread fallback contingency.