This guide helps you set up a development environment for the BESS Battery Manager add-on.
- SOFTWARE_DESIGN.md — Architecture, components, and data flow
- INSTALLATION.md — Add-on installation and sensor configuration
- USER_GUIDE.md — Dashboard and interface guide
- CLAUDE.md — Coding guidelines and architecture conventions for Claude Code
- TODO.md — Prioritized improvements and known issues
- Docker and Docker Compose
- Node.js 18 or higher
- Home Assistant instance (local or network-accessible)
- A long-lived access token from Home Assistant
The BESS Manager can be developed using either a local or production HomeAssistant instance:
When running HomeAssistant locally (e.g., on localhost:8123):
-
Set
HA_URL=http://localhost:8123in.env -
No token is required when running both services locally
-
Useful for rapid development and testing
When using a remote/production HomeAssistant:
-
Set
HA_URLto your HomeAssistant URL -
Generate and set a long-lived access token in
HA_TOKEN -
Enables testing with real production data
Example .env configuration:
# Local development
HA_URL=http://localhost:8123
HA_TOKEN= # Not needed for local development
# OR Production environment
HA_URL=https://your-ha-instance.com
HA_TOKEN=your_long_lived_access_token_here
```text
### Initial Setup
1. Clone the Repository:
```bash
git clone https://github.com/johanzander/bess-manager.git
cd bess-manager
```text
2. Create Environment File:
```bash
# Create .env in root directory
HA_URL=http://host.docker.internal:8123
HA_TOKEN=your_long_lived_access_token_here
FLASK_DEBUG=true
PORT=8080
# Optional database settings
HA_DB_URL=http://homeassistant.local:8086/api/v2/query
HA_DB_USER_NAME=your_db_username
HA_DB_PASSWORD=your_db_password
```text
3. Install frontend dependencies:
```bash
cd frontend && npm install && cd ..-
Start the development environment:
./dev-run.sh
This will build the frontend, then build and start the backend container via Docker Compose.
Start all services with:
./dev-run.shAccess the services:
- BESS backend + UI: http://localhost:8080
- Vite dev server (hot-reload): http://localhost:5174
If using VS Code with Remote-Containers:
-
Open the project in VS Code
-
Click the remote connection button (bottom-left corner)
-
Select "Remote-Containers: Reopen in Container"
├── backend/ # FastAPI application and add-on config
├── core/ # Core BESS system implementation
│ └── bess/ # BESS-specific modules
│ ├── tests/ # Test directory
│ └── ... # Implementation modules
├── frontend/ # React-based web interface
└── build/ # Build output (created by package-addon.sh)
├── bess_manager/ # Add-on build
└── repository/ # Repository build
```text
## Development Workflow
### API Development
1. Add new endpoints in `backend/app.py`
2. Update OpenAPI spec if needed
3. Generate frontend API client:
```bash
cd frontend
npm run generate-api
```text
### Frontend Development
1. Navigate to `frontend/`
2. Make changes to React components
3. Changes hot-reload automatically
4. Build for production:
```bash
npm run build
```text
### Python Environment
Each worktree requires its own `.venv`. Venvs contain absolute paths and cannot be shared or copied between machines/worktrees.
```bash
# Create .venv (run once per worktree, or after cloning/rsyncing to a new machine)
python3 -m venv .venv && .venv/bin/pip install -r backend/requirements.txt -r requirements-dev.txt
# Fast tests only (recommended during development, ~3s)
.venv/bin/pytest -m "not slow"
# Algorithm/integration tests (full optimizer, ~30min)
.venv/bin/pytest -m slow
# Run all tests
.venv/bin/pytest
# Run specific categories
.venv/bin/pytest core/bess/tests/unit/
.venv/bin/pytest core/bess/tests/integration/
.venv/bin/pytest backend/tests/
# Run with coverage
.venv/bin/pytest --cov=core.bess
# Frontend unit tests
cd frontend && npm test
# E2E tests (requires docker-compose environment running)
cd e2e && npx playwright test --project=chromiumTests are split using pytest.mark.slow. The fast set covers backend API and
non-optimizer unit tests. The slow set runs the full DP algorithm and integration
scenarios.
The savings the optimizer reports are a plan — derived from the battery
actions it chooses. The real inverter is driven by coarse modes
(load_first / grid_first / battery_first), which are policies, not exact
power setpoints, so a plan can claim economics the hardware can't actually
deliver. The plan-faithfulness simulator in core/bess/simulation/ executes a
plan through a model of the inverter and computes the realized economics,
asserting they match the plan (R == P). It runs as a pure unit test — no
hardware, no mock HA.
This is a strong correctness tool: it already caught the solar-export
over-crediting bug (savings inflated ~8–16% on sunny days, fixed in 9.6.0) and a
discharge-pacing limitation — both invisible to plan-only tests. If you change
the optimizer or inverter control, verify R == P still holds. Deep reference:
docs/agents/simulator.md.
End-to-end tests run a full browser against the backend + mock HA stack:
# Start the test environment
SCENARIO=ci-normal-day docker compose -f docker-compose.ci.yml up -d
# Wait for BESS to be ready
until curl -sf http://localhost:8080/api/settings > /dev/null; do sleep 2; done
# Run all E2E tests (smoke + pages + API contracts)
cd e2e && npx playwright test --project=chromium
# Run only wizard tests (requires wizard scenario)
cd e2e && npx playwright test --project=wizard
# Stop environment
docker compose -f docker-compose.ci.yml downRunning multiple worktrees without port conflicts:
# Automatic — finds free ports and isolates containers per-worktree
./e2e/run-e2e.sh # all tests
./e2e/run-e2e.sh --project=chromium # smoke only
./e2e/run-e2e.sh --project=wizard # wizard only
# Manual — pick a port to avoid collisions with other worktrees
BESS_PORT=8085 MOCK_HA_PORT=8185 docker compose -p my-branch -f docker-compose.ci.yml up -d
BESS_PORT=8085 npx playwright test --project=chromiumThe BESS_PORT env var is used by both docker-compose.ci.yml (host port mapping)
and playwright.config.ts (base URL). The -p flag gives each environment its own
container namespace so names don't collide.
E2E test structure:
| Spec file | What it tests |
|---|---|
api-smoke.spec.ts |
All API endpoints return 200 |
api-contracts.spec.ts |
Response shapes the frontend depends on |
dashboard.spec.ts |
Dashboard loading, content, resolution selector |
navigation.spec.ts |
All routes load without errors |
settings-page.spec.ts |
Tab navigation, form editing, save persistence |
inverter-page.spec.ts |
Schedule overview, TOU intervals, intents |
savings-page.spec.ts |
View modes, resolution, cost display |
mock-ha.spec.ts |
Mock HA connectivity and inverter commands |
setup-wizard.spec.ts |
Full wizard flow with auto-discovery |
To verify the production Docker image builds correctly and the app starts:
docker compose -f docker-compose.prod-test.yml up -d --build
curl http://localhost:8080/api/system-health
docker compose -f docker-compose.prod-test.yml downThis builds the real Dockerfile (which explicitly lists backend files to COPY)
and boots it with mock-HA. Catches missing files that would cause runtime
ImportError in production. Use BESS_PORT=8081 if port 8080 is in use.
CI runs on every PR (.github/workflows/ci.yml):
| Job | Trigger | What it runs |
|---|---|---|
| Fast tests | backend/ or core/ changed |
pytest -m "not slow" (~3s, 333 tests) |
| Algorithm tests | core/bess/ changed |
pytest -m slow (~30min, 116 tests) |
| Frontend checks | frontend/ changed |
npm test + type-check + lint |
| E2E tests | backend/frontend/e2e/docker changed | Playwright against docker-compose mock HA (2 phases: smoke + wizard) |
| Code quality | Always | Black + Ruff formatting/linting |
| Docker build & boot | backend/frontend/Dockerfile changed | Production image boots and responds |
The quality gate script (scripts/quality-check.sh) runs fast tests + linting
and is used by the automated issue-fix bot.
# Format code
black .
ruff check --fix .
# Type checking
mypy .
# Run all pre-commit hooks
pre-commit run --all-files
```text
## Debugging
### Backend Debugging
- Logs are available via:
```bash
docker-compose logs -f
```text
- Set `FLASK_DEBUG=true` in `.env` for debug mode
### Frontend Debugging
- Use browser developer tools
- React Developer Tools extension recommended
- Source maps enabled in development
### Common Issues
1. Connection Issues:
- Verify access token validity
- Check `HA_URL` configuration
- Ensure Docker network connectivity
2. Import Errors:
- Verify `requirements.txt` is complete
- Rebuild container: `docker-compose down && docker-compose up -d`
3. API Errors:
- Check logs for detailed messages
- Verify required Home Assistant entities exist
- Check access token permissions
## Deploying to a Local Home Assistant Instance
This is the developer workflow for testing a built add-on on real hardware before publishing.
Normal users should install from the GitHub repository — see [INSTALLATION.md](INSTALLATION.md).
**Prerequisites:** Node.js 18 or higher (required to build the frontend).
1. Build the add-on package:
```bash
chmod +x package-addon.sh
./package-addon.shThis runs npm ci && npm run build locally, then assembles the add-on into build/bess_manager/.
-
Transfer files to Home Assistant:
- Copy
build/bess_manager/contents to/addons/bess_manager/on the HA host - Via SSH, Samba, or the File Editor add-on
- Copy
-
Install the local add-on:
- Settings → Add-ons → Reload
- Find "BESS Battery Manager" under Local add-ons
- Click "Install"
The mock HA environment lets you run the full BESS stack (backend + frontend) without a
real Home Assistant instance or inverter. BESS runs unmodified — only HA_URL is pointed
at a local mock server that serves synthetic sensor data and records service calls.
Generate a scenario from a debug log, then run it:
python scripts/mock_ha/scenarios/from_debug_log.py bess-debug-2026-03-24-225535.md
./mock-run.sh 2026-03-24-225535To replay from a specific time of day (useful for testing optimizer behavior at
a particular hour), pass an optional HH:MM argument:
./mock-run.sh 2026-04-22-202249 09:00 # run as if it is 09:00 on that dateThe date is kept from the scenario; only the time is overridden.
- BESS UI: http://localhost:8080
- Service call log (inverter writes): http://localhost:8123/mock/service_log
- Sensor states: http://localhost:8123/mock/sensors
BESS container (unmodified)
↓ HA_URL=http://mock-ha:8123
mock-ha container (FastAPI)
→ GET /api/states/{entity_id} → sensor state from scenario JSON
→ POST /api/services/{domain}/{svc} → record call, return canned response
→ GET /mock/service_log → inspect what was written to inverter
The mock server loads a scenario JSON at startup. BESS reads sensors and writes schedules exactly as it would against real HA — the mock records every service call so you can verify what the optimizer decided to send to the inverter.
Scenarios live in scripts/mock_ha/scenarios/ and are named after the debug log
timestamp they were generated from (e.g. 2026-03-24-225535.json). Each scenario
contains:
sensors— all HA entity states BESS will readinverter_type—"min"or"sph", determines which inverter service calls BESS usesmock_time— frozen wall-clock time (e.g."@2026-03-24 22:55:35"); BESS believes it is running at this time vialibfaketimetime_segments— inverter TOU state at time of capture (active segments only; disabled slots are not reconstructable from debug logs)
The entire scenario — sensor states, inverter type, and BESS config (entity ID
mappings, battery settings, inverter config) — is generated from the debug log.
mock-run.sh mounts bess_config as /data/options.json in the BESS container.
Option A — System Health page: Open the BESS UI, go to System Health, and click
the Export Debug Data button. Save the downloaded .md file anywhere accessible.
Option B — Via Claude Code (MCP): With a running BESS instance and the MCP server configured, ask Claude to fetch and generate a scenario in one step:
"Fetch the current BESS debug log and generate a mock scenario from it"
Claude will use fetch_live_debug (saves to .bess-logs/) then run
from_debug_log.py automatically. No manual download needed.
The from_debug_log.py script extracts everything needed for a faithful replay:
- Battery SOC (converted from
initial_soe/total_capacity) - Nordpool prices — from
### Price Datain new-style logs (exact quarterly values), or reverse-calculated from marked-upbuy_pricein old logs - Solar forecast from
full_solar_production - Inverter TOU segments from
## Inverter TOU Segments(new-style logs) - Frozen timestamp from the debug log filename
# Generate scenario from a saved debug log
python scripts/mock_ha/scenarios/from_debug_log.py bess-debug-YYYY-MM-DD-HHMMSS.md
# Run replay — BESS runs as if it is that exact moment in time
./mock-run.sh YYYY-MM-DD-HHMMSS
# Run replay from a specific time (e.g. to test optimizer before solar hours)
./mock-run.sh YYYY-MM-DD-HHMMSS 09:00The debug log includes:
### Price Data— full raw Nordpool quarterly prices for both days, enabling exact replay without reverse-calculating through the markup formula## Inverter TOU Segments— active TOU slots held in memory, mirroring the current hardware state
GitHub issues are handled by a three-stage automated pipeline. See
docs/agents/workflow.md for the full process description.
.github/workflows/issue-triage.yml fires when an issue is opened. It
classifies the issue (bug / feature / question) and applies labels. For bugs
without debug info it posts instructions for getting a BESS debug export. For
bugs with debug info it posts an initial root-cause analysis immediately.
When a user replies with a debug log on a needs-debug-log issue, the workflow
fires again automatically and runs deeper analysis.
Commenting @claude-bot fix this on an issue triggers
.github/workflows/claude-bot.yml, which runs scripts/issue_fixer.py. The
bot explores the codebase, implements a fix, runs pytest, and opens a draft
PR. It will attempt to fix test failures before opening the PR.
Commenting @claude-bot on a PR triggers scripts/pr_reviewer.py, which
reviews the diff against docs/agents/rules.md and docs/agents/architecture.md
and posts a review comment.
Human gate: Draft PRs are never merged automatically. You approve and merge.
The docs/agents/ directory contains focused files loaded into agent system prompts:
| File | Purpose |
|---|---|
rules.md |
Hard constraints (loaded by every agent) |
architecture.md |
Component map and key files (agent summary) |
patterns.md |
Code patterns with good/bad examples |
testing.md |
Test philosophy and mock HA bug reproduction |
workflow.md |
PR/release process and label conventions |
Tests must verify behavior (what the system does), not implementation
(how it does it). A test that breaks when you swap two equivalent algorithms is
a bad test. See docs/agents/testing.md for full guidance and examples.
BESS Manager includes an MCP (Model Context Protocol) server for integration with Claude Code, enabling AI-assisted debugging and log analysis.
To enable, add to .claude/mcp.json:
{
"mcpServers": {
"bess": {
"command": "python3",
"args": ["scripts/bess-mcp-server.py"]
}
}
}Configure connection in .env:
HA_URL=https://your-homeassistant-url
HA_TOKEN=your-long-lived-access-token