- Go 60.6%
- Vue 22.3%
- Python 5.5%
- TypeScript 4.7%
- Shell 2.3%
- Other 4.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Ready to paste at enbility/ship-go. No SKIs, device serials, addresses or host names -- verified with a leak check against the identifiers present on the affected installation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
| .claude/skills/controlbox | ||
| .github | ||
| cli | ||
| deploy | ||
| frontend | ||
| images | ||
| protocol | ||
| qtgui | ||
| third_party | ||
| .dockerignore | ||
| .gitignore | ||
| .golangci.yml | ||
| .mockery.yaml | ||
| API.md | ||
| commissioning.go | ||
| commissioning_api.go | ||
| commissioning_db.go | ||
| commissioning_pdf.go | ||
| commissioning_scenario_api.go | ||
| commissioning_scenario_api_test.go | ||
| commissioning_test.go | ||
| commissioning_texts.go | ||
| commissioning_wizard_api.go | ||
| controlbox.go | ||
| controlbox_log.go | ||
| controlbox_test.go | ||
| Dockerfile | ||
| EEBUS.md | ||
| frontend.go | ||
| go.mod | ||
| go.sum | ||
| GUI.md | ||
| hub.go | ||
| INBETRIEBNAHME.md | ||
| LICENSE | ||
| live_api.go | ||
| live_commands.go | ||
| main.go | ||
| README.md | ||
| repository.json | ||
| REST-API.md | ||
| staticfiles.go | ||
| websocketclient.go | ||
⚡ ControlBox
ControlBox is a sample EEBUS GridGuard implementation that implements these EEBUS use cases:
- 🔻 EnergyGuard Limitation of Power Consumption (LPC)
- 🔺 EnergyGuard Limitation of Power Production (LPP)
- 📊 MonitoringAppliance Monitoring of Power Consumption (MPC)
- 🔌 MonitoringAppliance Monitoring of Grid Connection Point (MGCP)
Forked from andig/controlbox (itself forked from vollautomat's eebus-go repository), based on enbility's eebus-go implementation.
This project was developed with the assistance of Claude (Anthropic).
🚀 Installation & Execution
🧰 ControlBox
ControlBox embeds the built web frontend (frontend/dist) into its binary via go:embed and serves it on the WebSocket port (7080) alongside /ws -- so frontend/dist must exist before building or running the backend:
cd /path/to/controlbox/frontend
npm install
npm run build
Run ControlBox:
cd /path/to/controlbox
go run . 4712
2025-04-10 16:39:14 INFO Local SKI: A46D9C217B8F335E921C4FAA087E615C9D2A73F0
Note the local SKI which is logged on ControlBox startup. Certificate and key are automatically created and saved.
🔗 evcc
As of evcc 0.301.0, EEBUS is enabled by default with certificate/key being automatically created, dramatically simplifying setup.
Add ControlBox to the evcc config, e.g. evcc.yaml:
hems:
type: eebus
ski: A46D9C217B8F335E921C4FAA087E615C9D2A73F0 # local SKI of the ControlBox
Restarting evcc will automatically connect evcc to the ControlBox.
🌐 ControlBox Frontend
Install dependencies:
cd /path/to/controlbox/frontend
npm install
Run web server:
npm run dev
Open ControlBox UI via web browser URI:
http://localhost:7081/
This dev server (with hot reload, proxying /ws to the backend) is the recommended workflow while working on the frontend. npm run build (see above) instead produces the static frontend/dist build that gets embedded into the ControlBox binary and served directly on port 7080 -- that's what Docker and the Proxmox LXC deployment use, so no separate frontend hosting is needed in production.
The frontend shares the same card-based layout and panel set as the Qt GUI below (Remote Device/ControlBox identity, Consumption/Production Limit, Monitoring, Device Messages), including both GUIs' "Inspect..." popup (browse the selected device's raw SPINE actors/use cases/entities/features) and "Show Pairing QR..." popup (onboard a brand-new device) — see GUI.md for a field-by-field reference covering both.

