Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a299f71
feat: configurable loadpoint priority strategy (soc, deficit)
Jun 20, 2026
8b2f970
feat: add priorityHysteresis deadband to loadpoint priority sub-ordering
Jun 20, 2026
c156d76
feat: wire priorityStrategy and priorityHysteresis as runtime loadpoi…
Jun 20, 2026
4d8d964
feat(openapi): add priorityStrategy and priorityHysteresis loadpoint …
Jun 20, 2026
ba2ee38
feat(ui): add priorityStrategy and priorityHysteresis loadpoint controls
Jun 20, 2026
55a0572
feat: include priorityStrategy/priorityHysteresis in loadpoint Dynami…
Jun 20, 2026
2ce7052
feat(ui): expose priorityStrategy/priorityHysteresis in loadpoint con…
Jun 20, 2026
4e9f364
fix(test): gci import order + disambiguate Priority locator
Jun 20, 2026
89e09d7
feat: add priorityBasis (percent/energy) to loadpoint priority strategy
Jun 21, 2026
df69798
feat(ui): expose priorityBasis in both loadpoint modals
Jun 21, 2026
6f17f60
chore: regenerate mock + gofmt for priority basis
Jun 21, 2026
697d443
chore: fix gofmt comment alignment on capacity-unknown row
Jun 21, 2026
88a8662
fix: rank a priority tier on one basis to avoid mixing kWh and percent
Jun 21, 2026
4617906
refactor(loadpoint): address review — TextUnmarshaler guard, drop Str…
Jun 21, 2026
16abf9f
refactor(loadpoint): enumer-based priority enums, rename static->none
Jun 21, 2026
d7c0f4f
fix(ui): default new loadpoint priority strategy/basis to none/percent
Jun 21, 2026
216274c
chore(mcp): regenerate openapi.json for none enum (porcelain check)
Jun 21, 2026
a32c09c
Merge branch 'master' into feat/loadpoint-priority-strategy
Alexxtheonly Jun 21, 2026
4fe8836
feat: publish effectivePriorityScore via API
Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions api/prioritystrategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package api

import (
"fmt"
"strings"
)

// PriorityStrategy determines how a loadpoint is ranked against other loadpoints
// of the same priority when distributing surplus power. Valid values are static,
// soc and deficit.
type PriorityStrategy string

// Priority strategies
const (
// PriorityStatic ranks loadpoints by their configured priority only (default).
PriorityStatic PriorityStrategy = ""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Naming: what makes this "static"? Isn't it really first come, first serve? Or maybe "none"?

// PrioritySoc additionally prefers the loadpoint with the lower vehicle soc
// among loadpoints of the same priority.
PrioritySoc PriorityStrategy = "soc"
// PriorityDeficit additionally prefers the loadpoint with the larger gap
// between vehicle soc and its limit soc among loadpoints of the same priority.
PriorityDeficit PriorityStrategy = "deficit"
)

// String implements Stringer
func (s PriorityStrategy) String() string {
if s == PriorityStatic {
Comment thread
andig marked this conversation as resolved.
Outdated
return "static"
}
return string(s)
}

// PriorityStrategyString converts a string to PriorityStrategy
func PriorityStrategyString(s string) (PriorityStrategy, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The pattern here is to use enumer and textmarshaler instead of handrolling these.

switch strings.ToLower(strings.TrimSpace(s)) {
case "", "static":
return PriorityStatic, nil
case string(PrioritySoc):
return PrioritySoc, nil
case string(PriorityDeficit):
return PriorityDeficit, nil
default:
return PriorityStatic, fmt.Errorf("invalid priority strategy: %s", s)
}
}

// PriorityBasis determines whether a priority strategy ranks loadpoints by soc
// percentage or by absolute energy (kWh). Valid values are percent and energy.
type PriorityBasis string

// Priority bases
const (
// PriorityBasisPercent ranks the priority strategy in soc-% (default), so a
// percentage gap is compared regardless of battery capacity.
PriorityBasisPercent PriorityBasis = ""
// PriorityBasisEnergy ranks the priority strategy by absolute energy (kWh) by
// scaling the soc-% gap with the vehicle capacity, so a smaller battery is not
// over-prioritized just because its percentage is lower. Falls back to percent
// when the vehicle capacity is unknown.
PriorityBasisEnergy PriorityBasis = "energy"
)

// String implements Stringer
func (b PriorityBasis) String() string {
if b == PriorityBasisPercent {
return "percent"
}
return string(b)
}

