Multi-Bus Gateway

Software-defined protocol gateway · open source

One gateway for every field bus.

Multi-Bus Gateway reads the equipment you already have — energy meters, PV inverters, batteries, drives, heat pumps, sensors — over Modbus, HTTP or MQTT, and serves it to whoever needs it: MQTT, InfluxDB and Grafana, Home Assistant, SCADA over OPC UA, or a virtual Modbus meter that a Victron GX or a Fronius inverter takes for the real one.

Retrofit, don't replace. One container, your hardware.

  • AGPL-3.0 open source
  • Docker · amd64 + arm64
  • ~130 MB RAM
  • Raspberry Pi to server
  • English · Română
The gateway's dashboard: live values from a Janitza meter, with gauges and trends

Running every day on a real PV plant since March 2026: a Janitza UMG 512-PRO at the grid connection, polled every 250 ms and served again as two virtual meters — an EM24 that a Victron Ekrano GX uses as its grid meter, and a Fronius Smart Meter for the Fronius inverters. Four Fronius inverters with power-limit rules, a Seplos battery bank heard without transmitting, a Samsung heat pump over a remote serial bridge, and further meters on their own circuits. Add as many as you want to watch: a warehouse, a second building, a cold room.

How it works

Every device is described by a template. The gateway polls it, names every value the same way on every device, checks it, and routes it — each device with its own outputs.

Reads

  • Modbus TCP
  • Modbus RTU RS-485, local or over the network
  • Modbus ASCII · UDP
  • RTU listen-only tap
  • HTTP / JSON Fronius Solar API, Shelly, Tasmota…
  • MQTT Zigbee2MQTT, ESPHome, BLE…
Multi-Bus Gateway
  • Device templates · 22 bundled
  • Canonical names and units
  • Calculated values
  • Freshness and quality on every value
  • Rules, commands, sequences
  • Gated writes with dead-man leases

Serves

  • MQTT + Home Assistant discovery, and control: commands come back the same way
  • InfluxDB store-and-forward, Grafana
  • OPC UA server for SCADA and HMI
  • Virtual Modbus meters EM24, Fronius SM, SunSpec
  • REST push · JSON
  • Prometheus metrics and alerts
  • Alerts (Pro) Telegram, SMS, e-mail, push

What it does today

Everything below ships in the open-source gateway.

Acquisition

Modbus TCP, RTU, ASCII and UDP master with batched reads, retries and an error taxonomy. Poll groups from 50 ms to 24 h, hot-reloaded. Several slaves share one bus and take turns.

RS-485 over the network

Bridges on the Devices page: our serial bridge on a Raspberry Pi, a transparent converter (Waveshare, USR, Elfin) or a Modbus TCP gateway. Scan a bus, see how busy it is, move a device between bridges.

Listen without transmitting

The RTU tap decodes a bus that already has a master — a BMS master pack, a vendor datalogger — without ever sending a byte. Requests and responses paired, writes watched.

Device templates

The register map as a portable file, with an in-browser editor, CSV/YAML import and scan recognition. 22 bundled: Janitza UMG 512-PRO, Carlo Gavazzi EM24, Eastron, ABB, Schneider iEM3000, Fronius (SunSpec, Solar API, Smart Meter), Seplos BMS, Delta VFD-EL, Samsung heat pump, Zigbee2MQTT, Tasmota and more.

Virtual meters

Serve any source as a Carlo Gavazzi EM24, a Fronius Smart Meter or a SunSpec meter. Composite meters from several devices. Absence is never served as 0: fail, sentinel or hold — and an in-band quality block.

OPC UA server

Every device and value for SCADA and HMI panels, with quality and read time, under stable node ids. Encrypted, with the gateway's accounts or service tokens. Set points and methods with the same safety as every command. For FUXA, UaExpert, Ignition or a Weintek panel.

MQTT, Home Assistant, InfluxDB

Per device: on change or always, retain, QoS, TLS. Home Assistant autodiscovery, writable registers as number and select entities. InfluxDB with a disk-backed buffer: no data lost across an outage.

