Skip to content

About

SPAN panel eBus v1.0 reference publisher — a conformance-checked Homie 5 parent/child device tree over MQTT, for developing and verifying consumers against

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

690 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SPAN PanelBench

A SPAN panel on an MQTT broker: a conformance-checked publisher of the eBus v1.0 parent/child device tree, for building and verifying consumers against.

PanelBench is the current SPAN emulator, and it is built on the eBus emitter package (ebus-panel-sim). The emitter supplies the wire profiles, device manifests and publishing runtime; this repository supplies the SPAN overlay, the simulation engine, and the HTTP, TLS and mDNS surfaces a panel answers on.

People write software against this instead of against hardware. Being provably faithful is the product, not a nicety, and two checks enforce it: the vendored eBus capability catalogs are byte-compared against the specification, and everything published on the wire is checked against those catalogs. See DEVELOPER.md.

Includes a web dashboard for real-time configuration, grid simulation, Home Assistant history replay, and energy "what-if" modeling.

Which emulator you want

Schema Firmware emulated
this repo, SpanPanel/panelbench parent/child device tree (data-model-version 1.x) r202633 and later
SpanPanel/simulator flat single-device r202603–r202627

Both ship the same certificate authority, so stopping one and starting the other rehearses a firmware upgrade on a single panel instead of reading as a panel substitution. Cloning follows the same schema line: PanelBench reads a panel running r202633 or later and does not clone earlier firmware, because the two schemas are not convertible.

Workflow

Click a simulator configuration to view it. Templates are read-only. A running simulator appears as a discovered panel in the SpanPanel integration (default configs excluded).

  1. Examine templates — Load and run the included configs (default_MAIN_40.yaml, default_MAIN_32.yaml and default_MAIN_16.yaml) to see how circuits, PV, battery, and EVSE are modeled. Pick one as a starting point for your own configuration.
  2. Clone — The Clone button creates an editable copy from a template, or from a panel running r202633+ firmware; cloning a panel preserves recorder history per circuit.
  3. Model — The Model button on a running panel opens the what-if view; add battery, PV, or circuits and compare before/after. Edits mark equipment as SYN (synthetic); click the badge to revert to REC (recorded).
  4. Purge — The Purge button removes recorder history written by the simulated panel's sensors if you added the simulated panel to Home Assistant's integration.

Dashboard overview — grid offline with load shedding, live power chart, entity list with relay status

PV editor — solar production curve with geographic modeling and historical weather degradation

Battery editor — BESS charge and discharge profile

Modeling view — Before/After energy comparison with BESS, dual charts with range zoom and circuit overlays

Home Assistant App

PanelBench installs as a Home Assistant app from this repository. Images are published for amd64 and aarch64, and the app is not distributed through HACS. It requires the span-panel integration v2.1.2 or later, which reads the SPAN firmware release 202639 the shipped templates publish.

  1. Go to Settings > Apps > App Store > three-dot menu > Repositories
  2. Add https://github.com/SpanPanel/panelbench
  3. Install SPAN PanelBench from the store
  4. Start the App — a default panel config is included
  5. The span-panel integration discovers running panels automatically via the Supervisor Discovery API (default configs excluded)
  6. Open the web dashboard via Open Web UI to configure panels

The App runs the simulator in a container with its own Mosquitto broker. No real SPAN hardware is needed. Each panel runs on its own pair of ports: a bootstrap HTTP port (starting from base_http_port, default 8081) and a TLS port 1000 above it (9081), serving the same API. The dashboard shows both next to each running panel's serial number, and discovery publishes both, so a panel added from a discovery notification needs neither.

The pair mirrors hardware, and the integration needs both: it fetches the panel's CA over the plaintext port — it has nothing to trust yet at that point — and then refuses to pin it unless it validates the certificate served on the TLS port.

Quick Start (macOS standalone)

# Prerequisites
brew install mosquitto uv

# Run
./scripts/run-local.sh

# Run with debug logging
./scripts/run-local.sh --debug

# Stop / Restart / Status
./scripts/run-local.sh --stop
./scripts/run-local.sh --restart
./scripts/run-local.sh --status

