Introduction
After a period with a fairly attractive flat rate for selling energy, I had to switch to spot prices. This introduced a need to squeeze as much as possible from the setup to increase the return on investment (ROI).
As an owner of Wattsonic gen3, it’s prepared with this inverter in mind, though it can be easily reconfigured for other devices (see The Code section).
There are several mature yet complex solutions covering PV operation optimization, such as EVCC or PredBat. They are definitely worth checking out. My intention was to create something simple for a simple use case (and deliver fast). I also wanted to gain first-hand experience with building Home Assistant automation.
Some bits of the code as well as text polishing have been done with the help of AI.
It’s important to be aware that repeated charging and discharging of the battery might influence its wear.
The Automation Concept
The main idea is based on the observed evolution of spot prices during each day.
It usually features two peaks - morning and evening - and a trough around midday. The peaks - typically short - are a good opportunity to sell excess energy accumulated in the battery at the best prices. Recharging is shifted to the cheapest hours, allowing you to take advantage of dropping - but still good - prices to sell more of the generated PV energy.
To avoid battery energy shortages, it uses PV forecasts to support the decision logic.
The observations introduced above lead to a set of general automation rules:
- Sell energy stored in batteries during peak price periods
- Delay battery charging during morning hours to keep selling PV production at good prices
- Charge the battery during the cheapest price window
- Discharge only if enough PV energy is forecasted
- Maintain a minimum energy level in the battery to avoid buying energy for household operations
This translates into four phases.
Discharge (morning and evening)
While it could be limited to a single discharge, splitting it into two has benefits:
- Prices during evening hours are usually slightly higher
- Due to the narrowness of peaks, it’s better to distribute energy across both
- Splitting provides better margin management
- Morning PV forecast allows a last-minute decision to skip the morning discharge
Discharging must not result in an energy deficit later. Therefore, no discharge is triggered if insufficient PV energy is forecasted. At the same time, minimum battery levels must be maintained. The remaining energy must cover the period where the charge is delayed.
The configuration of edge conditions may differ between installations. It should be set with a safe margin to avoid buying energy.
To sell stored energy, the inverter is switched to a mode that exports battery energy to the grid. Depending on the inverter model and/or firmware version, such a mode may be available as a main operating mode or as a scheduled mode (Time of Use, ToU). This implementation uses a scheduled mode compatible with Wattsonic Gen3 firmware v2.
Watch the discharge rate. Discharging too quickly can increase battery wear. A recommended range is 0.3–0.7C. Also, ensure you do not exceed any applicable grid injection limits.
Charge Delay
This is the period between the morning discharge and the cheapest charging window.
During this time:
- The system should behave similarly to General mode, but
- PV production should prioritize export to the grid over charging the battery
This period includes several safeguards:
- It is activated only if it follows the morning discharge. This is based on the assumption that if discharge was skipped, there likely isn’t enough PV energy available.
- During this period, the battery still supplies energy to the household if PV does not cover demand. If battery SOC drops to the predefined limit, the mode is interrupted, and the inverter returns to normal operation, prioritizing battery charging from PV.
- If spot prices drop below a certain limit, selling no longer makes sense. Instead of suppressing PV production, it is better to use it to charge the battery.
This inverter operation is often referred to as Feed-in Mode. It prioritizes exporting PV energy to the grid instead of charging the battery. If this mode is unavailable, limiting the charging current can achieve a similar effect - although Feed-in Mode will still allow battery charging when PV production exceeds export limits. Wattsonic Gen3 with firmware 2.x does not offer Feed-in Mode; It was introduced in later versions.
Cheapest Charge
This phase turns inverter into general mode, thus it’s nothing special except it finishes the Charge Delay. Potentially we want to delay cheapest charge as much as we can, but it’s more imporant to to ensure enough time to fully charge the battery. Charging time depends mainly on weather conditions and may also be influenced by household usage.
Here are a few parameters that help calculate the required charging window:
- Solcast Balance – Solcast provides solar forecasts using three percentiles: 10%, 50%, and 90%. The balance parameter adjusts the forecast toward a more pessimistic outcome (negative values) or a more optimistic one (positive values). A value of 0% corresponds to the 50th percentile.
- Charging End Time – If the cheapest hours occur later than usual and/or poor weather requires more charging time, the battery might not charge sufficiently. This setting mitigates that by shifting the charging window earlier
- Charge Overhead – Normally, demand is calculated based on battery size, the remaining SOC, and forecasted PV energy capped by maximum charging velocity. It does not take a household consumption or other losses into account. This parameter increases the charging energy demand by a given factor, widening the charging window.
On top of that, if unselable prices extend the cheapest time span, the phase will start as early as possible.
Charging during the cheapest hours requires no special inverter mode - just the standard “General” mode.
The Package
For easier deployment (and no better option) the entire solution is implemented as a Home Assistant package:
Proxy Sensors These act as an API layer over third-party sensors (inverter state, solar forecast, prices), decoupling your code from their data formats. If an integration changes, only the proxy sensors need updating.
On top of that, the proxy sensors preprocess the data, making it easier to use in subsequent logic and reducing resource consumption.
Script Like proxy sensors, the script.pv_ctrl_inverter acts as an abstraction layer, this time for controlling the inverter. It is a single, parameterized script implementing inverter-specific commands.
TimeWindow Sensors These template sensors calculate the start and end of charging and discharging periods for the current day:
List of template sensors
sensor.pv_ctrl_most_expensive_hours_morning- morning discharge window.sensor.pv_ctrl_most_expensive_hours_afternoon– evening discharge windowsensor.pv_ctrl_cheapest_hours– cheapest charging window
They also store additional data in attributes.data, used by automation and by the dashboard.
Automation State and Config Entities
Both are implemented as input entities, surviving Home Assistant restarts. All of them are exposed on GUI.
List of input entities
| Entity | Description |
|---|---|
input_boolean.pv_ctrl_edit_mode |
Used for dashboard only, preventing accidental changes to the settings. It’s especially important for mobile views, where current HA UI makes an accidental change of parameters more then likely |
input_boolean.pv_ctrl_debug |
Toggles recording the debug informations to the Home Assistant log. Automation has to be in Active or Dry Run mode |
input_select.pv_ctrl_mode |
The defaul valuse (after the first init) is Disabled, preventing the automation to start witohut configuring and testing. It provides an option to run the automation either for real or in testing mode (Dry Run). The Dry-run does everything the Active mode does without calling Inverter for changing modes. When Disabled, the automation internally does nothing, though, like template sensors, it still collects data. |
input_select.pv_ctrl_phase |
Represents the current automation phase (under normal circumstances not intended for manual editing). Possible values are General, Morning Discharge, Charge Delay, Cheapest Charge, Evening Discharge. |
input_number.pv_ctrl_min_suncast_current_day |
Minimum forecasted energy for today; required for the morning discharge |
input_number.pv_ctrl_min_suncast_next_day |
Minimum forecasted energy for tomorrow; required for the evening discharge |
input_number.pv_ctrl_soc_limit_morning |
SOC limit for morning discharge |
input_number.pv_ctrl_soc_limit_evening |
SOC limit for evening discharge |
input_number.pv_ctrl_min_export_price |
Maximum energy price (per kWh), that prevents exporting energy (e.g., 0.25 CZK). |
input_number.pv_ctrl_charge_velocity |
Maximum charging power, the velocity the battery can be charged with. Used to cap PV energy provided by Solcast |
input_number.pv_ctrl_discharge_velocity |
Maximum discharge power, used to calculate a time needed to discharge battery to requested SOC |
input_number.pv_ctrl_battery_capacity |
Used in calculation of 1% of SOC |
input_number.pv_ctrl_solcast_forecast_balance |
Allows setting a balance between 10%, 50%, 90% percentile suncast prediction |
input_datetime.pv_ctrl_charge_delay_time_limit |
Limits predicted end time of cheapest charge time window. Might be helpful if cheapest hours (occasionally) starts late afternoon, but you don’t want to delay charging so much |
The Automation Finally, automation.pv_ctrl_executor is the core component that makes decisions based on its current state and inputs from sensors.
Home Assistant enables new automations immediately after they are added. Don’t worry - this one remains inactive until
pv_ctrl_modeis set by the user.
The code
The source code is GitHub:
If you are not familiar with HA packages, see the documentation.
Requirements
- Wattsonic gen3 integration by GiZMoSK (GitHub, HA forum)
- Solcast (GitHub, HA forum)
- CZ Spot prices (GitHub)
- Dashboard:
custom:apex-chartsfor graphscustom:button-cardfor controlscustom:restriction-cardfor locking UI elementscardmod/uixintegration
Adjusting for different integrations
It’s possible and requires:
- Adjusting the script, implementing inverter-specific commands (see below)
- Adjusting proxy sensors to provide expected data to automations (see below)
- Changing monetary units, since original code uses CZK.
- Adjust dashboard, since it uses some unproxied entities directly
Technical details
It is important to maintain consistent unit magnitudes for all sensors and inputs (kW / kWh).
The Script
Example:
action: script.pv_ctrl_inverter
data:
mode: general
The modes listed below represent automation modes rather than inverter modes. For instance, the general mode not only switches to the general inverter setting, but also resets other settings.
general– Resets inverter to general mode, including restoring unlimited battery chargingdischarge_grid– Enables discharge to the grid (implemented using Wattsonic scheduling)feedin– Prefers exporting energy to the grid instead of charging the battery (currently achieved by limiting charging current)charge_disabled– Sets charging current to zero (unused)charge_enabled– Sets charging current to maximum (unused)injection_enabled– Enables grid exportinjection_disabled– Disables grid export
The last two operations are used by a Prevent Export On Negative Price automation that disables injection to the grid when the price is below the configured threshold.
Proxy sensors
These are set of template sensors that prepare and optimize data for the rest of implementation.
sensor.pv_ctrl_battery_soc
Provides SOC of PV system battery. Valid values are from range 0-100 and represents percentage of battery charge.
sensor.pv_ctrl_solar_forecast_today and sensor.pv_ctrl_solar_forecast_tomorrow
Both provide forecasted energy (in kWh). Their state carry sum of energy forecasted for the day. In addition, it provides following attributes:
-
period- length of period. For Solcast it’s00:30:00(30 min) -
data- contains array of forecasted power and energy for intervals. The format is:
[{time: <time>,
energy: <energy>,
power_10: <power_10>,
power_50: <power_50>,
power_90: <power_90>,
power_balanced: <power_balanced>},
{...},
...
]
The power_XX represents respective percentiles provided by Solcast; The power_balanced is the result of balancing between pesimistic and optimistic values.
The energy carries a value of energy predicted for the period (30 min) derrived from the balanced power.
sensor.pv_ctrl_spot_electricity_prices
The state carries current price (for kWh), while attributes privide detailed data:
period- length of period00:15:00(15 min)data- contains array of prices in format:
[{time: <time>,
price: <val1>},
{...},
...
]