Rules and sequences

Steps, conditions with hysteresis, trim (follow a signal into a band), schedules. Timers, minimum run and stop, starts per hour, counters. Several rules on one target with layers and a referee. Sequences across devices with undo. New rules run in shadow first: they decide, and write nothing until you arm them. Rules export and import, and every decision goes to InfluxDB and MQTT, ready to chart.

One installation, several sources

An endpoint gathers units read several ways — Solar API and SunSpec, Modbus and MQTT — with a precedence per field, failover when a source stops answering, and group totals.

Safe commands and writes

Writes are off until you allow them. Bounds from the template, read-back, dead-man leases that revert to a safe value, equipment protection that every writer meets — rules, API, MQTT, OPC UA.

Calculated values

A safe expression engine with previous values, rates, time of day and cross-device references. Results flow to every output like measured values.

Commissioning tools

Discovery wizard (network scan, unit IDs, SunSpec walk, MQTT browse, Fronius, ESPHome). Frame-level bus monitor, the register probe for byte and word order, payload samples.

Configuration you can trust

A snapshot on every change, diff and rollback, a last-known-good boot, backups with secrets stripped by default, deleted devices restorable.

Security

Login with admin, operator and viewer roles, passkeys, service tokens, an audit trail, an IP allowlist, HTTPS and reverse-proxy support. Trusted-LAN by default, hardened when you need it.

Observability

Prometheus metrics, a status page with error causes, a persisted event log, infrastructure alerts over MQTT and webhooks, and a power-quality event recorder for Janitza meters.

A look inside

An installation: three inverters read over two paths, with totals and where they are published
A demo installation: three inverters read over two paths at once (Solar API and SunSpec), with group totals and where everything is published.

Examples from real installations

A Victron GX that needs an EM24

One meter at the grid connection measures everything. The gateway serves it again as the Carlo Gavazzi EM24 the Victron ESS expects. If the source goes stale the virtual meter goes silent, so the GX's own fail-safe takes over instead of acting on a frozen value.

Over-voltage: cut the inverters in steps

The real rules on the plant: one per inverter, each watching its own three phase voltages and asking for less power in four steps, down to 50 %, holding until the voltage is back under 248 V. Armed since 20 October 2026, after weeks in shadow next to the Node-RED controller they replaced, matching its steps within seconds on real over-voltage days.

See the rule
- id: ov-u1                         # one per inverter: ov-u1 … ov-u4
  target: { device: pv-u1, command: power_limit }
  mode: armed                       # weeks in shadow first, compared with the old controller
  kind: steps
  signal: "max(pv-u1.voltage_l1_n, pv-u1.voltage_l2_n, pv-u1.voltage_l3_n)"
  signal_valid: { min: 100, max: 350, max_step: 10 }   # night zeros, glitches
  steps:
    - { at: 250.0, value: 80, label: Warning }
    - { at: 251.0, value: 70, label: Moderate }
    - { at: 252.5, value: 60, label: Severe }
    - { at: 253.0, value: 50, label: Emergency, fast: true }
  release_below: 248.0
  normal: { value: 100 }
  params: { revert_s: 0, ramp_s: 10 }
  timing: { every_s: 2, debounce: 3, min_interval_s: 30, reassert_s: 120 }
  stale_after_s: 90
  on_stale: hold                    # at night the voltages read 0: keep, never curtail a sleeping inverter
  on_disable: safe                  # disarmed: back to 100 %

SCADA over OPC UA

The open-source FUXA SCADA is provisioned from the gateway in one command, and a check compares every tag with the gateway: 1199 of 1199 match, also after the gateway restarts.

A battery bank, heard and never disturbed

The bus already has a master. The gateway only listens, pairs each request with its response and publishes every pack — without a single frame of its own on the wire.

A heat pump in another building

The RS-485 bus is far from the server. A small bridge next to it carries the bus over the LAN; the gateway sets its baud and parity, and the devices follow the bridge if it moves to another address.