The script automatically creates a Python virtual environment, generates TLS certificates, starts Mosquitto (MQTTS on port 18883), and launches the simulator with mDNS advertising on your LAN IP. No sudo required.

Open the dashboard at http://localhost:18080.

Multi-panel in standalone mode

Home Assistant discovers a running panel over zeroconf with nothing to type in, provided HA and PanelBench sit on a network segment where mDNS reaches between them. Each panel advertises itself separately, publishing its own serial number, HTTP port and TLS port (default configs excluded). Where mDNS does not reach — HA in a bridge-networked container, or a different subnet or VLAN — nothing is discovered and every panel is added by hand with the steps below.

Only the first panel at a given address is discovered automatically. The integration's zeroconf handler stops as soon as the host address belongs to a config entry, so a second panel at that address never reaches the serial-number check that would tell it apart. The Home Assistant App has no such limit — Supervisor discovery deduplicates by serial, so every panel it runs is discovered — but in standalone mode all panels share one address, so add the rest by hand using the port shown in the dashboard panel list:

  1. In HA, go to Settings > Devices & Services > Add Integration
  2. Search for Span Panel and enter the host IP and bootstrap HTTP port (e.g. 192.168.1.50 port 8082)
  3. Because the port is not 80, the integration asks which port the panel serves TLS on — that is the second number in the dashboard's port pair (9082 for the panel above)

Each panel has a unique serial number, so there is no conflict between the auto-discovered panel and manually added ones.

Rehearsing a SPAN firmware upgrade

A SPAN panel reads its firmware once, when it starts, and an over-the-air upgrade takes it offline and brings it back reporting the new release. PanelBench rehearses an upgrade the same way: two configs for one panel, run one after the other. The upgrade rehearsed here is to SPAN release 202639. The shipped templates name spanos3/r202639/03 (see Firmware Version) and so are panels after it, which is why the first config names an earlier release.

A panel with a commissioned PV system has a circuit named "Commissioned PV System" before the upgrade. On a release before 202639 it is unlocked: its relay is switchable and not always-on, and its priority is OFF_GRID and can be changed, so it sheds like any other circuit. The upgrade to release 202639 locks it.

  1. Before. Clone your panel from the dashboard while it still runs a release before 202639, or clone one of the shipped templates. A clone keeps its source's firmware_version, so it publishes what that release publishes, and a panel clone's Update eBus Energy button, which refreshes its energy readings from the panel, never changes it. A clone of your panel already holds the "Commissioned PV System" circuit as the panel publishes it. A clone of a template holds it locked, as release 202639 publishes it, so take the clone back to the earlier release: set its firmware_version to a 202633 release, for example spanos3/r202633/02, and in the circuit's template remove commissioned_system: pv and set relay_behavior: controllable and priority: OFF_GRID.

  2. After. Copy the first config's YAML to a second file in the same config directory, under a name that does not start with default_ (that prefix marks a read-only template), and change two things in the copy:

    • Set firmware_version to a 202639 release, for example spanos3/r202639/03.
    • Lock the "Commissioned PV System" circuit as release 202639 does: give its template commissioned_system: pv, priority: NEVER and relay_behavior: non-controllable. A circuit named "Commissioned Backup System" is locked the same way, with commissioned_system: backup. PanelBench refuses commissioned_system on a config naming an earlier release, so these keys belong only in this file.

    That is all for a panel with one inverter; A second inverter adds to it. The serial number, circuits and tabs are what make the two configs one panel.

  3. Start the first instance with CONFIG_NAME=<before>.yaml ./scripts/run-local.sh and add the panel to Home Assistant.

  4. Stop the first instance and start the second: ./scripts/run-local.sh --stop from another terminal (or Ctrl+C in the first), then CONFIG_NAME=<after>.yaml ./scripts/run-local.sh. CONFIG_NAME=<after>.yaml ./scripts/run-local.sh --restart does both at once. Home Assistant sees the same panel come back on the new release.

