Matter support + flexible-load control (thermostats, smart plugs, floor heating)#487
Matter support + flexible-load control (thermostats, smart plugs, floor heating)#487hannesb90 wants to merge 25 commits into
Conversation
Myuplink integration
…ackend) Matter joins MQTT, Modbus, and HTTP as a supported transport in the Lua driver host. Drivers declare `capabilities: matter: host: localhost` in config.yaml and use host.matter_read / host.matter_write / host.matter_invoke to talk to commissioned devices via the python-matter-server WebSocket API. Changes: - go/internal/matter/client.go — new package; gorilla/websocket client to python-matter-server with request/response correlation, auto-reconnect, and explicit Close() for driver lifecycle management - go/internal/drivers/host.go — MatterCap interface, HostEnv.Matter field, WithMatter() builder, matterRead/Write/Invoke proxy methods - go/internal/config/config.go — MatterConfig struct, Capabilities.Matter field, validation updated to accept matter as a valid capability - go/internal/drivers/registry.go — MatterFactory, wiring in Add(), Close() on driver stop, sameDriverConfig comparison for Matter - go/internal/drivers/lua.go — host.matter_read/write/invoke exposed to Lua - go/cmd/forty-two-watts/main.go — reg.MatterFactory wired to matter.Dial - docker-compose.yml — python-matter-server sidecar service (privileged for BLE commissioning, host networking for mDNS, ./data/matter for fabric state) https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
Config-driven driver that works with any Matter-certified device. Operator specifies reads (attribute polls → metrics or structured DER telemetry) and commands (named attribute writes or cluster invocations) in config.yaml — no code change needed per device type. Typical uses: Wiser/Schneider thermostat, smart plugs with power metering, on/off switches, energy meters. The dispatch layer sends named commands (e.g. "setpoint") which map to Matter attribute writes, enabling MPC-controlled thermal load shifting. https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
Adds MPC-adjacent control for the two flexible-load categories Matter actually covers today — thermostats and on/off smart plugs — without touching the battery/EV DP (cramming thermal + interruptible state into it would explode the DP from ~100M to billions of evaluations). Flexible loads couple to the system only through the grid-power balance and the price signal, so they decompose into a separate scheduler that runs against the MPC plan's existing price/PV curve. New packages: - go/internal/thermalmodel — single-zone 1R1C building twin. RLS fits the heat-loss rate (τ) and heating gain from observed temp deltas, mirroring pvmodel. Physics priors give usable day-one behaviour; comfort-floor guarantees come from the learned time constant. - go/internal/flexload — PlanThermal (comfort-respecting, price-aware setpoint schedule with a forward-simulated MinC floor) + PlanDeferrable (cheapest-slots energy-budget scheduler with optional PV-surplus credit), plus a Service that trains thermal models from telemetry and dispatches per-slot directives to Matter drivers. Electric vs hydronic heating is modelled explicitly: a COP separates the thermal watts that drive the RC model from the electrical watts that hit the grid/fuse/cost accounting (direct electric COP=1; heat-pump-fed hydronic COP≈3, MaxHeatW = zone thermal output). Config: new `flexloads:` block. Service starts only when declared, so existing deployments are unaffected. Wired in main.go behind that gate; slots come from the live MPC plan, outdoor temp from the forecast cache. Tests cover RLS convergence to true τ, comfort-floor enforcement in a cold leaky zone, cheapest-slot selection, PV preference, and COP-correct electrical accounting. https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
Three additions driven by real-device realities: 1. Standalone Matter temperature sensors. A thermostat zone can now read its indoor temperature from a *separate* driver (`indoor_driver`) — a dedicated Matter Temperature Measurement sensor (cluster 0x0402) — instead of the thermostat's own mounting-biased probe. The generic matter.lua already reads arbitrary attributes; flexload just sources the metric from the named sensor driver. 2. Simple, MPC-independent valuation mode (`mode: simple`). A standalone block/heat rule: heat to keep TargetC, but block heating while the price is above a threshold whenever the building's own thermal inertia (learned RC time constant) keeps the target for the block horizon. Works with a fixed öre threshold and no forecast at all. Multi-zone ArbitrateSimple blocks the highest-power zone first when comfort allows, under a shared fuse budget — never a zone that needs the heat or one at its comfort floor. Price comes from priceforecast (not the MPC), so this runs with the planner disabled. 3. Self-learning plug loads (PlugProfile). A smart plug / switch / contactor can drive anything — spa, water heater, pump — at wildly different power. With `power_metric` set, the scheduler learns the actual running power and daily energy from the plug's own metering, so PowerW / EnergyWh become optional (learned when 0). A coarse Classify() labels the load (water_heater / spa_or_pool / …) for diagnostics. All new logic is pure + tested: block/heat decisions, power-priority arbitration with comfort protection, running-power learning, daily-energy integration with day-rollover folding, and device classification. https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
… window gating
Bug fixes from a critical review of the flex-load code:
- scheduler.go: the comfort-floor guard was dead code (`target < minC`
could never be true since target ∈ {minC,maxC}). Replaced with a
physically-correct three-regime rollforward (pull-up capped at setpoint /
pure coast / hold-at-setpoint) so the predicted indoor temp genuinely
never drops below MinC and EstHeatW reflects real draw.
- service.go: replan() bailed when the MPC slot curve was empty, which
prevented simple (MPC-independent) mode from ever running. Now only
planner-mode devices need slots; simple mode runs off PriceNow.
- service.go: metered deferrable plugs deadlocked at cold-start (off →
never runs → never learns → never scheduled). Now left on native control
until the profile is learned.
- matter/client.go: WriteMessage no longer holds the shared mutex — a slow/
half-open socket could block the readLoop from delivering responses. A
dedicated writeMu serializes writes instead.
- service.go: nil-guard the per-zone thermal model lookups.
New value features (operator-requested):
- External-heat (wood-stove) detection: a fast indoor rise the RC model
can't explain from metered electric heat is inferred as a free external
source (kamin, solar gain). 42W then pauses the zone's electric heating
(frost-protection setpoint) AND stops training the model — firing through
it would teach a false building. Learns per-firing energy + inferred
thermal power over cycles.
- COP/cost-aware arbitration: under a shared fuse budget, zones are shed by
cost-effectiveness (lowest COP first — cheapest heat to lose per watt
freed), not raw power; ties broken by power then coast buffer (which
already encodes water-vs-electric inertia via the learned time constant).
- Window/occupancy gating: a contact/occupancy metric (Matter cluster
0x0045 / 0x0406) drops the zone to frost protection and pauses training
when a window is open, so pre-heat isn't wasted and τ isn't corrupted.
Tests: stove fires on unexplained warming / ignores our own heat / folds
per-cycle energy; arbitration sheds the resistive zone before the heat
pump. All new logic pure + covered.
https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
…ing + economics
Per operator direction: no heat reduction may happen unless it was preceded
by learning AND a calculation proving it's a net win. No "dumb" lowering.
- Removed window/occupancy detection entirely (config, Device, wiring,
helpers). It blocked heat on a raw signal without an economic basis.
- Stove (external-heat) response is now strictly graded and per-zone:
* The detector reads only its own zone's indoor temperature, so a fire
in the living room can never lower the bathroom.
* Default (no learning required): suppress pre-heat and HOLD the comfort
target. The stove keeps the room warm so electric stays off naturally;
the room is never allowed to overcool.
* A deeper reduction toward MinC happens ONLY when (a) the RC model is
trained (Quality ≥ 0.4), (b) we've learned the stove's typical firing
energy/duration, and (c) pauseIsNetWin proves it: the energy saved now
outweighs the cost to reheat once the fire ends, using the FORECAST
price at the reheat moment. If reheating later is pricier than heating
now, we hold comfort. This captures the floor-heating checklist —
demand (HeatToHoldW vs outdoor), price now, inertia (energy integrated
over the learned coast), COP, and reheat price.
- Simple-mode price blocking now also gates on model confidence: an
untrained zone is never blocked, it maintains target. No reduction on an
unvalidated model.
- Generalized the price hook to PriceAt(t) so the reheat-side of the
economics can look up the future price, not just "now".
Tests: untrained model never blocks; pause only wins when reheat is cheaper
than now (and never without a price model); a detected stove with no learning
only holds comfort, never reduces.
https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
The cost to reheat after a pause depends on how much heat the hydronic loop is already storing — readable as the heat pump's supply (flow) temperature from its integration (Nibe/Thermia/myUplink/etc). A hot loop can deliver heat to the room without running the compressor, so recovery is nearly free; a cold loop must run the compressor and is expensive. pauseIsNetWin now discounts the reheat cost by reheatFactor(flow, room, nominal_delta): a flow temperature a full design-delta above the room (e.g. +15°C floor heating) credits the stored charge to ~zero reheat cost, so a deep pause becomes a net win even when the spot price will be higher at reheat time. With no flow signal the credit is never applied (assume full cost) — no speculative reductions. Config: flow_driver / flow_metric (read flow temp off the heat-pump driver) and nominal_flow_delta_c (design flow-above-room delta, default 15). Wired through Device → stoveDecision → pauseIsNetWin. Tests: reheatFactor scales 1→0 with flow headroom; a high flow temp flips a pricier-reheat case into a net-win pause. https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
Floor heating has two thermal masses with very different time constants: a slow concrete slab heated directly, and the faster room air the slab feeds. The single-mass RC fit lumps them together and badly underestimates how long a floor-heated room stays warm after the element switches off — the slab keeps radiating for hours. That under-estimate mis-prices every pause/block. New thermalmodel.TwoMass: Cs·dTs/dt = P − (Ts−Tr)/Rsr (slab) Cr·dTr/dt = (Ts−Tr)/Rsr − (Tr−To)/Rro (room) fit as two RLS regressions (slab + room) sharing the codebase's estimator, with physical priors, ForecastRoom (the horizon trajectory), and CoastHoursToRoomTarget — which correctly exceeds the single-mass coast when the slab is charged. Also added Model.CoastHoursToTarget for the single-mass case (flexload had its own copy). Wired into flexload: a thermostat with slab_driver/slab_metric (a floor probe, common on electric floor thermostats, or the hydronic flow temp as a proxy) trains a two-mass model alongside the single-mass one and prefers it for the simple-mode coast estimate AND the confidence gate. Persisted under flexload/twomass/<driver>; exposed via TwoMassModel() for diagnostics. Tests: 2R2C recovers true slab/room coefficients; its coast exceeds the single-mass coast with a hot slab; forecast shape holds; a two-mass coast override flips a simple-mode block that the single-mass would refuse. https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
|
Marking this as a draft — it's a direction proposal, not a merge-ready change, and I'd like a maintainer go/no-go before investing further. The ask: is Matter-as-a-transport + a decoupled flexible-load layer a direction you want in 42W? Specifically: Matter via a python-matter-server sidecar. It keeps the no-CGo rule (WebSocket to the sidecar, no Matter SDK linked in) and mirrors the existing ftw-updater/Mosquitto sidecar pattern. But it does add a privileged container (BLE commissioning) to the deploy. Acceptable, or would you rather Matter come in via the HA bridge only? Flexible loads as a separate scheduler, not new DP dimensions. I deliberately kept the battery/EV DP untouched — adding thermal + interruptible state would blow it up combinatorially — and ran the flex-load scheduler against the plan's existing price/PV curve instead. Does that decomposition match how you'd want it architected? Scope of the "smarts". Thermal RC models (1R1C + a 2R2C floor-heating model), a wood-stove detector, COP-aware arbitration, and an economic pause gate with a heat-pump flow-temp credit. Genuinely useful, or more than this project wants to own? Honest status: the analytical cores are pure and unit-tested. The Matter protocol layer is not yet verified against a real python-matter-server — the WS message shapes are from memory and likely need correcting. No commissioning flow, no API/UI, no docs/ pages yet. So this is "is the direction right?", not "please merge." If it's worth pursuing I'll: verify the Matter protocol against the real server, add docs/ + per-package CLAUDE.md, and an integration test. If it's not the direction you want, better to know now. 🙏 |
- matter.lua: anchor device_id on node_id (not model name) to prevent collision when two identical Wiser thermostats share the same fabric. host.set_sn(tostring(node_id)) replaces host.set_sn(cfg.model). - matter/client.go: fix attribute_path format from "ep/0xCLUSTER/0xATTR" to "ep/cluster/attr" (decimal integers) matching the python-matter-server WebSocket API contract. Applies to both ReadAttribute and WriteAttribute. - flexload/service.go: priceForNow fallback now returns slots[last] instead of slots[0] when now is past all horizon slots (oldest price is stale). - flexload/service.go: log Warn when indoor sensor is down and buildSimpleSpec falls back to mid-band, so operators can detect sensor outages. - config/config.go: add FlexLoad validation in Validate() — catches missing driver_name, invalid type, min_c >= max_c, negative COP, and bad mode. - config/config.go: mark HeatSourceDriver as reserved/not-yet-implemented so operators aren't confused by an undocumented no-op field. - flexload/service_test.go: new tests covering sample() (thermal model trains from telemetry, skips without HeatMetric), replan() (thermostat planner dispatch, deferrable on/off, simple-mode arbitration, no-op with no slots), and priceForNow fallback correctness. https://claude.ai/code/session_01QTQB2i2R9FTVeqYqVFJVPp
- thermalmodel/model.go + twomass.go: replace `denom == 0` guard with `denom < 1e-9` in the RLS update. In a healthy RLS the denominator is λ + xᵀPx ≥ 0.997, so exact-zero is unreachable, but numerical drift in a corrupted P matrix can produce near-zero values that cause K → ∞ and wild coefficient updates. The epsilon guard catches this. - flexload/scheduler.go: filter NaN/Inf prices before sorting in priceQuantile. A corrupted price feed returning NaN would propagate through sort.Float64s into the threshold and corrupt all scheduling decisions. Zero is now returned, which safely degrades to "never expensive" so simple-mode never blocks heating. - flexload/scheduler.go: fix misleading comment on pvCoverW (the 0.5 factor means 50%-surplus, not full-surplus) and clarify ScheduledWh may exceed EnergyWh by up to one slot (unavoidable ON/OFF rounding). https://claude.ai/code/session_01QTQB2i2R9FTVeqYqVFJVPp
Tests a fake python-matter-server (httptest + gorilla/websocket) to verify the client's protocol and error-handling paths without real hardware: - TestReadAttribute_Success: round-trip returns correct value - TestReadAttribute_AttributePath: path format is decimal "ep/cluster/attr", not hex — regression guard for the format fix - TestReadAttribute_ServerError: error_code propagates as a Go error - TestWriteAttribute_Success: write round-trip, value arrives at server - TestInvokeCommand_Success: send_command round-trip - TestConcurrentRequests: three concurrent calls are correctly correlated by message_id even with concurrent goroutines - TestCallToClosedServer: conn=nil fast-path rejects immediately (< 200 ms), not after the 10 s internal timeout - TestClose_WaitsForReadLoop: Close() unblocks and double-Close doesn't panic - TestReconnect: client reconnects after server drops the connection and subsequent calls succeed (needs 2 s for the reconnect back-off) - TestPendingCleared_OnDisconnect: in-flight calls receive connection_lost error when the server disconnects, not an infinite hang https://claude.ai/code/session_01QTQB2i2R9FTVeqYqVFJVPp
The test embedded JSON bodies directly inside a Lua double-quoted string
literal. When patchBody contained {"value":"42"}, the inner double-quotes
terminated the Lua string early, causing a parse error.
Fixed by using Lua long-string brackets ([[...]]) for the body argument,
which accepts any content including double quotes.
https://claude.ai/code/session_01QTQB2i2R9FTVeqYqVFJVPp
|
The python-matter-server is no longer maintained so matter-js or the offical C sdk I think would be better for the future. I would suggest that we do not support matter commisioning, so much to take care of. It is better to leave the first initial commisioning to another controller and then we can just add the device to our controller via multi-fabric. Then thread/wifi whatever transport layer it is doesn´t matter(badumtss!) You share your device from your other controller and then just provide a commissioning code into 42W. EDIT: some reasoning why we do not want to be the controller who does the initial commission(first time setup) most devices commission over Bluetooth or other means where you would need to have your phone in hand - and for that you need the platform specific api's and an app, which would be unecsary complex. Much better to delegate this to other platforms which have this tested and already done, multi-fabric is one of the key selling points of matter, would be nice to use it. |
|
I think it is also really good if we expose the enegry managment clusters for devices to consume with the prices etc also as a EMS-focused Matter controller. Thomas mcguiness did many energy related matter stuff which is cool: https://tomasmcguinness.com/2025/07/20/creating-a-matter-controller-with-matter-js/ |
…er review Owner feedback on PR #1: python-matter-server is unmaintained, swap to matter.js/official SDK; drop our own commissioning flow in favor of joining devices via multi-fabric (pairing code from whichever controller already commissioned the device); also explore running 42W as a Matter EMS server + bridge for non-Matter DERs. This commit only does the immediately-safe part: comments/docs updated to reflect the new scope (no BLE/privileged container needed since we no longer commission), and the matter-server compose service commented out pending the real sidecar. Wire protocol + EMS-server/bridge work tracked as follow-on in the PR thread.
Second pass over the maintainer's PR #1 feedback. Beyond the compose/ client provisional flags, make the whole branch tell one consistent story: 42W joins devices that another controller already commissioned, via a shared pairing code, as an additional fabric admin — it never commissions hardware itself. - drivers/matter.lua: replace the old we-commission / 'pairing UI coming soon' header with the concrete share-from-your-controller -> paste pairing code -> sidecar assigns node_id workflow; note BLE/transport is the other controller's problem, not ours. - MatterCap (host.go), MatterFactory (registry.go), MatterConfig (config.go): backend-neutral wording ('Matter sidecar', backend TBD) instead of hard-coding python-matter-server. - matter.lua verification_status beta -> experimental, matching the other unproven drivers and the provisional backend.
Implements the multi-fabric, no-commissioning model from PR #1 review: matter-sidecar/ is a real matter.js-based controller process speaking the same JSON-over-WebSocket protocol go/internal/matter/client.go already implements. It joins devices shared from another controller's fabric via a pairing code (`commission` command), persists a small logical node_id mapping for driver config (Matter NodeIds are 64-bit and not what drivers/matter.lua's config expects), and resolves generic numeric cluster/attribute/command IDs against matter.js's built-in standard cluster definitions for read/write/invoke. docker-compose.yml's matter-sidecar service now builds from matter-sidecar/ instead of being commented out pending a TBD image.
Phase 1 had no way to trigger the pairing-code join besides a raw WebSocket message to the sidecar. Adds a site-wide `matter:` config block (separate from each driver's own capabilities.matter, which talks to an already-joined node_id) wired to a Commission/ListNodes client in go/internal/matter, exposed as two REST endpoints so an operator can paste a pairing code and get back the node_id for their driver config without scripting a WebSocket call by hand.
Adds a CommodityPrice server endpoint (ElectricalEnergyTariffDevice) to the sidecar so other Matter controllers can read 42W's spot price + forecast directly. A new background loop in main.go pushes fresh data from internal/prices.Service every 5 minutes whenever both `matter:` and `price:` are configured. GET /api/matter/pairing_code exposes the codes a third-party controller needs to add 42W to its own fabric — the inverse of the Phase 1 commission flow. Also fixes a sidecar startup bug surfaced while testing this: setting @matter/nodejs/config's defaultStoragePath at the top of index.ts threw NodeJsAlreadyInitializedError, because ESM hoists all of the file's imports (including @matter/main, which initializes the default Environment) ahead of that assignment. Switched to setting MATTER_STORAGE_PATH via process.env, which matter.js's VariableService picks up lazily.
Each driver opted in via the new matter_bridge config flag becomes a bridged Matter device (Aggregator -> BridgedNode -> ElectricalMeter child endpoint) so other Matter ecosystems can see its live power draw. matterBridgeLoop pushes readings to the sidecar's sync_bridge command every 5 minutes, mirroring the Phase 2 price-feed loop. Uses ElectricalMeterDevice uniformly for all DER types rather than type-specific device types (battery/PV/EVSE), trading semantic richness for one tractable code path. SoC bridging is deferred — it needs its own feature-conformance work on PowerSourceServer's Battery feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
pushMatterBridge returned early when no driver had matter_bridge set, which meant disabling the flag on every (or one of several) bridged driver via hot-reload never told the sidecar to mark those endpoints unreachable -- they'd stay reachable:true forever. Always call SyncBridge now, including with an empty list. Also warns at startup if matter_bridge is set without a matter: block, and adds SyncBridge test coverage matching the existing capability test style.
… build - sidecar WS control plane binds to loopback only (was 0.0.0.0 on a host-networked container, exposing unauthenticated commission/write_attribute/send_command to the whole LAN) - client.go: set a write deadline before WriteMessage so a half-open socket can't block past the caller's context timeout; drop the connection on write error so readLoop reconnects instead of retrying writes on a dead socket forever - Dial() no longer fails permanently if the sidecar isn't up yet at startup — it keeps retrying via the existing reconnect loop instead of requiring a full 42W restart - matter.lua always sets a make (falling back to "matter") so ResolveDeviceID's make+serial path fires; without it, every node on a shared sidecar resolved to the same device_id (the shared matter://host:port endpoint), colliding learned state across devices - config.go: matter.host / capabilities.matter.host now required when matter: is configured (previously silent until first runtime call); thermostat flexloads now require min_c/max_c instead of silently allowing 0/0 through - Dockerfile: COPY + npm ci against package-lock.json instead of npm install against package.json alone, for reproducible builds Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01216f8gyh8UStcEYivzreQZ
|
Related: work has started in the device simulator to be able to simulate Matter devices — commissionable thermostats, smart plugs (with energy), EVSE, heat pumps, dishwashers/washing machines, and temperature/light sensors — so this can be tested against virtual devices without physical hardware. Draft PR: srcfl/device-simulator#5 |
|
Closing this draft because it is not deployable through the repository’s supported release paths and has since diverged architecturally. Concrete blockers: (1) scripts/install.sh downloads only docker-compose.yml and the RPi image copies only that file, while this PR declares build: ./matter-sidecar; the source directory is absent on real installs, so compose cannot build/start it. (2) No workflow publishes a multi-arch sidecar image and there are no CI checks for the TypeScript/matter.js runtime. (3) The flexload/thermalmodel implementation predates and conflicts with the comfort-bounded thermal.Asset contract now on master from #491. (4) The branch also carries an obsolete MyUplink implementation and combines controller transport, commissioning, EMS server, DER bridge, and a new scheduler in one unreviewable release unit. A replacement should be split: first publish and integration-test an authenticated/loopback matter.js sidecar image for amd64+arm64; then add a focused Go transport; then adapt flexible loads to internal/thermal with explicit comfort/failsafe tests. Do not port the current compose build block. |
Why this matters
42W already optimizes a fleet of DERs — batteries, EV chargers, inverters. The biggest uncontrolled energy in most homes is still the thermal and pumped loads: spa/pool heaters, floor-heating thermostats, water heaters, circulation pumps, panel/radiator heaters. These are exactly the high-kWh, time-shiftable loads price optimization cares most about, and they're increasingly Matter-native (or reachable through a Matter bridge).
This PR adds Matter as a transport so those loads become first-class managed devices — a natural extension of the DERs 42W already controls, planned against the same price/PV forecast the battery uses. A spa that heats on cheap/solar hours, a floor that pre-heats before the evening peak and coasts through it, a water heater that dodges the expensive window — all under the same optimizer, no extra per-device code.
Summary
Adds Matter as a first-class driver transport (like MQTT/Modbus/HTTP) and builds price-aware flexible-load control on top of it. The battery/EV DP is deliberately not touched (cramming thermal + interruptible state into it would explode it from ~100M to billions of evaluations); flexible loads decompose into a separate scheduler that runs against the existing MPC price/PV curve.
Motivating question: "will Matter support work with Wiser?" — yes, Wiser exposes Matter, and this makes 42W a Matter controller without Home Assistant in the loop.
Device categories this unlocks
Generic
drivers/matter.luahandles all of them config-only — no new Go per device type.What's included
Matter transport
go/internal/matter— WebSocket client topython-matter-server(request/response correlation, auto-reconnect, write serialized off the shared mutex). No CGo.MatterCapcapability +host.matter_read/write/invokeexposed to Lua, factory wired in the registry,capabilities.matterconfig block.drivers/matter.lua— generic, config-driven driver (any Matter device: thermostat, plug, temp sensor) + documented examples.docker-compose.yml—python-matter-serversidecar (host net for mDNS, privileged for BLE commissioning,./data/matterfor fabric state).Thermal models (
go/internal/thermalmodel)ForecastRoom,CoastHoursToRoomTarget.Flexible-load scheduler(
go/internal/flexload)PlanThermal— comfort-respecting, price/PV-aware setpoint schedule with a physically-correct comfort-floor guarantee.PlanDeferrable— cheapest-slots energy-budget scheduler for smart plugs, with PV-surplus preference and self-learning plug loads (learns running power + daily energy from the plug's own metering → a spa vs. a water heater self-calibrate).Safety / scope
flexloads:is declared in config.Notes
TestLuaHTTPPatchMethod) is pre-existing onmaster(a quoting bug in that test's embedded Lua) — unrelated to this branch.python-matter-server. Natural follow-ups: protocol verification, planner-mode using the two-mass forecast,docs/+ per-packageCLAUDE.md, and an API/UI surface for the learned models (τ, COP, stove cycles, plug classification).