sunspecctl is a command-line tool for reading and simulating SunSpec devices over Modbus. It detects whether a solar inverter, energy meter, battery or other DER device speaks SunSpec, lists the SunSpec models it exposes, and reads and decodes every point into scaled values with units, over Modbus TCP, RTU (RS-485), ASCII or a serial-to-Ethernet gateway. It can also be the device: sunspecctl serve runs a simulated SunSpec device from a JSON file, which sunspecctl capture writes from a real one. It is a single static binary for Linux, macOS and Windows, with the official SunSpec Alliance model definitions built in.
Use it to commission and troubleshoot a device, to check what it really exposes before writing integration code, to poll values into a script or a pipeline as JSON, or to develop and test an EMS, a logger or a SCADA integration against a simulated inverter, meter or battery. It is built on the otfabric/go-sunspec library, which is what to use from Go code.
Quick links: Quick start · Install · Commands · Global flags · Polling · Serving / simulating a device · FAQ
# Is this a SunSpec device, and at which base address?
sunspecctl detect --url tcp://192.168.1.100:502
# Which SunSpec models does it implement?
sunspecctl models --url tcp://192.168.1.100:502
# Read and decode everything
sunspecctl read --url tcp://192.168.1.100:502
# Read AC power from the inverter model every 5 seconds, as JSON
sunspecctl poll-point --model 103 --point W --interval 5s --json --url tcp://192.168.1.100:502
# The same device on RS-485, 9600 8N1, Modbus address 3
sunspecctl read --url rtu:///dev/ttyUSB0 --baud 9600 --stop-bits 1 --unit-id 3
# Copy the device into a file, and serve that copy on this machine
sunspecctl capture --url tcp://192.168.1.100:502 -o device.json
sunspecctl serve --file device.json --url tcp://127.0.0.1:1502sunspecctl
├── detect Detect SunSpec device presence
├── models List discovered SunSpec models
├── read Read and decode all models
├── read-model Read and decode a specific model by ID
├── read-point Read a single named point from a model
├── poll Repeatedly read and decode all models
├── poll-model Repeatedly read and decode a specific model by ID
├── poll-point Repeatedly read a single named point from a model
├── serve Serve a simulated SunSpec device from a device file
├── capture Read a device and write it as a device file for serve
├── template Write a device file skeleton for the given models
├── completion Generate shell completion script (bash/zsh/fish/powershell)
└── version Print the sunspecctl version
The easiest way to install sunspecctl is to download a pre-built binary from the GitHub Releases page. No Go toolchain is required.
| Platform | CPU | Download |
|---|---|---|
| Linux | x86-64 | sunspecctl-<version>-linux-amd64.tar.gz |
| Linux | 64-bit ARM (Raspberry Pi 4/5 with a 64-bit OS, AWS Graviton) | sunspecctl-<version>-linux-arm64.tar.gz |
| Linux | 32-bit ARM (Raspberry Pi with a 32-bit OS) | sunspecctl-<version>-linux-armv7.tar.gz |
| macOS | Apple Silicon (M1 and later) | sunspecctl-<version>-darwin-arm64.tar.gz |
| macOS | Intel | sunspecctl-<version>-darwin-amd64.tar.gz |
| Windows | x86-64 | sunspecctl-<version>-windows-amd64.zip |
| Windows | ARM64 | sunspecctl-<version>-windows-arm64.zip |
Each release also contains sunspecctl-<version>-checksums.txt (SHA-256) so you can verify your download. The commands below fetch the latest release, verify its checksum and install it. Afterwards, check that it works:
sunspecctl version# 1. Find the latest version and your CPU architecture
VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/otfabric/sunspecctl/releases/latest | sed 's|.*/v||')
case "$(uname -m)" in
x86_64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
armv7l) ARCH=armv7 ;;
*) echo "Unsupported architecture: $(uname -m)" ;;
esac
# 2. Download the archive and the checksum file
FILE="sunspecctl-${VERSION}-linux-${ARCH}.tar.gz"
BASE="https://github.com/otfabric/sunspecctl/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/${FILE}"
curl -fsSLO "${BASE}/sunspecctl-${VERSION}-checksums.txt"
# 3. Verify the download (should print "OK")
grep "${FILE}" "sunspecctl-${VERSION}-checksums.txt" | sha256sum -c -
# 4. Extract and install (the binary in the archive carries the version and platform in its name)
mkdir -p sunspecctl-dl && tar -xzf "${FILE}" -C sunspecctl-dl
sudo install -m 0755 "$(find sunspecctl-dl -type f -name 'sunspecctl*' | head -n1)" /usr/local/bin/sunspecctlNo sudo? Install into your home directory instead (and make sure ~/.local/bin is in your PATH):
mkdir -p ~/.local/bin
install -m 0755 "$(find sunspecctl-dl -type f -name 'sunspecctl*' | head -n1)" ~/.local/bin/sunspecctlTo talk to a device on a serial port (RS-485 adapter) without sudo, your user needs access to the port, usually through the dialout group: sudo usermod -aG dialout "$USER", then log out and back in.
# 1. Find the latest version and your CPU architecture (arm64 = Apple Silicon, x86_64 = Intel)
VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/otfabric/sunspecctl/releases/latest | sed 's|.*/v||')
case "$(uname -m)" in
arm64) ARCH=arm64 ;;
x86_64) ARCH=amd64 ;;
esac
# 2. Download the archive and the checksum file
FILE="sunspecctl-${VERSION}-darwin-${ARCH}.tar.gz"
BASE="https://github.com/otfabric/sunspecctl/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/${FILE}"
curl -fsSLO "${BASE}/sunspecctl-${VERSION}-checksums.txt"
# 3. Verify the download (should print "OK")
grep "${FILE}" "sunspecctl-${VERSION}-checksums.txt" | shasum -a 256 -c -
# 4. Extract and install (the binary in the archive carries the version and platform in its name)
mkdir -p sunspecctl-dl && tar -xzf "${FILE}" -C sunspecctl-dl
sudo install -m 0755 "$(find sunspecctl-dl -type f -name 'sunspecctl*' | head -n1)" /usr/local/bin/sunspecctlIf you downloaded the archive with a web browser instead of curl and macOS refuses to run the binary ("cannot be verified"), remove the quarantine flag: xattr -d com.apple.quarantine /usr/local/bin/sunspecctl.
# 1. Find the latest version and your CPU architecture
$version = (Invoke-RestMethod https://api.github.com/repos/otfabric/sunspecctl/releases/latest).tag_name.TrimStart('v')
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' }
# 2. Download the archive and the checksum file
$file = "sunspecctl-$version-windows-$arch.zip"
$base = "https://github.com/otfabric/sunspecctl/releases/download/v$version"
Invoke-WebRequest "$base/$file" -OutFile "$env:TEMP\$file"
Invoke-WebRequest "$base/sunspecctl-$version-checksums.txt" -OutFile "$env:TEMP\sunspecctl-checksums.txt"
# 3. Verify the download (throws an error on mismatch)
$expected = ((Select-String -Path "$env:TEMP\sunspecctl-checksums.txt" -Pattern $file).Line -split '\s+')[0]
$actual = (Get-FileHash "$env:TEMP\$file" -Algorithm SHA256).Hash
if ($expected -ne $actual) { throw "Checksum mismatch for $file" }
# 4. Extract to a per-user folder and name the program sunspecctl.exe
# (the .exe in the archive carries the version and platform in its name)
$dir = "$env:LOCALAPPDATA\Programs\sunspecctl"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Expand-Archive "$env:TEMP\$file" -DestinationPath "$env:TEMP\sunspecctl-dl" -Force
$exe = Get-ChildItem "$env:TEMP\sunspecctl-dl" -Recurse -Filter 'sunspecctl*.exe' | Select-Object -First 1
Copy-Item $exe.FullName "$dir\sunspecctl.exe" -Force
# 5. Add sunspecctl to your user PATH (then open a new terminal)
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($userPath -notlike "*$dir*") {
[Environment]::SetEnvironmentVariable('Path', "$userPath;$dir", 'User')
}If Windows SmartScreen warns about an unrecognised app, choose More info → Run anyway.
- Open the latest release.
- Download the archive for your platform from the table above.
- Extract it, rename the binary to
sunspecctl(sunspecctl.exeon Windows) and move it to a folder on yourPATH.
Run the same commands again. They always fetch the latest release and overwrite the previous binary.
If you have Go 1.23 or later, you can build and install the latest release straight from source:
go install github.com/otfabric/sunspecctl@latestsunspecctl used to be released from the go-sunspec repository, last as v0.4.2. Releases now come from this repository, and the version numbering starts again at v0.1.0: sunspecctl v0.1.0 is the successor of the sunspecctl that shipped with go-sunspec v0.4.2, although its number is lower. The two series are told apart by where they come from, not by comparing numbers:
- The install commands above replace the old ones; they always take the latest release of this repository.
go install github.com/otfabric/go-sunspec/cmd/sunspecctl@lateststays at v0.4.2 for good. Usego install github.com/otfabric/sunspecctl@latest.- A script or package recipe that only upgrades to a higher version number will not move from 0.4.2 to 0.1.0 by itself: point it at the new repository once.
The existing commands, their flags and their output are unchanged.
| Flag | Default | Description |
|---|---|---|
--url |
tcp://localhost:502 |
Modbus device URL |
--unit-id |
1 |
Modbus unit ID |
--timeout |
10s |
Operation timeout |
--json |
false |
Output as JSON |
--raw |
false |
Include raw register hex in output |
--baud |
0 (19200) |
Serial baud rate, for rtu:// and ascii:// URLs |
--data-bits |
0 (8 for RTU, 7 for ASCII) |
Serial data bits, 5–8 |
--parity |
none |
Serial parity: none, even or odd |
--stop-bits |
0 (2 without parity, 1 with) |
Serial stop bits, 1 or 2 |
--tls-cert |
Certificate (PEM) this side presents, for tcp+tls:// URLs |
|
--tls-key |
Private key (PEM) of --tls-cert |
|
--tls-ca |
CA certificates (PEM) that the other side's certificate must chain to |
For serve, --url is the address to listen on and --unit-id overrides the unit ID of the device file; see Serving / simulating a device.
--url selects the transport: tcp://host:port, rtu://<device> (for example rtu:///dev/ttyUSB0 or rtu://COM3), ascii://<device>, udp://host:port, rtuovertcp://host:port for a serial-to-Ethernet gateway, and the other go-modbus URL schemes. The serial flags are ignored for network URLs.
Many SunSpec devices on RS-485 use 9600 baud, 8 data bits, no parity and 1 stop bit, which is not the default: pass --baud 9600 --stop-bits 1 (and the device's Modbus address as --unit-id).
# Detect SunSpec presence
sunspecctl detect --url tcp://192.168.1.100:502
# List discovered models
sunspecctl models --url tcp://192.168.1.100:502
# Read and decode all models
sunspecctl read --url tcp://192.168.1.100:502
# Read a specific model by ID
sunspecctl read-model --id 101 --url tcp://192.168.1.100:502
# Read a single point from a model
sunspecctl read-point --model 101 --point W --url tcp://192.168.1.100:502
# SunSpec over Modbus RTU (RS-485) at 9600 8N1, device address 3
sunspecctl models --url rtu:///dev/ttyUSB0 --baud 9600 --stop-bits 1 --unit-id 3
# A serial device behind an RS-485-to-Ethernet gateway
sunspecctl read --url rtuovertcp://192.168.1.50:4001 --unit-id 3The poll, poll-model, and poll-point commands work like their read counterparts but repeat at a configurable interval.
| Flag | Default | Description |
|---|---|---|
--interval |
30s |
Interval between polls (supports s, m, h) |
--count |
0 |
Number of polls (0 = infinite, stop with Ctrl-C) |
# Poll all models every 10 seconds
sunspecctl poll --interval 10s --url tcp://192.168.1.100:502
# Poll model 101 five times, once per minute
sunspecctl poll-model --id 101 --interval 1m --count 5 --url tcp://192.168.1.100:502
# Poll a single point every 5 seconds as JSON (great for piping)
sunspecctl poll-point --model 101 --point W --interval 5s --json --url tcp://192.168.1.100:502sunspecctl serve runs a SunSpec device over Modbus TCP from a JSON device file: its models, their point values, its unit ID and base address. Anything that speaks SunSpec Modbus can read it — sunspecctl itself, your EMS, a logger, a SCADA driver.
# Serve the device described in device.json on port 1502 of this machine
sunspecctl serve --file device.json --url tcp://127.0.0.1:1502
# In another terminal: read it like any device
sunspecctl read --url tcp://127.0.0.1:1502$ sunspecctl serve --file device.json --url tcp://127.0.0.1:1502
Serving SunSpec device on tcp://127.0.0.1:1502 (unit ID 1, base address 40000)
ID NAME START LENGTH
1 Common 40002 66
103 Inverter (Three Phase) 40070 50
160 Multiple MPPT Inverter Extension Model 40122 48
64900 unknown_64900 40172 3The device runs until you press Ctrl-C or the process is terminated (SIGINT or SIGTERM); it then closes its connections, prints shutting down on standard error and exits with status 0. The file is validated completely before the device starts listening: an error names the model and point it concerns and the exit status is 1.
| Flag | Default | Description |
|---|---|---|
--file, -f |
(required) | The device file |
--url |
tcp://localhost:502 |
Address to listen on: tcp://host:port. Ports below 1024 usually need privileges; use tcp://0.0.0.0:1502 to accept connections from other machines |
--unit-id |
from the file, else 1 |
Modbus unit ID the device answers (overrides the file) |
--base-address |
from the file, else 40000 |
Register address of the SunS marker (overrides the file) |
--check |
false |
Validate the file, print the layout and exit without listening |
--json |
false |
Print the layout, and each client write, as JSON |
Only plain Modbus TCP is served. Modbus has no authentication: whoever can reach the port can read the device and write its writable points. See SECURITY.md.
{
"unit_id": 1,
"base_address": 40000,
"models": [
{
"id": 1,
"points": { "Mn": "Acme", "Md": "PV-5000", "SN": "A-0001" }
},
{
"id": 103,
"points": {
"A": 18.3, "A_SF": -1,
"PhVphA": 230.4, "V_SF": -1,
"W": 4200, "W_SF": 0,
"Hz": 50.01, "Hz_SF": -2,
"St": "MPPT",
"Evt1": ["GROUND_FAULT"]
}
},
{
"id": 160,
"points": { "DCW_SF": 0 },
"groups": {
"module": [
{ "points": { "ID": 1, "DCW": 2150 } },
{ "points": { "ID": 2, "DCW": 2050 } }
]
}
},
{ "id": 64900, "registers": [1, 2, 3] }
]
}| Member | Meaning |
|---|---|
unit_id |
Modbus unit ID, 1–255. Default 1. |
base_address |
Register address of the SunS marker. Default 40000. Clients find a device at 0, 40000 or 50000 without being told. |
models |
The models in chain order. Each has an id and either points (and groups) or registers. A model ID may occur more than once. |
points |
Point values by SunSpec point name, as in the model definition (sunspecctl read shows the names). |
groups |
Repeating groups: group name → list of instances, each with its own points and groups. Count points such as NPt or NCrv are filled in from the number of instances; give one explicitly to reserve more instances than you fill. |
registers |
The model's data registers (what follows the ID and length registers), as numbers 0–65535. For vendor models that have no schema, and for anything you want served bit for bit. |
name |
Ignored. capture and template write the model's name here for the reader. |
Values:
| Point type | Write it as |
|---|---|
Numbers (int16 … uint64, acc*, float*, sunssf, count) |
A JSON number. A point with a scale factor takes the engineering value: with "A_SF": -1, "A": 18.3 is stored as 183. Give the scale factor point as well; without it the number is stored as it is. A value that does not fit the point after scaling is an error. |
enum16, enum32 |
The symbol name ("MPPT") or the number. |
bitfield16 … bitfield64 |
A list of the names of the set bits (["GROUND_FAULT", "OVER_TEMP"], [] for none) or the number. |
string |
A JSON string. |
ipaddr, ipv6addr, eui48 |
A string: "192.168.1.10", "fe80::1", "00:11:22:33:44:55". |
| Any point | A list of register numbers, exactly as many as the point has registers: "TmpCab": [65036]. |
| Not implemented | null, or leave the point out. |
Unknown members, a point given twice and point names the model does not have are errors, so a typo does not silently serve an empty value.
The format is implemented by the devicefile package of go-sunspec, which Go programs can use to read, write and serve device files themselves. Its JSON Schema, devicefile/devicefile.schema.json ($id https://otfabric.io/schemas/sunspec/devicefile-1.schema.json), describes the form of a file for editors and validators; whether the models and points exist and the values fit is checked by sunspecctl serve --check.
# Copy a real device: everything it exposes, as it is now
sunspecctl capture --url tcp://192.168.1.100:502 -o device.json
# Or start from the models you want, every point present and null
sunspecctl template --model 1,103,160 -o device.json
# Check a file without serving it
sunspecctl serve --file device.json --checkcapture writes models that decode cleanly as point values, which are easy to read and edit. A model without a known schema, or one that does not match its schema exactly (shorter, longer, a value that would not read back the same), is written as registers. Either way, serving the file gives a device that reads the same as the original: the same points, values, scale factors and not-implemented points at the same addresses. With --raw, every model is written as registers and the copy is register-for-register identical. capture writes enum and bitfield points as numbers, so that values the model definition does not name survive.
A capture is a snapshot: the served values do not change by themselves. It can contain serial numbers and network addresses of the device; look before you share it.
The device answers one unit ID: the one of the device file, or --unit-id. A request for another unit ID is answered with Modbus exception 0x0B (gateway target device failed to respond), so a client learns at once that the unit is not there. A broadcast (unit ID 0) is not answered and not carried out.
tcp+tls:// is Modbus/TCP Security: TLS in which both sides present a certificate. serve and the client commands take the same three flags, each for its own side: --tls-cert and --tls-key are the certificate and key that side presents, --tls-ca the CA certificates the other side's certificate must chain to. All three are required for a tcp+tls:// URL and ignored for any other. The device certificate must name the host or IP address the clients connect to.
# The device presents device.pem and accepts clients with a certificate of ca.pem
sunspecctl serve --file device.json --url tcp+tls://0.0.0.0:802 \
--tls-cert device.pem --tls-key device.key --tls-ca ca.pem
# A client presents its own certificate and checks the device's against the same CA
sunspecctl read --url tcp+tls://192.168.1.100:802 \
--tls-cert client.pem --tls-key client.key --tls-ca ca.pemA client without a certificate of that CA, and a plain tcp:// client, cannot read the device.
A client can write the points SunSpec defines as writable (access RW in the model definition), with Modbus function codes 6 and 16. Each accepted write is printed, and later reads return the written value:
write from 127.0.0.1: model 123 at 40176: WMaxLimPct = 50.5
Writes to read-only points, to model headers, to the points that count group instances, to part of a multi-register point, to models given as registers and to unmapped addresses are refused with Modbus exception 2 (illegal data address). The simulated device stores what is written; it does not act on it — writing a power limit does not change the served power.
# Default: human-readable table
sunspecctl models --url tcp://192.168.1.100:502
# JSON output (structured, suitable for piping to jq)
sunspecctl models --url tcp://192.168.1.100:502 --json
# Include raw register hex alongside decoded values
sunspecctl read --url tcp://192.168.1.100:502 --rawSunSpec Modbus is an open standard that defines how solar inverters, meters, batteries and other distributed energy resources lay out their data in Modbus registers: a marker (SunS) at a known base address, followed by a chain of numbered models, each a block of named points. A SunSpec reader works with any compliant device, whatever the manufacturer.
sunspecctl detect probes the common base addresses (0, 40000 and 50000, and their off-by-one neighbours) in holding registers for the SunS marker, and prints every attempt and what the device answered. Check the Modbus unit ID (--unit-id; many inverters do not answer on unit 1), that Modbus TCP is enabled on the device, and on serial lines the baud rate, parity and stop bits.
Yes: --url rtu:///dev/ttyUSB0 (or rtu://COM3 on Windows) with --baud, --data-bits, --parity and --stop-bits. For a device behind a serial-to-Ethernet gateway use rtuovertcp://host:port.
They are still discovered and listed: sunspecctl models shows them with SCHEMA no, and reading them returns their raw registers with a warning instead of decoded points.
No. As a client it reads and decodes only; no command writes to a device. The Modbus client of detect, models, read*, poll* and capture is created with only the read function codes (3 and 4) allowed, so it refuses any other request before sending it. A device simulated with sunspecctl serve does accept writes from other clients, to the points SunSpec defines as writable.
Yes: sunspecctl serve runs a simulated device from a JSON file. Write the file by hand, generate a skeleton with sunspecctl template, or copy a real device once with sunspecctl capture and serve the copy wherever you need it.
Add --json to any command for structured output suitable for jq. The exit status is non-zero when a command fails.
They are compiled into go-sunspec from the official sunspec/models repository, and reach sunspecctl with a go-sunspec update.
make build # ./bin/sunspecctl
make build-all # cross-compile for Linux amd64/arm64/armv7, macOS amd64/arm64, Windows amd64/arm64
make install # build and install to /usr/local/binmake check # format, vet, lint, vulnerability scan, race tests, coverage gate
make test # tests with the race detectorTests need no hardware: they run against the SunSpec fixture server of go-sunspec's testutil package and against the devices serve runs in-process. See CONTRIBUTING.md.
- Go 1.23+ to build from source (not needed for the pre-built binaries)
- otfabric/go-sunspec v0.6.0+ and otfabric/go-modbus v1.5.0+
Bug reports, device-compatibility notes and pull requests are welcome. Please open an issue to discuss larger changes first, and run make check before submitting. See CONTRIBUTING.md.
Please report security vulnerabilities privately, as described in SECURITY.md, not in a public issue.
This project is licensed under the MIT License. See LICENSE.