All posts

solar / radio / home-assistant

Replacing the SolarCity monitoring box with a local radio collector

Reading a Power-One inverter locally with a SMLIGHT radio bridge and Python, without the original SolarCity monitoring box.

Code & setup instructions

My solar setup has a Power-One inverter and a SolarCity monitoring box. The inverter sends its readings over a Digi XBee radio. I wanted those readings on my own machine, so I replaced the monitoring box with a SMLIGHT bridge and Python software.

SolarCity Inverter Radio maintains the radio network, answers the inverter’s startup messages, and reads production and diagnostic registers.

I used a Power-One PVI-5000-OUTD-US-Z, its existing Digi XBee radio, and a SMLIGHT SLZB-06U. This is the only hardware combination tested so far. The collector currently requires the original box’s radio identity and network settings. Pairing with a new coordinator identity remains untested.

SolarCity-branded Power-One inverter with cooling fins, control panel, and DC disconnect
The Power-One inverter. Check the model label to identify yours; the front casing isn't enough.
Original white SolarCity monitoring collector with an external black radio antenna
The original SolarCity monitoring box.

Radio protocol

The measurements pass through these protocols:

Python collector
    Spinel over TCP
SMLIGHT raw radio
    IEEE 802.15.4 / Digi Zigbee
Inverter XBee radio
    Serial Modbus RTU
Power-One SunSpec registers

The radio payload contains Modbus RTU register reads and replies. These expose power, exported energy, voltage, current, frequency, temperature, and operating state.

Digi documents its serial-data service under application profile 0xc105, cluster 0x0011, and endpoint 0xe8. Those fields provide the route to the serial data behind the radio. Digi application profile documentation

The supported network uses legacy stack profile 0 and unencrypted packets. The collector implements the Digi network and application behavior this inverter expects; radio hardware compatibility alone isn’t sufficient.

Using a network radio from Python

The SLZB-06U exposes its radio through a network serial bridge. That lets the collector run on a machine with no USB connection to the radio.

The tested configuration uses SMLIGHT’s CC2652P OpenThread RCP firmware build 20260304, a 460800 baud serial bridge, and TCP port 6638. SMLIGHT documents this firmware mode.

Although the firmware is called OpenThread RCP, the project uses it as a raw IEEE 802.15.4 interface. Python supplies the legacy Digi Zigbee frames. There is no Thread network in this arrangement.

The radio handles transmission, reception, frame checksums, and MAC acknowledgments. Python handles coordinator messages, application acknowledgments, measurement requests, and decoding. The repository pins the Python radio dependency to a specific revision so that another person can reproduce the same interface.

A replacement must act as the coordinator

The inverter expects a coordinator that advertises the network and answers the messages needed to stay connected.

The implementation handles beacons, association, device announcements, address verification, direct routing, and recovery after the inverter leaves or rejoins. It accepts only the configured inverter and learns its current short address from identifiable traffic. A remembered address alone is not enough to start polling.

The tested XBee has coordinator verification enabled through JV=1. Digi’s JV documentation describes this startup check. The separate timer-based network watchdog was configured as NW=0, meaning disabled.

Startup exchange

The inverter also sends an application-level startup request. The observed exchange is:

Inverter:   f4 00 01 01 01
Collector:  f5 00 00 00

The collector sends the response on the same Digi profile, serial-data cluster, and endpoints. It uses a fresh APS counter and requests an APS acknowledgment. It also sends the ordinary APS acknowledgment for the incoming request.

A MAC acknowledgment means a radio frame arrived. An APS acknowledgment confirms delivery at the Zigbee application layer. The F5 message answers the startup request itself.

The collector reproduces the observed bytes. I haven’t fully decoded the proprietary fields or verified this exchange on other inverter models. An already joined inverter can also proceed to readings without sending this startup message on every reconnect.

Requesting and validating a measurement

A power read in the supported register map asks Modbus unit 1 for five registers starting at address 40360:

01 03 9d a8 00 05 2b 85

That is a complete Modbus RTU request, including its CRC. It sits inside the Digi radio message. TCP is used between Python and the bridge, but the payload is not Modbus TCP and has no Modbus TCP header.

The response contains a power value and scale factor. For a synthetic example, a raw value of 5000 with scale -1 means 500.0 W. Register values are big-endian; the Modbus CRC is sent low byte first.

The collector checks identity, addressing, application fields, expected length, and CRC before accepting a reading. It rejects radio errors and unsupported fragmentation. Missing readings remain gaps in the data.

The collector sends one measurement query per minute, with power alternating with energy and diagnostics. Power normally updates every two minutes. Network maintenance continues independently, and packet delivery retries are bounded. The integration reads inverter registers; it does not change inverter operating settings.

Reproducing the setup

The repository includes the collector, decoder, synthetic protocol tests, an optional dashboard, and separate container build targets. The detailed setup guide covers the exact configuration fields and startup sequence.

The main steps are:

  1. Confirm the inverter and radio match the supported legacy setup.
  2. Configure the SMLIGHT RCP bridge, then use the included discovery script and report-to-config guide to gather your channel, operating PAN IDs, expected collector EUI, and inverter EUI. Existing captures or accessible radio configuration are also useful sources.
  3. Review the evidence, put the observed values in radio.local.json, and validate the file with python config.py.
  4. Power off the original collector, if present, and give the Python collector exclusive access to the radio bridge after capture has finished.
  5. Validate fresh readings, then observe startup and overnight recovery.

Run python collector.py for collection and the JSON API on port 8766. The optional dashboard runs separately with python server.py on port 8765 and reads the collector API. Home Assistant can use the same API through its REST sensors; the repository includes a power and energy example. Starting or stopping the dashboard does not restart the radio connection, and API reads do not increase inverter polling frequency.

A capture used to inspect the application exchange needs to include unicast traffic. Stock TI RCP promiscuous reception can miss ACK-requested unicasts, so a quiet capture is not conclusive. An independent, verified sniffer can help during initial characterization; it is not needed for normal operation.

The repository contains fictional identities and synthetic telemetry. Installation configuration, captures, databases, and logs stay out of version control. The collector API and dashboard bind to localhost by default because the data includes information about the local equipment.

Testing and limitations

With the original box powered off, I tested recovery from a radio reset and a leave/rejoin cycle. The original implementation also resumed readings after an overnight quiet period. These tests cover one installation. The standalone repository has offline tests and still needs independent hardware reproductions and longer-term testing.

The included discovery tool can recover candidate settings from inverter traffic and explains the evidence for each value. On an operating replacement network, it recovered all five required radio settings from inverter-originated frames. An inverter that has already left its network may expose less information. Recovering from that state, or teaching it a new coordinator identity, remains unverified.

solarcity_sniff records and decodes SolarCity traffic. Other communities have built replacement coordinators for Enecsys and APsystems. Their protocols differ from this inverter’s. More references are in the repository’s sources and credits.

All posts