Control from Node-RED or Home Assistant, over MQTT

Every command a template declares listens on its own topic. A value sent to fronius-u1/cmd/power_limit meets the same gates as a click in the gateway: writes allowed, MQTT writes allowed, a broker with a login, the template's bounds, the equipment's protection. The answer comes back with the value before and after and how long it took; a request outside the bounds is refused, with the reason. In Home Assistant the registers behind the commands appear as number and select entities, executed through the same command.

Possible situations — and how you would set them up

Recipes for problems we hear about. Every one uses what the gateway already has — devices, rules, MQTT, outputs — and every number in them is yours to change.

Never export more than 45 kW

The grid connection allows 45 kW of export. A trim rule watches the headroom to the cap and lowers the inverters' power limit while it is exceeded, raising it again with a margin. The cap itself can come over MQTT — from the grid operator, an EMS, Node-RED — and change during the day. Tested on a real four-inverter plant with a 20 kW cap, exports swinging 10–46 kW under clouds: 18 kW on average, at most 21.2 kW, every command answered.

See the rules
# 1 · keep the export under 45 kW
- id: export-cap
  target: { endpoint: pv, group: inverters, command: power_limit }
  kind: trim
  # the room left under the cap, in W (export reads negative on this meter):
  #   exporting 40 kW → 45000 − 40000 =  5000 W of room
  #   exporting 47 kW → 45000 − 47000 = −2000 W: over the cap
  signal: "45000 + umg512.power_active_total"
  signal_unit: W under the cap
  below: 0          # over 45 kW: lower the inverters' limit
  above: 3000       # under 42 kW: raise it back (a 3 kW margin, so it does not hunt)
  step: 10          # % per adjustment, at most
  gain: 0.002       # % per W past the threshold: 5 kW over → 10 %
  min: 0
  max: 100
  params: { revert_s: 300 }   # the inverter's own timer: back to 100 % if the gateway stops…
  timing: { every_s: 5, debounce: 2, min_interval_s: 15, reassert_s: 120 }   # …refreshed every 2 min
  stale_after_s: 30
  on_stale: hold    # meter lost: keep the last limit
  on_disable: safe  # disarmed: back to 100 %
  # four inverters on one Fronius datamanager: ~5 s a write, so 15 s between
  # adjustments keeps it under the ~26 writes a minute it can take

# the cap from MQTT instead of fixed — an MQTT device `site` with
# export_cap_kw on site/limits/export (the grid operator, an EMS, Node-RED):
#   signal: "site.export_cap_kw * 1000 + umg512.power_active_total"

# 2 · with the over-voltage rule on the same inverters: the template's
#   channel "power" combines with min — the lower limit is the one written

Never waste a kWh: the batteries before the cap in progress

Before the export rule cuts the inverters at 45 kW, the batteries take what would go over: the Victron ESS through its GX, or the chargers, raise their charging as the export nears the cap. Only when they are full, or already charging at their maximum, does the cap curtail the inverters. Two rules on two channels, the battery one first — what is still being built is the Victron GX template with its ESS commands.

A pump that uses exactly the solar surplus

The rule nudges the drive's frequency up while power is exported and down while it is imported, between 10:00 and 16:00. A second rule tops up the day: if the sun gave the pump less than four hours, it runs from 17:00 until it has. The drive's own protection holds every writer: a minimum stop of 5 s and at most 30 starts an hour.

See the rules
# 1 · follow the solar surplus, 10:00–16:00
- id: vfd-surplus
  target: { device: vfd1, command: run_at }   # Delta VFD-EL: 0 Hz = stop
  kind: trim
  layer: optimize
  signal: '-site-meter.power_active_total if between("10:00", "16:00") else 0'
  above: 500        # exporting more than 500 W: faster
  below: 100        # under 100 W (or importing): slower
  step: 2           # Hz per adjustment
  min: 15
  max: 50
  start: 15
  off_value: 0
  timing: { every_s: 5, debounce: 2, min_interval_s: 10, reassert_s: 0 }
  stale_after_s: 60
  on_stale: safe    # meter lost: stop

