A Cloud-Free, Plug-In Solar Plant
A Cloud-Free, Plug-In Solar Plant
Over the last year I had started to take an interest in plug-in solar power plants and finally got my setup working in June 2024. However, one thing that always annoyed me was the need to use the manufacturer clouds to control the setup. While I get the pros manufacturers see in forcing use of their cloud services, I find it deeply unsettling for a variety of reasons. Most importantly because they are not a technical necessity, but instead present a possible source of errors.
Without digressing into a rant about how this is a waste of human-intellectual resources, let me introduce another way. One that…
- has no clouds, one where all communication does not leave the local network, and you can block internet access on all devices without breaking functionality.
- consists entirely of commercially available, consumer hardware.
- requires no physical tampering with the involved hardware.
- does not require you to buy additional parts.
- controls solar power production to match in-house consumption and to minimize grid return to maximize self-consumption.
Sounds interesting? Let’s go!
Setup

Not much to say about it, right? The components are pretty vanilla, everything is freely available and fairly cheap. No high-end stuff. Pairs of two panels are connected to one DC-coupled battery (the Growatt NOAH2000), which can store up to ~2 kWh each. The batteries are then connected to my Deye microinverter. All of these are supported by great open-source projects, which enable monitoring and controlling them over MQTT.
| Name | Software |
|---|---|
| Deye SUNM160G4 | deye-inverter-mqtt to connect the microinverter to MQTT |
| Growatt NOAH2000 | GroBro to connect the NOAH2000s to MQTT |
| Power Meter Reader | Using the Tasmota Smart Meter Interface |
After setting both of these up, e.g., as docker-containers, they will periodically provide updates of the device status over MQTT. By listening on specific MQTT topics, device parameters can be set, e.g., inverter output power. So now we have a solid setup that can be visualized in Home Assistant. But can we go further? Why not control the production based on the requirement, such that surplus power is stored in the batteries instead of being supplied to the grid?
Entering ctrl-solar
There are multiple other projects, such as OpenDTU-OnBattery or Solarflow-Control, which enable controlling solar production and reduce power return to the grid.
Unfortunately, those do not work with any hardware, and, as I decided on different hardware back when I bought my original plant (just panels and inverter), they do not support my setup.
So I created ctrl-solar: a controller for my MQTT-based setup, which
- controls and dynamically adjusts the solar production based on the current requirement
- adjusts the DC-coupled battery behavior to avoid discharging based on production forecasts
- with a flexible software architecture to add support for more devices and sensor sources as needed
The core of the system are Python-based control loops that continuously monitor sensor readings from the microinverter, batteries, and power meter. Currently three types of loops are implemented:
- A power controller for the solar power production
- A battery controller to control battery charging behavior
- A forecast controller to compute the expected daily yield
First, the power controller uses a time-averaged approach for adjusting inverter output power, filtering sensor readings a running-average filter. By responding to running averages rather than instantaneous values, it reduces oscillations and minimizes frequent updates to the microinverter. A built-in back-off factor helps reduce accidental grid exports.
Secondly, unlike manufacturer solutions, the battery controller does not rely on fixed schedules. Instead, it dynamically switches batteries between charging modes based on real-time demand and production forecast. This ensures batteries are never discharged when excess solar power is available, maximizing solar yield.
Finally, the forecast controller predicts the daily solar yield based on weather forecasts from open-meteo. To this end, you can specify the location of your plant, orientation and size of your panels. The forecast controller does not control anything itself, but is used in the battery-controller to optimize charging behavior.
The loop runs in a Docker container configured via a YAML file.
But why would you do this?
As I hinted at before, this project is currently mainly for technically advanced users who want to enable local-only control of their solar plants. Nevertheless, it circumvents technical limitations, which otherwise plague small-scale solar plants. Let’s quickly discuss three of them:
- Microinverters are configured to output whatever solar power is available at their input, capping to a fixed maximum power (e.g., 800 W in Germany)
- The baseline power consumption of a typical household is usually around 100-200 W, depending on the number of people and active appliances. Any produced excess power is often gifted to the grid operator without compensation.
- Therefore it is advisable to time solar production when it is produced. As a consequence, it is often recommended to perform energy intensive tasks (e.g., running a washing machine) when solar power is available, which is not very convenient.
- DC-coupled batteries are useful to mitigate this to some extent, by storing any solar surplus over this limit. But they also require a fixed output limit.
Additionally, while relatively cheap, the Growatt NOAH2000 batteries suffer from technical issues (as of writing 08/2025):
- Operating more than one battery in a stack can cause violation of the specified discharge limit. While some batteries in the stack do not discharge enough, others discharge almost entirely, which can potentially damage the battery. In a two-battery stack with a 10% discharge limit, the effective limit seems applied to the average state of charge across the stack rather than per device; in practice, the upper unit may stop at ~20% while the lower one discharges near 0%, which is undesirable and can accelerate wear. As a consequence, users report they switched to operating batteries separately instead of stacked.
- The NOAHs offer a
load_firstandbattery_firstmode. But those charging states cannot be controlled based on the charge-state, only based on a schedule. These modes can be set based on a fixed schedule, but not based on local conditions. This affects the efficiency of non-stacked setups with differently oriented panels (more on that later). - Zero-consumption (called “Nulleinspeisung” in Germany) is possible by pairing different power-measuring devices, such as a Shelly 3EM. However, readings are exchanged over the respective clouds, not locally between devices, so at higher latency and lower privacy.
Wrap-up
So, this is it, my fully local, plug-in solar plant. If you have questions or ideas for useful extensions, please don’t hesitate to contact me on GitHub or email. Special thanks to all the maintainers of the used software, without them, all this would not be possible!
Additional Details
Wow, still reading? Here are some additional details for the curious, you’ve earned them!
After all, we still have one question open to answer:
Why is it beneficial to switch the battery modes when panels have different orientations?
To understand why, we need to understand how the microinverter draws power from the batteries. Namely, they usually try to draw equal power from all MPPTs if possible. This behavior can cause discharging even when enough solar power is available.
To understand this, let’s look at an example with two batteries, each with two panels connected. The panels connected to battery 1 face east, while the panels connected to battery 2 face south.
The east-facing panels connected to the first battery deliver their peak power during the morning, with surplus energy being stored in battery 1. South-facing panels connected to the second battery do the same, but later during the second half of the day.
Let’s say now 14 o’clock and our home currently requires 500 W.
Battery 1 has panels pointed to the east, is fully charged, and the panels can still deliver 100 W of power.
Battery 2 has panels pointing to the south, is also fully charged, and the panels can still deliver 700 W of power.
Both batteries are in load_first with 800 W, forwarding their solar surplus because they are fully charged.
The microinverter tries to balance the power to draw equally from all inputs, so it tries to draw 250 W from both batteries.
Because Battery 1 has only 100 W of solar surplus, it discharges the battery with 150 W (excluding conversion losses).
Battery 2 could offer up to 700 W, but only 250 W are drawn.
Therefore, solar power is wasted even though enough is available.
The issue results from how the batteries emulate panels and advertise available power to the microinverter’s MPPTs.
To circumvent this behavior the battery controller in ctrl-solar implements a charge-state-based mode switching.
Once fully charged, it puts battery 1 into battery_first mode which prevents discharge and only forwards available solar power, while battery 2 remains in load_first.
Once the daily production cycle ends, the controller switches the battery back to load_first, such that both batteries can discharge to the specified threshold.