Skip to content

Tariffs: add temperature type and OpenMeteo#27780

Merged
naltatis merged 53 commits into
evcc-io:masterfrom
daniel309:feature/weather-dependent-household-load
Jul 17, 2026
Merged

Tariffs: add temperature type and OpenMeteo#27780
naltatis merged 53 commits into
evcc-io:masterfrom
daniel309:feature/weather-dependent-household-load

Conversation

@daniel309

@daniel309 daniel309 commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Update: restricted this PR to the new temperature tariff. The household load profile adjustments based on temperature will be handled separately in PR #28232

TODO

--

leaving the full PR description below for history and context of the discussion.

Problem

The evopt energy optimizer uses a household load profile (gt) to predict how much energy the home will consume in each future 15-minute slot. This profile is currently computed as a 30-day historical average — the same flat pattern is repeated regardless of weather conditions.

This is a significant blind spot: heating and cooling loads are strongly temperature-dependent. On a cold winter day, a home with a heat pump or electric heating can consume 30–80% more energy than on a mild day. When the optimizer doesn't know this, it:

  • Under-estimates household demand on cold days → schedules EV charging at times when grid power is actually needed for heating
  • Over-estimates household demand on warm days → unnecessarily avoids cheap/green charging windows
  • Misses opportunities to pre-charge the battery before a cold night when heating demand will spike

Solution

This PR adds two things:

  1. A new open-meteo weather tariff that fetches 10 days of hourly outdoor temperatures from Open-Meteo (free, no API key, no account required) in a single call: 7 days of past actuals + 3 days of forecast. The data is exposed as api.Rates where Rate.Value is the temperature in °C.

  2. Temperature correction of the household load profile in homeProfile(). The historical lookback is reduced from 30 days to 7 days as outdoor temperatures can significantly change within such a long period of time. Household life usually happens within 1 week rhythms so this seemed a better default. Before the profile is passed to the optimizer, each slot's load is adjusted based on how the forecast temperature at that slot deviates from the 7-day historical average at the same hour-of-day:

load[i] = load_avg[i] × (1 + heatingCoefficient × (T_past_avg[h] − T_forecast[i]))

where:

  • T_past_avg[h] = average temperature at hour-of-day h over the past 7 days
  • T_forecast[i] = forecast temperature at the wall-clock time of slot i
  • heatingCoefficient = fractional load sensitivity per °C (default 0.05 = 5%/°C)

The correction is gated on the 24h average actual temperature of the past 24 hours. If that average is at or above heatingThreshold (default 12°C), heating is considered off and no correction is applied to any slot. Using past actuals (not forecast) for the gate is more reliable: if it was cold yesterday, heating is likely still running today.

Example: With default settings (heatingThreshold=12°C, heatingCoefficient=0.05) and a 7-day historical average of 8°C at a given hour:

  • Forecast 8°C → no correction (delta = 0, factor = 1.0)
  • Forecast 3°C → +25% load (delta = +5°C, factor = 1.25)
  • Forecast −2°C → +50% load (delta = +10°C, factor = 1.50)
  • Forecast 13°C → −25% load (delta = −5°C, factor = 0.75)

The correction is relative to the historical average that produced the baseline load, so daily patterns (e.g. higher consumption in the evening) are preserved — only the magnitude is adjusted. This is consistent with standard heating degree-day methodology.

If no weather tariff is configured, applyTemperatureCorrection() is a no-op and behaviour is identical to before.

Files Changed

File Change
tariff/weather.go NewWeather tariff type polling Open-Meteo hourly (past_days=7&forecast_days=3)
api/tariff.go Added TariffTypeWeather and TariffUsageWeather constants
api/globalconfig/types.go Added Weather config.Typed to Tariffs struct
tariff/tariffs.go Added Weather api.Tariff field; Get() handles TariffUsageWeather
cmd/setup.go Wires up weather tariff in configureTariffs()
core/site.go Added HeatingThreshold and HeatingCoefficient config fields
core/site_optimizer.go Reduced lookback to 7 days; homeProfile() calls new applyTemperatureCorrection() and nearestRate() helper

