Ever wondered if a solar installation would pay off? I’ll show you how to find out with Python.
When we had ordered a new heat pump as replacement for the old gas boiler, we assumed that a solar installation would also be a good idea. Not only because we could make a little contribution to the rise of renewables, but with the additional electricity demand it was likely to pay off financially, too.
Of course, every installer will claim that their system will pay off in a few years and present a colourful brochure with glossy charts and excellent numbers. But as an economist, I have some distrust towards models and statistics presented by people who have an inherent interest in selling something.
So I decided to simulate the system myself with the help of the PVlib library. And I’ll explain how I did it, so you can do it too.
Step 1 – Estimating electricity demand
The easy part was the general electricity consumption, because I could simply load it from my Home Assistant instance, where it was collected in an influxDB database. I simply used the full data of a year and assumed it to be representative for the next years. The following chart shows the average power consumed throughout the day.

The next step was to estimate the electricity demand for the new heat pump. This can be done based on the same parameters also used by heating engineers to choose the right size: The “design outdoor temperature”, the “design heating load” and the “heating threshold”. The design outdoor temperature can be looked up in databases, for Germany for example at Bundesverband Wärmepumpe. The design heating load needs to be calculated based on the building envelope. There are many ways to do that. I used a proprietary online tool and entered the parameters for all of the perimeter walls and the roof, because I wanted to have accurate numbers for the heat pump. But there are also easier and free options. An estimation from your heating bill is possible as well. Finally, the heating threshold is the outdoor temperature above which heating is no longer needed, typically around 15°C.
Now, based on these parameters, the electricity demand for the heat pump can be calculated with the following simple formula:
P_heating = ((T_threshold - T_actual) / (T_threshold - T_design)) × P_design
For the actual outdoor temperature, you can use publicly available weather data. I found what I needed at Deutscher Wetterdienst, they offer a daily average temperature for each day of the last twenty years for many places in Germany.

For hot water, I simply used rule-of-thumb numbers based on the number of people living in the house. You can find such numbers in many places on the internet.
Overall, the annual demands are:
| Category | Heating | Hot Water | Household Electricity | Total |
|---|---|---|---|---|
| Yearly Demand (kWh) | 3,611 | 2,196 | 4,224 | 10,030 |
Step 2 – Solar power supply
The solar power generation can be calculated with the help of the PVlib library.
First, you need to load some weather data, define the components of the solar system, and finally run the simulation. Here a simplified version of the code is shown:
import pvlib
from pvlib.pvsystem import PVSystem, Array, FixedMount
from pvlib.location import Location
from pvlib.modelchain import ModelChain
module = pvlib.pvsystem.retrieve_sam("CECMod")["JA_Solar_JAM72S01_385_PR"]
inverter = pvlib.pvsystem.retrieve_sam("cecinverter")[
"Hoymiles_Converter_Technology_Co___Ltd___MI_1200T__240V_"
]
temperature_model_parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][
"open_rack_glass_glass"
]
latitude, longitude, name, altitude, timezone = (
50.941299,
6.958120,
"Home",
190,
"Etc/GMT-1",
)
location = Location(
latitude,
longitude,
name=name,
altitude=altitude,
tz=timezone,
)
weather = pvlib.iotools.get_pvgis_tmy(latitude, longitude)[0]
weather.index.name = "utc_time"
# Define PV system
array = [
Array(
mount=FixedMount(
surface_tilt=44,
surface_azimuth=210,
),
module_parameters=module,
temperature_model_parameters=temperature_model_parameters,
modules_per_string=1,
)
]
# Run simulation
w_peak = sum(module.STC * a.strings * a.modules_per_string for a in array)
inverter = {"pdc0": w_peak, "eta_inv": 0.96}
system = PVSystem(arrays=array, inverter_parameters=inverter)
mc = ModelChain(system, location, aoi_model="physical", spectral_model="no_loss")
mc.run_model(weather)
annual_energy = mc.results.ac.sum()
This gives a solid estimate of the production. The following chart shows the simulated production of a 10 kWp system on our southwest roof for a typical year:

This simulation can be repeated for different system sizes and orientations. For the example system it would be 10,702 kWh per year.
Step 3 – Matching demand and production
The simple example system with 10 kWp of solar modules would theoretically cover the full demand. The problem is timing, though. There will be an excess of production on very sunny days, while there is still demand when it is dark, especially in the winter months because of the heating demand.
So, for a realistic understanding, it is necessary to look at the production and consumption on a detailed hourly basis throughout a whole year. An additional home battery can help to shift the production to the right time, but it increases the investment cost. Also, it isn’t perfectly efficient; there are energy losses with every charge and discharge.
It is simple math to calculate the self-consumption and the grid feed-in from the simulation results. For every moment, the following energy flows can be calculated:
- Direct consumption = min(demand, production)
- Battery charge = max(direct consumption - production, 0)
- Battery discharge = max(production - direct consumption, battery level)
- Grid feed-in = max(production - consumption - battery charge, 0)
- Grid import = max(consumption - production - battery discharge, 0)
- New battery level = battery level + battery charge - battery discharge
The following chart shows the result for a day in four different months for a solar system of 12 kWp on the roof, plus additional 2 kWp at the facade and a home battery of 10 kWh. So, this is a system which can distribute the power delivery much better than a single-orientation system without battery.

