Skip to content

Read-only REST API, MCP server and dashboard - #457

Merged
MaStr merged 5 commits into
mainfrom
feat/dashboard-mcp
Oct 19, 2026
Merged

MaStr merged 5 commits into
mainfrom
feat/dashboard-mcp

Conversation

@MaStr

@MaStr MaStr commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

batcontrol can now serve, from the control-loop process and on one port:

  • a read-only REST API (/api/v1/*, FastAPI, typed Pydantic models)
  • a read-only MCP server (/mcp, streamable HTTP) for AI assistants
  • a Vue 3 dashboard (/) showing decision, SoC grid and price forecast

Everything is off by default (api.enabled: false), so existing installations see no change. Nothing in the web layer can change battery behaviour - all routes are GET, overrides stay on MQTT.

Design

  • One read path. REST routes and MCP tools both call web/service.py, which owns field names, units and rounding.
  • No inverter traffic from a request. The service reads only values the control loop has already cached (SoC, capacities, SoC limits), never the get_* accessors.
  • One snapshot per cycle. Forecasts are published as one frozen ForecastSnapshot, so a response never mixes two cycles. Slot 0 is the slot containing the current time; an expired forecast answers 503 forecast_expired.
  • Failure isolation. A web server that cannot bind, or an MCP server that cannot start, is logged and never takes battery control (or REST) down.
  • Offline-capable. Fonts and Swagger UI are bundled; the build fails on any external asset reference (no internet egress, HA ingress path prefix).
  • Security. Default bind 127.0.0.1; optional bearer tokens api.auth_token and a separate api.mcp_auth_token; /api/v1/config is 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 in docs/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 server
  • mqtt_api.py / interval_utils.py: forecast slots rendered by one shared generator (iter_slots); a golden test pins the MQTT payloads byte for byte
  • decision_journal.py: tracks the last mode change separately

Commits

  1. feat(api) - service layer, REST API, server, core integration
  2. feat(mcp) - MCP server
  3. feat(dashboard) - Vue dashboard + fixtures
  4. build - Docker node stage, release/docs/frontend workflows, wheel verification
  5. docs

Commits 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 build incl. offline check
  • Verified manually: Docker image, MCP over real HTTP (initialize + tools/list), both tokens through the real uvicorn stack, dashboard at desktop and phone width with all fixtures
  • Reviewed with several code-review / simplify / security-review passes on the full diff

Follow-ups

Merge order

Merge this before the companion add-on PR in MaStr/batcontrol_ha_addon: the development add-on builds the dashboard from batcontrol main.

🤖 Generated with Claude Code

image

MaStr and others added 5 commits October 16, 2026 13:27
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>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 11:29
@MaStr

MaStr commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

Companion add-on PR: MaStr/batcontrol_ha_addon#35 (merge this one first).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 under frontend/, all GET-only and off by default.
  • core.py/mqtt_api.py/interval_utils.py/decision_journal.py/modes.py refactored to cache API-facing values, share iter_slots, and expose mode constants without importing core.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.

Comment thread src/batcontrol/modes.py
Comment on lines +34 to +38
}



def mode_code(mode) -> str:
@MaStr
MaStr merged commit 8e1c96d into main Oct 19, 2026
13 checks passed
@MaStr
MaStr deleted the feat/dashboard-mcp branch October 19, 2026 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants