smalog — SMA inverter logger is a single-binary service that reads power production from SMA solar inverters over Speedwire/Ethernet or Bluetooth, stores it in SQLite or PostgreSQL, and publishes MQTT. All in one long-running process.
smalog is inspired by SBFspot, but it is not a 1:1 port. It is an independently structured Rust application with an always-on service, canonical storage model, web API and dedicated migration tools. smalog schema v1 is purpose-built and incompatible with the SBFspot database schema; existing databases are imported with the read-only-source migrator.
License: smalog is licensed under the European Union Public Licence 1.2 (EUPL-1.2).
- One service, many inverters — a single instance polls every configured inverter. Speedwire devices share one UDP socket; Bluetooth devices use independent RFCOMM sessions.
- SMA Speedwire (ethernet) protocol — discovery, login, spot data, and day/month/event archives, implemented in Rust using protocol behavior and accumulated fixes documented by SBFspot as a reference.
- Bluetooth (RFCOMM) transport — built in on Linux and Windows (no
flag); talks to older BT-only inverters and enumerates multiple inverters
behind a repeater. The OS socket is behind a
BtSockettrait, so a new platform is one file. See docs/bluetooth.md. - SQLite or PostgreSQL via sqlx, with a normalized schema, explicit units, dynamic MPPT rows and indexed daily rollups.
- In-process MQTT — native client, SBFspot
MQTT_Data-compatible JSON payload, one message per inverter. - Daylight-gated polling — computed sunrise/sunset, like SBFspot.
- Runs anywhere — static-ish binary, Docker image for amd64,
arm64 and armv7 (Raspberry Pi 3+), systemd unit,
/healthz+/statusHTTP endpoints. - Web dashboard — an optional React/shadcn UI (src/ui)
showing live and historic (day/week/month/year) production via a small
/api/*JSON API. Its System area shows every request the poll cycle sends to an inverter — stored, so it survives a restart — and the service's own log from an in-memory ring, both covering two days, so a silent inverter can be diagnosed without shell access to the host. See docs/ui.md. - Config in TOML with
${ENV_VAR}expansion so secrets stay out of the file.
The Cargo workspace keeps each responsibility in its owning crate:
smalog-connection provides one connection
interface for Speedwire/Ethernet, SMA Data 2 Plus over Bluetooth, and
SMA Data V1 transports. Speedwire and Bluetooth are operational; RS485 currently
exposes the stable SMA-Net interface, while RS232 and Powerline expose their
respective SMA Data V1 boundaries. These three legacy transports report that
their I/O is not yet implemented.
smalog-observation defines the strongly
typed, protocol-neutral Poll Cycle exchanged by connection, persistence,
status and exporters. smalog-storage owns the
canonical schema and persistence; smalog-export
owns CSV, MQTT and
the explicit catalog of planned export adapters; the
localized SMA catalog lives in smalog-tags; the
SBFspot migrator and
schema benchmark are standalone
tools. The smalog app composes these modules into the
logger service and CLI.
Choose either the native installation (recommended for Bluetooth and
Raspberry Pi hosts) or Docker. Both variants use the same config.toml.
-
Give Ethernet/Speedwire inverters fixed IP addresses when possible. smalog communicates with them on UDP port
9522. -
Put the smalog host on a network that can reach those addresses.
-
For serial-only Ethernet discovery, multicast
239.12.255.254:9522must reach the host. Docker bridge networking does not pass this multicast traffic. -
For Bluetooth, enable the host adapter and determine each inverter's MAC address:
bluetoothctl power on bluetoothctl scan on
Stop the scan after finding the SMA devices with
bluetoothctl scan off. Only one process can hold an inverter's RFCOMM channel at a time, so stop SBFspot or other Bluetooth pollers before starting smalog.
For Raspberry Pi OS and Debian on ARM, a .deb does the integration work —
system user, state directory, systemd unit — in one step. Download the package
for your architecture and SHA256SUMS from
GitHub Releases: arm64 for a
64-bit OS (Pi 3/4/5), armhf for a 32-bit one (Pi 2/3/4).
# Check `dpkg --print-architecture` if you are unsure which one you need.
sha256sum --check SHA256SUMS --ignore-missing
sudo apt install ./smalog_X.Y.Z-1_arm64.debThe package installs /usr/bin/smalog and the systemd unit but does not
start the service — it has no configuration yet, and starting it would only
produce a failure every 30 seconds. Two steps remain:
sudo cp /etc/smalog/config.example.toml /etc/smalog/config.toml
sudo $EDITOR /etc/smalog/config.toml # inverters, database, secrets
sudo smalog --config /etc/smalog/config.toml check-config
sudo systemctl enable --now smalogYour /etc/smalog/config.toml is a file the package never declares, so no
upgrade or removal can overwrite, prompt about or delete it. Continue at
section 4; sections 2 and 3 are already done.
Download the archive for your platform and SHA256SUMS from
GitHub Releases. Available
builds target Linux x86, amd64, ARMv7 and ARM64.
# Replace the file name with the archive you downloaded.
sha256sum --check SHA256SUMS --ignore-missing
tar -xzf smalog-vX.Y.Z-linux-amd64.tar.gz
sudo install -m 0755 smalog /usr/bin/smalog
smalog --versionRelease binaries include the embedded web dashboard.
Installed by hand before the Debian packages existed? The bundled
smalog.servicenow runs/usr/bin/smalog, not/usr/local/bin/smalog. Either move the binary, or editExecStart=in your copy of the unit — otherwise the service fails to start with "No such file or directory".
The service requires Rust 1.89 or newer. The CI and container builds use
Rust 1.97. On Debian or Ubuntu, install the native build tools and Rust:
sudo apt-get update
sudo apt-get install -y build-essential ca-certificates curl pkg-config
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
git clone https://github.com/teian/smalog.git
cd smalog
cargo build --release --locked -p smalog
sudo install -m 0755 target/release/smalog /usr/bin/smalogA backend-only binary needs no JavaScript toolchain. To embed the dashboard,
also install Node.js 24, enable the pinned pnpm version, build the UI, and
enable the Rust ui feature:
cd src/ui
corepack enable
corepack prepare pnpm@11.1.1 --activate
pnpm install --frozen-lockfile
pnpm run build
cd ../..
cargo build --release --locked -p smalog --features ui
sudo install -m 0755 target/release/smalog /usr/bin/smalogSkip this section if you installed the Debian package: it already created the
smalog user and /var/lib/smalog, and placed the example configuration.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin smalog
sudo install -d -m 0755 /etc/smalog
sudo install -d -o smalog -g smalog -m 0750 /var/lib/smalog
sudo install -m 0640 -o root -g smalog \
config.example.toml /etc/smalog/config.toml
sudoedit /etc/smalog/config.tomlAt minimum, configure [service], [plant], [database] and one
[[inverter]]. SQLite is created automatically:
locale = "de-DE"
[service]
interval = 300
timezone = "Europe/Berlin"
listen = "0.0.0.0:8080"
poll_at_night = false
[plant]
name = "Home"
latitude = 52.52
longitude = 13.405
sun_rs_offset = 900
[database]
url = "sqlite:///var/lib/smalog/smalog.db"
[[inverter]]
name = "Roof"
communication = "ethernet"
address = "192.168.1.50"
password = "${SMALOG_INV1_PASSWORD}"
user_group = "user"For an Ethernet inverter discovered by serial, omit address and set
serial instead. This requires multicast connectivity:
[[inverter]]
name = "Roof"
communication = "ethernet"
serial = 1234567890
password = "${SMALOG_INV1_PASSWORD}"For Bluetooth, configure the MAC address, not a serial number. The serial is read from the inverter during the handshake:
[[inverter]]
name = "Garage"
communication = "bluetooth"
address = "00:80:25:AA:BB:CC"
password = "${SMALOG_BT_PASSWORD}"
user_group = "user"
mis_enabled = falseThe usual SMA user password default is 0000. Passwords are limited to
12 characters. See Configuration for every
setting, including PostgreSQL, MQTT, CSV and archive collection.
Every ${NAME} in the TOML file must exist in the process environment,
even when it occurs in a disabled section. Keep unused secret lines
commented out.
For an interactive first run:
export SMALOG_INV1_PASSWORD='0000'
# For Bluetooth instead:
# export SMALOG_BT_PASSWORD='0000'For systemd, store the same variables in a root-owned environment file:
sudo install -m 0600 -o root -g root /dev/null /etc/smalog/smalog.env
sudoedit /etc/smalog/smalog.envIts contents use NAME=value syntax without export:
SMALOG_INV1_PASSWORD=0000
# SMALOG_BT_PASSWORD=0000
# SMALOG_MQTT_PASSWORD=change-me
# SMALOG_DB_PASSWORD=change-meRun these commands before enabling the long-running service:
# Discover the configured Ethernet/Bluetooth transports.
sudo --preserve-env=SMALOG_INV1_PASSWORD,SMALOG_BT_PASSWORD \
-u smalog smalog --config /etc/smalog/config.toml discover
# Validate the complete file, including environment expansion.
sudo --preserve-env=SMALOG_INV1_PASSWORD,SMALOG_BT_PASSWORD \
-u smalog smalog --config /etc/smalog/config.toml check-config
# Perform one normal polling cycle and initialize the database schema.
sudo --preserve-env=SMALOG_INV1_PASSWORD,SMALOG_BT_PASSWORD \
-u smalog smalog --config /etc/smalog/config.toml onceIf daylight gating is enabled, once may legitimately skip inverter
polling at night. Temporarily set poll_at_night = true, or set both
coordinates to 0.0, when testing outside daylight hours.
For Bluetooth, use the dedicated read-only test. It verifies RFCOMM, login and data retrieval without writing exports or changing the inverter clock:
sudo --preserve-env=SMALOG_BT_PASSWORD -u smalog \
smalog --config /etc/smalog/config.toml test-bluetooth
sudo --preserve-env=SMALOG_BT_PASSWORD -u smalog \
smalog --config /etc/smalog/config.toml test-bluetooth --allThe second command requests every applicable spot-data group and can take several minutes on older devices whose unsupported registers time out.
The Debian package already installed the unit at
/lib/systemd/system/smalog.service; go straight to systemctl enable --now smalog. Do not also copy a unit into /etc/systemd/system/ — a file
there overrides the packaged one and will keep pointing at whatever path it
names, including after an upgrade moves the binary.
From a source checkout:
sudo install -m 0644 packaging/smalog.service \
/etc/systemd/system/smalog.serviceAn unpacked release archive contains the unit at its top level instead:
sudo install -m 0644 smalog.service /etc/systemd/system/smalog.serviceThen enable the service:
sudo systemctl daemon-reload
sudo systemctl enable --now smalog
sudo systemctl status smalog
journalctl -u smalog -fThe supplied unit creates /var/lib/smalog as its state directory and
loads /etc/smalog/smalog.env.
The supplied unit already permits AF_BLUETOOTH, so Bluetooth inverters
need no further change. A unit installed before that fix still blocks the
address family and fails with socket(AF_BLUETOOTH): Address family not supported by protocol (os error 97). Reinstall the unit shown above, or
add an override:
sudo systemctl edit smalogEnter:
[Service]
RestrictAddressFamilies=
RestrictAddressFamilies=AF_INET AF_INET6 AF_BLUETOOTHThen apply the override:
sudo systemctl daemon-reload
sudo systemctl restart smalogWhen the binary contains the UI and service.listen is
0.0.0.0:8080, open:
http://<smalog-host>:8080/
Useful checks:
curl http://127.0.0.1:8080/healthz
curl http://127.0.0.1:8080/status
smalog --config /etc/smalog/config.toml healthcheck
journalctl -u smalog --since todayAllow inbound TCP port 8080 in the host firewall if the dashboard
should be reachable from another machine.
Docker is the shortest installation path for fixed-IP Ethernet inverters. Bluetooth is supported by enabling host networking in the Compose file.
git clone https://github.com/teian/smalog.git
cd smalog
cp config.example.toml config.toml
$EDITOR config.toml
# docker-compose.yml passes this variable into the container.
export SMALOG_INV1_PASSWORD='0000'
docker compose build
docker compose up -d
docker compose logs -f smalogThe Compose setup:
- builds a UI-enabled image;
- mounts
config.tomlread-only at/etc/smalog/config.toml; - persists SQLite in the
smalog-datavolume; - publishes the dashboard on http://localhost:8080;
- runs the built-in
/healthzhealthcheck.
Use a .env file instead of an interactive export for unattended
Compose deployments:
SMALOG_INV1_PASSWORD=0000Fixed inverter IPs work on Docker's default bridge network. For
serial-only multicast discovery, change the service to
network_mode: host and remove ports:. The same change is required for
Bluetooth. Uncomment the annotated network_mode line in
docker-compose.yml, remove the ports: section, and first power and
configure the adapter on the host:
export SMALOG_BT_PASSWORD='0000'
docker compose up -d
docker compose logs -f smalogsmalog uses the host's kernel RFCOMM interface directly; no privileged mode, D-Bus mount, or device mapping is required. See Docker for networking, PostgreSQL and multi-arch builds.
| Symptom | Check |
|---|---|
missing environment variable |
Define every ${VAR} referenced by config.toml, or comment out the unused setting. |
| Ethernet inverter not found | Verify its fixed IP and UDP 9522; multicast discovery also requires the same LAN/broadcast domain or host networking. |
Bluetooth Host is down |
Ensure the inverter is awake and in range, the adapter is powered, and the MAC belongs to that inverter. |
Bluetooth Device or resource busy |
Stop SBFspot, another smalog process, scans, or any process holding RFCOMM channel 1. |
| Bluetooth: some queries answer, others time out | Record the raw frames with test-bluetooth --capture <file> (or capture_file in the config) and check the debug log — see Bluetooth. |
Bluetooth socket(AF_BLUETOOTH): os error 97 under systemd |
The installed unit lacks AF_BLUETOOTH in RestrictAddressFamilies; reinstall the unit or apply the override above. |
SQLite unable to open database file |
Ensure /var/lib/smalog exists and is writable by the smalog user. |
healthcheck cannot connect |
Set service.listen; verify the service is running and the selected port is not already in use. |
| Dashboard returns 404 | Install a release binary or rebuild from source with pnpm run build and Cargo feature ui. |
More operational detail is available in Operations, Bluetooth, Database and Web UI.
The release archives and Debian packages can be built without a release or a pipeline run:
scripts/build-packages.sh # armhf + arm64 .deb, with the UI
scripts/build-packages.sh --all # every target the workflow builds
scripts/build-packages.sh --no-ui aarch64-unknown-linux-gnuThe artefacts land in dist/, named as in a release. It needs cross,
cargo-deb, jq, a running Docker or Podman for foreign targets, and pnpm
unless the UI is skipped. scripts/build-packages.sh --help lists the options.
GitHub Actions checks Rust formatting and linting, runs the Rust test suite, and
builds the web dashboard on every branch and pull request. To publish a release,
create a GitHub Release with a new semantic-version tag such as v0.1.0.
Publishing it creates the tag and triggers release builds for:
- x86 (32-bit)
- amd64
- ARMv7 hard-float (32-bit Raspberry Pi OS on Raspberry Pi 3 and newer)
- ARM64 (64-bit Raspberry Pi OS on Raspberry Pi 3 and newer)
Each release archive contains the UI-enabled smalog binary, the example
configuration, systemd unit, README, and license. The release also includes a
SHA256SUMS file for verifying the downloaded archives. Matching
multi-architecture images are published to
fgehann/smalog and
ghcr.io/teian/smalog
for linux/386, linux/amd64, linux/arm/v7 and linux/arm64. The release
workflow requires a DOCKERHUB_TOKEN repository secret with push access to
fgehann/smalog. After the builds finish, the workflow uploads the archives
and extends the release description with both versioned image links and
automatically generated notes about the changes since the previous release.
Stable releases additionally update both latest image tags. Any manually
entered description is retained.
Run the service and the dashboard directly from a source checkout — no installation, no systemd. Install the build prerequisites from Option C: build from source first.
Create a local configuration next to the checkout; config.toml is
gitignored:
cp config.example.toml config.toml
$EDITOR config.tomlFor development, point the database at a workspace-local file, for
example url = "sqlite://data/smalog.db", and keep
listen = "0.0.0.0:8080".
Every ${VAR} the file references must be exported before startup — an
unset variable is a hard error, and the names depend on what you kept in
your own config, not on the example. List them, then export each one:
grep -o '\${[A-Z0-9_]*}' config.toml | sort -u
export SMALOG_INV1_PASSWORD='0000'Validate the result, then run a debug build:
cargo run -p smalog -- --config config.toml check-config
cargo run -p smalog -- --config config.toml run--config is required because the default path is
/etc/smalog/config.toml. check-config reports a missing variable the
same way run does, so it is the quickest way to confirm the
environment before a poll. During iteration, once performs a single
poll cycle and exits (see CLI).
The Vite dev server serves the UI with hot reload and proxies /api to
the locally running backend on port 8080, so the Rust ui feature is
not needed during development:
cd src/ui
corepack enable
pnpm install
pnpm run dev # http://localhost:5173To point the dev server at a remote smalog instance instead, set
VITE_API_BASE (see src/ui/.env.example). More detail in
Web UI & API.
| Command | Purpose |
|---|---|
run |
Run the service (default). |
once |
Run a single poll cycle and exit. |
discover |
Scan the network and print SMA devices (IP, SUSyID, serial). |
test-bluetooth [--all] |
Connect, log in and fetch representative or all spot data from configured Bluetooth inverters without exporting it. |
check-config |
Validate the config file and exit. |
healthcheck |
Probe the running service's /healthz (Docker healthcheck). |
set-time |
Set and verify the clock of configured Bluetooth inverters. |
set-time2 |
Set Bluetooth inverter clocks without read-back when set-time cannot verify the change. |
All take --config <path> (default /etc/smalog/config.toml).
- Configuration reference — every TOML key.
- Database — schema v1, units, tables, indexes and queries.
- SBFspot migration — backup, dry run, cutover and rollback.
- CSV export — SBFspot-compatible files, formatting, scope.
- MQTT — the full item-key list and payload format.
- Bluetooth — optional RFCOMM transport on Linux and Windows.
- Connections and protocol — common interface, SMA Data 2 Plus over Speedwire/Bluetooth and SMA Data V1 over RS232/RS485/Powerline.
- Web UI & API — the dashboard and the
/api/*endpoints. - Docker — multi-arch build, compose, networking.
- Operations — systemd, endpoints, first run.
- Architecture — how the modules fit together.
- Domain glossary — Poll Cycle, Inverter Fleet, archive completion and best-effort export terminology.
- Rust style — workspace/module/error/style conventions.
SBFspot-inspired compatibility features (not 1:1 parity):
- CSV export — SBFspot-compatible spot / day / month / battery / event
files, off by default (
[csv]). Standard column layout only; the Webbox header variant and-123s123Solar stdout export are not implemented. See docs/csv.md. - Localization — event texts and CSV headers in en-US, de-DE, es-ES,
fr-FR, it-IT or nl-NL (
locale), adapted from SBFspot's TagLists into structured UTF-8 JSON files. - Inverter clock-sync —
smalog set-time/set-time2, plus automaticsynch_timeon each poll. Bluetooth only, matching the relevant SBFspot behavior (Speedwire devices get their time from the network). See docs/bluetooth.md.
Outside smalog's scope:
- MySQL — SQLite and PostgreSQL only (PostgreSQL via sqlx).
- Multigate / SB240 ethernet aggregation.
Platform-limited:
- Bluetooth — built into Linux and Windows binaries (no flag). Linux uses RFCOMM and Docker deployments require host networking. Ethernet/Speedwire is the default. See docs/bluetooth.md.
Independent design choices and improvements:
- One always-on service instead of a cron-driven poller.
- Parameterized SQL everywhere (SBFspot built SQL by string concatenation, unescaped).
- Schema v1 is created automatically for an empty database on first run.
- NaN temperature is stored as
NULLconsistently (SBFspot wrote a garbage sentinel into theInvertersrow). - Consumption monitoring writes canonical
site_consumption_measurementsfrom the inverter's consumer-power LRIs (poll_consumption). Needs an SMA consumption meter.
smalog is inspired by SBFspot, but is an independently structured implementation rather than a 1:1 port. SMA is a registered trademark of SMA Solar Technology AG. This project is not affiliated with or endorsed by SMA.