// PriorityBasisString converts a string to PriorityBasis
func PriorityBasisString(s string) (PriorityBasis, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dito- pls keep consistency with rest of the codebase

switch strings.ToLower(strings.TrimSpace(s)) {
case "", "percent", "percentage", "soc":
return PriorityBasisPercent, nil
case string(PriorityBasisEnergy), "absolute", "kwh":
return PriorityBasisEnergy, nil
default:
return PriorityBasisPercent, fmt.Errorf("invalid priority basis: %s", s)
}
}
108 changes: 108 additions & 0 deletions assets/js/components/Config/LoadpointModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,57 @@
/>
</FormRow>

<FormRow
v-if="showPriority"
id="loadpointParamPriorityStrategy"
:label="$t('config.loadpoint.priorityStrategyLabel')"
:help="$t('config.loadpoint.priorityStrategyHelp')"
>
<PropertyField
id="loadpointParamPriorityStrategy"
v-model="priorityStrategy"
type="Choice"
size="w-100"
class="me-2"
required
:choice="priorityStrategyOptions"
/>
</FormRow>

<FormRow
v-if="showPriority && priorityHysteresisAvailable"
id="loadpointParamPriorityBasis"
:label="$t('config.loadpoint.priorityBasisLabel')"
:help="$t('config.loadpoint.priorityBasisHelp')"
>
<PropertyField
id="loadpointParamPriorityBasis"
v-model="priorityBasis"
type="Choice"
size="w-100"
class="me-2"
required
:choice="priorityBasisOptions"
/>
</FormRow>

<FormRow
v-if="showPriority && priorityHysteresisAvailable"
id="loadpointParamPriorityHysteresis"
:label="$t('config.loadpoint.priorityHysteresisLabel')"
:help="$t('config.loadpoint.priorityHysteresisHelp')"
>
<PropertyField
id="loadpointParamPriorityHysteresis"
v-model="values.priorityHysteresis"
type="Float"
:unit="priorityHysteresisUnit"
size="w-25 w-min-200"
class="me-2"
required
/>
</FormRow>

<h6 v-if="!chargerIsSwitchDevice">
{{ $t("config.loadpoint.electricalTitle") }}
<small class="text-muted">{{
Expand Down Expand Up @@ -634,6 +685,8 @@ import { getModal, openModal, replaceModal, closeModal } from "@/configModal";
import {
CHARGE_MODE,
LOADPOINT_TYPE,
PRIORITY_STRATEGY,
PRIORITY_BASIS,
type DeviceType,
type LoadpointType,
type ConfigCharger,
Expand All @@ -654,6 +707,9 @@ const defaultValues = {
minCurrent: 6,
maxCurrent: 16,
priority: 0,
priorityStrategy: "",
priorityBasis: "",
priorityHysteresis: 0,
defaultMode: "",
thresholds: {
enable: { delay: 1 * nsPerMin, threshold: 0 },
Expand Down Expand Up @@ -795,6 +851,58 @@ export default {
result[10]!.name = "10 (highest)";
return result;
},
priorityStrategy: {
// backend returns "" for the static strategy; map to/from the explicit "static" choice
get(): PRIORITY_STRATEGY {
return this.values.priorityStrategy || PRIORITY_STRATEGY.STATIC;
},
set(value: PRIORITY_STRATEGY) {
this.values.priorityStrategy = value;
},
},
priorityStrategyOptions(): { key: PRIORITY_STRATEGY; name: string }[] {
return [
{
key: PRIORITY_STRATEGY.STATIC,
name: this.$t("config.loadpoint.priorityStrategyStatic"),
},
{
key: PRIORITY_STRATEGY.SOC,
name: this.$t("config.loadpoint.priorityStrategySoc"),
},
{
key: PRIORITY_STRATEGY.DEFICIT,
name: this.$t("config.loadpoint.priorityStrategyDeficit"),
},
];
},
priorityBasis: {
// backend returns "" for the percent basis; map to/from the explicit "percent" choice
get(): PRIORITY_BASIS {
return this.values.priorityBasis || PRIORITY_BASIS.PERCENT;
},
set(value: PRIORITY_BASIS) {
this.values.priorityBasis = value;
},
},
priorityBasisOptions(): { key: PRIORITY_BASIS; name: string }[] {
return [
{
key: PRIORITY_BASIS.PERCENT,
name: this.$t("config.loadpoint.priorityBasisPercent"),
},
{
key: PRIORITY_BASIS.ENERGY,
name: this.$t("config.loadpoint.priorityBasisEnergy"),
},
];
},
priorityHysteresisAvailable(): boolean {
return this.priorityStrategy !== PRIORITY_STRATEGY.STATIC;
},
priorityHysteresisUnit(): string {
return this.priorityBasis === PRIORITY_BASIS.ENERGY ? "kWh" : "%";
},
phasesOptions() {
return [
{ value: 1, name: this.$t("config.loadpoint.phases1p") },
Expand Down
Loading
Loading