Developer Guide
Architecture, contributing guidelines, and development setup
Architecture Overview
System Components
eos_connect.py
Main application entry point and orchestration
config.py
Bootstrap configuration, HA addon detection, ENV variable loading
config_web/
Web-based configuration module (schema, SQLite store, REST API, hot-reload)
interfaces/
Data source and control interfaces
web/
Web dashboard, config UI, setup wizard (HTML/CSS/JS)
Core Interfaces
| Interface | Purpose | File |
|---|---|---|
| Base Control | Core control state management | base_control.py |
| Battery Interface | Battery data retrieval and management | battery_interface.py |
| PV Interface | Solar forecast retrieval | pv_interface.py |
| Price Interface | Electricity price forecasts | price_interface.py |
| Load Interface | Household load data | load_interface.py |
| Inverter (Fronius) | Inverter control and monitoring | inverter_fronius_v2.py |
| EVCC Interface | EV charging control | evcc_interface.py |
| MQTT Interface | MQTT publishing and subscription | mqtt_interface.py |
| Optimization Interface | EOS/EVopt communication | optimization_interface.py |
Data Flow
1. Main Loop (every refresh_time minutes)
├─ Fetch battery SOC (battery_interface)
├─ Fetch PV forecast (pv_interface)
├─ Fetch price forecast (price_interface)
├─ Fetch load data (load_interface)
├─ Build optimization request
├─ Send to EOS/EVopt (optimization_interface)
├─ Process optimization response
├─ Update controls (base_control)
├─ Execute inverter commands (inverter_interface)
├─ Update EVCC (evcc_interface)
├─ Publish to MQTT (mqtt_interface)
└─ Serve via web API
2. Config Web Module (parallel)
├─ Serve config REST API (/api/config/...)
├─ On PUT: validate → store in SQLite → rebuild merged config
├─ Hot-reloadable fields → apply instantly via HotReloadAdapter
└─ Restart-required fields → flag in UI, apply on next restart
Contributing
Branch Strategy
| Branch Type | Purpose | Naming |
|---|---|---|
| main | Stable, tagged releases only | - |
| develop | Integration branch (PR target) | - |
| feature | New functionality | feature/<desc> or feature/<issue>-<desc> |
| bugfix | Fix for develop | bugfix/<issue>-<desc> |
| hotfix | Urgent production fix | hotfix/<issue>-<desc> |
| issue | GitHub auto-created | issue-<number>-<desc> |
Contribution Workflow
# 1. Update local
git fetch origin
git switch develop
git pull --ff-only
# 2. Create feature branch
git switch -c feature/better-forecast
# 3. Code + test + document
# - Write code
# - Add/update tests
# - Update docs (README / CONFIG_README / MQTT if behavior changes)
# 4. Format and lint
black .
pylint src/
# 5. Run tests
pytest tests/
# 6. Rebase before PR
git fetch origin
git rebase origin/develop
# 7. Push
git push -u origin feature/better-forecast
# 8. Create PR targeting develop
# - Link relevant issues (Closes #123)
# - Provide clear description
Code Quality Requirements
- Formatting: Use Black for all Python files
- Linting: Pylint score ≥9.0
- Testing: Add/update tests for logic changes
- Documentation: Update relevant docs
Commit Message Format (Conventional Commits)
feat: add battery forecast smoothing
fix: correct negative PV handling
docs: update MQTT topic table
test: add tests for price calculation
refactor: simplify config loading
chore: update dependencies
Testing
Test Structure
Tests are organized to mirror the source structure:
tests/
├── test_control_states.py
├── config_web/
│ ├── test_api.py
│ ├── test_export_schema.py
│ ├── test_ha_addon.py
│ ├── test_hot_reload.py
│ ├── test_merger.py
│ ├── test_migration.py
│ ├── test_schema.py
│ └── test_store.py
└── interfaces/
├── test_base_control.py
├── test_base_inverter.py
├── test_battery_interface.py
├── test_battery_price_handler.py
├── test_dynamic_override.py
├── test_load_interface.py
├── test_optimization_interface.py
├── test_price_interface.py
├── test_pv_interface.py
├── test_update_checker.py
├── inverters/
│ ├── test_base_inverter.py
│ ├── test_fronius_legacy.py
│ ├── test_fronius_v2.py
│ ├── test_inverter_ha.py
│ ├── test_null_inverter.py
│ ├── test_victron.py
│ └── test_victron_inverter.py
└── optimization_backends/
├── test_optimization_backend_eos.py
└── test_optimization_backend_evopt.py
Running Tests
# Run all tests
pytest tests/
# Run specific test file
pytest tests/interfaces/test_battery_interface.py
# Run with coverage
pytest --cov=src tests/
# Run with verbose output
pytest -v tests/
Writing Tests
Test File Naming
- Place in
tests/directory - Mirror source structure
- Name as
test_<module>.py
Example Test
import pytest
from src.interfaces.battery_interface import BatteryInterface
def test_battery_soc_calculation():
"""Test SOC calculation with known values"""
battery = BatteryInterface(capacity_wh=10000)
battery.set_soc(0.75)
assert battery.get_remaining_wh() == 7500
assert battery.get_soc() == 0.75
def test_battery_temperature_protection():
"""Test temperature-based power reduction"""
battery = BatteryInterface(
capacity_wh=10000,
max_charge_power_w=5000,
temperature_sensor="sensor.temp"
)
# Cold temperature should reduce power
power = battery.calculate_max_charge_power(
soc=0.50,
temperature=-5
)
assert power < 500 # Should be ~5-7.5% of max
Test Configuration
Configure pytest in pytest.ini:
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
Development Setup
Prerequisites
- Python 3.11 or higher
- Git
- pip
- (Optional) VS Code with Python extension
Setup Steps
# 1. Clone repository
git clone https://github.com/ohAnd/EOS_connect.git
cd EOS_connect
# 2. Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Install development dependencies
pip install black pylint pytest pytest-cov
# 5. Run EOS Connect
python src/eos_connect.py
# On first launch, a Setup Wizard in the web UI guides you through configuration.
# Only bootstrap settings (port, timezone, log level) can optionally be set in config.yaml.
VS Code Configuration
Recommended .vscode/settings.json:
{
"python.defaultInterpreterPath": "./venv/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.linting.pylintArgs": ["--rcfile=.pylintrc"],
"python.formatting.provider": "black",
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests"]
}
Environment Variables
For development, you can override config with environment variables:
# Linux/Mac
export EOS_CONNECT_LOG_LEVEL=debug
export EOS_CONNECT_PORT=8081
# Windows
set EOS_CONNECT_LOG_LEVEL=debug
set EOS_CONNECT_PORT=8081
Config Web Module
The web-based configuration system lives in src/config_web/ as a self-contained module. It replaces direct YAML editing with a browser UI, SQLite persistence, and a REST API — while keeping every existing interface working unchanged.
Module Files
| File | Purpose |
|---|---|
__init__.py | ConfigWebModule facade — single entry point used by eos_connect.py |
schema.py | SPOT (Single Point of Truth) — all FieldDef entries, BOOTSTRAP_KEYS, SECTION_META |
store.py | SQLite persistence (WAL mode, thread-safe, change callbacks) |
migration.py | config.yaml → SQLite and HA options.json → SQLite migration |
merger.py | Builds merged config dict — same shape interfaces always received |
api.py | Flask Blueprint with 10 REST endpoints at /api/config/ |
hot_reload.py | HotReloadAdapter — applies selected changes to running interfaces without restart |
Key Design Principles
- Zero interface changes: The merger produces the same dict shape interfaces always consumed. No code changes needed in existing interfaces.
- SPOT:
schema.pydefines all field metadata. The web UI, REST API validation, docs export, and merger all consume it — never duplicate field definitions. - Bootstrap vs Store: ~5 bootstrap keys (
web_port,time_zone,log_level, …) stay inconfig.yaml/ ENV / HA options. Everything else lives in SQLite. - 3-level progressive disclosure: Fields have a
level(getting_started,standard,expert) controlling visibility in the setup wizard and config UI. - Static asset caching: HTML is revalidated on each load, while local CSS/JS assets use versioned URLs (
?v=<app_version>) so browsers can cache aggressively and still fetch new assets automatically after upgrades.
SPOT Pipeline
schema.py (Python FieldDefs)
│
├──► export_config_schema.py ──► docs/assets/data/config_schema.json
│ (consumed by GitHub Pages docs)
│
└──► /api/config/schema (live REST endpoint)
│
├──► config.js (ConfigurationManager reads fields + sections)
└──► wizard.js (SetupWizard reads fields + sections)
When you change a field definition in schema.py, the web UI, API validation, docs table, and merger all update automatically. Run python scripts/export_config_schema.py to regenerate the docs JSON.
Data Flow: Config Lifecycle
Startup:
config.yaml ──► ConfigManager (bootstrap keys)
│
▼
migration.py: YAML values + HA options.json ──► SQLite store
│
▼
merger.py: bootstrap + SQLite + defaults ──► merged config dict
│
▼
Interfaces receive merged dict (same shape as before)
Runtime (user saves via web UI):
Browser PUT /api/config/ ──► api.py validates against schema
│
▼
store.py writes to SQLite ──► merger rebuilds config dict
│
├──► hot_reload.py applies hot-reloadable fields instantly
└──► restart-required fields flagged in UI banner
REST API Endpoints
All endpoints are under /api/config/:
| Method | Path | Purpose |
|---|---|---|
| GET | /schema | Full schema JSON (fields + section metadata) |
| GET | / | Current config values (passwords masked) |
| PUT | / | Partial update (validates, categorizes restart vs hot-reload) |
| GET | /section/<name> | Single section values |
| POST | /validate | Validate without saving |
| GET | /restart-required | Pending restart-required fields |
| GET | /export | Export all settings as flat JSON |
| POST | /import | Import settings from JSON |
| GET | /wizard-status | Setup wizard completion state |
| POST | /wizard-complete | Mark wizard as completed |
Hot Reload
HotReloadAdapter registers interface references at startup. When hot-reloadable fields are saved, changes are applied immediately without restart:
- Price fields — tibber token, awattar country, energycharts zone, etc.
- Battery SOC fields — min/max SOC percentages
Fields that require a restart (e.g., MQTT broker URL, web port) are flagged in the API response, and the UI shows a restart banner.
Frontend Architecture
| File | Purpose |
|---|---|
config.js | ConfigurationManager class — fetches schema, renders section tabs, field inputs, handles save/validate/import/export |
wizard.js | SetupWizard class — first-run wizard showing getting_started fields only |
config.css | Config page styles (section sidebar, field groups, badges) |
wizard.css | Wizard overlay and step styles |
Both config.js and wizard.js read the CONFIG_SECTIONS from the API response (schemaData.sections) — section icons and labels are never hardcoded in the frontend.
How to Add a New Config Field
- Add a
FieldDef(...)to_ALL_FIELDSinsrc/config_web/schema.py - Run
python scripts/export_config_schema.pyto update docs JSON - Done — Web UI, API validation, docs table, migration, and merger all pick it up automatically
- If the field is hot-reloadable: add mapping to
_PRICE_FIELD_MAPor_BATTERY_SOC_FIELDSinhot_reload.py
How to Add a New Config Section
- Add fields with the new
sectionname inschema.py - Add an entry to
SECTION_METAdict inschema.py(icon + label) - Run
python scripts/export_config_schema.py - Done — Frontend falls back gracefully, but
SECTION_METAgives it the correct icon and label
schema.py file is the single source of truth. If you need to change a field's label, default value, validation, or section — change it there and re-export.
Project Structure
EOS_connect/
├── src/
│ ├── eos_connect.py # Main application
│ ├── config.py # Bootstrap configuration
│ ├── config.yaml # Bootstrap + fallback config
│ ├── constants.py # Constants and enums
│ ├── log_handler.py # Logging setup
│ ├── version.py # Version information
│ ├── config_web/ # Web-based config system
│ │ ├── __init__.py # ConfigWebModule facade
│ │ ├── schema.py # SPOT — all field definitions
│ │ ├── store.py # SQLite persistence (WAL)
│ │ ├── migration.py # YAML/HA → SQLite migration
│ │ ├── merger.py # Builds merged config dict
│ │ ├── api.py # Flask Blueprint REST API
│ │ └── hot_reload.py # Live-apply changes
│ ├── interfaces/ # Data interfaces
│ │ ├── base_control.py
│ │ ├── base_inverter.py
│ │ ├── battery_interface.py
│ │ ├── battery_price_handler.py
│ │ ├── evcc_interface.py
│ │ ├── load_interface.py
│ │ ├── mqtt_interface.py
│ │ ├── optimization_interface.py
│ │ ├── port_interface.py
│ │ ├── price_interface.py
│ │ ├── pv_interface.py
│ │ ├── update_checker.py
│ │ ├── inverters/ # Inverter implementations
│ │ └── optimization_backends/
│ ├── web/ # Web dashboard + config UI
│ │ ├── index.html
│ │ ├── css/
│ │ │ ├── style.css # Main dashboard styles
│ │ │ ├── config.css # Config UI styles
│ │ │ └── wizard.css # Setup wizard styles
│ │ └── js/
│ │ ├── main.js # Dashboard entry point
│ │ ├── config.js # ConfigurationManager
│ │ ├── wizard.js # SetupWizard
│ │ └── ... # Other dashboard modules
│ └── json/ # JSON schemas and examples
├── scripts/
│ └── export_config_schema.py # SPOT → docs JSON export
├── tests/ # Test suite (659+ tests)
├── docs/ # Documentation (GitHub Pages)
│ └── assets/data/
│ └── config_schema.json # Exported schema for docs
├── docker-compose.yml # Docker deployment
├── Dockerfile # Docker image
├── requirements.txt # Python dependencies
├── pytest.ini # Pytest configuration
├── README.md # Project overview
├── CONTRIBUTING.md # Contribution guidelines
└── LICENSE # MIT License
Resources
Documentation
Related Projects
- Akkudoktor EOS - Main optimization backend
- EVCC - EV charging controller
- Home Assistant - Smart home platform
Tools
Get Involved!
We welcome contributions of all kinds:
Report Bugs
Open an issue with detailed reproduction steps
Suggest Features
Share your ideas in GitHub Discussions
Improve Docs
Documentation PRs are always welcome!
Submit PRs
Follow the contribution workflow and submit your changes
HA Addon Integration
EOS Connect runs as a Home Assistant add-on via the ohAnd/ha_addons repository. The web-based config system changes how configuration is handled.
How it Works
| Aspect | Old (config.yaml only) | New (SQLite + web UI) |
|---|---|---|
| Config storage | HA writes /data/options.json with all settings | SQLite at /data/eos_connect.db, web UI for editing |
| HA addon options | Full config in options.json | Bootstrap only: web_port, time_zone, log_level |
| User edits config | HA addon config panel | EOS Connect web UI (/config) |
| Data persistence | /data/options.json (HA managed) | /data/eos_connect.db (persistent volume) |
Detection & Data Path
ConfigManager.is_ha_addon returns True when any of these are true:
HASSIOenvironment variable is setHASSIO_TOKENenvironment variable is set/data/options.jsonexists
When detected, data_dir resolves to /data and the SQLite DB is created at /data/eos_connect.db.
Bootstrap from options.json
Bootstrap values (web_port, time_zone, log_level) are read from /data/options.json and override config.yaml defaults. These are the only settings managed through the HA addon config panel.
Legacy Migration
Older HA addon versions provided ALL configuration options in options.json. On first run with the new system:
- If SQLite store is empty AND
options.jsonhas non-bootstrap keys: migrate_ha_options_to_store()imports all non-bootstrap values into SQLite- Bootstrap keys are skipped (they stay in
options.json) - The migration is one-time and idempotent
Required Changes in ha_addons Repo
The following changes are needed in ohAnd/ha_addons:
# eos_connect/config.yaml (addon manifest)
options:
web_port: 8081
time_zone: "Europe/Berlin"
log_level: "INFO"
schema:
web_port: int
time_zone: str
log_level: list(DEBUG|INFO|WARNING|ERROR)
# Add persistent data mapping
map:
- addon_config:rw # persistent /data/ for SQLite DB
# Point to EOS Connect web UI
webui: http://[HOST]:[PORT:8081]The same changes apply to both eos_connect/config.yaml and eos_connect_develop/config.yaml. Update translations/en.yaml to match the reduced option set.