-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Config UI: add tariffs #26698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Config UI: add tariffs #26698
Conversation
|
This will also simplify adding additional fees ;) |
cmd/setup.go
Outdated
| } | ||
|
|
||
| // Track which types are configured via YAML | ||
| yamlConfigured := make(map[api.TariffUsage]bool) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Das ist viel Code fΓΌr yaml. KΓΆnnen wir das einfach nicht tun, fΓΌr teil-yaml Setups keine weitere UnterstΓΌtzung anbieten?
cmd/setup.go
Outdated
| }) | ||
| } | ||
|
|
||
| // Load device-based tariffs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wozu brauchts das? Die stehen in den tariffs.* Variablen doch schon drin?
server/http_config_tariff_handler.go
Outdated
| var res TariffRefs | ||
| if err := settings.Json(keys.TariffRefs, &res); err != nil { | ||
| // return defaults if not configured | ||
| res = TariffRefs{Currency: "EUR", Solar: []string{}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do this in line 24
server/http_config_tariff_handler.go
Outdated
| // return defaults if not configured | ||
| res = TariffRefs{Currency: "EUR", Solar: []string{}} | ||
| } | ||
| if res.Solar == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove
server/http_config_tariff_handler.go
Outdated
|
|
||
| // Validate and update tariff references | ||
| if payload.Grid != nil { | ||
| if *payload.Grid != "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this make sense? Either ignore in the first place or error. Loops instead of duplicated code?
|
Here the working ui for the new Bildschirmaufnahme.2026-01-19.um.20.18.53.webm |
|
I reworked the tariff initialization and device ref handling mechanics. Mixed mode (evcc.yaml | yaml ui) + device-based is not supported. Existing users have to remove their yaml based config and configure via UI. All changes should be non-breaking: opt-in. @andig You can now review. Only thing that's missing from my side is proper test coverage of the new device process. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 3 issues, and left some high level feedback:
- In
configureTariffDevicesyou start goroutines over theconfigurableslice using the loop variableconfdirectly; this will be captured by reference and can lead to all goroutines seeing the last elementβcreate a shadow variable inside the loop (e.g.c := conf) and close over that instead. - The new
fmtCurrencyNamehelper instantiatesIntl.DisplayNameson each call; consider caching theDisplayNamesinstance per-locale or per-component to avoid unnecessary allocations and potential performance issues when rendering lists of currencies.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `configureTariffDevices` you start goroutines over the `configurable` slice using the loop variable `conf` directly; this will be captured by reference and can lead to all goroutines seeing the last elementβcreate a shadow variable inside the loop (e.g. `c := conf`) and close over that instead.
- The new `fmtCurrencyName` helper instantiates `Intl.DisplayNames` on each call; consider caching the `DisplayNames` instance per-locale or per-component to avoid unnecessary allocations and potential performance issues when rendering lists of currencies.
## Individual Comments
### Comment 1
<location> `cmd/setup.go:901-910` </location>
<code_context>
return nil
}
+func configureTariffDevices(names ...string) error {
+ // load tariff devices from database
+ configurable, err := config.ConfigurationsByClass(templates.Tariff)
+ if err != nil {
+ return err
+ }
+
+ var eg errgroup.Group
+
+ for _, conf := range configurable {
+ eg.Go(func() error {
+ cc := conf.Named()
</code_context>
<issue_to_address>
**issue (bug_risk):** Fix goroutine closure over loop variable in configureTariffDevices
In `configureTariffDevices`, the goroutine inside the `for _, conf := range configurable` loop closes over `conf`, which is reused each iteration. This means all goroutines may see the same (last) `conf` value, leading to incorrect device instances or duplicate work.
Shadow `conf` inside the loop before starting the goroutine:
```go
for _, conf := range configurable {
conf := conf // shadow loop variable
eg.Go(func() error {
cc := conf.Named()
// ...
})
}
```
This ensures each goroutine captures the correct configuration, matching the pattern used elsewhere in the codebase.
</issue_to_address>
### Comment 2
<location> `assets/js/components/Config/PropertyZoneForm.vue:182` </location>
<code_context>
+ timeTo: timeTo || "00:00",
+ };
+ },
+ convertFromUiZone(uiZone: UiZone) {
+ const isAllDay = uiZone.timeFrom === "00:00" && uiZone.timeTo === "00:00";
+ return {
</code_context>
<issue_to_address>
**issue:** Avoid saving incomplete time ranges as invalid hour strings like '-'
With the current behaviour, creating/editing a zone with empty `timeFrom`/`timeTo` passes validation (`validateTimeRange` returns `false` β `hasValidationErrors` stays `false`), so `handleSave` allows saving and `convertFromUiZone` may emit `hours: "-"`, `"-HH:MM"` or `"HH:MM-"`. This is neither a valid range nor the "all day" representation and will be awkward to handle serverβside.
Please either:
- Normalise empty `timeFrom`/`timeTo` to an explicit "all day" value at the UI layer, or
- Treat partially filled/empty pairs as invalid and block saving until both are set (or both are `"00:00"`).
You can tighten `validateTimeRange` as in your example and then explicitly map the "no constraint" case to `hours: ""` in `convertFromUiZone`.
</issue_to_address>
### Comment 3
<location> `assets/js/mixins/formatter.ts:420` </location>
<code_context>
+ value: i,
+ }));
+ },
+ fmtConsecutiveRange(
+ selectedIndices: number[],
+ getNameFn: (transformedIndex: number) => string | undefined,
</code_context>
<issue_to_address>
**issue (complexity):** Consider replacing the generic fmtConsecutiveRange (and transformFn) with dedicated weekday/month range formatters and extracting time helpers into a separate module to make the mixin logic more direct and self-contained.
You can keep the new capabilities while reducing indirection by specializing the range formatters and dropping the generic `fmtConsecutiveRange` + `transformFn` layer.
### 1. Replace `fmtConsecutiveRange` with two explicit helpers
The current flow:
- `fmtWeekdaysRange` β `fmtConsecutiveRange` (with `transformFn`)
- `fmtMonthsRange` β `fmtConsecutiveRange`
For two very specific domains, the generic helper (plus transform step) makes it harder to trace what happens. You can keep *exactly* the same behavior with two small, local implementations.
Example replacement:
```ts
// keep fmtWeekdayByIndex / fmtMonthByIndex as-is
fmtWeekdaysRange(selectedWeekdays: number[]): string {
if (!selectedWeekdays || selectedWeekdays.length === 0) return "β";
// map Sunday(0) -> 7 for range detection, but keep original index to format
const mapped = selectedWeekdays.map(i => (i === 0 ? 7 : i));
const sorted = [...mapped].sort((a, b) => a - b);
const max = sorted[sorted.length - 1]!;
const parts: string[] = [];
let i = 0;
const name = (mappedIndex: number) =>
this.fmtWeekdayByIndex(mappedIndex % 7, "short");
while (i < sorted.length) {
const start = sorted[i]!;
let end = start;
while (i + 1 < sorted.length && sorted[i + 1] === end + 1) {
end = sorted[i + 1]!;
i++;
}
if (end - start > 1) {
parts.push(`${name(start)} β ${name(end)}`);
} else if (end > start) {
parts.push(`${name(start)}, ${name(end)}`);
} else {
parts.push(name(start));
}
i++;
}
return parts.join(", ");
},
fmtMonthsRange(selectedMonths: number[]): string {
if (!selectedMonths || selectedMonths.length === 0) return "β";
const sorted = [...selectedMonths].sort((a, b) => a - b);
const parts: string[] = [];
let i = 0;
const name = (idx: number) => this.fmtMonthByIndex(idx, "short");
while (i < sorted.length) {
const start = sorted[i]!;
let end = start;
while (i + 1 < sorted.length && sorted[i + 1] === end + 1) {
end = sorted[i + 1]!;
i++;
}
if (end - start > 1) {
parts.push(`${name(start)} β ${name(end)}`);
} else if (end > start) {
parts.push(`${name(start)}, ${name(end)}`);
} else {
parts.push(name(start));
}
i++;
}
return parts.join(", ");
},
```
Then you can delete `fmtConsecutiveRange` and the `transformFn` concept entirely. This keeps all behavior (including the Sunday=0 β 7 trick) but makes weekday/month formatting self-contained and easier to reason about.
### 2. Keep indexβname helpers, drop extra abstraction
`fmtWeekdayByIndex` / `fmtMonthByIndex` and `getWeekdaysList` / `getMonthsList` are reasonable primitives. With specialized range functions, theyβre still useful without having to route through a generic formatter:
```ts
getWeekdaysList(format: Intl.DateTimeFormatOptions["weekday"]) {
return Array.from({ length: 7 }, (_, i) => {
const value = (i + 1) % 7; // Mon=1...Sun=0
return { name: this.fmtWeekdayByIndex(value, format), value };
});
}
getMonthsList(format: Intl.DateTimeFormatOptions["month"]) {
return Array.from({ length: 12 }, (_, i) => ({
name: this.fmtMonthByIndex(i, format),
value: i,
}));
}
```
No other code needs to understand `transformFn` or the internal algorithm; they call the explicit `fmtWeekdaysRange` / `fmtMonthsRange`.
### 3. Consider moving time helpers to a separate module
To keep this mixin focused on βvalue β labelβ formatting and not time parsing, consider extracting:
```ts
// timeFormatUtils.ts (or similar)
export function fmtTimeStr(locale: string, timeStr: string): string { /* ... */ }
export function fmtTimeRange(locale: string, timeRange: string): string { /* ... */ }
```
and in the mixin:
```ts
fmtTimeStr(timeStr: string): string {
return fmtTimeStr(this.$i18n?.locale, timeStr);
},
fmtTimeRange(timeRange: string): string {
return fmtTimeRange(this.$i18n?.locale, timeRange);
},
```
This keeps all behavior, but reduces the sense of the mixin being a grabβbag and makes weekday/month label logic easier to find and understand.
</issue_to_address>Help me be more useful! Please click π or π on each comment and I'll use the feedback to improve your reviews.
Co-authored-by: Maschga <88616799+Maschga@users.noreply.github.com>





Add form based configuration for tariffs and forecast to config ui.
π΅πβοΈπ°οΈ make grid, feedin, co2, solar and planner tariffs ui configurable
π add
fixedandfixed-zonestariff template. keeping separate to simplify standard setup use caseπ introduce
zonesparam type to specify hour/day/month based pricesβ‘ introduce
pricePerKWhparam to support currency specific inputct/kWh,rp/kWh, ...π forecast visualization, using existing price chart
π migration: no auto-migration, config via yaml (file or ui) still works, tariffs can be migrated one by one. only one source allowed per tariff type
TODOs
zonesparamScreenshots

config.ui.tariff.webm
legacy ui yaml, upgrade hint