The monthly totals show even better how grid energy is needed only in the winter months, while from May to September the home is completely self-sufficient.

Step 4 – Return on investment
Based on investment costs, electricity price and feed-in payment, it is possible to calculate the return on investment. In finance mathematics, an investment is a cash flow stream starting with a negative value (the investment costs) and then positive values (the revenue).
While the investment costs here are obviously the price for the installation of the pv system, the revenue consists of two components:
- Self-consumption: Electricity produced by the pv system and used directly or through battery discharge. The price of this power is what you pay for your electricity coming from the grid. This varies over time, but I assumed an average price of 0.32 €/kWh.
- Feed-in: The electricity that is sold to the grid. In Germany, there’s usually a fixed price for feed-in, regulated in the “Erneuerbare Energien Gesetz” (EEG). In my case, this is 0.081 €/kWh.
To evaluate whether the investment is profitable, people often use the amortization period, which is the time it takes to recover the investment costs. This isn’t a sufficient metric here, because I wanted to compare offers with different investment costs and system sizes, which creates a different value stream. It may be better to buy a more expensive system and invest more money, because the absolute value of money returned is much higher, even though the amortization period may be longer. In short, the amortization period is not useful to compare investments of different sizes or durations.
Instead, I used the net present value (NPV) to compare the different offers. This is a metric that takes into account the time value of money and the discount rate. One simply needs to calculate the present value of each Euro spent or earned over time, by using the discount rate. The formula is:
NPV = Σ (Cash Flow / (1 + Discount Rate)^t)
Where t is the time period.
Comparing real offers
Up to now everything was simulated and based on assumption. Now we can come to the part where we use real values from the quotes sent by pv installation companies. As you can see, they vary completely in sizes and investment costs. Note that these are real offers, but I anonymized the companies.
| Metric | Large Power Company | Cheap Startup | Local Company | Recommended Expert (no Battery) | Recommended Expert (Battery) |
|---|---|---|---|---|---|
| kW SW | 12.9 | 13.8 | 12.8 | 14.0 | 14.0 |
| Battery (kWh) | 3.5 | 9.6 | 7.8 | 0.0 | 9.6 |
| Investment (€) | 23,661 | 22,524 | 25,916 | 19,360 | 25,240 |
| NPV (€) | 12,712 | 18,222 | 12,652 | 14,659 | 15,938 |
| Production (kWh) | 13,806 | 14,727 | 13,699 | 15,016 | 15,016 |
| Self-Consumption (kWh) | 5,650 | 6,580 | 6,308 | 4,576 | 6,605 |
| Feed-in (kWh) | 8,011 | 7,987 | 7,244 | 10,290 | 8,247 |
| Grid Import (kWh) | 4,188 | 3,258 | 3,530 | 5,262 | 3,233 |
| Self-Sufficiency (%) | 57 | 67 | 64 | 47 | 67 |
| Annual Revenue (€) | 2,457.84 | 2,753.35 | 2,606.17 | 2,298.77 | 2,782.54 |
Now, this would probably be enough for a controller to make a decision. But as I’m not a controller, but rather work in the world of data science, I need some colorful charts to show the results. And I find it makes the differences even clearer.

This gives a good picture, not only of the net present value, but also of some other aspects, such as the investment costs which need to be paid up front or the self-sufficiency which has environmental benefits.
What you don’t see here are qualitative aspects. For example, the company called “Cheap startup” in this list raised concerns about the quality of the installation in the negotiation process.
Summary
So, what did I learn from this exercise?
- A 14 kWp system on our southwest roof covers the annual demand on paper, but it would be better to have multiple module orientations and a battery.
- The battery makes a bigger difference than I expected. It pushed self-sufficiency from 47% to 67% – that’s 20 percentage points for about 6,000 € extra investment.
- Adding more kWp beyond a certain point has diminishing returns. The extra production mostly goes to the grid at a much lower price than what you save by self-consumption.
- The cheapest offer isn’t always the best, and the most expensive one isn’t either.
If you want to run a similar analysis for your own home, here’s a minimal recipe:
- Consumption data: Export from your smart meter, utility portal, or Home Assistant.
- Climate data: Historical temperature data from DWD or a similar national weather service.
- PV climate data: Use PVGIS directly or via PVlib’s
get_pvgis_tmy()function. - Simulation code: The PVlib library does the heavy lifting.
You can find the example code of this blog post on GitHub: https://github.com/chr1st1ank/blog-assets/tree/main/code/pv-simulation.
A word on limitations: This model is simplified. It doesn’t account for dynamic electricity prices, detailed grid tariffs, or the fact that heat pump efficiency varies with outdoor temperature (I used a linear approximation). Battery degradation over time is also ignored. For an investment decision, it’s good enough for me. But if you wanted to be on the safe side, you should perform a sensitivity analysis of the parameters.
python smarthome