# 2 · top up the day if the sun did not do it
- id: vfd-quota
  target: { device: vfd1, command: run_at }
  kind: condition
  layer: schedule   # above optimize: wins while it has an opinion
  # the hours of both rules count: it stops once the day has had four
  when: 'between("17:00", "20:00") and run_today_h("vfd-surplus") + run_today_h("vfd-quota") < 4'
  then: { params: { value: 30 } }
  else: null        # otherwise: no opinion

# the drive's protection comes from its template (channel "motor"):
# min stop 5 s, at most 30 starts an hour — for every writer

Negative electricity prices: stop paying to export

When the day-ahead price drops below zero, every exported kWh costs money. Read the price from your supplier's or the exchange's API as an HTTP/JSON device (or from an EMS over MQTT) and curtail the inverters while it is negative. No price, or a stale one: full power, so a broken API never stops production.

HTTP/JSON input · condition rule · inverter group · fail-safe on stale data

See the rule
- id: negative-price
  target: { endpoint: pv, group: inverters, command: power_limit }
  kind: condition
  when: "market.price_eur_mwh < 0"     # an HTTP/JSON or MQTT device
  then: { params: { value: 0 } }
  else: { params: { value: 100 } }
  timing: { every_s: 30, debounce: 2, min_interval_s: 300 }
  stale_after_s: 3600
  on_stale: 100                        # no price: full power

Peak shaving: keep the import under the connection limit

A site with a 40 kW import contract and a heat pump, chargers or a compressor. When the grid meter shows more than 40 kW, the heat pump pauses until the import falls back under 32 kW. As a safety layer it wins over schedules and comfort rules, and the minimum interval protects the compressor.

grid meter · condition with release · layers · minimum interval

See the rule
- id: import-cap
  target: { device: heat-pump, command: power }
  kind: condition
  layer: safety                        # wins over schedules and comfort rules
  when: "grid.power_active_total > 40000"
  release: "grid.power_active_total < 32000"
  then: { params: { value: 0 } }
  else: null                           # otherwise: let the other rules decide
  timing: { every_s: 5, debounce: 3, min_interval_s: 300 }

Grid code: over-frequency derating

Above 50.2 Hz the inverters reduce their power in steps, down to 30 % at 51 Hz, and come back only under 50.1 Hz. Readings outside 45–55 Hz or jumping more than 1 Hz are ignored as glitches. The same pattern protects a real plant from over-voltage.

steps rule · signal validation · dead band · inverter group

See the rule
- id: of-derate
  target: { endpoint: pv, group: inverters, command: power_limit }
  kind: steps
  signal: "grid.frequency"
  signal_valid: { min: 45, max: 55, max_step: 1 }
  steps:
    - { at: 50.2, value: 90 }
    - { at: 50.5, value: 60 }
    - { at: 51.0, value: 30, fast: true }
  release_below: 50.1
  normal: { value: 100 }
  on_stale: 100

A pumping station on a tank level

A level transmitter read through a Modbus I/O module, a pump on a frequency drive. The pump starts under 30 % and stops over 80 %, with a minimum run and stop time and at most 6 starts an hour. If the level reading is lost the pump stops, and the drive's fault code becomes an alarm.

Modbus I/O · drive template · condition with release · timers · alarms

See the rule
- id: tank
  target: { device: pump-drive, command: run_at }
  kind: condition
  when: "tank.level_pct < 30"
  release: "tank.level_pct > 80"
  then: { params: { value: 45 } }      # Hz
  else: { params: { value: 0 } }
  timing: { every_s: 5, debounce: 2, min_on_s: 300, min_off_s: 120,
            max_starts_per_h: 6 }
  stale_after_s: 30
  on_stale: safe                       # level lost: stop

Night tariff, weekends, and “run it for an hour now”

A boiler or a pump on a weekly schedule: on at 23:00 on the night tariff, off at 06:00, a different window at the weekend. The operator's “run for an hour now” button on an HMI or in Node-RED is a force on the same rule over MQTT: after the hour it goes back to the schedule by itself.