Configuration

The feature is opt-in — no changes needed for existing setups.

To enable, add to evcc.yaml:

tariffs:
  # ... existing tariff config ...
  weather:
    type: open-meteo
    latitude: 48.137   # your location
    longitude: 11.576

site:
  title: My Home
  meters:
    grid: grid0
    pv: pv0
  # Optional tuning (these are the defaults):
  heatingThreshold: 12.0    # °C — 24h avg of past-day temperatures above which all corrections are disabled
  heatingCoefficient: 0.05  # fraction — load changes by this fraction per °C delta from historical average

Tuning guidance

  • heatingThreshold: Set to the 24h average outdoor temperature above which your heating system is fully off. Typical values: 10–15°C depending on building insulation and personal preference. The default of 12°C suits an average insulated house.
  • heatingCoefficient: Represents how sensitive your home's energy consumption is to temperature, as a fraction of the average load per °C. A well-insulated passive house might use 0.02; a poorly insulated older building might use 0.08 or higher. You can estimate it by comparing your historical energy consumption on cold vs. mild days.

Notes

  • Open-Meteo is fetched once per hour with exponential backoff on failure. If the forecast is unavailable, the optimizer falls back to the unmodified historical profile (safe degradation).
  • The correction only applies to future slots in the optimizer horizon — historical data in the database is not modified.
  • Past temperatures (7 days) and future forecast (3 days) are fetched in a single Open-Meteo API call using past_days=7&forecast_days=3&timezone=UTC.
  • The 24h average gate (heatingThreshold) is based on past 24h actual temperatures (not forecast), preventing the correction from running in summer. Using actuals is more reliable: if it was warm yesterday, heating is off today regardless of what the forecast says.
  • This does not model cooling loads (summer A/C). A coolingThreshold / coolingCoefficient could be added similarly in a follow-up.
  • The open-meteo tariff type name follows the existing tariff registry naming convention (provider name, lowercase).

