Skip to content

Command-line tool

sunspecctl reads and simulates SunSpec devices over Modbus. As a client it detects a SunSpec device, lists its models, and reads or polls decoded points with scale factors and units applied; it never writes to a device. As a server, sunspecctl serve runs a simulated SunSpec device from a JSON device file, which sunspecctl capture writes from a real device and sunspecctl template starts for you. It is built on go-sunspec, with the official SunSpec Alliance model definitions compiled in.

For transport-level SunSpec work (marker, base address, model headers), modbusctl has its own SunSpec commands, covered at the end of this page.

Pre-built binaries for Linux (x86-64, ARM64, ARMv7), macOS (Apple Silicon, Intel) and Windows (x86-64, ARM64) are on the sunspecctl releases page, with a sunspecctl-<version>-checksums.txt file of SHA-256 sums. No Go toolchain is needed.

Terminal window
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 ;;
esac
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"
grep "${FILE}" "sunspecctl-${VERSION}-checksums.txt" | sha256sum -c -
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/sunspecctl

For a serial port without sudo, add your user to the dialout group: sudo usermod -aG dialout "$USER", then log out and back in.

The README also has a PowerShell script for Windows that verifies the checksum and adds the binary to your PATH. Check the install with sunspecctl version. To update, run the same commands again: they always fetch the latest release.

All commands share these global flags:

Flag Default Description
--url tcp://localhost:502 Modbus device URL; for serve, the address to listen on
--unit-id 1 Modbus unit ID; for serve, overrides the device file
--timeout 10s Operation timeout
--json false Output as JSON
--raw false Include raw register hex in the output
--baud 0 (19200) Serial baud rate for rtu:// and ascii://
--data-bits 0 (8 for RTU, 7 for ASCII) Serial data bits, 5 to 8
--parity none 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

--url takes any go-modbus scheme: tcp://host:port, tcp+tls://host:port, rtu://<device> (rtu:///dev/ttyUSB0, rtu://COM3), ascii://<device>, udp://host:port, rtuovertcp://host:port for a serial-to-Ethernet gateway, and the others listed in the Modbus overview. The serial flags are ignored for network URLs, and the TLS flags for anything but tcp+tls://.

sunspecctl
├── 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
  1. Check that the device speaks SunSpec and find its base address:

    Terminal window
    sunspecctl detect --url tcp://192.168.1.100:502
  2. List the models it implements:

    Terminal window
    sunspecctl models --url tcp://192.168.1.100:502
  3. Read one model, or one point:

    Terminal window
    sunspecctl read-model --id 101 --url tcp://192.168.1.100:502
    sunspecctl read-point --model 101 --point W --url tcp://192.168.1.100:502
  4. Read and decode everything, with the raw registers alongside:

    Terminal window
    sunspecctl read --url tcp://192.168.1.100:502 --raw

Over RS-485 or through a gateway:

Terminal window
# SunSpec over Modbus RTU 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 3

If detect finds nothing, it prints every base address it tried (0, 40000 and 50000, and their off-by-one neighbours, in holding registers) and what the device answered. Check the unit ID first (many inverters do not answer on unit 1), then whether SunSpec or Modbus has to be enabled in the device settings, and on serial lines the baud rate, parity and stop bits. Models sunspecctl has no definition for are still listed, with SCHEMA no, and read as raw registers with a warning.

poll, poll-model and poll-point work like their read counterparts but repeat:

Flag Default Description
--interval 30s Time between polls (s, m, h)
--count 0 Number of polls; 0 runs until Ctrl-C
Terminal window
# All models every 10 seconds
sunspecctl poll --interval 10s --url tcp://192.168.1.100:502
# Model 101 five times, once per minute
sunspecctl poll-model --id 101 --interval 1m --count 5 --url tcp://192.168.1.100:502
# AC power of a three-phase inverter every 5 seconds, as JSON
sunspecctl poll-point --model 103 --point W --interval 5s --json --url tcp://192.168.1.100:502

The poll commands discover the device once. When the device later has another model at an address (a firmware update, a reconfiguration), a read reports model changed since discovery for that model instead of decoding its registers with the old layout.

sunspecctl 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, an EMS, a data logger, a SCADA driver. Use it to develop and test an integration without an inverter, meter or battery on the desk.

Terminal window
# 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 3

The file is validated completely before the device listens: an error names the model and point it concerns, and the exit status is 1. The device runs until Ctrl-C or SIGTERM, then closes its connections, prints shutting down and exits with status 0.

Flag Default Description
--file, -f (required) The device file
--url tcp://localhost:502 Address to listen on. Ports below 1024 usually need privileges; tcp://0.0.0.0:1502 accepts connections from other machines
--unit-id from the file, else 1 Unit ID the device answers
--base-address from the file, else 40000 Register address of the SunS marker
--check false Validate the file, print the layout and exit without listening
--json false Print the layout, and each client write, as JSON
device.json
{
"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 to 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 to a 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
registers The model’s data registers as numbers 0 to 65535, for vendor models without a schema or anything you want served bit for bit
name Ignored. capture and template write the model’s name here for the reader

Values are written the way you read them:

Point type Write it as
Integers, accumulators, floats, 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 too; without it the number is stored as it is. A value that does not fit after scaling is an error
enum16, enum32 The symbol name ("MPPT") or the number
bitfield16 to 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 exactly as many register numbers as the point has: "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 go-sunspec’s devicefile package, shared with SunSpec Explorer, and has a JSON Schema for editors and other tools: devicefile/devicefile.schema.json in go-sunspec, published at otfabric.io/schemas/sunspec/devicefile-1.schema.json, its $id. To get completion and validation while you write a device file, map the schema to your device files in the editor settings (json.schemas in VS Code). Do not add a $schema member to the file itself: device files refuse unknown members.

Terminal window
# 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 --check

capture writes models that decode cleanly as point values, which are easy to read and edit, and anything without a known schema, or that does not match its schema exactly, as registers. Either way, serving the file gives a device that reads the same as the original. With --raw, every model is written as registers and the copy is identical register for register. Enum and bitfield points are captured as numbers, so values the model definition does not name survive.

A capture is a snapshot: its values do not change by themselves. It can hold serial numbers and network addresses of the device, so look before you share it.

  • Unit IDs. The device answers one unit ID. A request for another is answered with 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 neither answered nor carried out.
  • Writes. Clients can write the points SunSpec defines as writable (access RW) with function codes 6 and 16. Each accepted write is printed (write from 127.0.0.1: model 123 at 40176: WMaxLimPct = 50.5) and later reads return it. Writes to read-only points, model headers, count points, part of a multi-register point, models given as registers and unmapped addresses are refused with exception 2. The device stores what is written; it does not act on it, so writing a power limit does not change the served power.

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, and all three are required for a tcp+tls:// URL. The device certificate must name the host or address clients connect to.

Terminal window
# 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.pem

A client without a certificate of that CA, and a plain tcp:// client, cannot read the device.

The default output is a human-readable table. --json gives structured output for jq and scripts, and --raw adds the raw register hex next to the decoded values. When a value looks wrong, sunspecctl read --raw shows the registers behind it; include that output, together with detect and models, when you report a device problem.

A command that fails exits with status 1 (from main.go; the README only says non-zero), and there are no environment variables for the flags. In scripts, check for zero and parse --json output.

As a client, sunspecctl only reads, so it cannot change a setpoint: since v0.2.0 its reading commands use a Modbus client that allows function codes 03 and 04 only and refuses anything else before it is sent. It still sends real Modbus requests: use it only on devices and networks you are authorized to access, and keep inverter, meter and battery Modbus ports off the Internet. Output and captures can contain serial numbers, network addresses and production figures, so treat them as operational data.

serve has no authentication on plain TCP: whoever can reach the port can read the device and write its writable points. Keep it on loopback or a test network, or serve over tcp+tls:// with client certificates.

modbusctl works at the Modbus level. Its SunSpec commands find the marker and enumerate model headers, but do not decode points.

Command What it does
client sunspec detect Is the unit SunSpec, and at which base? --bases 0,40000,50000 to probe specific bases, --verbose for the attempt log
client sunspec models The model chain: start and end address, model ID, length, end model. --base skips detection; --max-models and --max-address-span bound the walk
client sunspec map A readable address map. --show-header-regs, --show-next, --compact
client sunspec probe One-shot summary: FC03, FC04 and FC43 support plus SunSpec detection
Terminal window
modbusctl client sunspec detect --ip 192.168.1.10 --unit 1 --regtype holding --bases 0,40000,50000 --verbose --format json
modbusctl client sunspec models --ip 192.168.1.10 --unit 1 --base 40000 --max-models 64 --format json
modbusctl client sunspec map --url tcp://192.168.1.10:502 --unit 1
modbusctl client sunspec probe --url tcp://192.168.1.10:502 --unit 1

--regtype accepts holding or input, which is how you reach a device that keeps its SunSpec map in input registers from the command line; the sunspecctl flags do not expose that option. All four commands take --format text|json|table.

To record every model body to an MBCAP capture at the Modbus level, use the SunSpec scan algorithm. It detects the base, walks the chain and reads each model in chunks of up to 125 registers:

Terminal window
modbusctl client scan --ip 192.168.1.10 --algo sunspec --output sunspec.mbcap
modbusctl client scan --ip 192.168.1.10 --algo sunspec --sunspec-base 40000 --output sunspec.mbcap

For a SunSpec copy you can read, edit and serve, sunspecctl capture and sunspecctl serve are the more direct route.