Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a0479d1
Myuplink integration
hannesb90 May 1, 2026
5ac0931
Add files via upload
hannesb90 May 1, 2026
6ceaa08
Add files via upload
hannesb90 May 1, 2026
20a348d
Add files via upload
hannesb90 May 1, 2026
835aef6
Add files via upload
hannesb90 May 1, 2026
8d06b8d
Add Matter as a first-class driver capability (python-matter-server b…
claude Jun 15, 2026
b9d0a82
Add generic Matter driver (drivers/matter.lua)
claude Jun 15, 2026
54622f1
Add flex-load control: thermal RC model + price-aware scheduler
claude Jun 15, 2026
db92405
flexload: simple (MPC-free) mode, temp sensors, learned plug loads
claude Jun 15, 2026
560d591
flexload: fix real bugs + add stove detection, COP-aware arbitration,…
claude Jun 15, 2026
9c1acb7
flexload: remove window detection; gate every heat reduction on learn…
claude Jun 15, 2026
0758510
flexload: factor heat-pump flow temperature into the pause economics
claude Jun 15, 2026
e808a5e
thermalmodel: add 2R2C (slab+room) model for floor-heating forecast
claude Jun 15, 2026
0bfbf07
Fix seven review findings on the Matter / flexload branch
claude Jun 16, 2026
0e40adc
Fix three logic findings from deep correctness audit
claude Jun 16, 2026
fd5706f
Add Matter client unit tests with embedded WebSocket stub
claude Jun 16, 2026
d8c994d
Fix Lua syntax error in TestLuaHTTPPatchMethod
claude Jun 16, 2026
311fe2a
matter: flag python-matter-server backend as provisional per maintain…
claude Jun 17, 2026
0cad7df
matter: align docs with multi-fabric (no-commission) onboarding model
claude Jun 17, 2026
23b9aeb
matter: build Phase 1 matter.js sidecar (replaces python-matter-server)
claude Jun 17, 2026
c32540f
matter: add /api/matter/commission + /api/matter/nodes admin endpoints
claude Jun 17, 2026
e696740
matter: Phase 2 — expose 42W's own price feed as a Matter server
claude Jun 17, 2026
46df70b
matter: Phase 3 — 42W as a Matter bridge for non-Matter DERs
claude Jun 17, 2026
24a5605
matter: fix bridge sync skipping unreachable-marking on full opt-out
claude Jun 17, 2026
e025edf
matter: fix reviewed issues — auth, reconnect, device_id, validation,…
claude Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ data/
*.test
*.out

# matter-sidecar (Node.js) build artifacts
/matter-sidecar/node_modules/
/matter-sidecar/dist/

# Local config for simulators
config.local.yaml

Expand Down
39 changes: 39 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,45 @@ services:
# lifecycle of individual services that mount the same volume).
- update-ipc:/run/ftw-update

# ---------------------------------------------------------------------
# Matter sidecar (matter-sidecar/, built on matter.js — replaces the
# earlier python-matter-server draft per maintainer review on PR #1).
#
# 42W does NOT perform initial commissioning (BLE/Thread/Wi-Fi
# provisioning of a brand-new device, which is why the old draft
# needed `privileged: true`). That stays the job of whichever
# controller the device shipped with (Apple Home, Google Home, Home
# Assistant, ...). Once commissioned there, open that controller's
# multi-admin "share device" flow to mint a pairing code, then hand
# that code to 42W via the sidecar's `commission` command, which
# joins as an additional fabric admin over plain IP — no BLE needed
# on this side. network_mode: host is required for mDNS discovery of
# the shared device. The sidecar's control-plane WebSocket (port 5580,
# commission/write_attribute/send_command/...) has no auth of its own,
# so it binds to loopback only (matter-sidecar/src/index.ts) — host
# networking would otherwise put an unauthenticated device-control API
# on the LAN.
#
# 42W also runs the other direction (Phase 2): as a Matter *server*,
# exposing a CommodityPrice cluster (spot price + forecast) other
# controllers can read. GET /api/matter/pairing_code returns the codes
# to add 42W onto a third-party fabric.
#
# Phase 3 — a *bridge* surfacing non-Matter DERs (EVSE, inverters,
# batteries) as Matter devices to other ecosystems. Opt in per driver
# with `matter_bridge: true` in config.yaml; see
# matter-sidecar/src/bridge.ts and docs/configuration.md.
# ---------------------------------------------------------------------
matter-sidecar:
build: ./matter-sidecar
container_name: ftw-matter-sidecar
restart: unless-stopped
network_mode: host
environment:
- FTW_MATTER_STORAGE_PATH=/data
volumes:
- ./data/matter:/data

# ---------------------------------------------------------------------
# MQTT broker (Eclipse Mosquitto)
#
Expand Down
85 changes: 85 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,91 @@ Handler: `go/internal/api/api.go:445`

---

## Matter sidecar

Admin actions for the 42W Matter sidecar (`matter-sidecar/`, built on
matter.js) — the one-time pairing-code join and node listing. 42W does not
commission devices itself; see `drivers/matter.lua`'s header comment for
the full onboarding flow. Both endpoints require `matter:` configured at
the config root (`go/internal/config/config.go`'s `Config.Matter`) —
otherwise they return `503`.

### POST /api/matter/commission

Join a device shared from another controller's fabric using a one-time
pairing code minted by that controller's "share device" / multi-admin
flow.

**Request body:**

```json
{ "pairing_code": "34970112332" }
```

**Response (200):**

```json
{ "node_id": 1 }
```

`node_id` is a small logical id (not Matter's own 64-bit NodeId) — paste
it into the driver's `config.node_id` field.

**Errors:** `400` missing `pairing_code`; `502` sidecar reachable but the
commission attempt failed (wrong/expired code, device unreachable); `503`
sidecar not configured.

Handler: `go/internal/api/api_matter.go`

### GET /api/matter/nodes

List every node the sidecar has joined so far, to confirm a `node_id`
before pasting it into driver config.

**Response (200):**

```json
[
{ "node_id": 1, "matter_node_id": "123456789012345" }
]
```

Handler: `go/internal/api/api_matter.go`

### GET /api/matter/pairing_code

Phase 2 — the inverse of `/api/matter/commission`: 42W itself is a
commissionable Matter device (exposing a CommodityPrice server endpoint
with spot price + forecast — see `matter-sidecar/src/priceserver.ts`).
This returns the one-time codes a third-party controller (Apple Home,
Home Assistant, ...) needs to add 42W onto their own fabric.

**Response (200):**

```json
{
"manual_pairing_code": "34970112332",
"qr_pairing_code": "MT:Y.K9042C00KA0648G00"
}
```

**Errors:** `502` sidecar reachable but the request failed; `503` sidecar
not configured.

Handler: `go/internal/api/api_matter.go`

### Phase 3 — Matter bridge (no REST endpoint)

42W as a Matter *bridge*: drivers with `matter_bridge: true` (see
`docs/configuration.md`) get their live power reading surfaced as a
bridged Matter device under the sidecar's Aggregator endpoint, so other
Matter ecosystems can see it. This is config-driven and push-only — a
background loop (`matterBridgeLoop` in `go/cmd/forty-two-watts/main.go`)
calls the sidecar's `sync_bridge` command every 5 minutes; there is no
operator-facing HTTP surface for it.

---

## MPC planner

### GET /api/mpc/plan
Expand Down
35 changes: 35 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ drivers:
max_charge_w: 10000 # per-driver cap; 0/unset → 5 kW default
max_discharge_w: 10000
inverter_group: ferroamp # optional — see "Inverter affinity" below
matter_bridge: true # optional — surface this driver's power on the Matter bridge
mqtt:
host: 192.168.1.153
port: 1883
Expand Down Expand Up @@ -208,6 +209,39 @@ batteries:

Keys must match `drivers[].name`. Leave blank to use BMS defaults.

#### Matter bridge (`matter_bridge`)

Set `matter_bridge: true` on any driver to surface its live power reading
as a bridged Matter device under the sidecar's Aggregator endpoint (Phase
3 — see `matter-sidecar/src/bridge.ts`), so other Matter ecosystems
(Apple Home, Home Assistant, ...) can see it. Off by default — this is a
deliberate per-driver opt-in, not automatic for every configured driver.
Requires the `matter:` block below to be configured. State of charge is
not bridged (left for a follow-up); only power.

### `matter` — Matter sidecar admin connection (optional)

```yaml
matter:
host: localhost # matter-sidecar address
port: 5580 # default 5580
```

Enables `POST /api/matter/commission` and `GET /api/matter/nodes` — the
one-time pairing-code join for devices shared from another Matter
controller's fabric, and a listing of nodes already joined. This is
separate from each driver's own `capabilities.matter` block (which talks
to an already-joined `node_id`); the two normally point at the same
sidecar. See `drivers/matter.lua`'s header comment for the full
onboarding flow, and `docs/api.md`'s Matter section for the endpoints.

This same `matter:` block also turns on Phase 2 — 42W exposing its own
spot price + forecast as a Matter CommodityPrice server endpoint, so other
Matter controllers can read it directly. If `price:` is also configured,
a background loop pushes fresh data to the sidecar every 5 minutes; no
extra config needed. `GET /api/matter/pairing_code` returns the codes a
third-party controller needs to add 42W to their own fabric.

## Hot-reload matrix

| Field | Hot? | Notes |
Expand All @@ -229,6 +263,7 @@ Keys must match `drivers[].name`. Leave blank to use BMS defaults.
| `price.*` | ✅ | Picked up next price-fetch cycle |
| `weather.*` | ✅ | Picked up next weather-fetch cycle |
| `batteries.*` | ✅ | Read fresh each control cycle |
| `drivers[].matter_bridge` | ✅ | Picked up by `matterBridgeLoop`'s next 5-min push |

## Atomic writes

Expand Down
Loading