Run both from the same checkout with the same HTTP_PORT, DASHBOARD_PORT and BROKER_PORT; the defaults are fine. The second instance must answer where the first did. The two must never run at once: run-local.sh keeps one simulator PID file per checkout, and both would advertise the same serial number.

From release 202639 the second instance does what SPAN's public CHANGELOG lists for that release. It publishes the battery's own power reading as positive while discharging, and leaves a SPAN Drive's user charge limit unpublished until one is set. It publishes the "Commissioned PV System" circuit locked, with no relay command and a priority fixed at NEVER. GET /api/v2/status reports hardwareVersion: 1.2 or 2.0 from the config's hardware_version, 1.2 when it sets none, and UNKNOWN for any other value. It publishes one solar device per inverter when the panel has more than one, and a panel with one keeps its device id.

A second inverter

Before release 202639 a panel publishes one solar device, so a clone holds one PV circuit and models any other inverter's circuit as a load. To rehearse the upgrade of a panel with a second inverter, also make that circuit a commissioned inverter's, locked, in the second config.

The second inverter's circuit is a 240 V two-pole circuit, two tabs on opposite legs, as the "Commissioned PV System" circuit is: a grid-tied inverter in a US panel, whether a string inverter or a set of microinverters, sits on a two-pole breaker. A clone of your panel already holds it, as a two-pole circuit modelled as a load, so pick that circuit. A clone of a template has no second inverter, so add its circuit to both configs on two free spaces on opposite legs, such as 24 and 26, with the same id and tabs in both, a two-pole breaker_rating such as 20, and a load's template in the first config; take the two spaces out of unmapped_tabs. PanelBench warns when a solar circuit sits on one tab. Then, in the second config:

  • Name the circuit as its inverter's, for example Solar Inverter 2, rather than leaving a load's name.
  • Give it a commissioned PV template: copy the "Commissioned PV System" circuit's template as this config has it, locked, under a new name. Release 202639 publishes each commissioned inverter fed by its own circuit and locks that circuit as it locks the "Commissioned PV System" one, so the copy keeps commissioned_system: pv, priority: NEVER and relay_behavior: non-controllable; the template, not the name, is what locks it. PanelBench warns when a solar circuit on a config naming release 202639 or later has no commissioned_system: pv. Set the copy's energy_profile.nameplate_capacity_w to the inverter's rating, its producer power_range to match, such as [-7600.0, 0.0], and its typical_power to about 60% of the rating, negative, such as -4560.0, and point the circuit's template at the copy. Production follows the rating and the time of day, but typical_power seeds the energy total the inverter's circuit starts from, so a copy that keeps the original's would start with a total sized for the original inverter. The dashboard's nameplate field sets all three for you.
  • Remove the circuit's own overrides. They were the load's: a power_range there would cap the inverter's production, and a typical_power would replace the template's.
  • Give the circuit, under circuits, its own inverter's vendor, model and serial_number. It takes pv.firmware_version unless it sets its own firmware_version, which a config with no pv section needs.
  • Where the config has a top-level pv section, as a clone of a template does, set pv.feed to the id of the original inverter's circuit. The section describes one inverter, the one whose circuit pv.feed names; with two PV circuits and no pv.feed it describes neither, and the original inverter would lose the identity the first config gave it. A clone of your panel names each inverter on its own circuit instead, and needs no pv.feed.

Running with Docker (Linux only)

docker compose up --build

Container-based approaches on macOS do not work for mDNS advertisement. All macOS container runtimes use VM networking that prevents containers from obtaining real LAN IPs. Use run-local.sh on macOS instead.

Dashboard

The dashboard runs on port 18080 and provides full control over the simulated panel.

Panel Management

  • Multi-panel — load multiple YAML configs; click a row to select, start/stop/restart individual panels. Running panels appear as discovered devices in the SpanPanel integration (default configs excluded).
  • Clone — create an editable copy from a template, or from a panel running r202633+ firmware (IP and passphrase, or broker credentials you already hold). PanelBench registers with a panel once, keeps the passphrase and credentials outside the config, and reuses them for every sync.
  • Model — open the energy what-if view for a running panel.
  • Purge — remove recorder history written by the simulated panel's sensors when the simulated panel was added to HA's integration.
  • File operations — import/export YAML, save & reload
  • Config persistence — the simulator remembers the last running config across restarts

