indigo-matter

PRD — Indigo Matter Plugin (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.md for the wire contract (the §5.1 endpoint shapes here are stale — e.g. decommission is now POST …?nodeId=, not DELETE), MATTER.md for architecture/landscape (incl. the corrected Thread position), INSTALL.md for setup, and HANDOVER.md for current state. The architecture decision (the share model) is summarised in MATTER.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)

1. Summary

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.

2. Goals

3. Non-Goals

4. Architecture

4.1 Process topology

┌─────────────────────────────────────────────────────────────────┐
│                       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

4.2 Process management — open question

The plugin and matterjs-server are two processes that must stay in sync. Three plausible approaches:

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.

4.3 Storage

5. Components

5.1 HTTP API (for Domio)

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?: {...} }

5.2 WebSocket client (to matterjs-server)

Uses the documented matterjs-server / python-matter-server WebSocket API (JSON-RPC-ish). Responsibilities:

5.3 Cluster mapping layer

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).

5.4 Cluster → Indigo device-type mapping (v1)

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.

5.5 Lifecycle hooks (Indigo plugin API)

5.6 Configuration UI

Plugin config dialog exposes:

Per-device config dialog exposes:

6. State Synchronisation

6.1 On startup

  1. Connect to matterjs-server, wait for ready.
  2. List all nodes.
  3. For each node, list endpoints and clusters. Reconcile against pluginProps:
    • Node known → ensure Indigo devices exist, re-create any that were deleted out-of-band, update vendor/product fields.
    • Node unknown → log warning; happens if a device was commissioned outside this plugin (e.g. via matter.js CLI for testing). Auto-create Indigo devices using cluster mapper.
    • Indigo device known but node missing → mark device unreachable, do not delete (user may have device temporarily offline).
  4. Subscribe to all relevant attributes for all known nodes.
  5. Start HTTP server.

6.2 Attribute push from device

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.

6.3 Indigo command

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.

6.4 Device unreachable

7. Commissioning (server-side, post-Domio)

When POST /matter/commission arrives:

  1. Validate setup code format.
  2. Generate jobId, return 202.
  3. Async: a. Call 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.
  4. On any failure, attempt to remove the node from matterjs-server (best-effort), mark job failed with structured error code.

8. Failure Modes & Recovery

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.

9. Logging & Diagnostics

10. Acceptance Criteria

11. Milestones

# 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.

12. Open Questions

13. Dependencies

14. Out of Scope for v1 (v2+ Candidates)