Skip to content

About

Turn any Modbus energy meter (e.g. a Smartfox) into a Fronius Smart Meter 63A-3: RS485 for the inverter, SunSpec over Modbus TCP for wallboxes and evcc. Web page, Raspberry Pi, Go.

Topics

Resources

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

smartmeter

Turn any Modbus energy meter into a Fronius Smart Meter — for the inverter on RS485 and for wallboxes on TCP.

CI Release License: MIT Go Raspberry Pi

🇩🇪 Deutsche Kurzfassung

smartmeter web page: energy flow from the source meter to the inverter and the TCP clients, 3 141 W export, the three phases, the energy counters and details   The same page on a phone

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.


Features

  • 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 /health reports 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

Quick start

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.gz

2. 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.pem

Why 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 browser

It 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.


Wiring

 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 with enable_uart=1 and without a serial console) and keep interFrameDelay: 20ms from the example configuration.

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: -2

Unit IDs

Every 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.

Spike filter

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.

Source failure

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.


Register maps

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.

Proprietary RS485 map (Fronius inverter)

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.

SunSpec meter (wallbox, evcc)

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)

REST API

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.

Web page

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.


Command-line Flags

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 smartmeter

Tokens

Every 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.


TLS Certificate

Warning — always configure a real certificate. With env: prod a missing certFile stops the start. With env: dev the 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)"

Hot-Reload

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

Security

  • The REST API and the web page are served over HTTPS only, TLS 1.3. Use your own certificate and env: prod on every device, see TLS Certificate.
  • Give every caller its own token (smartmeter token add <name>); everything except /, /version and 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/smartmeter owned by root, as in the Quick start, and run the unit from contrib/ 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.


Troubleshooting

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.


Releases

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.

Upgrading to 4.0.0

The API key is replaced by bearer tokens, HTTPS is TLS 1.3 only, /ready is gone, and the unit lives in the repository:

  1. Make /opt/smartmeter root-owned as in the Quick start.
  2. Put the new binary next to the old one as bin/smartmeter.new, and a copy of the config as etc/config.yaml.new with apiKey: … replaced by tokenFile: token-hashes. The running 3.x is not touched by either.
  3. 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.
  4. Move smartmeter.new and config.yaml.new into place, install contrib/smartmeter.service, then sudo systemctl daemon-reload && sudo systemctl restart smartmeter. A config that still has apiKey is 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.
  5. Every caller sends Authorization: Bearer <token>; X-API-Key is no longer accepted. The web page asks for the token once.
  6. Monitoring that polled /ready reads ready per meter and connected per RTU port from /health instead, with a token.
  7. A client limited to TLS 1.2 can no longer connect. The Modbus side is unchanged.

Disclaimer

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).


License

smartmeter is released under the MIT License - see LICENSE for the full text.

Third-party licenses

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

About

Turn any Modbus energy meter (e.g. a Smartfox) into a Fronius Smart Meter 63A-3: RS485 for the inverter, SunSpec over Modbus TCP for wallboxes and evcc. Web page, Raspberry Pi, Go.

Topics

Resources

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages