Repository navigation
Releases: ch3p4ll3/sitsolar-dongle
Release list
v0.2.1
Project rename
The project is renamed from sitsolar-dongle-reader to sitsolar-dongle.
Nothing changes for users. The PyPI distribution name, the import package
and the console script were already sitsolar-dongle, and all three are
untouched — pip install sitsolar-dongle, import sitsolar_dongle and the
sitsolar-dongle command work exactly as before, with no migration and no
change to the API.
Changed
- Repository
ch3p4ll3/sitsolar-dongle-reader→
ch3p4ll3/sitsolar-dongle.
GitHub redirects the old address, so existing links keep working. - Documentation site →
ch3p4ll3.github.io/sitsolar-dongle. - Attribution now names
ch3p4ll3as the author rather than
"sitsolar-dongle-reader contributors". - The background reader task is named
sitsolar-donglein place of
sitsolar-dongle-reader.
v0.2.0
Asyncio support, and a session that survives a long-lived connection.
This release adds AsyncDongleClient, a non-blocking counterpart to DongleClient for Home Assistant, FastAPI and other asyncio applications. It shares the same protocol core, register map and error types as the blocking client, so the returned values and exceptions are identical.
DongleClient is unchanged in behaviour and remains the right choice outside an event loop.
New: AsyncDongleClient
from sitsolar_dongle import AsyncDongleClient
async with AsyncDongleClient("192.168.1.50", "<DONGLE_SERIAL>") as dongle:
values = await dongle.poll()Nothing in the module blocks the event loop: there is no socket, time.sleep or synchronous recv anywhere in it. Concurrent callers are serialised with an asyncio.Lock, since a Modbus request/reply pair cannot be interleaved on one connection.
Heartbeats are answered by a background reader
The dongle pushes a heartbeat roughly every 37 s and closes a connection whose heartbeats go unanswered. Measured on real hardware, a session that never answered was dropped after about five missed pushes (roughly three minutes), while an identical session that answered stayed up for ten minutes and polled normally throughout.
A background task therefore owns the connection and replies the instant a push arrives, exactly as the official app does. Your polling interval has no bearing on whether the session survives, and reconnecting per poll is unnecessary.
A failure costs much less than it used to
Under the previous design every read owned the socket, so an interrupted read tore the connection down. Against the dongle's ~75 s reconnect lockout, that turned a routine cancellation into a 75 s outage. Now:
- Cancelling or timing out a request leaves the connection usable. The reader has already consumed whatever was on the wire, and a reply that arrives late is discarded by transaction id. This is the normal
asyncio.timeoutpattern in Home Assistant, and it no longer costs a reconnect. - A dead connection is noticed as it happens, not at the next poll. That covers a closed socket, an RST,
keepaliveseconds of silence, or three consecutive request timeouts. Any request waiting on it fails immediately instead of burning its full timeout. - A connection that is alive but stops answering is reclaimed.
connectedalone cannot see that failure, because a dongle can finish the handshake and then ignore every read. Three timeouts in a row now drop it. A successful reply resets the count, so a busy inverter is not mistaken for a broken one. connectedis trustworthy. It goesFalseexactly when the connection is finished, which is what a reconnect backoff should key off.close()is bounded, with a hard abort as fallback, so an unload handler cannot hang on a dead link.
One exception covers everything
DongleError is now the single base exception for every failure: refused connection, refused accreditation, bad HMAC, Modbus exception, dropped connection or timeout. One except is enough.
It has moved to sitsolar_dongle.errors, but is still exported from the top level and from sitsolar_dongle.client, so existing imports keep working. This release introduces no breaking changes.
Also included
py.typed: the package now declares its inline annotations under PEP 561, so type checkers use them.- A Home Assistant integration example with reconnect backoff, wired to a
DataUpdateCoordinator. It is extracted from the documentation and executed by the test suite, so it cannot silently drift. - A measured-behaviour section in the docs recording what was actually tested on hardware and, just as usefully, what was not, including the open questions.
For contributors
- Tests go from 22 to 61, including a scripted fake dongle that derives the same keys and speaks the real protocol end to end.
tools/hw_probe.pycontains the hardware experiments behind the claims above. It is a diagnostic, ships in neither the sdist nor the test suite, and takes the serial via--sn-fileso it stays out of your shell history.
v0.1.0
sitsolar-dongle 0.1.0
First release.
A read-only Modbus client for the SiNeng / FuturaPulse WiFi dongle that ships
with Sit Solar inverters — the "Si-Dongle-L" listening on TCP port 7078. It
implements the same secure Modbus frame the official Android app speaks, so you
can pull inverter telemetry straight off your LAN instead of routing it through
the vendor cloud.
Unofficial project. Not affiliated with, endorsed by, or supported by
SiNeng, FuturaPulse or Sit Solar. The protocol was reverse-engineered from the
vendor's Android app and its published Modbus protocol document. Use at your
own risk, on your own equipment.
Highlights
- Read-only by design — only Modbus FC
0x04and0x03reads. The writable
holding-register block40001–49999is deliberately absent from the map. - Full register map — 183 decoded values across 12 sections, transcribed from
the FuturaPulse hybrid inverter Modbus protocol v0.0.3, with enums, bit
fields, scale factors, and documented hardware quirks. - Authenticated transport — every frame is AES-128-CBC encrypted and
HMAC-SHA256 signed, with keys derived from the dongle serial. - Tested against real hardware — decoded lifetime totals cross-checked
against the vendor's own cloud API.
Features
Command line
# one-shot, human readable
sitsolar-dongle 192.168.1.50 --sn <DONGLE_SERIAL>
# machine readable
sitsolar-dongle 10.0.0.5 --sn ... --json
# follow forever, one sample every 15 s (Home Assistant / MQTT)
sitsolar-dongle 10.0.0.5 --sn ... --interval 15 --json
# filter by section or by key
sitsolar-dongle 10.0.0.5 --sn ... --section "AC_side" --raw
sitsolar-dongle 10.0.0.5 --sn ... --key grid_frequency --key battery_soc
# raw reads, bypassing the register map
sitsolar-dongle 10.0.0.5 --sn ... --read 31002:2 --read 30100:1
# print the built-in register map
sitsolar-dongle --listOptions cover --port, --slave, --fc {04,03}, --timeout, --errors {skip,raise}, --include-device-info, and --version. Exit codes: 0 success,
2 connection/protocol failure, 3 Modbus-level failure, 130 interrupted.
Library
from sitsolar_dongle import DongleClient
with DongleClient("192.168.1.50", "<DONGLE_SERIAL>") as dongle:
print(dongle.info) # factory, model, serial, clock
print(dongle.read(31002, 2)) # two 16-bit words, big endian
for key, reading in dongle.poll().items():
if "value" in reading:
print(key, reading["value"], reading.get("unit", ""))poll() reads every documented read-only register and returns decoded values.
Ranges a given firmware refuses are recorded under _unreadable_ranges rather
than raising — pass on_error="raise" to make them fatal.
Lower-level building blocks (derive_keys, seal, open_envelope, build_read,
parse_read_reply, crc16, decode) are exported too.
Protocol
- 7-byte clear header (
tid,protocol,length,unit) + AES-128-CBC
envelope:IV(16) | ciphertext | HMAC-SHA256(32). - HMAC verified before decryption, so a wrong serial fails cleanly with
HMAC mismatch (wrong serial number?)instead of producing garbage. - Vendor-specific CRC16 (not the standard Modbus CRC), big-endian on the wire,
guarded by an import-time self-test. - Full handshake:
BeforeAccredit→Accredit→CallAll→ FC0x04reads. - Heartbeat pushes (
43 01) answered automatically, mirroring the app.
Install
pip install sitsolar-dongleRequires Python 3.10+ and pycryptodome. From source:
git clone https://github.com/ch3p4ll3/sitsolar-dongle-reader.git
cd sitsolar-dongle-reader
uv venv && uv syncDocumentation
Published at https://ch3p4ll3.github.io/sitsolar-dongle-reader/
10 pages covering Getting Started, the CLI reference, the Python API, the wire
protocol and crypto, the complete register map, caveats/troubleshooting, and
development. Built with Retype from docs/ and deployed
automatically to GitHub Pages.
Automation
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
Push to main, PRs |
Tests on Python 3.10–3.14, builds sdist/wheel, validates metadata |
docs.yml |
Push to main (docs) |
Builds Retype site, deploys to GitHub Pages |
publish-pypi.yml |
GitHub Release published | Tests → builds → version-gate → publishes to PyPI via Trusted Publishing |
The distribution version is single-sourced from __version__
(src/sitsolar_dongle/__init__.py). Packaging uses PEP 639
(license = "GPL-3.0-or-later"), built with hatchling.
Known caveats
- One connection at a time. The dongle stops accepting new TCP connections
for ~75 s after a rapid reconnect. - Firmwares differ. Single-phase units have no S/T phase, and optional blocks
(EPS, meter, Italy self-test) may refuse a read — recorded under
_unreadable_ranges. - Where the document disagrees with the hardware is documented per register:
31039~31040tracks P rather than true apparent power,31022reads a fixed
setpoint0.999rather than instantaneous PF, and33002likely uses a0.01
scale rather than the documented0.1.
See Caveats & Troubleshooting
for the full list, including the two distinct kinds of refusal (dongle stub vs.
Modbus exception).
Security
Warning
The dongle serial number is a credential. Both session keys are a pure
function of the dongle serial:
aes_key = MD5(sn || crc16_be(sn))
hmac_key = MD5(reverse(sn) || crc16_be(reverse(sn)))
Anyone on your network who knows that serial can open an authenticated
session with your dongle. Do not commit it, paste it into issues, or share
logs and packet captures containing it.
Tested with
| Dongle | SiNeng / FuturaPulse "Si-Dongle-L" |
| Inverter | Futura Sun 3K-HS (single-phase high-voltage hybrid, device type 33) |
| Firmware | DSP1 0000090021, DSP2 0000060010, ARM 001002032 |
Other models and firmware versions may expose different register blocks —
reports are welcome, please redact your serial number first.
Verification
- 22 tests passing (CRC16 lookup tables pinned against vectors from the APK
and a live dongle, framing, and decoding). twine check --strictpasses on both sdist and wheel.
License
GNU General Public License v3.0 — see LICENSE.
Full Changelog: d1eb8f0...9a69e88