schedule rule · force over MQTT · timezone-aware

See the rule
- id: boiler-week
  target: { device: boiler, command: power }
  kind: schedule
  schedule:
    - { days: Mon-Fri, at: "23:00", value: 1 }   # night tariff
    - { days: Tue-Sat, at: "06:00", value: 0 }   # the morning after each night
    - { days: "Sat,Sun", at: "13:00", value: 1, label: weekend }
    - { days: "Sat,Sun", at: "17:00", value: 0 }
  off_value: 0
# run it for an hour now, from an HMI or Node-RED:
#   mbg/rules/boiler-week/set  {"force": {"value": 1, "seconds": 3600}}

Battery full and exporting hard

Once the battery is at 98 % and the site exports more than 5 kW, there is nowhere left to put the energy cheaply. The inverters are curtailed to 60 % until either condition goes away, with 30 seconds of agreement before each change and at most one change every two minutes.

BMS + grid meter · condition rule · debounce

See the rule
- id: full-and-exporting
  target: { endpoint: pv, group: inverters, command: power_limit }
  kind: condition
  when: "battery.soc >= 98 and grid.power_active_total < -5000"
  then: { params: { value: 60 } }
  else: { params: { value: 100 } }
  timing: { every_s: 5, debounce: 6, min_interval_s: 120 }
  on_stale: hold

A factory SCADA, without new meters

Compressors, energy meters and drives already sit on RS-485 lines across the plant. Bridges carry the lines over the LAN, templates name every value the same way, and the SCADA reads everything over OPC UA, with quality and read time. The same values go to InfluxDB and Grafana for the energy team.

RS-485 bridges · templates · OPC UA · InfluxDB

Industrial meters in Home Assistant

A Modbus energy meter, an inverter and a few Zigbee sensors appear in Home Assistant by themselves, through MQTT discovery, each as its own device with stable names. Settings the template allows writing, such as a power limit, become number and select entities.

MQTT · Home Assistant discovery · write entities

A cold room that must not warm up

A temperature sensor over MQTT (Zigbee or BLE) and a calculated flag, “too warm” over 8 °C, declared as an alarm that must hold for ten minutes. Then it becomes an alert: the alerts service sends it to the on-call phone by SMS and Telegram, at night as well, and pages again by SMS if nobody acknowledges it within 15 minutes.

MQTT sensor · calculated value · alarm with a hold time · alerts service (Pro)

Pro

High availability in testing

For installations that cannot go without their data: two gateways on the same devices, one active and one standby. Part of the Pro edition; tested on a bench, the tests on real hardware are under way.

  • Two links, double confirmation. A heartbeat on a dedicated RS-485 and on TCP; the standby takes over only when both are silent — or its partner's gateway reports itself down — and the bus is quiet.
  • The standby never puts a frame on the wire. It listens to the bus, holds a copy of the state and the configuration, and refuses every write.
  • One address. Clients (SCADA, virtual meters, the page) follow the active through keepalived or HAProxy, with the same OPC UA certificate.
  • You decide. Off, monitor with alerts, or automatic — only after a take-over test you run on your own pair.

An alarm that reaches the right people Pro

The heat pump reports error E101. The gateway raises the alarm its template declares once it has held for the alarm's hold time — here 30 seconds, so a blip never wakes anyone. The alerts service routes it: throttled, outside quiet hours, to Telegram, an SMS through the site's LTE router and e-mail; if nobody acknowledges a critical one within 15 minutes, it pages again by SMS. It says when the error clears. In production with 67 rules.

Coming soon