Import also accepts a panel definition file (panel-sim-definition/1, as panel-sim-capture writes one). PanelBench builds a config from the panel's makeup and gives every circuit a default behaviour, which you then tune as for a clone. Export definition writes a saved config the other way, as a panel definition file holding the panel's makeup without PanelBench's behaviour settings.

Simulation Controls

  • Time-of-day slider — scrub through the day to see solar curves, time-of-day profiles, and battery schedules respond
  • Speed acceleration — 1x to 360x time acceleration
  • Grid online/offline — toggle to test backup behavior and load shedding
  • Islandable toggle — controls whether PV operates during grid outage
  • Battery link — sets the health of the panel's link to its battery; Home Assistant's dominant power source control is honoured only while it is not OK
  • Live power chart — real-time grid, solar, and battery power flows

Recorder Replay

When connected to Home Assistant, the simulator replays recorded power data from the HA recorder for circuits with mapped entities. This grounds the simulation in actual household usage patterns rather than synthetic profiles.

./scripts/run-local.sh --ha-url http://192.168.1.10:8123 --ha-token YOUR_TOKEN

Circuits with recorder data show a REC badge in the entity list. Clicking the badge toggles to SYN (synthetic) mode, where the simulator uses the configured power profile instead of recorded data. Click again to switch back to recorder replay. This lets you compare how well a synthetic profile matches your real usage, or override a specific circuit while keeping the rest on recorded data.

Energy Modeling

The modeling view lets you answer "what if" questions about adding solar or battery storage to your panel. Start from a template or a clone of your own panel, then add or modify PV and Battery entities to see the projected impact on your grid consumption over historical data.

Typical workflow:

  1. Start from a template config, or clone your own panel, from the dashboard
  2. Connect to HA so circuits replay actual recorded power data
  3. Click Model on the running panel to enter the modeling view
  4. The Before chart shows your site power as-is (loads minus any existing solar)
  5. Add a Battery entity (or modify an existing one) — adjust capacity, charge/discharge schedule, and backup reserve
  6. The After chart immediately updates to show grid power with the BESS applied, along with kWh savings
  7. Add or resize a PV entity to see how additional solar offsets your consumption in the Before chart
  8. Experiment with different battery sizes, charge modes, and PV nameplate ratings — charts auto-refresh on every save

Modeling controls:

  • Horizon selector — last month, 3 months, 6 months, or 1 year
  • Range zoom — drag the slider to zoom into any time window
  • Circuit overlays — check individual circuits in the entity list to overlay their power traces on both charts
  • Toggleable legend — show/hide Solar and Battery traces
  • Energy summary — net kWh with import/export breakdown and savings percentage

Entity Management

Add, edit, and delete circuits with specialized editors per type:

  • PV — nameplate capacity, geographic sine-curve solar model, monthly weather degradation from Open-Meteo historical data
  • Battery — nameplate capacity (kWh), backup reserve %, charge mode (Custom / Solar Generation / Solar Excess), discharge presets, 24-hour charge/discharge/idle schedule
  • EVSE — charging schedule with presets (Peak Solar, Evening, Night) or custom start/duration, 24-hour visual timeline
  • Circuits — typical power, 24-hour usage profile with presets, HVAC type selector with seasonal power modulation

The dashboard adds one PV circuit per panel, and a panel has one battery. A panel with more than one inverter gives each inverter its own PV circuit in the config's YAML, as Rehearsing a SPAN firmware upgrade shows. Recorder-sourced entities preserve their original panel settings (priority, relay behavior) as read-only.

Relay Control and Load Shedding

  • Click status dots to toggle circuit relays
  • Changes from the dashboard or HA integration (via MQTT) are reflected in both directions
  • Grid offline triggers load shedding by priority: OFF_GRID circuits shed immediately, SOC_THRESHOLD circuits shed when battery SOC drops below threshold, NEVER circuits stay on

Theme

System, light, or dark theme via the header selector, with localStorage persistence.

Panel Configuration