TODO

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In applyTemperatureCorrection you do a nested loop over all forecast rates for every 15‑minute slot; since the rates are time-ordered, consider advancing an index or precomputing a per-slot slice to avoid O(n*m) behavior on longer horizons.
  • Using 0 as a sentinel for HeatingThreshold and HeatingCoefficient means users cannot explicitly configure these to zero; if that should be allowed, consider using pointers or a separate *Enabled flag instead of overloading 0 as "use defaults".
  • The generated enum files (tarifftype_enumer.go, tariffusage_enumer.go) have been hand-edited (including the // Made with Bob comments); this can easily diverge from the code generator, so it would be safer to adjust the source for generation and regenerate these instead of modifying them manually.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `applyTemperatureCorrection` you do a nested loop over all forecast rates for every 15‑minute slot; since the rates are time-ordered, consider advancing an index or precomputing a per-slot slice to avoid O(n*m) behavior on longer horizons.
- Using `0` as a sentinel for `HeatingThreshold` and `HeatingCoefficient` means users cannot explicitly configure these to zero; if that should be allowed, consider using pointers or a separate `*Enabled` flag instead of overloading `0` as "use defaults".
- The generated enum files (`tarifftype_enumer.go`, `tariffusage_enumer.go`) have been hand-edited (including the `// Made with Bob` comments); this can easily diverge from the code generator, so it would be safer to adjust the source for generation and regenerate these instead of modifying them manually.

## Individual Comments

### Comment 1
<location path="tariff/weather.go" line_range="94-99" />
<code_context>
+		}
+
+		data := make(api.Rates, 0, len(res.Hourly.Time))
+		for i, tsStr := range res.Hourly.Time {
+			if i >= len(res.Hourly.Temperature2m) {
+				break
+			}
+
+			// Open-Meteo returns ISO 8601 strings like "2024-01-15T14:00"
+			ts, err := time.ParseInLocation("2006-01-02T15:04", tsStr, time.Local)
+			if err != nil {
</code_context>
<issue_to_address>
**issue (bug_risk):** Parsing timestamps with time.Local is likely incorrect for Open-Meteo’s UTC defaults and may misalign temperatures with slots.

Open-Meteo timestamps are UTC by default unless a timezone is requested. Parsing them with `time.ParseInLocation(..., time.Local)` will reinterpret UTC timestamps as local time and shift all slots by the local offset. Please either: (a) request `&timezone=UTC` and parse with `time.UTC`, or (b) request `&timezone=auto` and parse using that timezone, so `Rate.Start`/`End` stay aligned with the forecast data.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tariff/weather.go Outdated
@andig

andig commented Mar 1, 2026

Copy link
Copy Markdown
Member

You've hit an actual problem spot-on, but I'm not sure that the solution is the right one, conceptually and technically:
you're assuming that household energy depends on heating demand and imply a heatpump- what if you don't have one?

Imho the "right" approach here would be to measure and collect heating demands separately and have a different "heating" optimizer model for those.

/cc @ekkea @t0mas

@andig andig added the heating Heating label Mar 1, 2026
@andig
andig marked this pull request as draft March 1, 2026 10:18
@andig

andig commented Mar 1, 2026

Copy link
Copy Markdown
Member

The weather tariff is a nice touch (really: it's temperature, not weather?) and might be a separate PR on its own that will become useful sooner or later.

@daniel309

daniel309 commented Mar 1, 2026

Copy link
Copy Markdown
Contributor Author

you're assuming that household energy depends on heating demand and imply a heatpump- what if you don't have one?

If you dont have a heatpump (or more general, heating that uses electricity), you dont configure/add the heatingCoefficient or heatingThreshold parameters to evcc.yaml (under site).

Imho the "right" approach here would be to measure and collect heating demands separately and have a different "heating" optimizer model for those.

Yes, that would be most clean. It would require the heating meter and a bunch other things (separate optimizer model) likely making config and implementation more complicated. And I am really wondering if all that additional code, effort and complication (to the user) is really worth it.

Assume a single-family home household like ours: when heating (heatpump in our case) is active, daily energy consumption is dominated by that one energy consumer. In numbers:

  • Household consumption (appliances, cooking, lights, ...): 5-9 kwh/day, through the entire year.
  • Car: 5-7 kwh/day (we drive less than 50km/day on avg)
  • Heatpump: 17 kwh/day (mildest day this winter) to 45 kwh (coldest day this winter, Jan 7th 2026 for us)

The weather tariff is a nice touch (really: it's temperature, not weather?) and might be a separate PR on its own that will become useful sooner or later.

Happy to split this out (and rename to temperature) or do it all at once together with the heating code that uses it. Let me know how to proceed.

@andig

andig commented Mar 1, 2026

Copy link
Copy Markdown
Member

Yes, that would be most clean.

Yes :)

It would require the heating meter

Yes. And we'd need to figure out how meters (not even necessarily) chargers would be handed to the optimizer. Collecting arbitrary metrics is already "almost" done: #23185 (btw, would love help with this PR).

and a bunch other things (separate optimizer model)

That's already in the backlog, examples exist.

likely making config and implementation more complicated.

Happy to discuss how to do this, maybe in separate issue. Or join the #optimizer group on Slack.

@daniel309

daniel309 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

hmm, ok, not sure how to proceed.

It sounded like you would like to break this down into re-useable parts, and maybe get some use early to test how well it works now that heating is still on for a few weeks. Otherwise next opportunity is October 2026.

so, I could split this work into 3 pieces:

  1. introduce "temperature" tariff as per above. Unrelated to heating and no users/exploiters (yet). Just to have a clean and separate PR.

  2. then, on top, a second PR that introduces temperature-based correction of the household profile (func (site *Site) applyTemperatureCorrection). This would introduce the new site config params (heatingThreshold, heatingCoefficient), use the temperature tariff and apply it to the general household consumption profile (gt). this would give immediate value to heating users and give early test results, although not perfect as gt profile is mixing up heating with other consumers today.

it would also serve as a testbed for debugging etc.

  1. split out heating consumption (based on Collect 15min energy metrics #23185), apply the temperature correction only there and use a separate optimizer model/run. this is the "clean" solution above and the desired final state.

does that sound feasible?

(btw. not sure how much time I can dedicate to this, 3. for sure is not a quick change. I do think 1. and 2. are possible relatively short-term though).

@t0mas

t0mas commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

@andig instead of a separate optimizer, my first thought on this would be to make the household load prediction more similar to how we handle tariffs. That would add flexibility for users to configure it with just the "historic" strategy that we have today, or create a more complex calculation with temperature adjustment factors.

I guess some users would be able to remove the heating energy from their basic prediction (set a different meter?), make a separate time series just for heating, adjust that one based on temperature, and then combine those two time series to generate the input for the optimizer.

If you agree this would be helpful I could probably write that? Then combined with the weather tariff and adjustment strategy that @daniel309 made you have full flexibility. The only thing is... It won't be easy to make a visual UX for configuration of these kinds of structures.

@daniel309

daniel309 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

I guess some users would be able to remove the heating energy from their basic prediction (set a different meter?)

most heatpump "chargers" already provide energy and power, wouldnt that be sufficient to track heating consumption as a separate timeseries?

example:

@andig

andig commented Mar 2, 2026

Copy link
Copy Markdown
Member

I would really appreciate if we could move this brain storming out or PRs ;)

introduce "temperature" tariff as per above. Unrelated to heating and no users/exploiters (yet). Just to have a clean and separate PR.

Yes, that's absolutely obvious

most heatpump "chargers" already provide energy and power, wouldnt that be sufficient to track heating consumption as a separate timeseries?

Yes, still needs to tracking PR to complete. Who/ when?

split out heating consumption (based on #23185), apply the temperature correction only there and use a separate optimizer model/run. this is the "clean" solution above and the desired final state.

make a separate time series just for heating, adjust that one based on temperature, and then combine those two time series to generate the input for the optimizer.

Yes and yes- but I don't have any good idea how to do that right now (but also didn't try). We've really always tried to not shoot ourselves into the foot and do things properly and in steps.

@daniel309
daniel309 force-pushed the feature/weather-dependent-household-load branch 2 times, most recently from 481dacf to 5db4fac Compare March 2, 2026 15:32
Comment thread tariff/temperature.go Outdated
@daniel309
daniel309 marked this pull request as ready for review March 2, 2026 15:44

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The run loop uses time.Tick(time.Hour) which never gets GC’d and also delays the first fetch by an hour; consider switching to time.NewTicker with an immediate initial fetch (and proper Stop) to avoid leaks and to populate data as soon as the tariff starts.
  • The configuration check if cc.Latitude == 0 && cc.Longitude == 0 forbids the valid (0,0) coordinate; if you only want to guard against missing config, consider using a separate boolean or range validation instead of treating 0,0 as an error.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `run` loop uses `time.Tick(time.Hour)` which never gets GC’d and also delays the first fetch by an hour; consider switching to `time.NewTicker` with an immediate initial fetch (and proper `Stop`) to avoid leaks and to populate data as soon as the tariff starts.
- The configuration check `if cc.Latitude == 0 && cc.Longitude == 0` forbids the valid (0,0) coordinate; if you only want to guard against missing config, consider using a separate boolean or range validation instead of treating 0,0 as an error.

## Individual Comments

### Comment 1
<location path="tariff/temperature.go" line_range="79" />
<code_context>
+
+	client := request.NewHelper(t.log)
+
+	for tick := time.Tick(time.Hour); ; <-tick {
+		uri := fmt.Sprintf(
+			"https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&hourly=temperature_2m&past_days=7&forecast_days=3&timezone=UTC",
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Hourly ticker delays the first fetch by one hour, which can leave the tariff empty for a long time.

Because `for tick := time.Tick(time.Hour); ; <-tick {` only enters the body after one hour, no data is fetched on startup. Given the `runOrError` pattern, callers likely expect an initial fetch (or error) soon after start. Consider fetching once before starting a ticker:

```go
func (t *Temperature) run(done chan error) {
    client := request.NewHelper(t.log)

    fetch := func() {
        // existing body of the loop
    }

    fetch() // initial fetch

    ticker := time.NewTicker(time.Hour)
    defer ticker.Stop()
    for range ticker.C {
        fetch()
    }
}
```

This also avoids the unbounded lifetime of `time.Tick` and makes startup behavior predictable.

Suggested implementation:

```golang
func (t *Temperature) run(done chan error) {
	var once sync.Once

	client := request.NewHelper(t.log)

	ticker := time.NewTicker(time.Hour)
	defer ticker.Stop()

	for range ticker.C {

```

To fully implement your suggestion (initial fetch + avoiding `time.Tick`):

1. Extract the *entire* body of the original `for` loop into a `fetch` closure that captures `client`, `once`, `done`, and `t`:
   ```go
   fetch := func() {
       uri := fmt.Sprintf(
           "https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&hourly=temperature_2m&past_days=7&forecast_days=3&timezone=UTC",
           t.latitude, t.longitude,
       )

       var res openMeteoResponse
       if err := backoff.Retry(func() error {
           return backoffPermanentError(client.GetJSON(uri, &res))
       }, bo()); err != nil {
           once.Do(func() { done <- err })
           t.log.ERROR.Println(err)
           return
       }

       // keep the rest of the loop body here (updating tariff, logging, etc.)
   }
   ```
2. Place `fetch()` once before the ticker loop so that an initial fetch happens immediately:
   ```go
   fetch()

   ticker := time.NewTicker(time.Hour)
   defer ticker.Stop()

   for range ticker.C {
       fetch()
   }
   ```
3. Remove the now-obsolete `ticker := time.NewTicker(...)` / `for range ticker.C` from inside the loop (replaced by the code above).

You’ll need to adjust the exact contents of `fetch` to match the full original loop body, which is not completely visible in the provided snippet.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tariff/temperature.go Outdated
@daniel309

daniel309 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author
  1. updated PR description as per latest discussion and removed the temperature adjustment logic.
  2. marked as ready for review
  3. will open discussion to continue the load adjustment topic for eletrical heatings (heatpumps, IR heating, distributed AC heating devices, ...)

update: discussion on temperature adjustment logic: #27873

Adds a new tariff type that fetches hourly outdoor temperatures from
Open-Meteo (free, no API key) and exposes them as api.Rates where
Rate.Value is the temperature in °C.

The tariff fetches 7 days of past actuals + 3 days of forecast in a
single API call (past_days=7&forecast_days=3&timezone=UTC), so consumers
have access to both historical and future temperature data.

Files changed:
- tariff/temperature.go: new Temperature tariff type, registered as
  'open-meteo' in the tariff registry
- api/tariff.go: TariffTypeTemperature, TariffUsageTemperature
- api/globalconfig/types.go: Temperature config.Typed in Tariffs struct
- tariff/tariffs.go: Temperature api.Tariff field + Get() case
- cmd/setup.go: wire up conf.Temperature in configureTariffs()

Configuration:
  tariffs:
    temperature:
      type: open-meteo
      latitude: 48.137
      longitude: 11.576
- INFO on startup: configured lat/lon
- DEBUG on each hourly fetch: slot count and time range
- WARN on timestamp parse errors (was already present)
- ERROR on fetch failures (was already present)
- Remove stray comment at end of file
- Use 'for ; true; <-time.Tick(interval)' pattern (same as solcast.go)
  so the first fetch happens immediately on startup instead of after 1h
- Replace (0,0) guard with proper range validation: lat in [-90,90],
  lon in [-180,180], allowing the valid Gulf of Guinea coordinate
- Add Temperature field to TariffRefs struct
- Update configureTariff call to use new signature (conf, deviceName, target)
- Move Temperature configuration to 'resolve tariff roles' section
@daniel309
daniel309 force-pushed the feature/weather-dependent-household-load branch from f56dc54 to 80710fc Compare March 2, 2026 16:27
@daniel309

Copy link
Copy Markdown
Contributor Author

ok, build is green. can you have another look @andig ?

@andig andig added the backlog Things to do later label Mar 3, 2026
@andig andig changed the title Feature/weather dependent household load Tariffs: add temperature type and OpenMeteo Mar 3, 2026
@andig
andig marked this pull request as draft March 3, 2026 08:28
@daniel309

daniel309 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

note on the open-meteo-temperature.yaml: forecast-base inherited solar-specific required params (az, dec, kwp) that make no sense for a temperature tariff. Replaced with explicit latitude/longitude params matching what the render template already uses.

@daniel309

daniel309 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

ok, here is how the history .csv now looks like

new column for the heatpump loadpoint: loadpoint.Luxtronik.temp.degC showing current heating water temp
2 new columns for the temperature tariff: temperature.energy.Wh (with 0 as value) and temperature.temp.degC showing outdoor temperature of this slot as per tariff/forecast.

the "temperature" prefix comes from the new constant in collector.go.

all good from my end. Thoughts @andig @naltatis ?

history-energy-2026-06-28 (1).csv

@florian240483

Copy link
Copy Markdown

A thought on this: currently, the history only stores data that has actually been measured. In this case, we would be storing purely forecast-based data. Wouldn't it make sense to enable the feature for logging of an external temperature sensor—such as one from the heating system—and integrate it into the history, while using the outdoor temperature forecast only to predict heating energy consumption?

@daniel309

daniel309 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

A thought on this: currently, the history only stores data that has actually been measured. In this case, we would be storing purely forecast-based data. Wouldn't it make sense to enable the feature for logging of an external temperature sensor—such as one from the heating system—and integrate it into the history, while using the outdoor temperature forecast only to predict heating energy consumption?

Good point. There should be a follow-on PR at some point to add multiple temperature values to a heating "loadpoint". Today, a loadpoint can only have a temp or a soc%. In reality though, heating has at least 3 relevant temperatures that all should be logged separately and named properly: warmwater temp, heatwater temp and outdoor temp. In the example above, all those would be variants of the "temp" (e.g. wwtemp, heattemp, outdoortemp) parameter of loadpoint.Luxtronik.temp.degC

The temperature.energy.Wh prefix for the tariff is independent of the heating "loadpoint". But its probably misleading to see the tariff (which is about forecasting) columns being prefixed with "temperature".

Should I rename to temperature-tariff, temp-tariff or simply tariff?

@andig @naltatis ^^

@andig

andig commented Jun 29, 2026

Copy link
Copy Markdown
Member

What you use as „tariff“ is up to you. No problem if this is a current value measurement. All that takes is a custom tariff of the right type.

@daniel309

daniel309 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

What you use as „tariff“ is up to you. No problem if this is a current value measurement. All that takes is a custom tariff of the right type.

right. so renaming doesnt make sense. This is about temperature from various sources computed by a tariff.

@daniel309

Copy link
Copy Markdown
Contributor Author

I think this is ready to merge @andig

@naltatis

Copy link
Copy Markdown
Member

@daniel309 thanks for your work 🙌, and sorry for my late, late response. First skim of the diff looks good. I like the focus on temperature as a single value and not trying to build a complete weather forecast model here. I'll give this a closer look now and see what needs to be adjusted in UI and docs for this to be finally merged.

Note: Contradicting our earlier discussions in the metrics PRs - I'm not sure any more if it's a good idea to log outside temperature similar to a regular meter. Yes, we do this for solar forecast which also is a tariff but conceptually is power/energy. For outside temperature this feels a little wrong. Broader context: I also see value in logging the historical data of other tariffs (grid import, grid export, co2) for better historical analysis options and visualizations. It might be a good idea to introduce a dedicated "tariffs db log schema" to capture these datapoints in a lean way. This is a separate discussion and should not block this PR. So I'll likely remove the metrics part here for now.

@andig

andig commented Jul 16, 2026

Copy link
Copy Markdown
Member

So I'll likely remove the metrics part here for now.

Lets not do this before we know how to proceed. This PR doesn't have value without storage.

- introduce template group temperature, move Open-Meteo template
- add demo temperature forecast template, enable in demo mode
- config card with temperature values, range and bar chart
- forecast page card; merge co2/temperature charts into ValueChart
- support negative temperatures in chart, co2 capped at 0
- fix stale temperature refs on boot and device deletion
- e2e: cover UI and evcc.yaml configuration
@naltatis

naltatis commented Jul 16, 2026

Copy link
Copy Markdown
Member

UI now looks good. Introduced a new tariff temperature group (for docs), extended config ui, added tests and introduced an outdoor temperature chart to the forecast tab (if configured). Long/lat are now auto-populated (reuses detect mechanism) and a demo temperature tariff was added.

add temp form temp card forecast light forecast dark forecast empty

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bug: wrapping SetSocTemp in process() causes the very first temperature reading after every collector initialization to be silently discarded.

core/metrics/collector.go L86-L132:

process() runs fun() (which calls accu.setSocTemp(value)) before checking whether this is the first-ever call (c.started.IsZero()). On that first call, setSocTemp sets SocTemp since it is nil, but the switch statement then falls through to the reset block at the bottom of process() (c.accu.SocTemp = nil), wiping the value that was just set in the same invocation.

This is a new failure mode introduced by this PR's change to SetSocTemp. Unlike SetEnergyMeterTotal/SetReturnEnergyMeterTotal (which store their baseline in a separate field not touched by the reset), setSocTemp() (core/metrics/accumulator.go line 22, "keeps the first reading per slot") keeps its value in the same SocTemp field that process() clears, and writes unconditionally when nil.

Since the new Temperature collector (core/site.go) is populated only via SetSocTemp (never AddEnergy), its very first call after every evcc restart will always hit this path and silently lose that reading. Impact is limited — the next call within the same slot re-populates the value — but it's a concrete, deterministic bug in the new code.


🤖 Generated with Claude Code

@naltatis

Copy link
Copy Markdown
Member

So I'll likely remove the metrics part here for now.

Lets not do this before we know how to proceed. This PR doesn't have value without storage.

Keep the logging temp as meter logic for now.
Created a dedicated PR with the suggested tariffs logging for discussion. #31845

…endent-household-load

# Conflicts:
#	core/site_tariffs.go
@naltatis
naltatis enabled auto-merge (squash) July 17, 2026 12:50
@naltatis
naltatis merged commit d68e5e5 into evcc-io:master Jul 17, 2026
9 checks passed
@naltatis

Copy link
Copy Markdown
Member

@daniel309 it finally landed. 🔥🌡️
Thanks for your work and patience! 🙌

@tantive

tantive commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

@daniel309 @naltatis Nice idea!

Tried it, works great.

I created #31907 for a small issue with the documentation.

I was wondering why only the next 3 days are requested as a forecast while evcc seems to generally prefer something like 5 days. This causes the last days in my forecast view to be empty.

@daniel309

Copy link
Copy Markdown
Contributor Author

I was wondering why only the next 3 days are requested as a forecast while evcc seems to generally prefer something like 5 days. This causes the last days in my forecast view to be empty.

hmm, no reason really. openmeteo does 7 day forecast by default. let me do a small follow-on pr and change forecast to 5 days.

@daniel309

daniel309 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@tantive @andig @naltatis

#31960

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backlog Things to do later heating Heating

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants