diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue
index c79e3ac97..ddceb4c80 100644
--- a/assets/js/components/Loadpoint.vue
+++ b/assets/js/components/Loadpoint.vue
@@ -197,12 +197,15 @@ export default {
phaseRemaining: Number,
pvRemaining: Number,
pvAction: String,
+ guardRemaining: Number,
+ guardAction: String,
},
data() {
return {
tickerHandler: null,
phaseRemainingInterpolated: this.phaseRemaining,
pvRemainingInterpolated: this.pvRemaining,
+ guardRemainingInterpolated: this.guardRemaining,
chargeDurationInterpolated: this.chargeDuration,
chargeRemainingDurationInterpolated: this.chargeRemainingDuration,
};
@@ -237,6 +240,9 @@ export default {
pvRemaining() {
this.pvRemainingInterpolated = this.pvRemaining;
},
+ guardRemaining() {
+ this.guardRemainingInterpolated = this.guardRemaining;
+ },
chargeDuration() {
this.chargeDurationInterpolated = this.chargeDuration;
},
@@ -258,6 +264,9 @@ export default {
if (this.pvRemainingInterpolated > 0) {
this.pvRemainingInterpolated--;
}
+ if (this.guardRemainingInterpolated > 0) {
+ this.guardRemainingInterpolated--;
+ }
if (this.chargeDurationInterpolated > 0 && this.charging) {
this.chargeDurationInterpolated++;
}
diff --git a/assets/js/components/Vehicle.story.vue b/assets/js/components/Vehicle.story.vue
index eccbe30d2..ef32c8e9d 100644
--- a/assets/js/components/Vehicle.story.vue
+++ b/assets/js/components/Vehicle.story.vue
@@ -100,6 +100,9 @@ const hoursFromNow = function (hours) {
:pvRemainingInterpolated="155"
/>
+
+
+
(data ? `:${JSON.stringify(data)}` : "");
+config.global.mocks["$t"] = (key, data) => `${key}${serializeData(data)}`;
+config.global.mocks["$i18n"] = { locale: "de-DE" };
+
+const expectStatus = (props, messageKey, data) => {
+ const wrapper = mount(VehicleStatus, { props });
+ expect(wrapper.find("div").text()).eq(`main.vehicleStatus.${messageKey}${serializeData(data)}`);
+};
+
+describe("basics", () => {
+ test("no vehicle is connected", () => {
+ expectStatus({ connected: false }, "disconnected");
+ });
+ test("vehicle is connected", () => {
+ expectStatus({ connected: true }, "connected");
+ });
+ test("show waiting for vehicle if charger is enabled but not charging", () => {
+ expectStatus({ enabled: true, connected: true }, "waitForVehicle");
+ });
+ test("vehicle is charging", () => {
+ expectStatus({ connected: true, charging: true }, "charging");
+ });
+});
+
+describe("min charge", () => {
+ test("active when vehicle soc is below", () => {
+ expectStatus({ connected: true, minSoc: 20, vehicleSoc: 10 }, "minCharge", { soc: 20 });
+ });
+ test("not active when vehicle soc is above", () => {
+ expectStatus({ connected: true, minSoc: 20, vehicleSoc: 21 }, "connected");
+ });
+ test("not active when vehicle soc is equal", () => {
+ expectStatus({ connected: true, minSoc: 20, vehicleSoc: 20 }, "connected");
+ });
+ test("not active when limit is 0", () => {
+ expectStatus({ connected: true, minSoc: 0, vehicleSoc: 10 }, "connected");
+ });
+});
+
+describe("target charge", () => {
+ const targetTime = "2020-03-16T06:00:00Z";
+ const planProjectedStart = "2020-03-16T02:00:00Z";
+ test("active if target time is set and status is charging", () => {
+ expectStatus({ targetTime, charging: true, connected: true }, "targetChargeActive");
+ });
+ test("waiting for vehicle if a target time is set, the charger is enabled but not charging", () => {
+ expectStatus({ targetTime, enabled: true, connected: true }, "targetChargeWaitForVehicle");
+ });
+ test("show projected start if not enabled yet", () => {
+ expectStatus({ targetTime, planProjectedStart, connected: true }, "targetChargePlanned", {
+ time: "Mo 03:00",
+ });
+ });
+});
+
+describe("timer", () => {
+ test("show pv enable timer if not enabled yet and timer exists", () => {
+ expectStatus(
+ {
+ pvAction: "enable",
+ connected: true,
+ pvRemainingInterpolated: 90,
+ },
+ "pvEnable",
+ { remaining: "1:30m" }
+ );
+ });
+ test("don't show pv enable timer if value is zero", () => {
+ expectStatus(
+ {
+ pvAction: "enable",
+ connected: true,
+ pvRemainingInterpolated: 0,
+ },
+ "connected"
+ );
+ });
+ test("show pv disable timer if charging and timer exists", () => {
+ expectStatus(
+ {
+ pvAction: "disable",
+ connected: true,
+ charging: true,
+ pvRemainingInterpolated: 90,
+ },
+ "pvDisable",
+ { remaining: "1:30m" }
+ );
+ });
+ test("show phase enable timer if it exists", () => {
+ expectStatus(
+ {
+ phaseAction: "scale1p",
+ connected: true,
+ charging: true,
+ phaseRemainingInterpolated: 90,
+ },
+ "scale1p",
+ { remaining: "1:30m" }
+ );
+ });
+ test("show phase disable timer if it exists", () => {
+ expectStatus(
+ {
+ phaseAction: "scale3p",
+ connected: true,
+ charging: true,
+ phaseRemainingInterpolated: 90,
+ },
+ "scale3p",
+ { remaining: "1:30m" }
+ );
+ });
+ test("show guard timer if it exists", () => {
+ expectStatus(
+ {
+ guardAction: "enable",
+ connected: true,
+ guardRemainingInterpolated: 90,
+ },
+ "guard",
+ { remaining: "1:30m" }
+ );
+ });
+ test("don't show guard timer if charging", () => {
+ expectStatus(
+ {
+ guardAction: "enable",
+ connected: true,
+ charging: true,
+ guardRemainingInterpolated: 90,
+ },
+ "charging"
+ );
+ });
+});
+
+describe("vehicle target soc", () => {
+ test("show target reached if charger enabled but soc has reached vehicle limit", () => {
+ expectStatus(
+ {
+ connected: true,
+ enabled: true,
+ vehicleTargetSoc: 70,
+ vehicleSoc: 70,
+ },
+ "vehicleTargetReached",
+ { soc: 70 }
+ );
+ });
+ test("show reached message even if vehicle is slightly below its limit", () => {
+ expectStatus(
+ {
+ connected: true,
+ enabled: true,
+ vehicleTargetSoc: 70,
+ vehicleSoc: 69,
+ },
+ "vehicleTargetReached",
+ { soc: 70 }
+ );
+ });
+});
diff --git a/assets/js/components/VehicleStatus.vue b/assets/js/components/VehicleStatus.vue
index 029c454be..7b8d1e3a0 100644
--- a/assets/js/components/VehicleStatus.vue
+++ b/assets/js/components/VehicleStatus.vue
@@ -21,6 +21,8 @@ export default {
phaseRemainingInterpolated: Number,
pvAction: String,
pvRemainingInterpolated: Number,
+ guardAction: String,
+ guardRemainingInterpolated: Number,
targetChargeDisabled: Boolean,
},
computed: {
@@ -35,6 +37,9 @@ export default {
this.pvRemainingInterpolated > 0 && ["enable", "disable"].includes(this.pvAction)
);
},
+ guardTimerActive() {
+ return this.guardRemainingInterpolated > 0 && this.guardAction === "enable";
+ },
message: function () {
const t = (key, data) => {
return this.$t(`main.vehicleStatus.${key}`, data);
@@ -43,7 +48,6 @@ export default {
if (!this.connected) {
return t("disconnected");
}
-
// min charge active
if (this.minSoc > 0 && this.vehicleSoc < this.minSoc) {
return t("minCharge", { soc: this.minSoc });
@@ -93,6 +97,12 @@ export default {
return t("charging");
}
+ if (this.guardTimerActive) {
+ return t("guard", {
+ remaining: this.fmtShortDuration(this.guardRemainingInterpolated, true),
+ });
+ }
+
return t("connected");
},
},
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 887bd336a..ac5e62279 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -39,6 +39,9 @@ const (
pvEnable = "enable"
pvDisable = "disable"
+ guardTimer = "guard"
+ guardEnable = "enable"
+
phaseTimer = "phase"
phaseScale1p = "scale1p"
phaseScale3p = "scale3p"
@@ -562,6 +565,7 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
lp.publish(phasesActive, lp.activePhases())
lp.publishTimer(phaseTimer, 0, timerInactive)
lp.publishTimer(pvTimer, 0, timerInactive)
+ lp.publishTimer(guardTimer, 0, timerInactive)
// charger features
for _, f := range []api.Feature{api.IntegratedDevice} {
@@ -652,9 +656,10 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error {
// set enabled/disabled
if enabled := chargeCurrent >= lp.GetMinCurrent(); enabled != lp.enabled {
if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 && !force {
- lp.log.DEBUG.Printf("charger %s: contactor delay %v", status[enabled], remaining)
+ lp.publishTimer(guardTimer, lp.GuardDuration, guardEnable)
return nil
}
+ lp.elapseGuard()
// remote stop
// TODO https://github.com/evcc-io/evcc/discussions/1929
@@ -877,9 +882,9 @@ func (lp *Loadpoint) elapsePVTimer() {
lp.log.DEBUG.Printf("pv timer elapse")
lp.pvTimer = elapsed
- lp.guardUpdated = elapsed
-
lp.publishTimer(pvTimer, 0, timerInactive)
+
+ lp.elapseGuard()
}
// resetPVTimer resets the pv enable/disable timer to disabled state
@@ -1047,6 +1052,9 @@ func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action strin
if name == phaseTimer {
timer = lp.phaseTimer
}
+ if name == guardTimer {
+ timer = lp.guardUpdated
+ }
remaining := delay - lp.clock.Since(timer)
if remaining < 0 {
@@ -1401,6 +1409,14 @@ func (lp *Loadpoint) publishSocAndRange() {
}
}
+func (lp *Loadpoint) elapseGuard() {
+ if lp.guardUpdated != elapsed {
+ lp.log.DEBUG.Print("charger: guard elapse")
+ lp.guardUpdated = elapsed
+ lp.publishTimer(guardTimer, 0, timerInactive)
+ }
+}
+
// addTask adds a single task to the queue
func (lp *Loadpoint) addTask(task func()) {
// test guard
diff --git a/i18n/de.toml b/i18n/de.toml
index 064dd4065..84d172ce2 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -176,20 +176,21 @@ ready = "bereit"
vehicleTarget = "Fahrzeuglimit: {soc}%"
[main.vehicleStatus]
-charging = "Ladevorgang aktiv..."
+charging = "Ladevorgang aktiv…"
connected = "Verbunden."
disconnected = "Nicht verbunden."
+guard = "Starte in {remaining}…"
minCharge = "Mindestladung bis {soc}%."
-pvDisable = "Zu wenig Überschuss. Pausiere in {remaining}."
-pvEnable = "Überschuss verfügbar. Starte in {remaining}."
-scale1p = "Reduziere auf einphasig in {remaining}."
-scale3p = "Erhöhe auf dreiphasig in {remaining}."
-targetChargeActive = "Zielladen aktiv."
+pvDisable = "Zu wenig Überschuss. Pausiere in {remaining}…"
+pvEnable = "Überschuss verfügbar. Starte in {remaining}…"
+scale1p = "Reduziere auf einphasig in {remaining}…"
+scale3p = "Erhöhe auf dreiphasig in {remaining}…"
+targetChargeActive = "Zielladen aktiv…"
targetChargePlanned = "Zielladen geplant. Ladung startet {time} Uhr."
-targetChargeWaitForVehicle = "Zielladen bereit. Warte auf Fahrzeug..."
+targetChargeWaitForVehicle = "Zielladen bereit. Warte auf Fahrzeug…"
unknown = ""
vehicleTargetReached = "Fahrzeuglimit {soc}% erreicht."
-waitForVehicle = "Ladebereit. Warte auf Fahrzeug..."
+waitForVehicle = "Ladebereit. Warte auf Fahrzeug…"
[notifications]
dismissAll = "Meldungen entfernen"
diff --git a/i18n/en.toml b/i18n/en.toml
index 538428e53..ae8bbbfee 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -179,6 +179,7 @@ vehicleTarget = "Vehicle limit: {soc}%"
charging = "Charging…"
connected = "Connected."
disconnected = "Disconnected."
+guard = "Starting in {remaining}…"
minCharge = "Minimum charging to {soc}%."
pvDisable = "Not enough surplus. Pausing in {remaining}…"
pvEnable = "Surplus available. Starting in {remaining}…"