Releases: frenck/python-solaredged
Release list
v0.2.0: Clear skies ☀️
☀️ Clear skies
This release is about honest readings. solaredge already decoded a point the device does not implement to None. This round teaches it to distrust a few values that look like data but are not, and to stop assuming a register block is there before it has proof. Each change lines up with what the long-standing community integrations learned the hard way against real hardware.
Probe the grid status extension instead of assuming it
The grid on/off status (40113) and extended vendor status (40119) registers are a SolarEdge extension that not every firmware serves. They were declared on the always-present Inverter, and because the poll pools the identity and model block into one read spanning 40004 through 40120, an inverter that rejects those registers failed the entire poll with a Modbus exception. The probe never touched 40113, so such a device probed fine and was then permanently unreadable.
They now live on InverterExtended, and async_probe detects them the same way it probes the control blocks: an illegal-address answer on 40113 means absent. Firmware that serves the extension sees no change. Firmware that does not now polls cleanly, with on_grid and vendor_status_extended reading None. A consumer that needs to tell "extension absent" from "not reported right now" can check isinstance(inverter, InverterExtended).
This is the one breaking change, and only for consumers that build SolarEdge(unit) through the constructor: grid status is now off by default there. Pass the new grid_status=True flag, or use async_probe, which detects it for you. async_probe consumers see no difference.
Out-of-range battery percentages decode to None
An initializing battery, and the odd communication glitch, reports a state of energy or health outside 0-100: negatives, values just over 100, or plain float garbage. Those are not readings, and a graph cannot tell them from real data. state_of_energy and state_of_health are now properties over raw fields (the same pattern as site_limit): a value outside an inclusive 0-100 reads None, so an empty or a perfectly healthy battery still reports a real value.
A lifetime energy of 0 decodes as not accumulated
SunSpec types the inverter's lifetime energy (40093) as acc32, where 0 means "not accumulated", and some firmware reports 0 transiently around the sleep/wake transition. It was modelled as uint32, so that transient 0 posed as a genuine reading of zero, which is poison for anything tracking a monotonic counter. A factory-new inverter now reads ac_energy as None until its first watt-hour. The meter accumulators deliberately stay uint32, where a freshly commissioned meter's 0 is a genuine reading.
Under the hood
Now built and verified against modbus-connection 3.7.0. The register map gained a second, independent conformance oracle (the official SunSpec model generator) alongside the hand-parsed one, plus a regression test for partial block reads.
pip install solaredgedA note on the version
Still 0.x, still pre-1.0. The decode contract is holding steady (a point the device does not serve, or a value that cannot be a real reading, is None, never a guess), but parts of the API may still shift before 1.0.
Clear skies. ☀️
../Frenck
Blogging my personal ramblings at frenck.dev
What's changed
🚨 Breaking changes
🐛 Bug fixes
- Probe the grid status extension instead of assuming it @frenck (#9)
- Decode out-of-range battery percentages to None @frenck (#10)
- Decode a lifetime energy of 0 as not accumulated @frenck (#11)
🚀 Enhancements
⬆️ Dependency updates
- ⬆️ Update dependency modbus-connection to v3.7.0 @renovate[bot] (#8)
v0.1.0: First light ☀️
☀️ First light
This is the first release of solaredged, an asynchronous Python client for SolarEdge inverters over Modbus. It reads and controls inverters, meters, and batteries, decoding the SunSpec information model and SolarEdge's proprietary register blocks into typed values.
It does one job and leans on others for the rest. This library declares the register layout and detects the device; the transport and the field decoding live in modbus-connection underneath. The consumer owns the connection and hands over a single unit, so a whole site of inverters shares one connection instead of every integration opening its own. That is the shared-connection approach from the Modernizing Modbus developer blog.
What you get
- The whole device. Inverter, per-string DC (multiple-MPPT / SunSpec 160), up to three meters, batteries, and the storage, export, and power control blocks, all decoded to typed values with the right units.
- Detection built in.
async_probevalidates the SunSpec header and works out which meters, batteries, and control blocks are actually present. - Writable control. Typed
set_*methods for storage mode, export and site limit, active power limit, power factor, backup reserve, charge policy, and remote charge and discharge. Only on hardware you own. - Honest values. A point the device does not implement decodes to
None. The library never turns a real reading intoNone, nor lets a read failure masquerade as one. - Transport-agnostic. Modbus TCP, RS485/RTU, or an RTU-to-TCP gateway. Anything that hands it a
ModbusUnitwill do. - EV charger detection. Recognises a SolarEdge EV charger answering on its own unit id (identity only; it exposes no telemetry over Modbus).
- One error contract. Reads and writes surface as
SolarEdgeConnectionError(transport) orSolarEdgeError(a bad value or undecodable data). No raw exceptions leak. - Optional CLI and TUI. Read your inverter from the terminal, dump the register map, or watch a live dashboard.
A taste
import asyncio
from modbus_connection.tmodbus import connect_tcp
from solaredged import SolarEdge
async def main() -> None:
connection = await connect_tcp("solaredge.local", port=1502)
try:
unit = connection.for_unit(1)
solaredge = await SolarEdge.async_probe(unit) # detects the layout
await solaredge.async_update() # one pooled read
print(solaredge.inverter.ac_power, "W")
for battery in solaredge.batteries:
print(battery.state_of_energy, "%")
finally:
await connection.close()
asyncio.run(main())pip install solaredgedThe CLI ships under an extra and drives the tmodbus backend:
pip install "solaredged[cli,tmodbus]"Tested where it counts
100% test coverage on the package. The register map is checked against the vendored SunSpec model definitions, and against real device dumps contributed by the community. The decode path is exercised with property-based fuzzing, so a garbled register cannot crash a read: it decodes to None or surfaces as our own error.
A note on the version
This is 0.1.0, and python-solaredged is pre-1.0. The register map is validated as spec-accurate and against real hardware, and the error contract is firm. Parts of the API may still shift before 1.0 as the library gathers production feedback.
Not affiliated with, endorsed by, or supported by SolarEdge Technologies. Writing control registers changes how your inverter and battery behave, so only do it on hardware you own and understand.
Thanks for taking it for a spin. Issues, ideas, and pull requests are welcome ☀️
../Frenck
Blogging my personal ramblings at frenck.dev