Repository navigation
Read-only REST API, MCP server and dashboard - #457
Conversation
A FastAPI application served by uvicorn in a daemon thread, enabled with api.enabled. Every route is GET; all of them read through one service layer (web/service.py), which owns field names, units and rounding. - The service never calls the core's get_* accessors: it reads values the loop has cached (SoC, capacities, SoC limits), so a request can never cause inverter traffic or reset the loop's cache flags. - Forecasts come from one frozen ForecastSnapshot the loop publishes per cycle, so a response never mixes two cycles. Slot 0 is the slot that contains the current time; an expired forecast answers 503. - online/stale also covers a loop that never completes a cycle and a forecast that is no longer refreshed. - Optional static bearer token (api.auth_token); /api/v1/health stays open. - Swagger UI and the OpenAPI schema only with api.docs_enabled (default off). - Forecast slot rendering is shared with the MQTT forecasts (interval_utils.iter_slots); a golden test pins the MQTT payloads. - The decision journal tracks the last mode change separately, so "mode since" does not move on charge rate changes. - A failing web server never takes battery control down: bind and lifespan errors are logged and the loop keeps running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Streamable HTTP transport on the same port (api.mcp_enabled). The tools are thin wrappers around the same service layer as REST, annotated read-only, with output schemas and paged forecast series (24 hours by default). /mcp has its own bearer token (api.mcp_auth_token), inheriting api.auth_token when unset. A failing MCP start-up is logged and leaves REST and the dashboard running. No Host header validation, so reverse proxies work; the token is the protection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Terminal-style dashboard (decision, SoC grid, price) built by npm into src/batcontrol/web/static and shipped as package data. Fonts and Swagger UI are bundled; the build fails on any external asset reference, because batcontrol runs without internet egress and behind HA ingress (relative base path). Frozen API fixtures (stored as patches over default.json) drive both "npm run dev:fixtures" and the unit tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dockerfile builds the dashboard in a separate node stage (BUILDPLATFORM pinned, so multi-arch builds run npm natively once) - the release workflow builds the dashboard and verify_wheel_dashboard.py fails the release if the wheel lacks it - frontend CI (build, tests, offline check), dependabot for npm - the docs build publishes the OpenAPI schema via dump_openapi.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
User docs for the REST API (conventions, error codes, security), the MCP server and the dashboard, the design specs under docs/development, the api: parameters in installation, and agent instructions in CLAUDE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Companion add-on PR: MaStr/batcontrol_ha_addon#35 (merge this one first). |
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a large new externally-consumed API/MCP/dashboard surface plus Docker and release-workflow changes whose scope and contract implications warrant final human review.
1 open finding
What changed in this PR
This PR adds an optional, read-only web layer that runs inside the batcontrol control-loop process on a single port: a FastAPI REST API (/api/v1/*), an MCP server (/mcp), and a Vue 3 dashboard (/). Everything is gated behind api.enabled: false, so existing installations are unaffected. The design enforces three hard invariants — a single read path (web/service.py), no inverter traffic from a web request (only control-loop-cached values are read), and one frozen ForecastSnapshot per cycle so a response never mixes two cycles. A web-server failure is isolated and never stops battery control. Supporting changes outside web/ cache the values the API exposes, publish the snapshot, share one slot generator (interval_utils.iter_slots) between MQTT and the API, and track the last mode change separately.
Changes:
- New
src/batcontrol/web/package (service, routes, models, snapshot, config, auth, server, app, MCP) plus a Vue dashboard underfrontend/, allGET-only and off by default. core.py/mqtt_api.py/interval_utils.py/decision_journal.py/modes.pyrefactored to cache API-facing values, shareiter_slots, and expose mode constants without importingcore.py.- Extensive tests (Python + vitest), docs, Docker node build stage, release/frontend/docs workflows, wheel verification, and third-party license bundling.
| File | Description |
|---|---|
src/batcontrol/web/service.py, models.py, routes.py, app.py, mcp.py |
The single read path, Pydantic responses (units in field descriptions), GET-only routes, FastAPI factory and MCP tool wrappers. |
src/batcontrol/web/snapshot.py |
Immutable per-cycle forecast snapshot; slots() renders via the shared iter_slots. |
src/batcontrol/web/config.py, auth.py, server.py |
api: block parsing + loopback warning, byte-safe constant-time token check, uvicorn daemon thread with failure isolation. |
src/batcontrol/interval_utils.py |
Adds align_to_interval/iter_slots, the one slot generator shared by MQTT and the web API. |
src/batcontrol/core.py |
Caches API-facing values (incl. SoC limits), publishes the snapshot, starts/stops the web server. |
src/batcontrol/modes.py |
Mode constants/labels extracted so consumers avoid importing core.py. |
src/batcontrol/decision_journal.py |
Tracks the last mode change separately from the last status change. |
src/batcontrol/mqtt_api.py |
Forecast topics now render through iter_slots; byte-for-byte golden test retained. |
frontend/** (App/SocGrid/PricePanel/DecisionPanel/DecisionTrace, lib/*.js, fixtures, build config, offline check, license texts) |
Vue dashboard, view-model mapping in one api.js, offline-asset guard, bundled fonts/Swagger licenses. |
tests/batcontrol/web/**, tests/batcontrol/test_mqtt_api.py, tests/batcontrol/test_decision_journal.py |
New endpoint/service/server/auth/readonly/snapshot tests and regression coverage. |
config/batcontrol_config_dummy.yaml, docs/**, mkdocs.yml, README.md, THIRD-PARTY-LICENSES.md |
api: block template, user/dev docs, nav registration, license inventory. |
Dockerfile, .github/workflows/**, scripts/verify_wheel_dashboard.py, scripts/dump_openapi.py, pyproject.toml, .gitignore, .dockerignore, .github/dependabot.yml |
Node build stage, release/frontend/docs CI, wheel-dashboard verification, OpenAPI dump, new deps and packaging. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
|
|
||
|
|
||
|
|
||
| def mode_code(mode) -> str: |

Summary
batcontrol can now serve, from the control-loop process and on one port:
/api/v1/*, FastAPI, typed Pydantic models)/mcp, streamable HTTP) for AI assistants/) showing decision, SoC grid and price forecastEverything is off by default (
api.enabled: false), so existing installations see no change. Nothing in the web layer can change battery behaviour - all routes areGET, overrides stay on MQTT.Design
web/service.py, which owns field names, units and rounding.get_*accessors.ForecastSnapshot, so a response never mixes two cycles. Slot 0 is the slot containing the current time; an expired forecast answers503 forecast_expired.127.0.0.1; optional bearer tokensapi.auth_tokenand a separateapi.mcp_auth_token;/api/v1/configis an explicit allow-list and never reads the config file. There is deliberately no Host header validation (it breaks reverse proxies) - the docs say to set a token behind a proxy.New configuration (
api:block)enabled(false),host(127.0.0.1),port(8080),dashboard_enabled(true),docs_enabled(false - Swagger UI + OpenAPI schema),mcp_enabled(false),auth_token,mcp_auth_token. Documented indocs/integrations/rest-api.md,docs/integrations/mcp-server.md,docs/features/dashboard.md.Small changes outside
web/core.py: caches the values the API reads (incl. SoC limits) and publishes the forecast snapshot; starts/stops the web servermqtt_api.py/interval_utils.py: forecast slots rendered by one shared generator (iter_slots); a golden test pins the MQTT payloads byte for bytedecision_journal.py: tracks the last mode change separatelyCommits
feat(api)- service layer, REST API, server, core integrationfeat(mcp)- MCP serverfeat(dashboard)- Vue dashboard + fixturesbuild- Docker node stage, release/docs/frontend workflows, wheel verificationdocsCommits 1 and 2 each pass the full test suite on their own.
Testing
pytest: 1625 passed; pylint 9.68 (unchanged)vitest: 58 passed;npm run buildincl. offline checkFollow-ups
Merge order
Merge this before the companion add-on PR in
MaStr/batcontrol_ha_addon: the development add-on builds the dashboard from batcontrolmain.🤖 Generated with Claude Code