What we are building next. Tell us which one your site needs first — it moves up the list.

  • Victron GXRead the system, the batteries and the Multiplus over Modbus TCP, and drive ESS — so the batteries can take the surplus before an export cap.
  • CAN busRead devices over CAN, and present a battery bank to Victron or Deye inverters over their BMS-CAN protocol, from the values the gateway already has.
  • OPC UA client for PLCsFeed a soft PLC (OpenPLC, CODESYS) with live values and read its outputs back as a device — logic in the PLC, the buses in the gateway.
  • MCP for AI assistantsAn assistant such as Claude reads live values, history and diagnostics, and proposes changes as a dry run you approve.
  • Siemens S7Read S7-300/400/1200/1500 PLCs directly as a data source, next to Modbus, HTTP and MQTT.
  • IEC 60870-5-104An outstation for PV parks and grid operators: telemetry and set points over the protocol the utility speaks.
  • BACnet/IPServe every value to a building management system as BACnet objects.
  • Profinet IO deviceAppear on a Siemens PLC's Profinet as an IO device, with the buses behind it.
  • M-BusWired heat, water and gas meters as a data source.
  • Point list in and outExport the site's point list to CSV or Excel for the SCADA designer, and import one to configure many devices at once.
  • Commissioning reportA FAT/SAT report from the gateway itself: every point, its value, its quality and when it was checked.
  • Alarms with acknowledgementAlarms an operator acknowledges, kept with who and when, and passed to the SCADA as OPC UA alarms.
  • Security for tendersTLS everywhere, logs to syslog, an IEC 62443 description and a software bill of materials.
  • More devicesTwenty more templates and counting: Siemens PAC, Schneider, ABB, SMA, Huawei, Deye.

Tell us what you need

Editions

The gateway is open source. The Pro edition adds the modules a critical installation needs, and the people who stand behind it.

Community

Free · AGPL-3.0

  • Everything on this page under “What it does today”
  • Every protocol, output, template and rule
  • Docker images for amd64 and arm64
  • Documentation, manual in English and Romanian
  • Issues and discussions on GitHub
Get it on GitHub

Pro

Price on request

Everything in Community, plus:

  • High availability — two gateways, one active and one standby. A heartbeat on RS-485 and TCP, a copy of the state and the configuration, take-over on your word or automatically after a test you run. The standby never puts a frame on the wire. Clients keep one address (keepalived or HAProxy).
  • Alerts and notifications — rules on any value, Telegram, SMS through an LTE router, e-mail, push and webhooks; throttling, quiet hours, acknowledgement and an audit trail.
  • ESP nodes — flash and manage ESP32/ESP8266 boards from the browser, behind the gateway's login.
  • MCP server for AI assistants coming soon — let an assistant such as Claude read the plant's live values and history, and propose changes as a dry run you approve.
  • Support — a direct line to the people who build it, with agreed response times.
  • Implementation — survey, templates for your devices, commissioning, rules, SCADA integration, documentation for your site.
Ask for an offer

Get started in five minutes

Docker with Compose v2. The stack brings the gateway, an MQTT broker, InfluxDB and Grafana; use only the gateway if you have your own.

mkdir multi-bus-gateway && cd multi-bus-gateway
curl -fsSLO https://raw.githubusercontent.com/sm26449/multi-bus-gateway/main/docker-compose.yml
curl -fsSL  https://raw.githubusercontent.com/sm26449/multi-bus-gateway/main/.env.example -o .env
mkdir -p mosquitto/config && curl -fsSL https://raw.githubusercontent.com/sm26449/multi-bus-gateway/main/mosquitto/config/mosquitto.conf -o mosquitto/config/mosquitto.conf
# edit .env: set the four required secrets
docker compose pull && docker compose up -d
docker compose logs multi-bus-gateway | grep -A3 'FIRST RUN'   # the admin password, once
# → http://localhost:8080

Contact, support, implementation

Tell us about your installation: what you have, what you need it to do. The message comes straight to us, and we answer by e-mail.

  • Support for a running gateway: version, what happened, what you expected.
  • Implementation for a new site: the devices, the buses, where the data should go.
  • Pro edition high availability, ESP nodes, MCP, support terms.
  • Bugs and ideas for everyone: GitHub issues.

Or write directly: [email protected]

We use what you send only to answer you, and never publish it. Privacy