How to estimate PV production using a simple lux sensor?
In this article, I'll share how I estimate the production of my solar panel system using a simple Lux/UV sensor from my Bresser 7-in-1 weather station, after losing direct access to the inverter data when I got a home battery system.
This was helpful for me to get back the solar production information that I had lost, and optimize my electricity usage again based on that - and while the home battery system (by Elisa) now does have an API, it seems to be a bit flaky, so I'm keeping the approximation as a backup.
I'll walk you through the background, the problem I faced, and the solution I implemented using Home Assistant and some clever calculations to get a pretty good estimate of my solar production based on the sensor data and sun position.
Background
As usual, little background (and ranting) to get us started on the right foot. I've been a pretty happy solar panel owner since 2019. I paid about 6 000 EUR for my 6 kWp system, that I got installed on the roof of my barn, including a 6 kW inverter. It's been producing almost 6000 kWh per year ever since, and while in Finland it produces absolutely nothing when I most need it (December-February), it's still been a great investment overall. For 9 months of the year, it covers some if not all of my electricity needs.
About 1.5 years ago I jumped on board the home battery bandwagon. A Finnish teleoperator, Elisa, offered a pretty attractively priced 28 kWh home battery system, and I figured that's going to make my solar panel system even more useful, as I can store the excess energy produced during the day and use it at night.
Coupled with Home Assistant optimizing the use of my electricity around the house pretty aggressively, I figured I'll get the cheap juice and can even store the extra solar in the battery to use during the night.
The reality has been a little bit more disappointing than that.
One of the key selling points of the home battery system ("Elisa Kotiakku") was its "Artificial Intelligence" (that's what an algorithm is called now), which would mean it learns from my usage patterns and is able to optimize electricity usage and storage in the battery.
And I'm sure it learns some patterns - but there's one pretty key pattern it does NOT learn: my house already optimizes electricity usage based on price!
What do I mean by that?
Well - the battery expects that we will always charge our cars at the same time. It thinks we will always use water heater at the same time. It likely thinks our floor heating is always on. It expects AC to always run on the same schedule. It expects us to turn the lights on and off based on the same pattern every single day. And it simply can't fathom that someone could even occasionally wash their clothes when it's cheap (the last thing is the only part that Home Assistant doesn't control in the house!)
And since this was a turnkey solution, there's absolutely nothing I can do about it.
Problem
That brings us to the problem at hand.
At the very basic level, the battery's algorithm DOES work. It mostly charges when it's cheap, it often discharges when it's expensive, and for the most part it uses excess solar production to charge itself, which is great.
But it can not learn the pattern of "there's about 18kW of flexible load in the house which will turn on and off based on multiple factors". So it will end up competing with other flexible loads in the house for cheap electricity, sometimes discharge only to charge a car battery even if electricity was borderline free, charge when elecricity costs 5c/kWh only to discharge 15 minutes later when electricity costs 2c/kWh, and so on.
To add insult to injury, the battery initially (for well over a year) took away any knowledge I had of my solar production, so that signal was removed from my other optimization workloads.
My old Fronius inverter had pretty good APIs that I used to optimize electricity usage based on solar production. Some of my automations ran on that information - for example, I had an automation that would turn on the water heater when there was excess solar production. And one that only enabled AC when there was excess solar production (we don't need AC for most of the year in Finland). And a bunch of others.
The infuriating part was that the inverter that Elisa is using actually DOES have pretty good APIs, and even a "guest" or "kiosk" mode that would allow me to pull the information and let Elisa be the owner of the system. But that was not an option for some reason. I had to choose between giving up all information about my solar production, or giving up the battery. I chose the former, but begrudgingly.
That was until earlier this year, when I got my Bresser 7-in-1 weather station, which includes a pretty good Lux/UV sensor. I figured that I could use the Lux/UV sensor to estimate the solar production, and use that information to optimize my electricity usage again.
The configuration experience of the Bresser was quite the nightmare - probably worth another blog post - but unlike the even-more-German TFA Dostmann weather station I had before, I was eventually able to get it working. The configuration experience of the TFA device was very German in all the wrong ways:
- A lot of steps
- Detailed documentation (only on paper)
- Most of it doesn't match the actual product
- No signing in (for privacy reasons of course), so no devices are persisted if the app has to be reset (which you have to do for any debugging reasons)
- No logging of any kind (for privacy reasons I assume)
- Failing sensors are automatically and brutally removed from the system (any joke I could make about this would be so on the nose that I won't even make it)
- Home Assistant sync was local, which is great, but since none of the sensors worked, it was pretty much useless (kinda like having heated seats in your BMW but due to a timeout the car can't check if you paid for the subscription so the heated seats don't work)
TFA Dostmann support were at least very helpful, though, and after about 2 weeks of reinstallations and resets they just told me to return the device, and get a new one.
Which I did, it just wasn't a TFA Dostmann device.
Anyway, Bresser was a can of worms too, but eventually I got it working, and it started pulling the data into Home Assistant.
And when it did, it was pretty outstanding, so of course I decided to use it to get back the Solar PV production information that I had lost when I got the battery.
Solution
So I got hacking. Or, well, GitHub copilot did.
- trigger:
- platform: state
entity_id:
- sensor.bresser_bresser_solar_radiation
- sun.sun
- platform: homeassistant
event: start
- platform: time_pattern
minutes: "/5"
sensor:
# Approximate PV production based on measured global horizontal irradiance,
# current sun position and a few fixed assumptions about the array.
- name: Approximate solar output
unique_id: approximate_solar_output
icon: mdi:solar-power-variant-outline
device_class: power
unit_of_measurement: "kW"
state_class: measurement
state: >
{% set irradiance = states('sensor.bresser_bresser_solar_radiation')|float(0) %}
{% set elevation = state_attr('sun.sun', 'elevation')|float(-90) %}
{% set azimuth = state_attr('sun.sun', 'azimuth')|float(0) %}
{% set bresser_temp_state = states('sensor.bresser_bresser_outdoor_temperature') %}
{% set bresser_temp = bresser_temp_state|float(0) %}
{% set fallback_temp = states('sensor.openweathermap_temperature')|float(0) %}
{% set ambient_air_temp = bresser_temp if bresser_temp_state not in ['unknown', 'unavailable', 'none', ''] else fallback_temp %}
{% set panel_kwp = 6 %}
{# Empirical system calibration: this SHOULD of course be under 1, but these panels produce way over their nameplate even years after installation; set to slightly above 1.0 to keep cold, clear peak conditions accurate. #}
{% set performance_ratio = 1.06 %}
{# Rough tilt estimate until the real mounting angle is known. #}
{% set panel_tilt_deg = 35 %}
{# Rough azimuth estimate for a South-South-East facing array. #}
{% set panel_azimuth_deg = 157.5 %}
{# Simplified diffuse-vs-direct irradiance split for cloudy Nordic conditions. #}
{% set diffuse_share = 0.35 %}
{# NOCT-style panel heating estimate: about 22 C rise per 1000 W/m2 on the panel plane. #}
{% set panel_heating_per_wm2 = 0.022 %}
{# Typical crystalline silicon power temperature coefficient. #}
{% set temp_coefficient_per_c = 0.0035 %}
{% set deg_to_rad = 0.017453292519943295 %}
{% if irradiance <= 0 or elevation <= 0 %}
0
{% else %}
{% set elevation_rad = elevation * deg_to_rad %}
{% set tilt_rad = panel_tilt_deg * deg_to_rad %}
{% set azimuth_delta_rad = ((azimuth - panel_azimuth_deg)|abs) * deg_to_rad %}
{% set cos_incidence =
(elevation_rad|sin * tilt_rad|cos)
+ (elevation_rad|cos * tilt_rad|sin * azimuth_delta_rad|cos)
%}
{% set horizontal_sun_component = [elevation_rad|sin, 0.17]|max %}
{% set direct_ratio = [cos_incidence / horizontal_sun_component, 0]|max %}
{% set sky_view_factor = (1 + tilt_rad|cos) / 2 %}
{% set poa_factor_raw = (diffuse_share * sky_view_factor) + ((1 - diffuse_share) * direct_ratio) %}
{% set poa_factor = [[poa_factor_raw, 0]|max, 1.35]|min %}
{% set poa_irradiance = irradiance * poa_factor %}
{% set estimated_cell_temp = ambient_air_temp + (poa_irradiance * panel_heating_per_wm2) %}
{% set temperature_factor = [1 - ((estimated_cell_temp - 25) * temp_coefficient_per_c), 0.88]|max %}
{{ (panel_kwp * performance_ratio * (irradiance / 1000) * poa_factor * temperature_factor)|round(2) }}
{% endif %}
attributes:
sampled_at: "{{ now().isoformat() }}"
source_sensor: sensor.bresser_bresser_solar_radiation
temperature_source_primary: sensor.bresser_bresser_outdoor_temperature
temperature_source_fallback: sensor.openweathermap_temperature
panel_peak_power_kwp: 6
panel_azimuth_degrees: 157.5
panel_tilt_degrees: 35
performance_ratio: 1.06
performance_ratio_note: Empirical calibration so cold, clear, well-aligned Finnish peak conditions can still land a bit over the 6 kWp nameplate after temperature derating.
panel_heating_per_wm2: 0.022
panel_heating_note: Simple NOCT-style cell-temperature estimate based on panel-plane irradiance.
temperature_coefficient_per_c: 0.0035
temperature_coefficient_note: About 0.35 percent power loss per degree C above the 25 C STC cell temperature.
I would've never created a template with this many variables myself; Copilot suggested the whole logic for figuring out the part of irradiance coming from diffuse vs direct sunlight, the panel heating estimate and the temperature derating, and all I had to do was to add some fixed assumptions about the array and a bit of empirical calibration to get it to match the actual production pretty well.
It has some magic numbers (like the floor for the "horizontal sun component" that is used to split the diffuse vs direct share), but since it works...
Yeah. It'll do for now.
Follow-up
Elisa launched their first Kotiakku API version about a month ago. And 2 weeks BEFORE they did, an enthusiast had already hacked together a Home Assistant integration for it.
Nice.
I've been pulling the data from their API for just enough time to validate that my approximation was actually quite good.
So next, I'll be flipping over from the approximation to using the actual data from the battery, and see how that goes.
The approximation stays as a backup - since it is so good, and (unlike Elisa's currently fairly flaky API) it is available all the time.
It is a local solution, after all, so it doesn't need to run to Chinese servers(*) to get the data. So even if Elisa's API is down, I can still optimize my electricity usage based on the approximation, and get pretty much the same result as with the real data.
Footnotes
* I have no idea where Elisa's API is actually hosted, but the inverter IS a Huawei, so I'll just assume that they're first sending all of the data to Chairman Xi, who then checks the receiver's social score before deciding whether to send the data back to Finland or not. I mean, it is 2026, after all.
- This is how my automation for turning on the lights in my kitchen pantry looks like. A roundtrip to China takes about a second, so the lights turn on with a pretty noticeable delay, but it is what it is.
** Here's the madlad with the Home Assistant integration for the Elisa Kotiakku: https://github.com/Jarauvi/elisa_kotiakku
PS. How do you like the footless cover image? Figured mentioning that in the FOOTNOTES would be pretty fitting!
I don't usually use AI to generate feet, so I was surprised to learn how Copilot was seemingly unable to create any for the person in the picture, even after yours truly asking multiple times with different wording.
Must be some sort of new content guideline by Microsoft - no feet pics for y'all today!
Oh well, that'll just drive more traffic to Grok...
Comments
No comments yet.