Each YAML file in the config directory defines one simulated panel.

Minimal Example

panel_config:
  serial_number: "SPAN-TEST-001"
  total_tabs: 8
  main_size: 100

circuit_templates:
  kitchen:
    energy_profile:
      mode: "consumer"
      power_range: [0.0, 1800.0]
      typical_power: 150.0
      power_variation: 0.3
    relay_behavior: "controllable"
    priority: "NEVER"
  pool_pump:
    energy_profile:
      mode: "consumer"
      power_range: [0.0, 1500.0]
      typical_power: 1200.0
      power_variation: 0.1
    relay_behavior: "controllable"
    priority: "OFF_GRID"
    # Commissioned to receive no backup power, so the shed priority is locked:
    # `load-shed/priority` publishes with no `$settable` and the panel accepts no
    # `/set` for it. A locked circuit *is* permanently `OFF_GRID`, so `priority` must
    # say so — any other value is rejected when the panel starts rather than silently
    # rewritten. Independent of `relay_behavior`, and not the same thing as
    # `priority: "NEVER"`, which is an ordinary settable value meaning "never shed".
    never_backup: true

circuits:
  - id: "kitchen_outlets"
    name: "Kitchen Outlets"
    template: "kitchen"
    tabs: [1, 3]
  - id: "pool_pump"
    name: "Pool Pump"
    template: "pool_pump"
    tabs: [5]

unmapped_tabs: [2, 4, 6, 7, 8]

simulation_params:
  update_interval: 5

Config Selection

To start a specific config:

CONFIG_NAME=default_MAIN_16.yaml ./scripts/run-local.sh

Without CONFIG_NAME, the simulator resumes the config it last ran, or starts with no panel running until you pick one in the dashboard.

Included Configs

File Tabs Description
default_MAIN_40.yaml 40 Full residential with solar, battery, 2 EVSE
default_MAIN_32.yaml 32 Full residential with solar, battery, 1 EVSE
default_MAIN_16.yaml 16 Minimal test: lights, outlets, HVAC, solar

Firmware Version

firmware_version is the firmware string a panel reports over MQTT, its HTTP status endpoint and mDNS. It also decides which side of SPAN release 202639 the panel emulates: the battery's power sign, whether a SPAN Drive's user charge limit is published before a user sets one, one solar device per inverter, the status endpoint's hardware version, and commissioned-system circuits.

The included configs name spanos3/r202639/03, as a SPAN panel on that release publishes it, and so are panels after the upgrade to release 202639: their solar circuit is the locked "Commissioned PV System" circuit, and they report hardware version 1.2. Read them with the span-panel integration v2.1.2 or later; v2.1.1 shows the battery's Meter Power with its sign flipped.

To emulate the firmware before that upgrade, clone a template, since templates are read-only, and in the clone's YAML under configs/ name a 202633 release and remove commissioned_system from its solar template, which PanelBench refuses on an earlier release:

firmware_version: spanos3/r202633/02

A config naming no firmware reports sim/v<package version>, which names no SPAN release, and publishes r202639's conventions. A clone keeps its source panel's firmware.

Environment Variables

All variables can also be passed as CLI arguments (--help for full list).

Variable Default Description
CONFIG_DIR ./configs Directory containing panel YAML configs
CONFIG_NAME last config run Specific config file to load
TICK_INTERVAL 1.0 Seconds between simulation ticks
LOG_LEVEL INFO DEBUG, INFO, WARNING, ERROR
HTTP_PORT 8081 Bootstrap HTTP server port (TLS on +1000)
DASHBOARD_PORT 18080 Dashboard web UI port
BROKER_HOST localhost MQTT broker hostname
BROKER_PORT 18883 MQTTS broker port
ADVERTISE_ADDRESS auto-detected IP to advertise via mDNS

Development

See DEVELOPER.md for setup, testing, pre-commit hooks, full config schema, HTTP/MQTT API reference, and simulation engine internals.

About

SPAN panel eBus v1.0 reference publisher — a conformance-checked Homie 5 parent/child device tree over MQTT, for developing and verifying consumers against

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages