Turn any Modbus energy meter into a Fronius Smart Meter — for the inverter on RS485 and for wallboxes on TCP.
A Fronius inverter only accepts a Fronius Smart Meter at its meter input. If the grid connection is already measured by another device — a Smartfox, an energy manager, any meter with a Modbus interface — smartmeter reads that device and answers as a Fronius Smart Meter 63A-3:
- on RS485 (Modbus RTU), unit ID 1, with the proprietary register map the inverter polls,
- on Modbus TCP, unit ID 200, with the SunSpec meter block (model 203) that wallboxes, evcc and other Fronius-compatible clients read.
One upstream meter is polled once per interval and served on every unit ID and both transports at the same time, so all clients see the same values from the same moment.
- Fronius Smart Meter emulation: proprietary RS485 map (4096 ff.) and SunSpec block (40000 ff.)
- Any Modbus TCP or RTU meter as source: registers, data types, byte/word order and scaling are
configured per field; fixed values and expressions (
{power_l1} + {power_l2}) are supported - Derived values: line-to-line voltages, apparent/reactive power, total PF and per-phase energy are computed when the source does not provide them
- Several unit IDs per meter and several meters per instance
- Spike filter: implausible readings (beyond what a meter of the configured rated current can measure) are discarded, the last valid values stay in place
- Reconnects to the source after a dropped connection or a restart of the source meter
- Source failure is visible to the inverter: without valid values for
staleTimeout(30 s) the unit IDs stop answering, so the inverter sees a failed meter instead of frozen values; they never answer before the first valid values - Block reads: neighbouring registers are fetched with one request
- Consistent reads: a client never sees a 32-bit value half updated
- Robust RS485: a failed serial port (e.g. an unplugged USB adapter) is reopened
automatically, and
/healthreports it meanwhile - Web page with energy flow, power, phases, counters and the served registers, embedded in the binary and usable offline
- HTTPS REST API (TLS 1.3) with one bearer token per caller, stored only as a SHA-256 hash and
managed with
smartmeter token; IP allowlist / blocklist; the log names who calls - Hot reload of the configuration via
SIGHUP; a broken file is refused, the emulation goes on - Release builds for every Raspberry Pi architecture, from the Pi Zero (ARMv6) to 64-bit systems
1. Download the archive for your Pi from the latest release:
| Archive | Raspberry Pi model |
|---|---|
linux_armv6 |
Pi 1 and Zero (1st gen) |
linux_armv7 |
Pi 2 / 3 / 4 / 5 / Zero 2 W with a 32-bit OS |
linux_arm64 |
Pi 3 / 4 / 5 / Zero 2 W with a 64-bit OS |
VERSION=4.0.1 ARCH=armv6 # see the release page for the latest version
BASE=https://github.com/womat/smartmeter/releases/download/v$VERSION
curl -LO $BASE/smartmeter_${VERSION}_linux_$ARCH.tar.gz -LO $BASE/checksums.txt
sha256sum -c checksums.txt --ignore-missing
tar xzf smartmeter_${VERSION}_linux_$ARCH.tar.gz2. Install binary, example configuration and a certificate. Everything belongs to root; the
service user smartmeter may read etc/ and write nothing. It needs the dialout group for the
serial port:
sudo groupadd -r -f smartmeter
sudo useradd -r -s /usr/sbin/nologin -g smartmeter smartmeter
sudo usermod -aG dialout smartmeter
sudo install -d -o root -g root -m 755 /opt/smartmeter /opt/smartmeter/bin
sudo install -d -o root -g smartmeter -m 750 /opt/smartmeter/etc
sudo install -o root -g root -m 755 smartmeter /opt/smartmeter/bin/
sudo install -o root -g smartmeter -m 640 config/config.yaml /opt/smartmeter/etc/
sudo openssl req -x509 -nodes -newkey rsa:2048 -days 825 \
-keyout /opt/smartmeter/etc/key.pem -out /opt/smartmeter/etc/cert.pem -subj "/CN=$(hostname)"
sudo chown root:smartmeter /opt/smartmeter/etc/key.pem /opt/smartmeter/etc/cert.pem
sudo chmod 640 /opt/smartmeter/etc/key.pemWhy root: whoever can write config.yaml or the token file can add a token of their own, and
whoever can write bin/smartmeter can replace the program. smartmeter itself needs neither.
3. Configure /opt/smartmeter/etc/config.yaml: set env: prod, the serial port of the RS485
adapter and the address of your upstream meter — see Configuration. Then create
a token for each caller, here the browser:
sudo /opt/smartmeter/bin/smartmeter token add browserIt prints the token once; keep it, e.g. in a password manager. See Tokens.
4. Start it as a service, with the unit from the archive:
sudo install -m 644 contrib/smartmeter.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now smartmeter
journalctl -u smartmeter -n 20 # "Module started successfully"The unit runs smartmeter in a sandbox: CAP_NET_BIND_SERVICE for port 502 as the only capability,
a read-only system, only /dev/ttyS0 as a device. For another serial port, e.g. /dev/ttyUSB0,
change DeviceAllow with sudo systemctl edit smartmeter.
5. Check that the values are current: open https://<your-pi>:8443/, accept the self-signed
certificate and enter the token — the meter shows Ready — and the inverter's web interface shows
the meter.
upstream meter ──(Modbus TCP or RTU)──► Raspberry Pi ──(RS485, Modbus RTU)──► Fronius inverter
│
└──(Modbus TCP, port 502)──► wallbox / evcc
- Connect an RS485 adapter (UART HAT on
/dev/ttyS0, or USB on/dev/ttyUSB0) to the meter input of the inverter: D+ to D+, D− to D−, GND to GND. Terminate the bus with 120 Ω at both ends. - Configure the inverter for a Fronius Smart Meter on Modbus RTU, address 1, 9600 baud, 8N1 —
the defaults of a real meter and of
listen.rtu. - Only one meter may answer on the bus at address 1: remove or readdress a real Fronius meter.
- On a Raspberry Pi, use
/dev/ttyS0(the UART on the GPIO header, enable it withenable_uart=1and without a serial console) and keepinterFrameDelay: 20msfrom the example configuration.
The configuration file is a YAML file, by default /opt/smartmeter/etc/config.yaml. Unknown keys are
an error, so a misspelled key cannot silently fall back to its default. Environment variables are
expanded in the ${VAR} form only, e.g. for a password; put it in an EnvironmentFile= of the
unit (root, 0600), not in the config. Durations are Go duration strings (300ms, 1s).
The example configures a Smartfox Pro as upstream meter:
# =============================================================================
# smartmeter configuration
# =============================================================================
#
# smartmeter reads an upstream meter (Modbus TCP or RTU) and serves its values
# as a Fronius Smart Meter 63A-3:
# - the proprietary RS485 map (4096 ff.) polled by Fronius inverters, and
# - the SunSpec block (40000 ff., model 203) read by wallboxes, evcc, etc.
# The Fronius target registers are fixed in the code; this file only says where
# the values come from and on which unit IDs the emulated meter answers.
# logLevel defines the minimum log level.
# Messages with at least this level are logged.
# Allowed values: debug | info | warn | error
logLevel: info
# logDestination defines where logs are written to.
# Supported values: stdout | stderr | /path/to/logfile
logDestination: stdout
# environment: dev | prod
# With prod, a missing webserver.certFile is an error; dev falls back to the embedded,
# publicly known development certificate.
env: dev
# =============================================================================
# Webserver configuration (HTTPS)
# =============================================================================
webserver:
# Host address the HTTPS server listens on (0.0.0.0 = all interfaces)
listenHost: 0.0.0.0
# Port the HTTPS server listens on
listenPort: 8443
# Bearer tokens accepted on the protected endpoints, one "<name> <sha256>" per line.
# Manage it with "smartmeter token add|remove|list"; a relative path is relative to this file.
# The file holds hashes only, but whoever can write it can add a token: owner root.
tokenFile: /opt/smartmeter/etc/token-hashes
# TLS private key file
keyFile: /opt/smartmeter/etc/key.pem
# TLS certificate file
certFile: /opt/smartmeter/etc/cert.pem
# Blocked IP addresses or networks (empty = none blocked)
# Examples: 192.168.0.1, 192.168.0.0/16, 10.0.0.0/8
blockedIPs: [ ]
# - 192.168.0.1
# - 192.168.0.0/16
# Allowed IP addresses or networks (empty = all allowed)
# Note: ::1 is the IPv6 loopback address
# Examples: 127.0.0.1, ::1, 192.168.0.0/16
allowedIPs: [ ]
# - 127.0.0.1
# - ::1
# - 192.168.0.0/16
# =============================================================================
# Modbus server (facing the inverter and other clients)
# =============================================================================
# TCP and RTU can be active at the same time; both serve every unit ID of every meter.
# A listener is active when its block is present: delete or comment out a block to turn it
# off. Defaults: tcp 0.0.0.0:502, rtu 9600 8N1 (port is required).
listen:
tcp:
host: 0.0.0.0
port: 502
# RS485 to the Fronius inverter (the inverter polls unit ID 1 at 9600 8N1)
rtu:
port: /dev/ttyS0
baudRate: 9600
dataBits: 8
parity: N # N | E | O
stopBits: 1
# Silence that ends a request (default: t3.5 of the Modbus specification, ~4ms at
# 9600 baud). Raise it when requests arrive split ("Error parsing RTU frame", CRC
# errors in the log): USB adapters need 20ms-40ms, and so does the UART of a
# Raspberry Pi (/dev/ttyS0), which split about one request per minute with the
# default and none with 20ms.
interFrameDelay: 20ms
# =============================================================================
# Emulated meters, keyed by name
# =============================================================================
# unitIds - Unit IDs the meter answers on (1-247, unique across all meters).
# All IDs serve the same values. Fronius convention:
# 1 = inverter via RS485, 200 = SunSpec meter via TCP.
# maxCurrent - Rated current per phase in A (Fronius Smart Meter 63A-3: 63).
# A poll cycle is discarded as a spike, keeping the last valid values, if
# |current per phase| > maxCurrent * 1.2
# |power per phase| > 253 V * maxCurrent * 1.2 (63 A: ~19 kW)
# |total power| > 3 * the limit per phase (63 A: ~57 kW)
# 0 or unset = no check.
# staleTimeout - Go duration (default 30s). Without a valid snapshot from the source for this
# long, the unit IDs stop answering (RTU silent, TCP exception 11): the
# inverter then sees a failed meter instead of acting on frozen values. The next
# valid snapshot brings them back. 0s = keep answering with the last values.
# The unit IDs never answer before the first valid snapshot.
# limits - Limits of the installation; they only scale the web page.
# importPower - W drawn from the grid at most (default 3 * 230 V * current)
# exportPower - W fed into the grid at most, e.g. the inverter's export limit
# (default 3 * 230 V * current)
# current - A per phase, e.g. the main fuse (default maxCurrent, else 63)
#
# source - Upstream meter
# type - tcp | rtu
# unitId - Unit ID of the upstream meter
# timeout - Request timeout as Go duration string (default 2s)
# tcp - host, port (default 502)
# rtu - port, baudRate, dataBits, parity, stopBits (default 9600 8N1)
#
# poll - Reading the upstream meter
# interval - Poll interval as Go duration string (default 1s)
# maxBlockGap - Unused registers read along to merge fields into one request (default 4)
# maxBlockSize - Registers per request, at most 125 (default 125)
#
# map - Canonical field -> source. Required for a Fronius meter:
# power_total, power_l1..3 [W], voltage_l1..3 [V], current_l1..3 [A],
# energy_import, energy_export [Wh], frequency [Hz]
# Optional: pf_l1..3 (default 1), voltage_l1_l2/l2_l3/l3_l1 [V]
# (default phase voltage * sqrt(3)), device_id, firmware, serial_number,
# serial_str. Everything else (apparent/reactive power, PF total, ...) is derived.
# type: register - address (0-based), dtype (uint16 | uint32 | uint64 | int16 | int32 |
# int64 | float32), byteOrder, wordOrder (big | little, default big),
# scale (value = (raw + offset) * 10^scale), offset
# type: fixed - value
# type: expr - expr, e.g. "{power_l1} + {power_l2} + {power_l3}"; operators + - * /,
# parentheses, SQRT(), ABS(). Write {field}, not ${field}: ${...} is
# replaced by an environment variable when the file is loaded.
# =============================================================================
meter:
# Smartfox Pro as upstream meter, served to a Fronius inverter (RS485) and wallbox/evcc (TCP).
# Register list (1-based in the document, 0-based here):
# https://smartfox.at/wp-content/uploads/2022/12/Modbus-Register-SMARTFOX-Pro-SMARTFOX-Pro-2-v22e-00.01.03.10.xlsx
primary_meter:
unitIds: [ 1, 200 ]
maxCurrent: 63
staleTimeout: 30s
limits:
importPower: 15000
exportPower: 4500
current: 20
source:
type: tcp
unitId: 1
timeout: 300ms
tcp:
host: smartfox.local
port: 502
poll:
interval: 1s
maxBlockGap: 10 # one request for 40999-41037 (39 registers)
maxBlockSize: 125
map:
# Identification as expected by the inverter (registers 18, 768, 4176)
device_id:
type: fixed
value: 285
firmware:
type: fixed
value: 117
serial_number:
type: fixed
value: 99999999
energy_import: # Wh
type: register
address: 40999
dtype: uint64
energy_export: # Wh
type: register
address: 41003
dtype: uint64
power_total: # W
type: register
address: 41017
dtype: int32
power_l1:
type: register
address: 41019
dtype: int32
power_l2:
type: register
address: 41021
dtype: int32
power_l3:
type: register
address: 41023
dtype: int32
voltage_l1: # 0.1 V
type: register
address: 41025
dtype: uint16
scale: -1
voltage_l2:
type: register
address: 41026
dtype: uint16
scale: -1
voltage_l3:
type: register
address: 41027
dtype: uint16
scale: -1
current_l1: # mA
type: register
address: 41028
dtype: uint32
scale: -3
current_l2:
type: register
address: 41030
dtype: uint32
scale: -3
current_l3:
type: register
address: 41032
dtype: uint32
scale: -3
pf_l1: # 0.0001
type: register
address: 41034
dtype: int16
scale: -4
pf_l2:
type: register
address: 41035
dtype: int16
scale: -4
pf_l3:
type: register
address: 41036
dtype: int16
scale: -4
frequency: # 0.01 Hz
type: register
address: 41037
dtype: uint16
scale: -2Every meter answers on all of its unitIds, on TCP and RTU. The values are the same; only the
SunSpec Modbus address (register 40068) reports the unit ID that was asked. Fronius uses unit ID 1
on RS485 (inverter) and 200 for the meter over TCP, so unitIds: [1, 200] serves both.
With maxCurrent set, a poll cycle whose current or power exceeds what a meter of that rated current
can measure is discarded as a whole, keeping the last valid values. The limits allow for the upper
grid voltage tolerance (253 V) and a 20 % short overload:
| Value | Limit | 63 A |
|---|---|---|
| current per phase | maxCurrent × 1.2 | 75.6 A |
| power per phase | 253 V × maxCurrent × 1.2 | ≈ 19 kW |
| total power | 3 × the limit per phase | ≈ 57 kW |
A discarded cycle is logged as Implausible Modbus snapshot discarded and counted in /health.
The unit IDs do not answer before the first valid values from the source, so a client never reads
empty registers (an inverter would take them as 0 W). When the source stops delivering valid values,
the last ones are served on; after staleTimeout (default 30 s) the unit IDs stop answering: silence
over RTU, exception 11 over TCP. The inverter then sees a failed meter and falls back to its own
settings, e.g. the export limit, instead of acting on frozen values. The next valid values bring the
unit IDs back, with no moment of empty registers. Both steps are logged (unit IDs no longer answer,
unit IDs answer). staleTimeout: 0s keeps answering with the last values.
After a failed read smartmeter closes the connection to the source and opens it again before the
next poll, so a source that dropped the connection (broken pipe, connection reset by peer) or
was restarted is read again at the next poll interval without a restart of smartmeter. The log
shows the first failed poll, then one line a minute with the number of failed polls
(failedPolls) while the source stays down, and Source answers again when it is back.
The target registers are fixed in the code (pkg/fronius/registers.go)
and served on every unit ID. Addresses are 0-based, as sent on the wire; 32-bit values are high word
first.
Fronius does not publish this map. It was recorded in 2020 by listening to the RS485 traffic between a Fronius inverter and a real Fronius Smart Meter 63A-3, and is partly confirmed by the LoxWiki. It is served exactly as recorded; registers not listed stay 0. The Smart Meter TS 65A-3 uses a different map.
| Register | Value | Type | Unit |
|---|---|---|---|
| 18 | device type (285) | uint16 | |
| 768 | firmware (117) | uint16 | |
| 4096/4098/4100 | voltage L1/L2/L3–N | uint32 | mV |
| 4102/4104/4106 | current L1/L2/L3 | uint32 | mA |
| 4110/4112/4114 | voltage L1-L2/L2-L3/L3-L1 | uint32 | mV |
| 4116 | active power total | int32 | 0.01 W |
| 4124 | energy import | uint32 | Wh |
| 4128 | energy export | uint32 | Wh |
| 4132 | power factor total | int16 | 0.01 |
| 4134 | frequency | uint16 | 0.1 Hz |
| 4140/4142/4144 | active power L1/L2/L3 | int32 | 0.01 W |
| 4164/4165/4166 | power factor L1/L2/L3 | int16 | 0.01 |
| 4176 | serial number | uint32 |
Positive power is import from the grid, negative power is export.
Specified by the SunSpec Alliance (common model 1, meter model 203 with
integer and scale factors) and by Fronius in Smart_Meter_Register_Map_Int&SF.xlsx
(gen24-modbus-api-external-docs.zip).
The documents count from 1: register 40001 there is address 40000 here.
| Address | Content |
|---|---|
| 40000 | SunS marker |
| 40002–40068 | common model 1: Fronius, Smart Meter 63A, serial number, Modbus address (unit ID) |
| 40069–40175 | meter model 203: current, voltage, frequency, power, apparent/reactive power, PF, energy |
| 40176–40177 | end block (0xFFFF, 0) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
— | Web page; it asks for a token and reads the endpoints below |
| GET | /version |
— | Application name and version |
| GET | /health |
Token | Runtime metrics, every meter with its served values, the Modbus listeners |
| GET | /registers?unit=N |
Token | Both register maps of unit ID N as a client reads them |
Authentication with a bearer token from smartmeter token add: Authorization: Bearer <token>.
HTTPS is TLS 1.3 only. Errors are returned as {"error": "..."} with the HTTP status (401 without
a valid token).
curl -k https://localhost:8443/version
curl -k -H "Authorization: Bearer $TOKEN" https://localhost:8443/health
curl -k -H "Authorization: Bearer $TOKEN" "https://localhost:8443/registers?unit=1"/health reports, besides the runtime metrics, per meter, the state of the RTU port and the Modbus
listeners with the requests they answered since start:
"meters": {
"primary_meter": {
"unitIds": [1, 200],
"source": "tcp 192.168.1.10:502 unit 1",
"ready": true,
"online": true,
"lastSuccess": "2026-10-08T22:16:52+02:00",
"ageSeconds": 0.4,
"latencyMs": 12.1,
"polls": 3600,
"lastError": "power_total = 1.048576e+06 exceeds limit 57380.4",
"lastErrorTime": "2026-10-08T21:03:11+02:00",
"discardedSnapshots": 1,
"pollIntervalSeconds": 1,
"staleTimeoutSeconds": 30,
"maxCurrent": 63,
"limits": { "importPower": 15000, "exportPower": 4500, "current": 20 },
"values": { "power_total": -7, "power_l1": 110, "voltage_l1": 230.2, "energy_import": 66517823, "...": 0 }
}
},
"rtu": [
{ "port": "/dev/ttyS0", "connected": true, "since": "2026-10-08T22:16:51+02:00" }
],
"modbus": {
"tcp": { "address": "0.0.0.0:502", "requests": 3612, "clients": 1 },
"rtu": { "port": "/dev/ttyS0", "line": "9600 8N1", "requests": 32410 }
}online is false before the first valid values and after staleTimeout; offlineSince then says
since when. /registers returns every entry of the proprietary map and the SunSpec block with
address, canonical field (empty for a fixed value), type, scale factor, raw words and decoded value,
taken from one consistent copy.
https://<your-pi>:8443/ shows the state at a glance and refreshes every 2 seconds:
- Flow, in the direction a request runs: inverter (RS485) and TCP clients → smartmeter → source meter, as on the sibling pages. A line turns red when a link fails; its LED flashes once per request of a client or per poll of the source.
- Power with direction (import/export) on a bar from the export to the import limit, the three phases with current against the fuse, the energy counters and details such as frequency, power factor, discarded readings and the last error.
- Registers (fold-out): what the inverter and the TCP clients read, per unit ID.
- The logo turns like the disc of a Ferraris meter: forward on import, backward on export, faster with more power, still while the values are frozen.
The page is part of the binary and loads nothing from the internet. It asks for a token once
(smartmeter token add browser), keeps it in the browser (localStorage) and sends it as a bearer
token; a rejected token brings the prompt back. The bars are scaled by limits in the meter configuration.
| Flag | Default | Description |
|---|---|---|
--config |
/opt/smartmeter/etc/config.yaml |
Path to the configuration file |
--debug |
false |
Enable debug logging to stdout (overrides log settings from config) |
--version |
false |
Print the application version and exit |
--about |
false |
Print application details and exit |
--help |
false |
Print this help message and exit |
smartmeter token add|remove|list manages the bearer tokens, see Tokens.
The config file path can also be set via the environment variable CONFIG_FILE; --config wins
over it.
smartmeter --config /opt/smartmeter/etc/config.yaml
smartmeter --debug
CONFIG_FILE=/opt/smartmeter/etc/config.yaml smartmeterEvery caller gets its own token: a browser, Node-RED, a script. smartmeter keeps only the SHA-256
hash of each, in the file named by webserver.tokenFile, one line per caller:
# /opt/smartmeter/etc/token-hashes
browser 3f2a…c91e
nodered 8b04…d7a2
The token itself exists only with the caller. Whoever reads the file cannot call smartmeter with
it, so it needs no more protection than the config — but whoever can write it can add a token
of their own: keep it root:smartmeter 0640, like config.yaml.
| Command | Does |
|---|---|
sudo smartmeter token add <name> |
Writes the hash, then prints the token once, on stdout alone |
sudo smartmeter token add <name> -dry-run |
Prints token and line, writes nothing |
sudo smartmeter token remove <name> |
Deletes the line |
sudo smartmeter token list |
Prints the names, never a hash |
sudo systemctl reload smartmeter |
Applies a change |
-config works as for the service. A new token file gets 0640 and the owner and group of
config.yaml. add refuses a name that is taken and prints no token when it cannot write the
file, so a token that is shown but stored nowhere cannot happen. A name is 1–64 letters, digits,
., _ or -.
Rotating without a gap: smartmeter token add nodered-2, move the caller to the new token,
smartmeter token remove nodered, reload. A lost token is not recovered but replaced the same way.
In the log: at start and after every reload smartmeter names the tokens it accepts
(Bearer tokens loaded … names=[browser nodered]). The first request of each caller from each
address is logged at info (user=browser request.client=… firstAccess=true), every rejected one
as a warning with its address; the requests in between, the web page polls every 2 s, at debug.
Warning — always configure a real certificate. With
env: proda missingcertFilestops the start. Withenv: devthe server instead falls back to a self-signed certificate compiled into the binary and only logs a warning. That certificate's private key ships inside every published release archive, so it is public knowledge. It exists solely so a fresh checkout starts up during development.
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout /opt/smartmeter/etc/key.pem \
-out /opt/smartmeter/etc/cert.pem \
-days 825 -subj "/CN=$(hostname)"Send SIGHUP to reload the configuration without restarting the process. The Modbus server and the
polling are stopped and started again with the new settings.
The new configuration is loaded and validated before anything is torn down. If it fails, the
reload is refused with Config reload rejected, keeping the running configuration and the service
keeps running. If the new settings only fail on start (TLS certificate missing, port 502 or the
serial port in use, upstream meter unreachable), Start with the new configuration failed, continuing with the previous one is logged and the previous settings are used again. A reload also rereads the
token file, so a token added or removed with smartmeter token applies without a restart.
sudo systemctl reload smartmeter # requires ExecReload in the unit, see Quick start- The REST API and the web page are served over HTTPS only, TLS 1.3. Use your own certificate and
env: prodon every device, see TLS Certificate. - Give every caller its own token (
smartmeter token add <name>); everything except/,/versionand the TLS handshake needs one. smartmeter stores only their hashes and logs who calls. The REST API is read-only. - The Modbus TCP server has no authentication, as Modbus has none: whoever reaches port 502 reads the meter values. Keep it in the home network.
- Keep
/opt/smartmeterowned by root, as in the Quick start, and run the unit fromcontrib/with its sandbox. - Limit who can reach the HTTPS port with
allowedIPs, e.g. to your home network, and do not expose it to the internet.
Report vulnerabilities privately, see SECURITY.md.
The inverter shows no meter
Check the web page first. If the meter shows Ready, the problem is on the RS485 side: wiring (D+/D− swapped is
the most common fault), termination, a second device answering on address 1, or the inverter not set
to a Fronius Smart Meter on Modbus RTU. Error parsing RTU frame (CRC errors) in the log means
requests arrive split: set listen.rtu.interFrameDelay, e.g. to 20ms. USB adapters need it, and
so does the UART of a Raspberry Pi (/dev/ttyS0): with the default t3.5 (~4 ms at 9600 baud) about
one request per minute arrived in two pieces, with 20ms none. The inverter retries a lost request,
so the meter keeps working, but the log fills with warnings.
--debug logs every poll of the upstream meter.
Serial port failed in the log
The serial port went away, e.g. a USB adapter was unplugged. smartmeter reopens it every 5 seconds
and logs Serial port reopened once it is back; meanwhile rtu in /health shows the error and
the web page Not ready.
The meter is not ready ("ready": false in /health)
The upstream meter has not delivered valid values for three poll intervals. lastError in
/health and Modbus poll failed in the log give the reason, typically a timeout or a wrong
address; while the source stays down the log repeats it once a minute with failedPolls, and
smartmeter keeps reconnecting on its own (see Source failure). Implausible Modbus snapshot discarded means the source delivered values beyond the
spike filter; check maxCurrent and the scaling of the power fields.
Service is dead immediately after start
A configuration error: smartmeter exits with code 78, and the unit does not restart it
(RestartPreventExitStatus=78); systemctl status smartmeter shows the reason. Failed to load config file names a key
that does not exist (a renamed key fails with a hint) or a duration without unit;
config validation failed names the invalid value. bind: permission denied
for port 502 means AmbientCapabilities=CAP_NET_BIND_SERVICE is missing in the unit; permission denied on the serial port, that it is not in DeviceAllow or the user not in dialout.
Every release on the releases page carries archives
for all Raspberry Pi architectures with the binary, config/config.yaml,
contrib/smartmeter.service, README.md and LICENSE,
plus a checksums.txt and a changelog. Versions follow semantic versioning;
a breaking change of the API or the configuration raises the major version.
smartmeter --version reports the release a binary was built from. A local build reports something
like 4.0.1-3-g0c13781-dirty instead.
Building from source needs Go and make: clone the repository and run make help for the targets.
The API key is replaced by bearer tokens, HTTPS is TLS 1.3 only, /ready is gone, and the unit
lives in the repository:
- Make
/opt/smartmeterroot-owned as in the Quick start. - Put the new binary next to the old one as
bin/smartmeter.new, and a copy of the config asetc/config.yaml.newwithapiKey: …replaced bytokenFile: token-hashes. The running 3.x is not touched by either. - Create the tokens with the new binary:
sudo /opt/smartmeter/bin/smartmeter.new token add browser -config /opt/smartmeter/etc/config.yaml.new. This only writes the token file; it opens no port. - Move
smartmeter.newandconfig.yaml.newinto place, installcontrib/smartmeter.service, thensudo systemctl daemon-reload && sudo systemctl restart smartmeter. A config that still hasapiKeyis refused with a message that says so. An environment file that only held the API key is no longer needed; the unit does not load one. - Every caller sends
Authorization: Bearer <token>;X-API-Keyis no longer accepted. The web page asks for the token once. - Monitoring that polled
/readyreadsreadyper meter andconnectedper RTU port from/healthinstead, with a token. - A client limited to TLS 1.2 can no longer connect. The Modbus side is unchanged.
smartmeter is an independent project and is not affiliated with, endorsed or supported by Fronius International GmbH. "Fronius" and product names are used only to describe compatibility.
The inverter controls with the values it reads from the meter — for example a dynamic feed-in
limitation or battery charging. Wrong values (a misconfigured map, a wrong scale or sign) lead
to wrong control decisions. Verify the readings against the upstream meter and the inverter's web
interface before relying on them, especially where a grid operator requires a feed-in limit.
The software is provided as is, without warranty (see LICENSE).
smartmeter is released under the MIT License - see LICENSE for the full text.
The source tree contains no third-party code, but a compiled binary statically links the modules below. Their terms apply to anyone distributing that binary, not to the sources here.
| Module | License |
|---|---|
github.com/simonvetter/modbus |
MIT |
github.com/womat/mbserver |
MIT |
go.bug.st/serial |
BSD-3-Clause |
github.com/creack/goselect |
MIT |
github.com/goburrow/serial |
MIT |
github.com/womat/golib/v2 |
MIT |
github.com/swaggo/swag, http-swagger |
MIT (swagger builds only) |
gopkg.in/yaml.v3 |
MIT and Apache-2.0 |
golang.org/x/sys |
BSD-3-Clause |
