Skip to main content

sofar-modbus

Read Sofar Solar inverters over Modbus, as typed Python objects rather than register numbers.

The library maps Sofar's register set onto modbus-connection's device model: you hand it a ModbusUnit, call async_update(), and read sub-systems as attributes. It owns no connection and no I/O policy — the caller does.

Supported devices

Sofar ships two quite different register maps, so there are two device objects.

SofarInverter — the current generation. HYD hybrids and KTL-X / KTLM PV inverters, over the 0x0400 (state and identity), 0x0480 (grid), 0x0500 (off-grid), 0x0580 (PV), 0x0600 (battery), 0x0680 (energy), 0x1000 (settings) and 0x9000 (BTS battery tower) blocks. Serial prefixes: SP1, SP2, ZP1, ZP2, SM2E, ZM2E, SH3E, SS2E, ZS2E, SQ1ES1, SA1, SB1, SC1, SD1, SF4, SH1, SL1, SJ2, SS1. Includes the Azzurro and ZCS rebadges. This is the only generation with writable registers.

SofarLegacyInverter — the older generation. The earlier PV inverters (SA1, SA3, SB1, ZA3, SC1, SD1, SF4, SH1, SJ2, SL1, SM1) and the SE1E / SM1E / ZE1E / ZM1E storage inverters, over the 0x0000 and 0x0200 blocks, with the serial number in the input-register space. Read-only.

Within a generation, what an inverter serves depends on its model: single or three phase, PV-only or hybrid, how many MPPT trackers, whether off-grid (EPS) and parallel-system registers exist. The first update reads the serial number and settles this into an InverterType bitmask; each component declares the mask it applies to, and a poll reads only the matching ones. Nothing else is touched — an inverter without batteries never sees a battery register.

Usage

import asyncio

from modbus_connection import ModbusTcpParams
from modbus_connection.tmodbus import ModbusConnection
from sofar_modbus import SofarInverter


async def main() -> None:
    connection = ModbusConnection(
        ModbusTcpParams(host="192.168.1.50", port=502, framer="rtu")
    )
    try:
        inverter = SofarInverter(connection.for_unit(1), read_eps=True)
        await inverter.async_update()

        print("Model:", inverter.model, inverter.serial_number)
        print("State:", inverter.state.system_state)
        print("Grid power:", inverter.grid.active_power_output_total, "kW")
        print("PV power:", inverter.pv_1_2.pv_power_total, "kW")
        print("Battery SoC:", inverter.battery_totals.battery_capacity_total, "%")
        print("Solar today:", inverter.energy.solar_generation_today, "kWh")
    finally:
        await connection.close()


asyncio.run(main())

A poll reads each sub-system independently, the way the integration reads its blocks: one slow or refused block does not take the rest of the poll with it. async_update() returns an UpdateReport — a failed component keeps its previous values, does not notify its listeners, and is listed by attribute name with its error, while every other component refreshes and notifies once the whole poll is done. Only a dead link (ModbusConnectionError) raises:

report = await inverter.async_update()
for name, error in report.failed.items():
    print(f"{name} kept its previous values: {error}")

Writing works the same way — a plain field write for the registers that take one, and a method for the registers the device insists on receiving as a block:

from sofar_modbus.modern import ChargerUseMode, FeedinLimitationMode

await inverter.charger.write("charger_use_mode", ChargerUseMode.PASSIVE_MODE)
await inverter.feed_in.async_write_limit(FeedinLimitationMode.DISABLED, 3000)
await inverter.passive.async_write_power(
    grid_power=-2000, battery_min=0, battery_max=5000
)
await inverter.active_power_control.async_write_active_power_limit(True, 70)

active_power_control is a live throttle on the inverter's own output — distinct from feed_in, which caps power exported to the grid. It applies to PV-only inverters as well as hybrids, and takes effect within seconds.

A BTS battery tower multiplexes every pack onto one register block, so packs are read one at a time rather than polled:

if inverter.has_battery_tower:
    pack = await inverter.async_read_pack(string_nr=0, pack_nr=0)
    print(pack.pack_serial_number, pack.soc, pack.cell_1_voltage)

For an issue report, async_read_raw() dumps every register the inverter reads undecoded, keyed by address space and address — every block a poll covers, for the sub-systems this model serves. The pack block is not in it: a dump of it would be whichever pack happened to be selected, with nothing to say which.

ASCII over TCP is not supported

Sofar inverters are reached over RTU or RTU-over-TCP. This library never accepts or forwards framer="ascii", and it exposes no connect helper that could: the caller builds the ModbusUnit and hands it over. Build it from an RTU serial or RTU-over-TCP connection — an ASCII-framed TCP connection is unsupported and untested, and nothing here works around it.

Attribution

The register maps are derived from homeassistant-solax-modbus (Apache-2.0), specifically its plugin_sofar.py and plugin_sofar_old.py. This library keeps that project's field keys, scale factors, units and per-model filtering, and is released under the same licence.

Where upstream declares two entities on one register, or the same key twice, this library keeps both rather than picking a winner — the docstring on each field carries upstream's name, and the tests spell out the cases.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sofar_modbus-0.1.4.tar.gz (65.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sofar_modbus-0.1.4-py3-none-any.whl (34.8 kB view details)

Uploaded Python 3

File details

Details for the file sofar_modbus-0.1.4.tar.gz.

File metadata

  • Download URL: sofar_modbus-0.1.4.tar.gz
  • Upload date:
  • Size: 65.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sofar_modbus-0.1.4.tar.gz
Algorithm Hash digest
SHA256 bac3730b6af858797ab56e366e38d93a597ed52bb963178a01105dcd71df4619
MD5 b2044a8e58bd655a426d19abb267c5be
BLAKE2b-256 d9d8d9a3d49c8acdbcbc325adc268c618f6b613126419a7254826cdb10a1c40f

See more details on using hashes here.

Provenance

The following attestation bundles were made for sofar_modbus-0.1.4.tar.gz:

Publisher: publish.yml on darkrain-nl/sofar-modbus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sofar_modbus-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: sofar_modbus-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 34.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sofar_modbus-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 a57bc123773d774791b3a8d1acd1244bffaf1c09f768e3b0397a319549b6b1ea
MD5 02fefe9de854fe99b276de2dad42f53b
BLAKE2b-256 2f29743b78b60ba65d4d50f4cb4fd6f425e8599417cb49c9556adfa54e8adab4

See more details on using hashes here.

Provenance

The following attestation bundles were made for sofar_modbus-0.1.4-py3-none-any.whl:

Publisher: publish.yml on darkrain-nl/sofar-modbus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.11.1

2 files

0.11.0

2 files

0.10.0

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

This release

0.1.4 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page