📱 Responsive down to phone widths. Below ~700px viewport width, panels stack into a single column instead of squeezing two side by side into unreadably narrow columns, and content that would otherwise overflow (the SKI value, the LPC/LPP unit suffixes, the Inspector's glossary terms) wraps or shrinks instead — see GUI.md for exactly what changes.
🖥️ ControlBox Qt GUI
A native Qt6 desktop client, built with miqt. It speaks the same WebSocket protocol as the web frontend and can run at the same time as it (and as multiple browser tabs) — the backend broadcasts state to every connected client.
It lives in its own Go module (qtgui/) so the main controlbox module can still be built without Qt6 installed.
One-time prerequisite (Fedora):
sudo dnf install qt6-qtbase-devel
On Debian/Ubuntu, the equivalent is qt6-base-dev.
Build and run:
cd /path/to/controlbox/qtgui
go run . -host localhost -port 7080
-port is the backend's WebSocket port (always 7080, independent of the EEBUS port passed to go run . <port>).
See GUI.md for what each panel/field means and a few non-obvious behaviors (e.g. the LPP sign convention, what "Set" actually writes to).
🔌 API
Both frontends are clients of ControlBox's API. The Qt GUI, and the web frontend's pushed updates, use the WebSocket protocol — see API.md for the full reference and examples of querying/controlling it manually (e.g. via curl or a short Python script), without either GUI. The web frontend's own commands (device selection/pairing, LPC/LPP limits), the Verwaltung tab's master data, the commissioning wizard, and a stateless scripted test-case runner for CI are all plain REST — see REST-API.md.
A third, dependency-free way to drive the REST API from a terminal: cli/controlboxctl.py, a Python CLI covering every REST endpoint — see cli/README.md.
📦 Deployment
Besides go run ., the backend can be persisted as a Docker container or a Proxmox LXC container. Both need to expose the WebSocket port (7080) and, more importantly, be reachable on the LAN as their own network participant -- ControlBox uses mDNS/Zeroconf for SHIP discovery, which real EEBUS devices (evcc, Solar Manager, etc.) rely on to find and pair with it.
Whichever form you use, if you're migrating an existing go run . instance that's already paired with real devices, copy its .env file into the new deployment's persistent location first. .env holds CERT_PEM/KEY_PEM, which determine the local SKI -- a fresh .env means a new SKI, and every paired device has to be re-paired from scratch. This is deliberately a manual step, not something the deploy scripts do for you -- a fresh SKI is the normal case when setting up a genuinely new ControlBox instance, so automatically carrying over an old identity would be wrong more often than not.
If you skip this on a migration, the symptom isn't an obvious error on the ControlBox side -- discovery still finds the peers fine. It shows up on the peer's side instead: evcc logs delaying connection to <new SKI> ... followed by WARN missing heartbeat - entering failsafe mode, because that SKI was never paired/trusted. Fix: stop the service, drop the old, already-trusted .env into place, restart.
DISTRUSTED_SKIS: an optional, comma-separated .env entry listing SKIs this instance should actively refuse to pair with, even if they're visible on the network and would otherwise auto-trust -- useful when migrating one real device's HEMS/CEM connection away from this instance to another one, without touching the device itself. Same effect as clicking Unpair on that device in either GUI (see GUI.md) -- both toggle the same runtime state, so the env var is only really a way to seed it at startup or set it without a GUI open.
PAIRING_GRACE_SECONDS: how long a newly (re)discovered remote SKI has to stay continuously visible before this instance trusts it and lets SHIP pair (HA add-on option pairing_grace_seconds, default 30, 0 disables the wait). Exists because a peer can accept SHIP connections before it is ready to be paired with: evcc opens its listener early in startup but only registers the SKI it expects once meters, chargers and loadpoints are up, denies anything that knocks in between, and then dies on its own 90s timeout with cannot create hems type 'eebus': timeout. That window is ~12s on slow hardware (HA Green/aarch64) and near zero on fast x86 — raise this if evcc still times out on startup. Full analysis in EEBUS.md.
mDNS hostname resolution: discovery finds peers by .local name (e.g. solarmanager.local), and actually connecting to them requires resolving that name. Go's default (pure-Go) DNS resolver ignores /etc/nsswitch.conf and can never do this, so both images are built with CGO_ENABLED=1 and ship libnss-mdns (which pulls in avahi-daemon) so glibc's resolver can answer .local lookups. This also means both images are glibc-based (Debian), not Alpine/musl, since musl has no NSS plugin support at all.
🔒 WebGUI over HTTPS: both the Docker and Proxmox LXC deployments put an nginx reverse proxy in front of the backend -- port 80 redirects to 443, and 443 terminates TLS and proxies both / (WebGUI) and /ws (WebSocket API) to the backend's plain-HTTP 7080, which keeps working directly and unencrypted in parallel (used by the Qt GUI and API.md's examples). This only makes sense because both deployments already give the container its own LAN IP (macvlan for Docker, normal LXC networking) -- there's no port conflict with anything else on the host to work around.
The TLS certificate is self-signed and generated on first start (deploy/docker/entrypoint.sh / deploy/distrobuilder/controlbox.yaml's controlbox-gen-cert.sh), the same way the backend already generates its own SHIP cert/key into .env on first run. Browsers will warn about it being untrusted -- that's expected; click through, or replace it with a real one. If you drop in your own certificate, it will never be overwritten: both cert-generation scripts only run if the target files (cert.pem/key.pem, under /data/ssl/ for Docker or /etc/ssl/controlbox/ for the LXC container) don't already exist yet.
🐳 Docker
docker build -t controlbox .
Plain bridge/NAT networking does not work for mDNS discovery, and --network host isn't available on all Docker hosts (e.g. TrueNAS SCALE's app framework). Use a macvlan network instead, so the container gets its own LAN IP -- see deploy/docker/docker-compose.macvlan.example.yml for a template (fill in your parent interface, subnet, gateway and static IP). Mount a volume at /data to persist .env across restarts.
📦 Proxmox LXC
The container is based on a custom Debian 12 image (deploy/distrobuilder/, built with distrobuilder) that already ships systemd, sshd and libnss-mdns -- unlike a stock template, nothing needs to be apt-get installed into the container at deploy time. Build/update that image (only needed when its package set changes, not on every code change) from the dedicated builder CT (deploy/terraform/builder.tf):
ssh root@<builder-ct-ip> # or whatever hostname/IP the builder CT got
cd /path/to/controlbox/deploy/distrobuilder
./build.sh # produces debian-12-controlbox_*.tar.gz in $HOME on the builder CT
# The builder CT lives on the same VLAN as the controlbox container itself
# (needed for outbound internet access to debootstrap/apt) -- on a network
# where that VLAN can't route to the PVE host's management address (as on
# this project's own cluster), fetch the result from a host that can reach
# both instead of uploading directly from the builder CT:
scp <builder-ct-ip>:debian-12-controlbox_*.tar.gz .
scp debian-12-controlbox_*.tar.gz pve1:/var/lib/vz/template/cache/
The container itself is managed declaratively with Terraform (deploy/terraform/, using the bpg/proxmox provider), which also injects your public key as root's authorized_keys (var.ssh_public_key_path, defaults to ~/.ssh/id_rsa.pub) so deploy.sh can reach the container directly:
cd deploy/terraform
cp terraform.tfvars.example terraform.tfvars # fill in your PVE API endpoint + token
terraform init
terraform plan -out=tfplan # review, then:
terraform apply tfplan
../proxmox/deploy.sh <host> # builds the binary + frontend, pushes them + the systemd unit, (re)starts the service -- also used for updates
The Proxmox user backing the API token needs, in addition to a role covering VM.*/Datastore.AllocateSpace (an IaC-style role): an SDN.Use grant on the specific SDN path used by each container's bridge/VLAN (e.g. /sdn/zones/localnetwork/vmbr0/4 for a tagged NIC, /sdn/zones/localnetwork/vmbr0 for an untagged one like the builder CT) -- scope these ACLs narrowly rather than widening the general role; and, only for creating the privileged builder CT, Sys.Modify on / (Proxmox has no narrower path for this specific check) -- granted via a separate minimal role (IaC-Privileged), not by adding it to IaC itself, so it doesn't leak to every other holder of that role. Note that PVE only allows root@pam (no API token, regardless of granted privileges) to change a privileged container's feature flags (e.g. nesting) at all -- which is also why builder.tf doesn't (can't) set nesting itself, even though the builder CT does need it despite being privileged: without it, systemd-networkd/systemd-logind fail to start (AppArmor blocks their mount-namespacing), and more importantly distrobuilder's own chroot setup fails outright ("Failed to mount filesystems: permission denied"). After terraform apply creates the builder CT, enable it once directly on the PVE host (bypasses the API/token restriction, since pct run locally as root isn't mediated by pveum ACLs at all): ssh pve1 pct set <builder_vm_id> -features nesting=1 && ssh pve1 pct reboot <builder_vm_id>.
Builder CT networking: the builder CT sits on the same VLAN as the controlbox container itself (var.vlan_id), not the untagged native segment -- confirmed by testing that the untagged segment has no outbound internet access for guest containers on this cluster (both HTTP and HTTPS to deb.debian.org failed there, while the same test from a VLAN-var.vlan_id container succeeded), which distrobuilder needs for apt/debootstrap. If apt-get update inside a container hangs for minutes instead of failing fast, that's usually IPv6 routing being broken on the segment it's on (SLAAC hands out a global address, but the route goes nowhere) -- echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4 works around it if moving to a working segment isn't an option.
Normal LXC bridged networking already behaves like a real LAN device, so no macvlan equivalent is needed here (unlike Docker). deploy.sh cross-compiles the backend locally (needs a C toolchain for the cgo build, see above) and pushes only the binary + systemd unit directly via scp/ssh to the container; the container needs neither Go nor git installed. /opt/controlbox/.env is never touched by the script, so redeploying is safe for existing pairings.
🏠 Home Assistant Add-on
A third option (deploy/hassio/) for anyone already running Home Assistant Supervisor (HAOS or Supervised): add this repo's URL under Settings → Add-ons → Add-on Store → ⋮ → Repositories, then install "ControlBox" from the store. Either https://opencommit.eu/HWE70/controlbox-emulator (works from anywhere) or https://git.highwire.de/HWE70/controlbox-emulator (development happens here; a self-hosted Forgejo instance only reachable from its own home network -- OpenCommit is a public push mirror of it, kept in sync automatically) works.
Like the evcc add-on (the closest real-world precedent for this exact kind of EEBUS/mDNS add-on), this runs with host_network: true -- required for SHIP/mDNS discovery, same reasoning as above. It also uses Home Assistant's Ingress feature (host_network doesn't rule that out, evcc's own add-on combines both too) -- the WebGUI shows up in the HA sidebar and is reachable through HA's own port (443) instead of gui_port directly; the raw http://<host>:<gui_port>/ URL keeps working too, unencrypted, alongside Ingress. Two options: ship_port (default 4712) and gui_port (default 7080, since the backend's WebGUI/WebSocket port is otherwise fixed -- see above -- a socat forward inside the add-on honors a different gui_port without touching that). See deploy/hassio/DOCS.md for the full add-on documentation (also shown in the HA UI once installed).
The add-on's Dockerfile clones this repo from OpenCommit as its first build stage, rather than assuming the rest of the repo is available as build context -- Supervisor builds git-repo add-ons using only the add-on's own subfolder as context, so plain COPY frontend/...-style paths reaching outside deploy/hassio/ wouldn't resolve otherwise. Both that git clone and Supervisor's own discovery of the repository need anonymous HTTPS read access, so the repo has to stay public.
📖 EEBUS domain notes
EEBUS.md collects practical findings about the underlying EEBUS/SPINE use cases (LPC/LPP/MGCP/MPC) gathered from testing against a real device — where ControlBox fits in the §14a architecture, wire-format gotchas, and behavior that's spec-defined vs. left up to individual device implementations.
📋 Guided commissioning test
The Web frontend's Verwaltung tab adds a second feature on top of the live simulator: a guided §14a commissioning-test workflow for installers, covering company/customer/installation/device master data, a step-by-step LPC/LPP wizard that drives the actual limit tests against the selected remote device, and a generated PDF test report (Deckblatt + technischer Anhang). Backed by its own SQLite database (commissioning.db) and file storage (uploads/) alongside the existing .env, both created on first run in the working directory. See INBETRIEBNAHME.md for the full walkthrough and the regulatory background (BK6-22-300). Web-only by design — not part of the Qt GUI, since this is an admin/document workflow rather than live device control.
⚖️ License
MIT — see LICENSE.