feat: add optional heating device model (leaky thermal store)#85
Conversation
Loadpoints modeled as batteries can only be filled with energy. Heating devices don't fit that pattern: they leak, losing energy proportionally to their temperature. This adds an optional per-device thermal model: temp[t] = temp[t-1] + alpha*temp[t-1] + beta*h[t] + gamma with alpha (leak rate), beta (°C per Wh) and gamma (ambient drift) fitted by least squares from 15-minute history of temperatures and consumed energy (fit_heating_model). The MILP extension is pure LP (no new binaries): heat input joins the energy balance as consumption, and a soft comfort band [temp_min, temp_max] is penalized like battery SOC violations. The price signal then banks heat in cheap slots and coasts through expensive ones. API: optional `heating` list (band, c_max, history_temp, history_energy) on the request; per-device heating_energy and temperature series in the response. Documented in openapi.yaml. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/cc @daniel309 just for sake of experiment. This would replace part of the solution you've put into Go code. I'm undecided where the better place is? |
|
Another idea: we could split the learning algorithm for the historic data to determine the parameters from actually running the optimizer. Should we just make this a different endpoint? |
POST /optimize/heating-parameters takes the 15-minute history (temperatures + consumed energy) and returns the fitted leaky thermal model coefficients (alpha, beta, gamma). The same fit runs implicitly inside the charge-schedule optimization; the endpoint exposes it for inspection and validation. Also fixes the BadRequest errorhandler to return the JSON body for message-only api.abort(400, msg) calls instead of re-raising into a non-JSON response. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Added in 493fe81: |
The JSON-body-on-abort fix is independent of the heating model and affects existing endpoints on main; it now lives in its own PR. The short-history rejection test only asserts the 400 status until #86 lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
interesting approach @andig. I tried to understand what this is doing, let me know if this is correct
The optimizers job is to decide when to store energy given prices etc (as usual, just like charging home battery or car) and to stay within the temp_min and temp_max target parameters. If this is correct, then I think you have a nice model for treating a warmwater tank as energy storage and optimize "loading" this storage just like a battery. I am not sure if this is very useful though, because
Now, I know that there also is a SGReady "boost" for room heating water. The idea is to use the building itself as energy storage (concrete floors), and to heat floors a bit higher (1, max 2 degrees more. cant do more as it will become uncomfortable otherwise). I can tell you from experience that neither boost option makes a lot of sense and most people that have experimented with it eventually turned this off again (including myself). Reasons are that higher temperatures in the water heater cause increased wear on a heatpump compressor, and that using concrete as storage just causes your heatpump to turn off (after boost flag is removed). And then on again some time later. This again is bad for the heatpump, as you are forcing on/off mode instead of nicely modulating through the entire day. For room heating especially, I dont think this PR makes sense. Room heating energy consumption cant be controlled or "planned" by an optimizer. You cant "shift" energy consumption around (i.e. turn on heater a few hours a day only) as this usually comes with comfort loss (humans detect very small changes in room temperature: 20C feels "cold", 22C feels "hot". Plus, room heaters (especially heatpumps, but even oil or gas heaters) need to run in long intervals for longevity/durability reasons. You cant cycle them on and off a lot, this will cause damage to their compressors. A room heating's energy demand is in direct relationship with outdoor temperature. Future outdoor temp by far is the best predictor for future room heating energy consumption. The optimizer relies on future household consumption to create good schedules for charging (home and car) batteries. my verdict: I see big value in predicting future loads of a heater to enable the optimizer to make better decisions for charging. I dont see value in trying to use a warmwater tank or concrete floors as "battery" and plan their charging via optimizer. Sorry for the wall of text, I hope this all makes sense. |
|
edit: this PR may make sense for "heating rods", i.e. electric warm water heaters. these dont suffer from on/off cycling or increased wear if target water temperatures are higher. not sure how much value though, as these are modelled as "chargers" today. Their primary purpose is to use pv surplus, and often their priority is such that they should only be used when all other batteries are full already and there is still pv surplus available. |
|
@daniel309 thanks for the feedback! I've cooked up this PR as a small experiment, not necessarily as "the final solution" for what ever ;). I have specifically done that with evcc-io/evcc#28232 in mind. That said, the model isn't supposed to target any specific device type in terms of controlling it. The only goal (and maybe the model doesn't do that yet) is identifying the temperature-adjusted energy demand and adjusting the baseline demand profile accordingly (493fe81). We'll still need to decide, which parts should be part of the model and what should be part of the calling Go side. |
I see. If the intention is purely forecast, I think its required to treat these 2 cases: warmwater and room heating separately and with different forecast models. Your approach above could be a solution for warmwater. but maybe looking into timeseries models (to predict when people shower) would be worth looking into (google timeseriesfm or tranditional methods). As the problem here is less to predict the steady loss of temperature (your gamma) but rather when excessive energy is used for shower/bathtub, etc. This often is periodic behavior (e.g. period of a day). Also, most heatings have configuration set by users to account for that periodic behavior (schedules for "zirkulationspumpe", block time (night) for when to not re-heat despite minimum reached, etc) which amplifies and solidifies the repeating power consumption. So the job would be to discover this periodic behavior by observing consumption and creating a predictor from that. However, I dont think it will work for house heating at all. Energy demand here is largely dependent on outdoor temperature (plus dampening to account for heating loss through insulation, but thats a minor factor). The best predictor for outdoor temparature is a weather forecast. |
Why? With lower outside temp, the HP will require more power for room heating, too?
Exactly! |
|
Ahh, now I see what you mean: https://github.com/evcc-io/optimizer/pull/85/changes#diff-d910ba2ef878f7db0223a966b81c8b3f3b65027bb39e4431bb05140171eece39R523. The model takes the wrong temperature. This should be the forecasted outside temp, not the target thermal store temp. |
|
Updated the approach based on further discussion: the heating model no longer correlates history against the device's own storage/room temperature, and it's no longer schedulable by the optimizer. New design:
All tests updated and passing (20/20), ruff clean. |
|
yes, this is what I meant. I dont fully understand the approach though. can you explain a bit?
|
Fix #48
Motivation
The MILP treats loadpoints as vehicles with batteries — they can only be filled with energy. Heating devices (hot water tanks, buffer storage) don't fit that pattern: they are leaky and lose energy proportionally to their temperature. This PR adds an optional model for them.
Model
Per 15-minute slot, a heating device is a leaky thermal store:
alpha < 0— leak rate (losses grow with temperature)beta— temperature gain per consumed Whgamma— constant drift, absorbs ambient conditionsAll three coefficients are fitted by least squares (
fit_heating_model) from the device's history of temperatures and consumed energy in 15-minute slots — exactly the data evcc has available. Coefficients scale bydt/900for other slot lengths.MILP integration
Pure LP addition, no new binaries:
h[t](bounded byc_max) enters the energy balance as consumption[temp_min, temp_max], penalized like battery SOC violations (°C converted to Wh-equivalent viabeta)The price signal alone then produces the right behavior: bank heat while electricity is cheap, coast through expensive slots on stored heat.
API
heatinglist withtemp_min,temp_max,temp_initial,c_max,history_temp,history_energy(fit happens server-side; unusable history → 400)heating_energyandtemperatureseriesopenapi.yamlTests
All 13 existing golden cases unchanged; 17/17 tests pass, ruff clean.
Deliberately out of scope
c_min+ binary like batteries when hardware needs it)gammacarries historic ambient)solver/) mirror — batteries only for now🤖 Generated with Claude Code