Add min/max physical current to loadpoint (#21187)

This commit is contained in:
Michael Geers 2025-06-18 21:36:49 +02:00 • committed by GitHub
parent b3267cdeef
commit ebbcabb838
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 696 additions and 342 deletions

View file

@ -308,13 +308,7 @@
<FormRow
id="chargerPower"
:label="$t('config.loadpoint.chargerTypeLabel')"
:help="
chargerPower === '11kw'
? $t('config.loadpoint.chargerPower11kwHelp')
: chargerPower === '22kw'
? $t('config.loadpoint.chargerPower22kwHelp')
: $t('config.loadpoint.chargerPowerCustomHelp')
"
:help="chargerPowerHelp"
>
<SelectGroup
id="chargerPower"
@ -331,14 +325,18 @@
<div v-if="chargerPower === 'other'" class="row ms-3 mb-5">
<FormRow
id="loadpointMinCurrent"
:label="$t('config.loadpoint.minCurrentLabel')"
id="loadpointMinPhysicalCurrent"
:label="$t('config.loadpoint.minPhysicalCurrentLabel')"
class="col-sm-6 mb-sm-0"
:help="values.minCurrent < 6 ? $t('config.loadpoint.minCurrentHelp') : null"
:help="
values.minPhysicalCurrent < 6
? $t('config.loadpoint.minPhysicalCurrentHelp')
: null
"
>
<PropertyField
id="loadpointMinCurrent"
v-model="values.minCurrent"
id="loadpointMinPhysicalCurrent"
v-model="values.minPhysicalCurrent"
type="Float"
unit="A"
size="w-25 w-min-200"
@ -348,18 +346,18 @@
</FormRow>
<FormRow
id="loadpointMaxCurrent"
:label="$t('config.loadpoint.maxCurrentLabel')"
id="loadpointMaxPhysicalCurrent"
:label="$t('config.loadpoint.maxPhysicalCurrentLabel')"
class="col-sm-6 mb-sm-0"
:help="
values.maxCurrent < values.minCurrent
? $t('config.loadpoint.maxCurrentHelp')
values.maxPhysicalCurrent < values.minPhysicalCurrent
? $t('config.loadpoint.maxPhysicalCurrentHelp')
: null
"
>
<PropertyField
id="loadpointMaxCurrent"
v-model="values.maxCurrent"
id="loadpointMaxPhysicalCurrent"
v-model="values.maxPhysicalCurrent"
type="Float"
unit="A"
size="w-25 w-min-200"
@ -557,6 +555,7 @@ import GenericModal from "../Helper/GenericModal.vue";
import deepClone from "@/utils/deepClone";
import deepEqual from "@/utils/deepEqual";
import formatter, { POWER_UNIT } from "@/mixins/formatter";
import test from "./mixins/test";
import EditIcon from "../MaterialIcon/Edit.vue";
const nsPerMin = 60 * 1e9;
@ -566,6 +565,8 @@ const defaultValues = {
phasesConfigured: 3,
minCurrent: 6,
maxCurrent: 16,
minPhysicalCurrent: 6,
maxPhysicalCurrent: 16,
priority: 0,
defaultMode: "",
thresholds: {
@ -590,7 +591,7 @@ const defaultThresholds = {
export default {
name: "LoadpointModal",
components: { FormRow, PropertyField, GenericModal, SelectGroup, EditIcon },
mixins: [formatter],
mixins: [formatter, test],
props: {
id: Number,
name: String,
@ -623,6 +624,13 @@ export default {
}
return this.$t(`config.loadpoint.titleEdit`);
},
chargerPowerHelp() {
const baseText = this.$t("config.loadpoint.chargerPowerHelp");
const powerType = this.chargerPower === "other" ? "Custom" : this.chargerPower;
const specificText = this.$t(`config.loadpoint.chargerPower${powerType}Help`);
return `${baseText} ${specificText}`;
},
isNew() {
return this.id === undefined;
},
@ -704,11 +712,11 @@ export default {
},
chargerPower(value) {
if (value === "11kw") {
this.values.minCurrent = 6;
this.values.maxCurrent = 16;
this.values.minPhysicalCurrent = 6;
this.values.maxPhysicalCurrent = 16;
} else if (value === "22kw") {
this.values.minCurrent = 6;
this.values.maxCurrent = 32;
this.values.minPhysicalCurrent = 6;
this.values.maxPhysicalCurrent = 32;
}
},
solarMode(value) {
@ -742,13 +750,13 @@ export default {
async update() {
this.saving = true;
try {
this.validate();
const values = deepClone(this.values);
await api.put(`config/loadpoints/${this.id}`, values);
this.$emit("updated");
this.$refs.modal.close();
} catch (e) {
console.error(e);
alert("update failed");
this.handleUpdateError(e);
}
this.saving = false;
},
@ -758,20 +766,18 @@ export default {
this.$emit("updated");
this.$refs.modal.close();
} catch (e) {
console.error(e);
alert("delete failed");
this.handleRemoveError(e);
}
},
async create() {
this.saving = true;
try {
this.validate();
await api.post("config/loadpoints", this.values);
this.$emit("updated");
this.$refs.modal.close();
} catch (e) {
console.error(e);
const error = e.response?.data?.error;
alert(`failed to create loadpoint: ${error}`);
this.handleCreateError(e);
}
this.saving = false;
},
@ -800,10 +806,10 @@ export default {
this.values.charger = charger;
},
updateChargerPower() {
const { minCurrent, maxCurrent } = this.values;
if (minCurrent === 6 && maxCurrent === 16) {
const { minPhysicalCurrent, maxPhysicalCurrent } = this.values;
if (minPhysicalCurrent === 6 && maxPhysicalCurrent === 16) {
this.chargerPower = "11kw";
} else if (minCurrent === 6 && maxCurrent === 32) {
} else if (minPhysicalCurrent === 6 && maxPhysicalCurrent === 32) {
this.chargerPower = "22kw";
} else {
this.chargerPower = "other";
@ -832,6 +838,16 @@ export default {
return;
}
},
validate() {
const {
minCurrent: min,
maxCurrent: max,
minPhysicalCurrent: minP,
maxPhysicalCurrent: maxP,
} = this.values;
this.values.minCurrent = Math.max(minP, Math.min(min, maxP));
this.values.maxCurrent = Math.min(maxP, Math.max(max, minP));
},
},
};
</script>

View file

@ -190,6 +190,8 @@ export default defineComponent({
chargerSinglePhase: Boolean,
minCurrent: Number,
maxCurrent: Number,
minPhysicalCurrent: Number,
maxPhysicalCurrent: Number,
offeredCurrent: Number,
connectedDuration: Number,
chargeCurrents: Array,

View file

@ -1,159 +1,117 @@
<template>
<Teleport to="body">
<div
:id="`loadpointSettingsModal_${id}`"
ref="modal"
class="modal fade text-dark modal-xl"
data-bs-backdrop="true"
tabindex="-1"
role="dialog"
aria-hidden="true"
data-testid="loadpoint-settings-modal"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("main.loadpointSettings.title", [title]) }}
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="container">
<SmartCostLimit
v-if="isModalVisible"
v-bind="smartCostLimitProps"
:possible="smartCostAvailable"
:smartCostLimit="smartCostLimit"
class="mt-2"
/>
<LoadpointSettingsBatteryBoost
v-if="batteryBoostAvailable"
v-bind="batteryBoostProps"
class="mt-2"
@batteryboost-updated="changeBatteryBoost"
/>
<h6>
{{ $t("main.loadpointSettings.currents") }}
</h6>
<div v-if="phasesOptions.length" class="mb-3 row">
<label
:for="formId(`phases_${phasesOptions[0]}`)"
class="col-sm-4 col-form-label pt-0"
>
{{ $t("main.loadpointSettings.phasesConfigured.label") }}
</label>
<div class="col-sm-8 pe-0">
<p v-if="!chargerPhases1p3p" class="mt-0 mb-2">
<small>
{{
$t(
"main.loadpointSettings.phasesConfigured.no1p3pSupport"
)
}}</small
>
</p>
<div
v-for="phases in phasesOptions"
:key="phases"
class="form-check"
>
<input
:id="formId(`phases_${phases}`)"
v-model.number="selectedPhases"
class="form-check-input"
type="radio"
:name="formId('phases')"
:value="phases"
@change="changePhasesConfigured"
/>
<label
class="form-check-label"
:for="formId(`phases_${phases}`)"
>
{{
$t(
`main.loadpointSettings.phasesConfigured.phases_${phases}`
)
}}
<small v-if="phases > 0">
{{
$t(
`main.loadpointSettings.phasesConfigured.phases_${phases}_hint`,
{
min: minPowerPhases(phases),
max: maxPowerPhases(phases),
}
)
}}
</small>
</label>
</div>
</div>
</div>
<div class="mb-3 row">
<label
:for="formId('maxcurrent')"
class="col-sm-4 col-form-label pt-0 pt-sm-2"
>
{{ $t("main.loadpointSettings.maxCurrent.label") }}
</label>
<div class="col-sm-8 pe-0 d-flex align-items-center">
<select
:id="formId('maxcurrent')"
v-model.number="selectedMaxCurrent"
class="form-select form-select-sm w-50"
@change="changeMaxCurrent"
>
<option
v-for="{ value, name } in maxCurrentOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
<small class="ms-3">~ {{ maxPower }}</small>
</div>
</div>
<div class="mb-3 row">
<label
:for="formId('mincurrent')"
class="col-sm-4 col-form-label pt-0 pt-sm-2"
>
{{ $t("main.loadpointSettings.minCurrent.label") }}
</label>
<div class="col-sm-8 pe-0 d-flex align-items-center">
<select
:id="formId('mincurrent')"
v-model.number="selectedMinCurrent"
class="form-select form-select-sm w-50"
@change="changeMinCurrent"
>
<option
v-for="{ value, name } in minCurrentOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
<small class="ms-3">~ {{ minPower }}</small>
</div>
</div>
</div>
<GenericModal
:id="`loadpointSettingsModal_${id}`"
:title="$t('main.loadpointSettings.title', [title])"
size="xl"
data-testid="loadpoint-settings-modal"
@open="modalVisible"
@closed="modalInvisible"
>
<div class="container">
<SmartCostLimit
v-if="isModalVisible"
v-bind="smartCostLimitProps"
:possible="smartCostAvailable"
:smartCostLimit="smartCostLimit"
class="mt-2"
/>
<LoadpointSettingsBatteryBoost
v-if="batteryBoostAvailable"
v-bind="batteryBoostProps"
class="mt-2"
@batteryboost-updated="changeBatteryBoost"
/>
<h6>
{{ $t("main.loadpointSettings.currents") }}
</h6>
<div v-if="phasesOptions.length" class="mb-3 row">
<label
:for="formId(`phases_${phasesOptions[0]}`)"
class="col-sm-4 col-form-label pt-0"
>
{{ $t("main.loadpointSettings.phasesConfigured.label") }}
</label>
<div class="col-sm-8 pe-0">
<p v-if="!chargerPhases1p3p" class="mt-0 mb-2">
<small>
{{ $t("main.loadpointSettings.phasesConfigured.no1p3pSupport") }}</small
>
</p>
<div v-for="phases in phasesOptions" :key="phases" class="form-check">
<input
:id="formId(`phases_${phases}`)"
v-model.number="selectedPhases"
class="form-check-input"
type="radio"
:name="formId('phases')"
:value="phases"
@change="changePhasesConfigured"
/>
<label class="form-check-label" :for="formId(`phases_${phases}`)">
{{ $t(`main.loadpointSettings.phasesConfigured.phases_${phases}`) }}
<small v-if="phases > 0">
{{
$t(
`main.loadpointSettings.phasesConfigured.phases_${phases}_hint`,
{
min: minPowerPhases(phases),
max: maxPowerPhases(phases),
}
)
}}
</small>
</label>
</div>
</div>
</div>
<div class="mb-3 row">
<label :for="formId('maxcurrent')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ $t("main.loadpointSettings.maxCurrent.label") }}
</label>
<div class="col-sm-8 pe-0 d-flex align-items-center">
<select
:id="formId('maxcurrent')"
v-model.number="selectedMaxCurrent"
class="form-select form-select-sm w-50"
@change="changeMaxCurrent"
>
<option
v-for="{ value, name } in maxCurrentOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
<small class="ms-3">~ {{ maxPower }}</small>
</div>
</div>
<div class="mb-3 row">
<label :for="formId('mincurrent')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ $t("main.loadpointSettings.minCurrent.label") }}
</label>
<div class="col-sm-8 pe-0 d-flex align-items-center">
<select
:id="formId('mincurrent')"
v-model.number="selectedMinCurrent"
class="form-select form-select-sm w-50"
@change="changeMinCurrent"
>
<option
v-for="{ value, name } in minCurrentOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
<small class="ms-3">~ {{ minPower }}</small>
</div>
</div>
</div>
</Teleport>
</GenericModal>
</template>
<script lang="ts">
@ -162,13 +120,15 @@ import formatter from "@/mixins/formatter";
import SmartCostLimit from "../Tariff/SmartCostLimit.vue";
import smartCostAvailable from "@/utils/smartCostAvailable";
import SettingsBatteryBoost from "./SettingsBatteryBoost.vue";
import GenericModal from "../Helper/GenericModal.vue";
import { defineComponent, type PropType } from "vue";
import { CURRENCY, PHASES } from "@/types/evcc";
const V = 230;
const range = (start: number, stop: number, step = -1) =>
Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
const range = (start: number, end: number) => {
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
};
const insertSorted = (arr: number[], num: number) => {
const uniqueSet = new Set(arr);
@ -176,14 +136,15 @@ const insertSorted = (arr: number[], num: number) => {
return [...uniqueSet].sort((a, b) => b - a);
};
// TODO: add max physical current to loadpoint (config ui) and only allow user to select values in side that range (main ui, here)
const MAX_CURRENT = 64;
const { AUTO, THREE_PHASES, ONE_PHASE } = PHASES;
export default defineComponent({
name: "LoadpointSettingsModal",
components: { SmartCostLimit, LoadpointSettingsBatteryBoost: SettingsBatteryBoost },
components: {
SmartCostLimit,
LoadpointSettingsBatteryBoost: SettingsBatteryBoost,
GenericModal,
},
mixins: [formatter, collector],
props: {
id: [String, Number],
@ -196,6 +157,8 @@ export default defineComponent({
minSoc: Number,
maxCurrent: { type: Number, default: 0 },
minCurrent: { type: Number, default: 0 },
minPhysicalCurrent: Number,
maxPhysicalCurrent: Number,
title: String,
smartCostLimit: { type: Number as PropType<number | null>, default: null },
smartCostType: String,
@ -254,17 +217,19 @@ export default defineComponent({
}
return this.fmtW(this.minCurrent * V * this.phasesConfigured);
},
currentOptions() {
const low = [0.125, 0.25, 0.5, 1, 2, 3, 4, 5]; // mostly heating
const regular = range(6, 64); // ac charging
const high = [75, 100, 150, 200, 250, 300, 400, 500, 600]; // dc charging
return [...low, ...regular, ...high].filter(this.isInRange);
},
minCurrentOptions() {
const opt1 = [...range(Math.floor(this.maxCurrent), 1), 0.5, 0.25, 0.125];
// ensure that current value is always included
const opt2 = insertSorted(opt1, this.minCurrent);
return opt2.map((value) => this.currentOption(value, value === 6));
const opt1 = this.currentOptions.filter((value) => value <= this.maxCurrent);
return insertSorted(opt1, this.minCurrent).map(this.currentOption);
},
maxCurrentOptions() {
const opt1 = range(MAX_CURRENT, Math.ceil(this.minCurrent));
// ensure that current value is always included
const opt2 = insertSorted(opt1, this.maxCurrent);
return opt2.map((value) => this.currentOption(value, value === 16));
const opt1 = this.currentOptions.filter((value) => value >= this.minCurrent);
return insertSorted(opt1, this.maxCurrent).map(this.currentOption);
},
smartCostLimitProps() {
return this.collectProps(SmartCostLimit);
@ -287,14 +252,6 @@ export default defineComponent({
this.selectedPhases = value;
},
},
mounted() {
this.$refs["modal"]?.addEventListener("show.bs.modal", this.modalVisible);
this.$refs["modal"]?.addEventListener("hidden.bs.modal", this.modalInvisible);
},
unmounted() {
this.$refs["modal"]?.removeEventListener("show.bs.modal", this.modalVisible);
this.$refs["modal"]?.removeEventListener("hidden.bs.modal", this.modalInvisible);
},
methods: {
maxPowerPhases(phases: PHASES) {
return this.fmtW(this.maxCurrent * V * phases);
@ -314,11 +271,9 @@ export default defineComponent({
changePhasesConfigured() {
this.$emit("phasesconfigured-updated", this.selectedPhases);
},
currentOption(value: number, isDefault: boolean) {
let name = `${this.fmtNumber(value, 0)} A`;
if (isDefault) {
name += ` (${this.$t("main.loadpointSettings.default")})`;
}
currentOption(value: number) {
const digits = value < 1 ? undefined : 0;
const name = `${this.fmtNumber(value, digits)} A`;
return { value, name };
},
modalVisible() {
@ -330,6 +285,11 @@ export default defineComponent({
changeBatteryBoost(boost: boolean) {
this.$emit("batteryboost-updated", boost);
},
isInRange(value: number) {
const min = this.minPhysicalCurrent ?? 0;
const max = this.maxPhysicalCurrent ?? 64;
return min <= value && value <= max;
},
},
});
</script>

View file

@ -90,7 +90,7 @@ export default defineComponent({
fmtWh(watt: number, format = POWER_UNIT.KW, withUnit = true, digits?: number) {
return this.fmtW(watt, format, withUnit, digits) + (withUnit ? "h" : "");
},
fmtNumber(number: number, decimals: number, unit?: string) {
fmtNumber(number: number, decimals?: number, unit?: string) {
const style = unit ? "unit" : "decimal";
return new Intl.NumberFormat(this.$i18n?.locale, {
style,

View file

@ -2,26 +2,28 @@ package keys
const (
// loadpoint settings
Title = "title" // loadpoint title
Mode = "mode" // charge mode
DefaultMode = "defaultMode" // default charge mode
Charger = "charger" // charger ref
Meter = "meter" // meter ref
Circuit = "circuit" // circuit ref
DefaultVehicle = "vehicle" // default vehicle ref
Priority = "priority" // priority
MinCurrent = "minCurrent" // min current
MaxCurrent = "maxCurrent" // max current
MinSoc = "minSoc" // min soc
LimitSoc = "limitSoc" // limit soc
LimitEnergy = "limitEnergy" // limit energy
Soc = "soc"
Thresholds = "thresholds"
EnableThreshold = "enableThreshold"
DisableThreshold = "disableThreshold"
EnableDelay = "enableDelay"
DisableDelay = "disableDelay"
BatteryBoost = "batteryBoost"
Title = "title" // loadpoint title
Mode = "mode" // charge mode
DefaultMode = "defaultMode" // default charge mode
Charger = "charger" // charger ref
Meter = "meter" // meter ref
Circuit = "circuit" // circuit ref
DefaultVehicle = "vehicle" // default vehicle ref
Priority = "priority" // priority
MinCurrent = "minCurrent" // min current
MaxCurrent = "maxCurrent" // max current
MinPhysicalCurrent = "minPhysicalCurrent" // min physical current
MaxPhysicalCurrent = "maxPhysicalCurrent" // max physical current
MinSoc = "minSoc" // min soc
LimitSoc = "limitSoc" // limit soc
LimitEnergy = "limitEnergy" // limit energy
Soc = "soc"
Thresholds = "thresholds"
EnableThreshold = "enableThreshold"
DisableThreshold = "disableThreshold"
EnableDelay = "enableDelay"
DisableDelay = "disableDelay"
BatteryBoost = "batteryBoost"
PhasesConfigured = "phasesConfigured" // desired phase mode (0/1/3, 0 = automatic), user selection
PhasesActive = "phasesActive" // expectedly active phases, taking vehicle into account (1/2/3)

View file

@ -107,15 +107,17 @@ type Loadpoint struct {
MinCurrent_ float64 `mapstructure:"minCurrent"` // ignored, present for compatibility
MaxCurrent_ float64 `mapstructure:"maxCurrent"` // ignored, present for compatibility
title string // UI title
priority int // Priority
minCurrent float64 // PV mode: start current Min+PV mode: min current
maxCurrent float64 // Max allowed current. Physically ensured by the charger
phasesConfigured int // Charger configured phase mode 0/1/3
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
smartCostLimit *float64 // always charge if cost is below this value
batteryBoost int // battery boost state
title string // UI title
priority int // Priority
minCurrent float64 // PV mode: start current Min+PV mode: min current
maxCurrent float64 // Max allowed current. Physically ensured by the charger
minPhysicalCurrent *float64 // Min physical current
maxPhysicalCurrent *float64 // Max physical current
phasesConfigured int // Charger configured phase mode 0/1/3
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
smartCostLimit *float64 // always charge if cost is below this value
batteryBoost int // battery boost state
mode api.ChargeMode
enabled bool // Charger enabled state
@ -613,6 +615,8 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
lp.publish(keys.Priority, lp.GetPriority())
lp.publish(keys.MinCurrent, lp.GetMinCurrent())
lp.publish(keys.MaxCurrent, lp.GetMaxCurrent())
lp.publish(keys.MinPhysicalCurrent, lp.GetMinPhysicalCurrent())
lp.publish(keys.MaxPhysicalCurrent, lp.GetMaxPhysicalCurrent())
lp.publish(keys.EnableThreshold, lp.Enable.Threshold)
lp.publish(keys.DisableThreshold, lp.Disable.Threshold)

View file

@ -65,6 +65,12 @@ type API interface {
GetMaxCurrent() float64
// SetMaxCurrent sets the max charging current
SetMaxCurrent(float64) error
// GetMinPhysicalCurrent returns the min physical charging current
GetMinPhysicalCurrent() *float64
// GetMaxPhysicalCurrent returns the max physical charging current
GetMaxPhysicalCurrent() *float64
// SetPhysicalCurrentRange sets min/max physical current
SetPhysicalCurrentRange(*float64, *float64) error
// GetMode returns the current charge mode
GetMode() api.ChargeMode

View file

@ -17,18 +17,20 @@ type StaticConfig struct {
type DynamicConfig struct {
// dynamic config
Title string `json:"title"`
DefaultMode string `json:"defaultMode"`
Priority int `json:"priority"`
PhasesConfigured int `json:"phasesConfigured"`
MinCurrent float64 `json:"minCurrent"`
MaxCurrent float64 `json:"maxCurrent"`
SmartCostLimit *float64 `json:"smartCostLimit"`
PlanEnergy float64 `json:"planEnergy"`
PlanTime time.Time `json:"planTime"`
PlanPrecondition int64 `json:"planPrecondition"`
LimitEnergy float64 `json:"limitEnergy"`
LimitSoc int `json:"limitSoc"`
Title string `json:"title"`
DefaultMode string `json:"defaultMode"`
Priority int `json:"priority"`
PhasesConfigured int `json:"phasesConfigured"`
MinCurrent float64 `json:"minCurrent"`
MaxCurrent float64 `json:"maxCurrent"`
MinPhysicalCurrent *float64 `json:"minPhysicalCurrent"`
MaxPhysicalCurrent *float64 `json:"maxPhysicalCurrent"`
SmartCostLimit *float64 `json:"smartCostLimit"`
PlanEnergy float64 `json:"planEnergy"`
PlanTime time.Time `json:"planTime"`
PlanPrecondition int64 `json:"planPrecondition"`
LimitEnergy float64 `json:"limitEnergy"`
LimitSoc int `json:"limitSoc"`
Thresholds ThresholdsConfig `json:"thresholds"`
Soc SocConfig `json:"soc"`
@ -73,6 +75,9 @@ func (payload DynamicConfig) Apply(lp API) error {
err = lp.SetPhasesConfigured(payload.PhasesConfigured)
}
if err == nil {
err = lp.SetPhysicalCurrentRange(payload.MinPhysicalCurrent, payload.MaxPhysicalCurrent)
}
if err == nil && payload.MinCurrent != 0 {
err = lp.SetMinCurrent(payload.MinCurrent)
}

View file

@ -349,6 +349,20 @@ func (mr *MockAPIMockRecorder) GetMaxPhaseCurrent() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhaseCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxPhaseCurrent))
}
// GetMaxPhysicalCurrent mocks base method.
func (m *MockAPI) GetMaxPhysicalCurrent() *float64 {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMaxPhysicalCurrent")
ret0, _ := ret[0].(*float64)
return ret0
}
// GetMaxPhysicalCurrent indicates an expected call of GetMaxPhysicalCurrent.
func (mr *MockAPIMockRecorder) GetMaxPhysicalCurrent() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhysicalCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxPhysicalCurrent))
}
// GetMeterRef mocks base method.
func (m *MockAPI) GetMeterRef() string {
m.ctrl.T.Helper()
@ -377,6 +391,20 @@ func (mr *MockAPIMockRecorder) GetMinCurrent() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinCurrent", reflect.TypeOf((*MockAPI)(nil).GetMinCurrent))
}
// GetMinPhysicalCurrent mocks base method.
func (m *MockAPI) GetMinPhysicalCurrent() *float64 {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMinPhysicalCurrent")
ret0, _ := ret[0].(*float64)
return ret0
}
// GetMinPhysicalCurrent indicates an expected call of GetMinPhysicalCurrent.
func (mr *MockAPIMockRecorder) GetMinPhysicalCurrent() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinPhysicalCurrent", reflect.TypeOf((*MockAPI)(nil).GetMinPhysicalCurrent))
}
// GetMode mocks base method.
func (m *MockAPI) GetMode() api.ChargeMode {
m.ctrl.T.Helper()
@ -870,6 +898,20 @@ func (mr *MockAPIMockRecorder) SetPhasesConfigured(arg0 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhasesConfigured", reflect.TypeOf((*MockAPI)(nil).SetPhasesConfigured), arg0)
}
// SetPhysicalCurrentRange mocks base method.
func (m *MockAPI) SetPhysicalCurrentRange(arg0, arg1 *float64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetPhysicalCurrentRange", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SetPhysicalCurrentRange indicates an expected call of SetPhysicalCurrentRange.
func (mr *MockAPIMockRecorder) SetPhysicalCurrentRange(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhysicalCurrentRange", reflect.TypeOf((*MockAPI)(nil).SetPhysicalCurrentRange), arg0, arg1)
}
// SetPlanEnergy mocks base method.
func (m *MockAPI) SetPlanEnergy(arg0 time.Time, arg1 time.Duration, arg2 float64) error {
m.ctrl.T.Helper()

View file

@ -654,6 +654,10 @@ func (lp *Loadpoint) SetMinCurrent(current float64) error {
lp.Lock()
defer lp.Unlock()
if lp.minPhysicalCurrent != nil && current < *lp.minPhysicalCurrent {
return fmt.Errorf("min current must be greater or equal than min physical current: %.1fA", *lp.minPhysicalCurrent)
}
if current > lp.maxCurrent {
return errors.New("min current must be smaller or equal than max current")
}
@ -690,6 +694,10 @@ func (lp *Loadpoint) SetMaxCurrent(current float64) error {
lp.Lock()
defer lp.Unlock()
if lp.maxPhysicalCurrent != nil && current > *lp.maxPhysicalCurrent {
return fmt.Errorf("max current must be smaller or equal than max physical current: %.1fA", *lp.maxPhysicalCurrent)
}
if current < lp.minCurrent {
return errors.New("max current must be greater or equal than min current")
}
@ -702,6 +710,80 @@ func (lp *Loadpoint) SetMaxCurrent(current float64) error {
return nil
}
// GetMinPhysicalCurrent returns the min physical current
func (lp *Loadpoint) GetMinPhysicalCurrent() *float64 {
lp.RLock()
defer lp.RUnlock()
return lp.getMinPhysicalCurrent()
}
// getMinPhysicalCurrent returns the min physical current
func (lp *Loadpoint) getMinPhysicalCurrent() *float64 {
return lp.minPhysicalCurrent
}
// setMinPhysicalCurrent sets the min physical current (no mutex)
func (lp *Loadpoint) setMinPhysicalCurrent(current *float64) {
lp.minPhysicalCurrent = current
lp.publish(keys.MinPhysicalCurrent, *current)
lp.settings.SetFloatPtr(keys.MinPhysicalCurrent, current)
}
// GetMaxPhysicalCurrent returns the max physical current
func (lp *Loadpoint) GetMaxPhysicalCurrent() *float64 {
lp.RLock()
defer lp.RUnlock()
return lp.getMaxPhysicalCurrent()
}
// getMaxPhysicalCurrent returns the max physical current
func (lp *Loadpoint) getMaxPhysicalCurrent() *float64 {
return lp.maxPhysicalCurrent
}
// setMaxPhysicalCurrent sets the max physical current (no mutex)
func (lp *Loadpoint) setMaxPhysicalCurrent(current *float64) {
lp.maxPhysicalCurrent = current
lp.publish(keys.MaxPhysicalCurrent, *current)
lp.settings.SetFloatPtr(keys.MaxPhysicalCurrent, current)
}
// SetPhysicalCurrentRange sets min/max physical current
func (lp *Loadpoint) SetPhysicalCurrentRange(min, max *float64) error {
lp.Lock()
defer lp.Unlock()
if min != nil && max != nil && *min > *max {
return fmt.Errorf("min physical current must be smaller or equal than max physical current: %.1fA", *max)
}
lp.log.DEBUG.Println("set physical current range:", printPtr("%.1f", min), printPtr("%.1f", max))
lp.setMinPhysicalCurrent(min)
lp.setMaxPhysicalCurrent(max)
// adjust to min limit
if min != nil && lp.minCurrent < *min {
lp.log.DEBUG.Println("adjust min current to comply with min physical current:", *min)
lp.setMinCurrent(*min)
}
if min != nil && lp.maxCurrent < *min {
lp.log.DEBUG.Println("adjust max current to comply with min physical current:", *min)
lp.setMaxCurrent(*min)
}
// adjust to max limit
if max != nil && lp.maxCurrent > *max {
lp.log.DEBUG.Println("adjust max current to comply with max physical current:", *max)
lp.setMaxCurrent(*max)
}
if max != nil && lp.minCurrent > *max {
lp.log.DEBUG.Println("adjust min current to comply with max physical current:", *max)
lp.setMinCurrent(*max)
}
return nil
}
// IsFastChargingActive indicates if fast charging with maximum power is active
func (lp *Loadpoint) IsFastChargingActive() bool {
lp.RLock()

View file

@ -0,0 +1,130 @@
package core
import (
"testing"
evbus "github.com/asaskevich/EventBus"
"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/core/settings"
"github.com/evcc-io/evcc/util"
"github.com/stretchr/testify/assert"
)
func TestSetPhysicalCurrentRange(t *testing.T) {
lp := &Loadpoint{
log: util.NewLogger("test"),
clock: clock.New(),
bus: evbus.New(),
settings: settings.NewDatabaseSettingsAdapter("foo"),
minCurrent: 6,
maxCurrent: 16,
}
// Test with valid range
min, max := 5.0, 32.0
err := lp.SetPhysicalCurrentRange(&min, &max)
assert.NoError(t, err)
assert.Equal(t, min, *lp.getMinPhysicalCurrent())
assert.Equal(t, max, *lp.getMaxPhysicalCurrent())
assert.Equal(t, 6.0, lp.minCurrent) // No adjustment needed
assert.Equal(t, 16.0, lp.maxCurrent) // No adjustment needed
// Test with invalid range (min > max)
min, max = 20.0, 15.0
err = lp.SetPhysicalCurrentRange(&min, &max)
assert.Error(t, err)
// Test with min adjustment needed
min, max = 10.0, 32.0
lp.minCurrent = 6
lp.maxCurrent = 16
err = lp.SetPhysicalCurrentRange(&min, &max)
assert.NoError(t, err)
assert.Equal(t, 10.0, lp.minCurrent) // Should be adjusted to minPhysical
// Test with max adjustment needed
min, max = 5.0, 15.0
lp.minCurrent = 6
lp.maxCurrent = 16
err = lp.SetPhysicalCurrentRange(&min, &max)
assert.NoError(t, err)
assert.Equal(t, 15.0, lp.maxCurrent) // Should be adjusted to maxPhysical
}
func TestSetMinMaxCurrentWithPhysicalLimits(t *testing.T) {
lp := &Loadpoint{
log: util.NewLogger("test"),
clock: clock.New(),
bus: evbus.New(),
settings: settings.NewDatabaseSettingsAdapter("foo"),
minCurrent: 6,
maxCurrent: 16,
}
// Set physical limits
min, max := 5.0, 32.0
err := lp.SetPhysicalCurrentRange(&min, &max)
assert.NoError(t, err)
// Test valid min current
err = lp.SetMinCurrent(10.0)
assert.NoError(t, err)
assert.Equal(t, 10.0, lp.minCurrent)
// Test min current below physical min
err = lp.SetMinCurrent(4.0)
assert.Error(t, err)
// Test valid max current
err = lp.SetMaxCurrent(20.0)
assert.NoError(t, err)
assert.Equal(t, 20.0, lp.maxCurrent)
// Test max current above physical max
err = lp.SetMaxCurrent(40.0)
assert.Error(t, err)
// Test min current > max current
lp.maxCurrent = 15.0
err = lp.SetMinCurrent(16.0)
assert.Error(t, err)
// Test max current < min current
lp.minCurrent = 10.0
err = lp.SetMaxCurrent(9.0)
assert.Error(t, err)
}
func TestPhysicalCurrentRangeAdjustment(t *testing.T) {
lp := &Loadpoint{
log: util.NewLogger("test"),
clock: clock.New(),
bus: evbus.New(),
settings: settings.NewDatabaseSettingsAdapter("foo"),
}
tests := []struct {
minPhysical, maxPhysical float64
min, max, minExpected, maxExpected float64
}{
{5, 32, 10, 16, 10, 16}, // No adjustment needed
{10, 32, 6, 16, 10, 16}, // Min current below physical min
{5, 16, 6, 32, 6, 16}, // Max current above physical max
{10, 20, 6, 32, 10, 20}, // Both currents outside physical range
{5, 16, 20, 32, 16, 16}, // Min current > max physical current
{10, 32, 6, 8, 10, 10}, // Max current < min physical current
}
for _, tc := range tests {
lp.minCurrent = tc.min
lp.maxCurrent = tc.max
minPhysical, maxPhysical := tc.minPhysical, tc.maxPhysical
err := lp.SetPhysicalCurrentRange(&minPhysical, &maxPhysical)
assert.NoError(t, err)
assert.Equal(t, tc.minExpected, lp.minCurrent)
assert.Equal(t, tc.maxExpected, lp.maxCurrent)
}
}

View file

@ -17,11 +17,13 @@ func TestPrioritzer(t *testing.T) {
lo.EXPECT().GetTitle().AnyTimes()
lo.EXPECT().GetPriority().Return(0).AnyTimes() // prio 0
lo.EXPECT().EffectivePriority().Return(0).AnyTimes() // prio 0
lo.EXPECT().GetMaxPhysicalCurrent().Return((*float64)(nil)).AnyTimes()
hi := loadpoint.NewMockAPI(ctrl)
hi.EXPECT().GetTitle().AnyTimes()
hi.EXPECT().GetPriority().Return(1).AnyTimes() // prio 1
hi.EXPECT().EffectivePriority().Return(1).AnyTimes() // prio 1
hi.EXPECT().GetMaxPhysicalCurrent().Return((*float64)(nil)).AnyTimes()
// no additional power available
lo.EXPECT().GetChargePowerFlexibility(nil).Return(300.0)

View file

@ -204,10 +204,10 @@
"energyMeterHelp": "Doplňkový měřič, pokud nabíječka nemá integrovaný.",
"energyMeterLabel": "Elektroměr",
"estimateLabel": "Odhadovat úroveň nabití vozu mezi aktualizacemi z API",
"maxCurrentHelp": "Musí být větší než minimální proud.",
"maxCurrentLabel": "Maximální proud",
"minCurrentHelp": "Pod 6 A jděte pouze tehdy, pokud víte, co děláte.",
"minCurrentLabel": "Minimální proud",
"maxPhysicalCurrentHelp": "Musí být větší než minimální proud.",
"maxPhysicalCurrentLabel": "Maximální proud",
"minPhysicalCurrentHelp": "Pod 6 A jděte pouze tehdy, pokud víte, co děláte.",
"minPhysicalCurrentLabel": "Minimální proud",
"noVehicles": "Není nakonfigurováno žádné vozidlo.",
"phases1p": "1 fáze",
"phases3p": "3 fáze",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Ekstra måler, hvis opladeren ikke har en integreret.",
"energyMeterLabel": "Energi måler",
"estimateLabel": "Interpoler opladningsniveauet mellem API-opdateringer",
"maxCurrentHelp": "Skal være større end minimum strømstyrken.",
"maxCurrentLabel": "Maksimal strømstyrke",
"minCurrentHelp": "Brug kun lavere end 6 A, hvis du er sikker på hvad du gør.",
"minCurrentLabel": "Minimum strømstyrke",
"maxPhysicalCurrentHelp": "Skal være større end minimum strømstyrken.",
"maxPhysicalCurrentLabel": "Maksimal strømstyrke",
"minPhysicalCurrentHelp": "Brug kun lavere end 6 A, hvis du er sikker på hvad du gør.",
"minPhysicalCurrentLabel": "Minimum strømstyrke",
"noVehicles": "Ingen køretøjer er konfigureret.",
"phases1p": "1-fase",
"phases3p": "3-faser",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Zusätzlicher Zähler, wenn die Wallbox keinen integrierten hat.",
"energyMeterLabel": "Energiezähler",
"estimateLabel": "Ladestand zwischen API-Updates interpolieren",
"maxCurrentHelp": "Muss größer als der minimale Strom sein.",
"maxCurrentLabel": "Maximaler Strom",
"minCurrentHelp": "Gehe nur unter 6 A, wenn du weißt, was du tust.",
"minCurrentLabel": "Minimaler Strom",
"maxPhysicalCurrentHelp": "Muss größer als der minimale Strom sein.",
"maxPhysicalCurrentLabel": "Maximaler Strom",
"minPhysicalCurrentHelp": "Warnung: 6 A ist der im IEC Standard vorgeschriebene Mindestwert für das Laden von Elektrofahrzeugen.",
"minPhysicalCurrentLabel": "Minimaler Strom",
"noVehicles": "Keine Fahrzeuge konfiguriert.",
"phases1p": "1-phasig",
"phases3p": "3-phasig",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Πρόσθετος μετρητής εάν ο φορτιστής δεν έχει ενσωματωμένο.",
"energyMeterLabel": "Μετρητής ενέργειας",
"estimateLabel": "Παρεμβολή επιπέδου φόρτισης μεταξύ ενημερώσεων API",
"maxCurrentHelp": "Πρέπει να είναι μεγαλύτερο από την ελάχιστη ένταση.",
"maxCurrentLabel": "Μέγιστη ένταση",
"minCurrentHelp": "Πηγαίνετε κάτω από 6A μόνο εάν ξέρετε τι κάνετε.",
"minCurrentLabel": "Ελάχιστη ένταση",
"maxPhysicalCurrentHelp": "Πρέπει να είναι μεγαλύτερο από την ελάχιστη ένταση.",
"maxPhysicalCurrentLabel": "Μέγιστη ένταση",
"minPhysicalCurrentHelp": "Πηγαίνετε κάτω από 6A μόνο εάν ξέρετε τι κάνετε.",
"minPhysicalCurrentLabel": "Ελάχιστη ένταση",
"noVehicles": "Δεν έχουν προστεθεί οχήματα.",
"phases1p": "μονοφασικό",
"phases3p": "τριφασικό",

View file

@ -186,11 +186,12 @@
"chargerError": "Configuring a charger is required.",
"chargerLabel": "Charger",
"chargerPower11kw": "11 kW",
"chargerPower11kwHelp": "Will use a current range of 6 to 16 A.",
"chargerPower11kwHelp": "Use 6 to 16 A.",
"chargerPower22kw": "22 kW",
"chargerPower22kwHelp": "Will use a current range of 6 to 32 A.",
"chargerPower22kwHelp": "Use 6 to 32 A.",
"chargerPowerCustom": "other",
"chargerPowerCustomHelp": "Define a custom current range.",
"chargerPowerCustomHelp": "Custom current range.",
"chargerPowerHelp": "Allowed current range. Limits the user-selectable options in UI and API.",
"chargerTypeLabel": "Charger type",
"chargingTitle": "Charging",
"circuitHelp": "Load management assignment to ensure power and current limits are not exceeded.",
@ -205,10 +206,10 @@
"energyMeterHelp": "Additional meter if the charger doesn't have an integrated one.",
"energyMeterLabel": "Energy meter",
"estimateLabel": "Interpolate charge level between API updates",
"maxCurrentHelp": "Must be greater than minimum current.",
"maxCurrentLabel": "Maximum current",
"minCurrentHelp": "Only go below 6 A if you know what you're doing.",
"minCurrentLabel": "Minimum current",
"maxPhysicalCurrentHelp": "Must be greater than minimum current.",
"maxPhysicalCurrentLabel": "Maximum current",
"minPhysicalCurrentHelp": "Warning: 6 A is the minimum value defined by the IEC standard for EV charging.",
"minPhysicalCurrentLabel": "Minimum current",
"noVehicles": "No vehicles are configured.",
"phases1p": "1-phase",
"phases3p": "3-phase",

View file

@ -190,10 +190,10 @@
"energyMeterHelp": "Lisämittari, jos laturissa ei ole integroitua energiamittaria.",
"energyMeterLabel": "Energiamittari",
"estimateLabel": "Arvioi lataustaso API-päivitysten välillä",
"maxCurrentHelp": "On oltava suurempi kuin minimivirta.",
"maxCurrentLabel": "Maksimivirta",
"minCurrentHelp": "Valitse alle 6 A vain, jos tiedät mitä olet tekemässä.",
"minCurrentLabel": "Minimivirta",
"maxPhysicalCurrentHelp": "On oltava suurempi kuin minimivirta.",
"maxPhysicalCurrentLabel": "Maksimivirta",
"minPhysicalCurrentHelp": "Valitse alle 6 A vain, jos tiedät mitä olet tekemässä.",
"minPhysicalCurrentLabel": "Minimivirta",
"noVehicles": "Ajoneuvoja ei ole määritetty.",
"phases1p": "1-vaihe",
"phases3p": "3-vaihe",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Compteur supplémentaire si le chargeur n’en a pas un intégré.",
"energyMeterLabel": "Compteur d’énergie",
"estimateLabel": "Interpoler le niveau de charge entre deux mises à jour via l’API",
"maxCurrentHelp": "Doit être supérieur au courant minimum.",
"maxCurrentLabel": "Courant maximum",
"minCurrentHelp": "Allez en dessous de 6 A seulement si vous savez ce que vous faîtes.",
"minCurrentLabel": "Courant minimum",
"maxPhysicalCurrentHelp": "Doit être supérieur au courant minimum.",
"maxPhysicalCurrentLabel": "Courant maximum",
"minPhysicalCurrentHelp": "Allez en dessous de 6 A seulement si vous savez ce que vous faîtes.",
"minPhysicalCurrentLabel": "Courant minimum",
"noVehicles": "Aucun véhicule n’est configuré.",
"phases1p": "Monophasé",
"phases3p": "Triphasé",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Dodatni strujomjer ako punjač nema integrirani.",
"energyMeterLabel": "Strujomjer",
"estimateLabel": "Intepolirati razine punjenja između API očitavanja",
"maxCurrentHelp": "Mora biti veće od minimalne struje.",
"maxCurrentLabel": "Maksimalna struja",
"minCurrentHelp": "Postavite ispod 6A samo ako znate što radite.",
"minCurrentLabel": "Minimalna struja",
"maxPhysicalCurrentHelp": "Mora biti veće od minimalne struje.",
"maxPhysicalCurrentLabel": "Maksimalna struja",
"minPhysicalCurrentHelp": "Postavite ispod 6A samo ako znate što radite.",
"minPhysicalCurrentLabel": "Minimalna struja",
"noVehicles": "Niti jedno vozilo nije postavljeno.",
"phases1p": "1 faza",
"phases3p": "3 faze",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "További fogyasztásmérő, ha a töltő nem rendelkezik egy integrálttal.",
"energyMeterLabel": "Fogyasztásmérő",
"estimateLabel": "Interpolálja a töltési szintet az API frissítések között",
"maxCurrentHelp": "Nagyobbnak kell lennie mint a minimális áram.",
"maxCurrentLabel": "Maximum áram",
"minCurrentHelp": "Csak akkor menj 6 A alá, ha tudod, hogy mit csinálsz.",
"minCurrentLabel": "Minimum áram",
"maxPhysicalCurrentHelp": "Nagyobbnak kell lennie mint a minimális áram.",
"maxPhysicalCurrentLabel": "Maximum áram",
"minPhysicalCurrentHelp": "Csak akkor menj 6 A alá, ha tudod, hogy mit csinálsz.",
"minPhysicalCurrentLabel": "Minimum áram",
"noVehicles": "Nincs jármű konfigurálva.",
"phases1p": "1-fázis",
"phases3p": "3-fázis",

View file

@ -196,10 +196,10 @@
"energyMeterHelp": "Contatore supplementare se il caricabatterie non ne possiede uno integrato.",
"energyMeterLabel": "Misuratore di energia",
"estimateLabel": "Stima il livello di carica tra gli aggiornamenti dell'API",
"maxCurrentHelp": "Deve essere maggiore della corrente minima.",
"maxCurrentLabel": "Corrente massima",
"minCurrentHelp": "Scendere al di sotto dei 6A solo se si sa quello che si sta facendo.",
"minCurrentLabel": "Corrente minima",
"maxPhysicalCurrentHelp": "Deve essere maggiore della corrente minima.",
"maxPhysicalCurrentLabel": "Corrente massima",
"minPhysicalCurrentHelp": "Scendere al di sotto dei 6A solo se si sa quello che si sta facendo.",
"minPhysicalCurrentLabel": "Corrente minima",
"noVehicles": "Nessun veicolo configurato.",
"phases1p": "Monofase",
"phases3p": "Trifase",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Zousätzlechen Zieler, falls d'Wallbox keen integréierten huet.",
"energyMeterLabel": "Energiezieler",
"estimateLabel": "Luedstand zwëschen API-Updates interpoléieren",
"maxCurrentHelp": "Muss méi grouss wéi de minimale Stroum sinn.",
"maxCurrentLabel": "Maximale Stroum",
"minCurrentHelp": "Géi nëmmen ënner 6 A, wann s du weess, wat s du mëss.",
"minCurrentLabel": "Minimale Stroum",
"maxPhysicalCurrentHelp": "Muss méi grouss wéi de minimale Stroum sinn.",
"maxPhysicalCurrentLabel": "Maximale Stroum",
"minPhysicalCurrentHelp": "Géi nëmmen ënner 6 A, wann s du weess, wat s du mëss.",
"minPhysicalCurrentLabel": "Minimale Stroum",
"noVehicles": "Kee Gefier konfiguréiert.",
"phases1p": "1-phaseg",
"phases3p": "3-phaseg",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Papildomas skaitiklis, jei įkroviklyje nėra integruoto.",
"energyMeterLabel": "Energijos skaitiklis",
"estimateLabel": "Interpoliuoti įkrovos lygį tarp API naujinimų",
"maxCurrentHelp": "Turi būti didesnė nei minimali srovė.",
"maxCurrentLabel": "Max srovė",
"minCurrentHelp": "Tik jei žinote, ką darote, leiskite žemiau 6 A.",
"minCurrentLabel": "Min srovė",
"maxPhysicalCurrentHelp": "Turi būti didesnė nei minimali srovė.",
"maxPhysicalCurrentLabel": "Max srovė",
"minPhysicalCurrentHelp": "Tik jei žinote, ką darote, leiskite žemiau 6 A.",
"minPhysicalCurrentLabel": "Min srovė",
"noVehicles": "Nėra sukonfigūruotų automobilių.",
"phases1p": "1-fazė",
"phases3p": "3-fazės",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Extra meter voor laadstations zonder geïntegreerde meter.",
"energyMeterLabel": "Energiemeter",
"estimateLabel": "“Interpoleer batterijpercentage tussen API updates”",
"maxCurrentHelp": "Moet groter zijn dan de minimum stroom.",
"maxCurrentLabel": "“Maximale stroom”",
"minCurrentHelp": "Kies 6 A enkel wanneer je weet wat je aan het doen bent.",
"minCurrentLabel": "“Minimale stroom”",
"maxPhysicalCurrentHelp": "Moet groter zijn dan de minimum stroom.",
"maxPhysicalCurrentLabel": "“Maximale stroom”",
"minPhysicalCurrentHelp": "Kies 6 A enkel wanneer je weet wat je aan het doen bent.",
"minPhysicalCurrentLabel": "“Minimale stroom”",
"noVehicles": "Geen voertuigen ingesteld.",
"phases1p": "“1-fase”",
"phases3p": "“3-fase”",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Contador adicional se o Carregador não tiver um integrado.",
"energyMeterLabel": "Contador de energia",
"estimateLabel": "Interpolar o nível de carga entre actualizações da API",
"maxCurrentHelp": "Deve ser superior à Corrente mínima.",
"maxCurrentLabel": "Corrente máxima",
"minCurrentHelp": "Defina apenas abaixo dos 6 A, se souber o que está a fazer.",
"minCurrentLabel": "Corrente mínima",
"maxPhysicalCurrentHelp": "Deve ser superior à Corrente mínima.",
"maxPhysicalCurrentLabel": "Corrente máxima",
"minPhysicalCurrentHelp": "Defina apenas abaixo dos 6 A, se souber o que está a fazer.",
"minPhysicalCurrentLabel": "Corrente mínima",
"noVehicles": "Nenhum veículo configurado.",
"phases1p": "Monofásico",
"phases3p": "Trifásico",

View file

@ -197,10 +197,10 @@
"energyMeterHelp": "Dodatni števec, če polnilec nima vgrajenega.",
"energyMeterLabel": "Energijski števec",
"estimateLabel": "Interpoliraj raven zaračunavanja med posodobitvami API-ja",
"maxCurrentHelp": "Mora biti večji od minimalnega toka.",
"maxCurrentLabel": "Maksimalni tok",
"minCurrentHelp": "Pod 6 A pojdi samo, če veš, kaj delaš.",
"minCurrentLabel": "Minimalni tok",
"maxPhysicalCurrentHelp": "Mora biti večji od minimalnega toka.",
"maxPhysicalCurrentLabel": "Maksimalni tok",
"minPhysicalCurrentHelp": "Pod 6 A pojdi samo, če veš, kaj delaš.",
"minPhysicalCurrentLabel": "Minimalni tok",
"noVehicles": "Nobeno vozilo ni konfigurirano.",
"phases1p": "1-fazni",
"phases3p": "3-fazni",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Extern mätare (Om laddaren inte har en integrerad mätare).",
"energyMeterLabel": "Energimätare",
"estimateLabel": "Interpolera laddningsnivå mellan API uppdateringar",
"maxCurrentHelp": "Måste vara högre än minimi strömstyrka.",
"maxCurrentLabel": "Maximal strömstyrka",
"minCurrentHelp": "Ange endast under 6 A om du vet vad du gör.",
"minCurrentLabel": "Minimum strömstyrka",
"maxPhysicalCurrentHelp": "Måste vara högre än minimi strömstyrka.",
"maxPhysicalCurrentLabel": "Maximal strömstyrka",
"minPhysicalCurrentHelp": "Ange endast under 6 A om du vet vad du gör.",
"minPhysicalCurrentLabel": "Minimum strömstyrka",
"noVehicles": "Inga fordon är konfigurerade.",
"phases1p": "1-fas",
"phases3p": "3-fas",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Doldurma cihazında bütünleşik bir sayaç yoksa ek sayaç.",
"energyMeterLabel": "“Enerji sayacı”",
"estimateLabel": "“API güncellemeleri arasında doldurma seviyesini hesapla”",
"maxCurrentHelp": "Asgari akımdan büyük olmalı.",
"maxCurrentLabel": "“Azami akım”",
"minCurrentHelp": "Sadece ne yaptığını biliyorsan 6 A altına in.",
"minCurrentLabel": "“Asgari akım”",
"maxPhysicalCurrentHelp": "Asgari akımdan büyük olmalı.",
"maxPhysicalCurrentLabel": "“Azami akım”",
"minPhysicalCurrentHelp": "Sadece ne yaptığını biliyorsan 6 A altına in.",
"minPhysicalCurrentLabel": "“Asgari akım”",
"noVehicles": "Hiç araç yapılandırılmamış.",
"phases1p": "“1 aşamalı”",
"phases3p": "“3 aşamalı”",

View file

@ -205,10 +205,10 @@
"energyMeterHelp": "Додатковий лічильник, якщо зарядний пристрій не має вбудованого.",
"energyMeterLabel": "Енерголічильник",
"estimateLabel": "Інтерполювати рівень оплати між оновленнями API",
"maxCurrentHelp": "Повинен бути більшим за мінімальний струм.",
"maxCurrentLabel": "Максимальний струм",
"minCurrentHelp": "Опускайтеся нижче 6 А, лише якщо знаєте, що робите.",
"minCurrentLabel": "Мінімальний струм",
"maxPhysicalCurrentHelp": "Повинен бути більшим за мінімальний струм.",
"maxPhysicalCurrentLabel": "Максимальний струм",
"minPhysicalCurrentHelp": "Опускайтеся нижче 6 А, лише якщо знаєте, що робите.",
"minPhysicalCurrentLabel": "Мінімальний струм",
"noVehicles": "Транспортні засоби не налаштовані.",
"phases1p": "1-фаза",
"phases3p": "3-фазний",

View file

@ -29,20 +29,22 @@ func getLoadpointStaticConfig(lp loadpoint.API) loadpoint.StaticConfig {
func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig {
planTime, planPrecondition, planEnergy := lp.GetPlanEnergy()
return loadpoint.DynamicConfig{
Title: lp.GetTitle(),
DefaultMode: string(lp.GetDefaultMode()),
Priority: lp.GetPriority(),
PhasesConfigured: lp.GetPhasesConfigured(),
MinCurrent: lp.GetMinCurrent(),
MaxCurrent: lp.GetMaxCurrent(),
SmartCostLimit: lp.GetSmartCostLimit(),
Thresholds: lp.GetThresholds(),
Soc: lp.GetSocConfig(),
PlanEnergy: planEnergy,
PlanTime: planTime,
PlanPrecondition: int64(planPrecondition.Seconds()),
LimitEnergy: lp.GetLimitEnergy(),
LimitSoc: lp.GetLimitSoc(),
Title: lp.GetTitle(),
DefaultMode: string(lp.GetDefaultMode()),
Priority: lp.GetPriority(),
PhasesConfigured: lp.GetPhasesConfigured(),
MinCurrent: lp.GetMinCurrent(),
MaxCurrent: lp.GetMaxCurrent(),
MinPhysicalCurrent: lp.GetMinPhysicalCurrent(),
MaxPhysicalCurrent: lp.GetMaxPhysicalCurrent(),
SmartCostLimit: lp.GetSmartCostLimit(),
Thresholds: lp.GetThresholds(),
Soc: lp.GetSocConfig(),
PlanEnergy: planEnergy,
PlanTime: planTime,
PlanPrecondition: int64(planPrecondition.Seconds()),
LimitEnergy: lp.GetLimitEnergy(),
LimitSoc: lp.GetLimitSoc(),
}
}

View file

@ -80,6 +80,21 @@ test.describe("loadpoint settings", async () => {
await expect(page.getByText("~ 3.7 kW")).toBeVisible();
await expect(page.getByText("~ 1.4 kW")).toBeVisible();
});
test("min/max current", async ({ page }) => {
await page.getByTestId("loadpoint-settings-button").last().click();
const max = page.getByLabel("Max. current");
const min = page.getByLabel("Min. current");
await expect(max).toHaveValue("16");
await expect(min).toHaveValue("6");
// default option range, no physical current limit
await expect(max.locator("option[value='100']")).toHaveCount(0); // not selectable by default
await expect(max.locator("option[value='64']")).toHaveCount(1);
await expect(max.locator("option[value='32']")).toHaveCount(1);
await expect(min.locator("option[value='5']")).toHaveCount(1);
await expect(min.locator("option[value='0.125']")).toHaveCount(1);
});
});
test.describe("network requests", async () => {

View file

@ -417,6 +417,91 @@ test.describe("loadpoint", async () => {
).toBeVisible();
});
test("physical current", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
/**
* 6A to 16A (default 11kW)
*/
// add loadpoint, add charger
await newLoadpoint(page, "Carport");
await addDemoCharger(page);
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
// go do main ui (/) and open loadpoint settings modal
await page.goto("/");
await page.getByTestId("loadpoint-settings-button").last().click();
const settingsModal = page.getByTestId("loadpoint-settings-modal");
await expectModalVisible(settingsModal);
const max = settingsModal.getByLabel("Max. current");
const min = settingsModal.getByLabel("Min. current");
await expect(max).toHaveValue("16");
await expect(min).toHaveValue("6");
// no out of range values
await expect(max.locator("option[value='32']")).toHaveCount(0);
await expect(min.locator("option[value='5']")).toHaveCount(0);
// avoid overlap by reducing options
await expect(max.locator("option[value='7']")).toHaveCount(1);
await expect(min.locator("option[value='15']")).toHaveCount(1);
await max.selectOption("14 A");
await min.selectOption("8 A");
await expect(max.locator("option[value='7']")).toHaveCount(0);
await expect(min.locator("option[value='15']")).toHaveCount(0);
await settingsModal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(settingsModal);
await page.reload();
// check persistance
await page.getByTestId("loadpoint-settings-button").last().click();
await expectModalVisible(settingsModal);
await expect(max).toHaveValue("14");
await expect(min).toHaveValue("8");
/**
* range 0.5A to 10A (e.g. heating)
*/
await page.goto("/#/config");
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByTestId("chargerPower-other").click();
await lpModal.getByLabel("Minimum current").fill("0.5");
await lpModal.getByLabel("Maximum current").fill("10");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint settings
await page.goto("/");
await page.getByTestId("loadpoint-settings-button").last().click();
await expectModalVisible(settingsModal);
await expect(max).toHaveValue("10"); // adjusted max
await expect(min).toHaveValue("8"); // kept min
// check range
await expect(max.locator("option[value='11']")).toHaveCount(0);
await expect(max.locator("option[value='10']")).toHaveCount(1);
await expect(min.locator("option[value='1']")).toHaveCount(1);
await expect(min.locator("option[value='0.5']")).toHaveCount(1);
await expect(min.locator("option[value='0.25']")).toHaveCount(0);
});
test("user-defined charger", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");