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}…"