-
-
-
-
-
-
-
- {{ $t("main.loadpointSettings.currents") }}
-
-
-
-
-
-
- {{
- $t(
- "main.loadpointSettings.phasesConfigured.no1p3pSupport"
- )
- }}
-
-
-
-
-
-
-
-
-
-
-
-
- ~ {{ maxPower }}
-
-
-
-
-
-
-
- ~ {{ minPower }}
-
-
-
+
+
+
+
+
+
+ {{ $t("main.loadpointSettings.currents") }}
+
+
+
+
+
+
+ {{ $t("main.loadpointSettings.phasesConfigured.no1p3pSupport") }}
+
+
+
+
+
+
+
+
+
+ ~ {{ maxPower }}
+
+
+
+
+
+
+
+ ~ {{ minPower }}
+
+
-
+
diff --git a/assets/js/components/Site/Site.vue b/assets/js/components/Site/Site.vue
index e7d2dd686..7b90e3f7e 100644
--- a/assets/js/components/Site/Site.vue
+++ b/assets/js/components/Site/Site.vue
@@ -50,8 +50,11 @@
:loadpoints="loadpoints"
:vehicles="vehicleList"
:smartCostType="smartCostType"
+ :smartCostAvailable="smartCostAvailable"
+ :smartFeedInPriorityAvailable="smartFeedInPriorityAvailable"
:tariffGrid="tariffGrid"
:tariffCo2="tariffCo2"
+ :tariffFeedIn="tariffFeedIn"
:currency="currency"
:gridConfigured="gridConfigured"
:pvConfigured="pvConfigured"
@@ -146,6 +149,8 @@ export default defineComponent({
uploadProgress: Number,
sponsor: { type: Object as PropType
, default: () => ({}) },
smartCostType: String as PropType,
+ smartCostAvailable: Boolean,
+ smartFeedInPriorityAvailable: Boolean,
fatal: Object,
forecast: Object as PropType,
},
diff --git a/assets/js/components/Tariff/SmartCostLimit.vue b/assets/js/components/Tariff/SmartCostLimit.vue
index 8f86d386e..1ee997f75 100644
--- a/assets/js/components/Tariff/SmartCostLimit.vue
+++ b/assets/js/components/Tariff/SmartCostLimit.vue
@@ -1,382 +1,99 @@
-
-
{{ title }}
-
- {{ description }}
-
-
-
-
-
-
-
-
{{ $t("smartCost.saved") }}
-
-
-
-
-
-
- {{ $t("smartCost.activeHoursLabel") }}
-
-
- {{
- $t("smartCost.activeHours", {
- total: totalSlots.length,
- charging: chargingSlots.length,
- })
- }}
-
-
-
-
- {{ activeSlotName }}
- {{ $t("smartCost.co2Label") }}
- {{ $t("smartCost.priceLabel") }}
-
-
- {{ activeSlotCost }}
-
-
- {{ chargingCostRange }}
-
-
- {{ totalCostRange }}
-
-
-
-
-
-
+
-
-
diff --git a/assets/js/components/Tariff/SmartFeedInPriority.vue b/assets/js/components/Tariff/SmartFeedInPriority.vue
new file mode 100644
index 000000000..4f1b0896e
--- /dev/null
+++ b/assets/js/components/Tariff/SmartFeedInPriority.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/assets/js/components/Tariff/SmartTariffBase.vue b/assets/js/components/Tariff/SmartTariffBase.vue
new file mode 100644
index 000000000..91af519d6
--- /dev/null
+++ b/assets/js/components/Tariff/SmartTariffBase.vue
@@ -0,0 +1,394 @@
+
+
+
{{ title }}
+
+ {{ description }}
+
+
+
+
+
+
+
+
{{ $t("smartCost.saved") }}
+
+
+
+
+
+
+ {{ activeHoursLabel }}
+
+
+ {{ activeHoursText }}
+
+
+
+
+ {{ activeSlotName }}
+ {{ currentPriceLabel }}
+
+
+ {{ activeSlotCost }}
+
+
+ {{ fmtActiveCostRange }}
+
+
+ {{ fmtTotalCostRange }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/js/components/Vehicles/Status.vue b/assets/js/components/Vehicles/Status.vue
index 51a991250..8871b0929 100644
--- a/assets/js/components/Vehicles/Status.vue
+++ b/assets/js/components/Vehicles/Status.vue
@@ -8,171 +8,41 @@
{{ chargerStatus }}
-
-
-
-
-
{{ fmtDuration(pvRemainingInterpolated) }}
-
-
-
-
{{ fmtDuration(phaseRemainingInterpolated) }}
-
-
-
-
-
- {{ fmtTemperature(vehicleLimitSoc) }}
-
-
-
-
-
-
-
- {{ fmtPercentage(vehicleLimitSoc) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ {{ smartCostNow }}
+ ≤ {{ smartCostLimitFmt }}
+
+ ({{ fmtAbsoluteDate(new Date(smartCostNextStart)) }})
+
+
+
+
+
+ {{ feedInNow }}
+ ≥
+ {{
+ smartFeedInPriorityLimitFmt
+ }}
+
+ ({{ fmtAbsoluteDate(new Date(smartFeedInPriorityNextStart)) }})
+
+
+
+
@@ -182,10 +52,13 @@ import "@h2d2/shopicons/es/regular/sun";
import "@h2d2/shopicons/es/regular/eco1";
import "@h2d2/shopicons/es/regular/angledoublerightsmall";
import "@h2d2/shopicons/es/regular/clock";
-import DynamicPriceIcon from "../MaterialIcon/DynamicPrice.vue";
import { DEFAULT_LOCALE } from "@/i18n.ts";
import formatter from "@/mixins/formatter";
+import { defineComponent, type PropType } from "vue";
+import { SMART_COST_TYPE, type CURRENCY, type Timeout } from "@/types/evcc";
+
import ClimaterIcon from "../MaterialIcon/Climater.vue";
+import DynamicPriceIcon from "../MaterialIcon/DynamicPrice.vue";
import PlanEndIcon from "../MaterialIcon/PlanEnd.vue";
import PlanStartIcon from "../MaterialIcon/PlanStart.vue";
import ReconnectIcon from "../MaterialIcon/Reconnect.vue";
@@ -193,34 +66,23 @@ import RfidWaitIcon from "../MaterialIcon/RfidWait.vue";
import SunDownIcon from "../MaterialIcon/SunDown.vue";
import SunUpIcon from "../MaterialIcon/SunUp.vue";
import TempLimitIcon from "../MaterialIcon/TempLimit.vue";
-import Tooltip from "bootstrap/js/dist/tooltip";
import VehicleLimitIcon from "../MaterialIcon/VehicleLimit.vue";
import VehicleLimitReachedIcon from "../MaterialIcon/VehicleLimitReached.vue";
import VehicleLimitWarningIcon from "../MaterialIcon/VehicleLimitWarning.vue";
import VehicleMinSocIcon from "../MaterialIcon/VehicleMinSoc.vue";
import WelcomeIcon from "../MaterialIcon/Welcome.vue";
import BatteryBoostIcon from "../MaterialIcon/BatteryBoost.vue";
-import { defineComponent, type PropType } from "vue";
-import { SMART_COST_TYPE, type CURRENCY, type Timeout } from "@/types/evcc";
+import SunPauseIcon from "../MaterialIcon/SunPause.vue";
+
+import StatusItem from "./StatusItem.vue";
+
const REASON_AUTH = "waitingforauthorization";
const REASON_DISCONNECT = "disconnectrequired";
export default defineComponent({
name: "VehicleStatus",
components: {
- ClimaterIcon,
- DynamicPriceIcon,
- PlanEndIcon,
- PlanStartIcon,
- ReconnectIcon,
- RfidWaitIcon,
- SunDownIcon,
- SunUpIcon,
- VehicleLimitIcon,
- VehicleMinSocIcon,
- WelcomeIcon,
- TempLimitIcon,
- BatteryBoostIcon,
+ StatusItem,
},
mixins: [formatter],
props: {
@@ -251,8 +113,13 @@ export default defineComponent({
smartCostLimit: { type: Number, default: null },
smartCostNextStart: String,
smartCostType: String,
+ smartFeedInPriorityActive: Boolean,
+ smartFeedInPriorityDisabled: Boolean,
+ smartFeedInPriorityLimit: { type: Number, default: null },
+ smartFeedInPriorityNextStart: String,
tariffCo2: { type: Number, default: 0 },
tariffGrid: { type: Number, default: 0 },
+ tariffFeedIn: { type: Number, default: 0 },
vehicleClimaterActive: Boolean,
vehicleWelcomeActive: Boolean,
vehicleLimitSoc: { type: Number, default: 0 },
@@ -260,33 +127,10 @@ export default defineComponent({
emits: ["open-loadpoint-settings", "open-minsoc-settings", "open-plan-modal"],
data() {
return {
- pvTooltip: null as Tooltip | null,
- phaseTooltip: null as Tooltip | null,
- planStartTooltip: null as Tooltip | null,
- planActiveTooltip: null as Tooltip | null,
- minSocTooltip: null as Tooltip | null,
- vehicleClimaterTooltip: null as Tooltip | null,
- vehicleWelcomeTooltip: null as Tooltip | null,
- smartCostTooltip: null as Tooltip | null,
- vehicleLimitTooltip: null as Tooltip | null,
- tempLimitTooltip: null as Tooltip | null,
- awaitingAuthorizationTooltip: null as Tooltip | null,
- disconnectRequiredTooltip: null as Tooltip | null,
- batteryBoostTooltip: null as Tooltip | null,
interval: null as Timeout,
- planProjectedEndDuration: null as string | null,
- smartCostNextStartDuration: null as string | null,
- planProjectedStartDuration: null as string | null,
};
},
computed: {
- phaseTimerActive() {
- return (
- this.phaseRemainingInterpolated &&
- this.phaseRemainingInterpolated > 0 &&
- ["scale1p", "scale3p"].includes(this.phaseAction)
- );
- },
pvTimerActive() {
return (
this.pvRemainingInterpolated &&
@@ -294,78 +138,12 @@ export default defineComponent({
["enable", "disable"].includes(this.pvAction)
);
},
- pvTimerVisible() {
- return this.pvTimerActive;
- },
- pvTimerContent() {
- if (!this.pvTimerVisible) {
- return "";
- }
- const key = this.pvAction === "enable" ? "pvEnable" : "pvDisable";
- return this.$t(`main.vehicleStatus.${key}`);
- },
- phaseTimerVisible() {
- return !this.pvTimerActive && this.charging && this.phaseTimerActive;
- },
- phaseIconClass() {
- return this.phaseAction === "scale1p" ? "phaseUp" : "phaseDown";
- },
- phaseTimerContent() {
- if (!this.phaseTimerVisible) {
- return "";
- }
- return this.$t(`main.vehicleStatus.${this.phaseAction}`);
- },
- vehicleLimitVisible() {
- const limit = this.effectiveLimitSoc || 100;
- return this.connected && this.vehicleLimitSoc > 0 && this.vehicleLimitSoc < limit;
- },
- awaitingAuthorizationVisible() {
- return this.chargerStatusReason === REASON_AUTH;
- },
- awaitingAuthorizationTooltipContent() {
- if (!this.awaitingAuthorizationVisible) {
- return "";
- }
- return this.$t("main.vehicleStatus.awaitingAuthorization");
- },
- disconnectRequiredVisible() {
- return this.chargerStatusReason === REASON_DISCONNECT;
- },
- disconnectRequiredTooltipContent() {
- if (!this.disconnectRequiredVisible) {
- return "";
- }
- return this.$t("main.vehicleStatus.disconnectRequired");
- },
- vehicleLimitTooltipContent() {
- if (!this.vehicleLimitVisible) {
- return "";
- }
- if (this.vehicleLimitReached) {
- return this.$t("main.vehicleStatus.vehicleLimitReached");
- }
- if (this.vehicleLimitWarning) {
- return this.$t("main.targetCharge.targetIsAboveVehicleLimit");
- }
- return this.$t("main.vehicleStatus.vehicleLimit");
- },
- tempLimitTooltipContent() {
- if (!this.tempLimitVisible) {
- return "";
- }
- return this.$t("main.heatingStatus.vehicleLimit");
- },
- minSocVisible() {
- return this.connected && this.minSoc > 0 && this.vehicleSoc < this.minSoc;
- },
- minSocTooltipContent() {
- if (!this.minSocVisible) {
- return "";
- }
- return this.$t("main.vehicleStatus.minCharge", {
- soc: this.fmtPercentage(this.minSoc),
- });
+ phaseTimerActive() {
+ return (
+ this.phaseRemainingInterpolated &&
+ this.phaseRemainingInterpolated > 0 &&
+ ["scale1p", "scale3p"].includes(this.phaseAction)
+ );
},
vehicleLimitReached() {
return (
@@ -378,81 +156,6 @@ export default defineComponent({
vehicleLimitWarning() {
return this.effectivePlanSoc > this.vehicleLimitSoc;
},
- vehicleLimitClass() {
- if (this.vehicleLimitWarning) {
- return "text-warning";
- }
- return "";
- },
- tempLimitVisible() {
- return this.heating && this.vehicleLimitSoc > 0;
- },
- vehicleLimitIconComponent() {
- if (this.vehicleLimitReached) {
- return VehicleLimitReachedIcon;
- }
- if (this.vehicleLimitWarning) {
- return VehicleLimitWarningIcon;
- }
- return VehicleLimitIcon;
- },
- planStartVisible() {
- return this.planProjectedStart && !this.planActive && !this.chargingPlanDisabled;
- },
- batteryBoostVisible() {
- return this.batteryBoostActive;
- },
- batteryBoostTooltipContent() {
- if (!this.batteryBoostVisible) {
- return "";
- }
- return this.$t("main.vehicleStatus.batteryBoost");
- },
- planStartTooltipContent() {
- if (!this.planStartVisible) {
- return "";
- }
- return this.$t("main.vehicleStatus.targetChargePlanned", {
- duration: this.planProjectedStartDuration,
- });
- },
- planActiveVisible() {
- return this.planProjectedEnd && this.planActive && !this.chargingPlanDisabled;
- },
- planActiveClass() {
- return this.planTimeUnreachable ? "text-warning" : "text-primary";
- },
- planActiveTooltipContent() {
- if (!this.planActiveVisible) {
- return "";
- }
- if (this.planTimeUnreachable) {
- return this.$t("main.targetCharge.notReachableInTime", {
- overrun: this.fmtDuration(this.planOverrun, true, "h"),
- });
- }
- return this.$t("main.vehicleStatus.targetChargeActive", {
- duration: this.planProjectedEndDuration,
- });
- },
- smartCostVisible() {
- return this.smartCostLimit !== null;
- },
- smartCostTooltipContent() {
- if (!this.smartCostVisible) {
- return "";
- }
- const prefix = `main.vehicleStatus.${this.smartCostPrice ? "cheap" : "clean"}`;
- if (this.smartCostNowVisible) {
- return this.$t(`${prefix}EnergyCharging`);
- }
- if (this.smartCostNextStart) {
- return this.$t(`${prefix}EnergyNextStart`, {
- duration: this.smartCostNextStartDuration,
- });
- }
- return this.$t(`${prefix}EnergySet`);
- },
smartCostPrice() {
return this.smartCostType !== SMART_COST_TYPE.CO2;
},
@@ -474,26 +177,11 @@ export default defineComponent({
}
return this.fmtCo2Short(this.smartCostLimit);
},
- smartCostClass() {
- if (this.smartCostDisabled) {
- return "opacity-25";
- }
- if (this.smartCostActive) {
- return "text-primary";
- }
- return "";
+ feedInNow() {
+ return this.fmtPricePerKWh(this.tariffFeedIn, this.currency, true);
},
- vehicleClimaterTooltipContent() {
- if (!this.vehicleClimaterActive) {
- return "";
- }
- return this.$t("main.vehicleStatus.climating");
- },
- vehicleWelcomeTooltipContent() {
- if (!this.vehicleWelcomeActive) {
- return "";
- }
- return this.$t("main.vehicleStatus.welcome");
+ smartFeedInPriorityLimitFmt() {
+ return this.fmtPricePerKWh(this.smartFeedInPriorityLimit, this.currency, true);
},
chargerStatus() {
const t = (key: string) => {
@@ -524,74 +212,190 @@ export default defineComponent({
return t("connected");
},
- },
- watch: {
- planActiveTooltipContent() {
- this.$nextTick(this.updatePlanActiveTooltip);
- },
- planStartTooltipContent() {
- this.$nextTick(this.updatePlanStartTooltip);
- },
- minSocTooltipContent() {
- this.$nextTick(this.updateMinSocTooltip);
- },
- phaseTimerContent() {
- this.$nextTick(this.updatePhaseTooltip);
- },
- pvTimerContent() {
- this.$nextTick(this.updatePvTooltip);
- },
- vehicleClimaterTooltipContent() {
- this.$nextTick(this.updateVehicleClimaterTooltip);
- },
- vehicleWelcomeTooltipContent() {
- this.$nextTick(this.updateVehicleWelcomeTooltip);
- },
- smartCostTooltipContent() {
- this.$nextTick(this.updateSmartCostTooltip);
- },
- vehicleLimitTooltipContent() {
- this.$nextTick(this.updateVehicleLimitTooltip);
- },
- tempLimitTooltipContent() {
- this.$nextTick(this.updateTempLimitTooltip);
- },
- awaitingAuthorizationTooltipContent() {
- this.$nextTick(this.updateAwaitingAuthorizationTooltip);
- },
- disconnectRequiredTooltipContent() {
- this.$nextTick(this.updateDisconnectRequiredTooltip);
- },
- batteryBoostTooltipContent() {
- this.$nextTick(this.updateBatteryBoostTooltip);
- },
- planProjectedStart() {
- this.updateDurations();
- },
- planProjectedEnd() {
- this.updateDurations();
- },
- smartCostNextStart() {
- this.updateDurations();
+ statusItems() {
+ const t = (key: string, params?: Record
) =>
+ this.$t(`main.vehicleStatus.${key}`, params ?? {});
+
+ const items = [
+ {
+ id: "pvTimer",
+ visible: Boolean(this.pvTimerActive),
+ content: this.fmtDuration(this.pvRemainingInterpolated),
+ tooltipContent: this.pvAction === "enable" ? t("pvEnable") : t("pvDisable"),
+ iconComponent: this.pvAction === "enable" ? SunUpIcon : SunDownIcon,
+ testId: "vehicle-status-pvtimer",
+ },
+ {
+ id: "phaseTimer",
+ visible: !this.pvTimerActive && this.charging && this.phaseTimerActive,
+ content: this.fmtDuration(this.phaseRemainingInterpolated),
+ tooltipContent: t(this.phaseAction),
+ iconComponent: "shopicon-regular-angledoublerightsmall",
+ iconClass: this.phaseAction === "scale1p" ? "phaseUp" : "phaseDown",
+ testId: "vehicle-status-phasetimer",
+ },
+ {
+ id: "tempLimit",
+ visible: this.heating && this.vehicleLimitSoc > 0,
+ content: this.fmtTemperature(this.vehicleLimitSoc),
+ tooltipContent: t("vehicleLimit"),
+ iconComponent: TempLimitIcon,
+ testId: "vehicle-status-limit",
+ },
+ {
+ id: "minSoc",
+ visible:
+ !this.heating &&
+ this.connected &&
+ this.minSoc > 0 &&
+ this.vehicleSoc < this.minSoc,
+ content: this.fmtPercentage(this.minSoc),
+ tooltipContent: t("minCharge", {
+ soc: this.fmtPercentage(this.minSoc),
+ }),
+ iconComponent: VehicleMinSocIcon,
+ itemClass: "text-danger text-decoration-underline",
+ testId: "vehicle-status-minsoc",
+ clickable: true,
+ clickHandler: () => this.openMinSocSettings(),
+ },
+ {
+ id: "vehicleLimit",
+ visible:
+ !this.heating &&
+ this.connected &&
+ this.vehicleLimitSoc > 0 &&
+ this.vehicleLimitSoc < (this.effectiveLimitSoc || 100),
+ content: this.fmtPercentage(this.vehicleLimitSoc),
+ tooltipContent: this.vehicleLimitReached
+ ? t("vehicleLimitReached")
+ : this.vehicleLimitWarning
+ ? t("targetIsAboveVehicleLimit")
+ : t("vehicleLimit"),
+ iconComponent: this.vehicleLimitReached
+ ? VehicleLimitReachedIcon
+ : this.vehicleLimitWarning
+ ? VehicleLimitWarningIcon
+ : VehicleLimitIcon,
+ itemClass: this.vehicleLimitWarning ? "text-warning" : "",
+ testId: "vehicle-status-limit",
+ clickable: Boolean(this.vehicleLimitWarning),
+ clickHandler: this.vehicleLimitWarning ? () => this.openPlanModal() : undefined,
+ },
+ {
+ id: "vehicleClimater",
+ visible: !this.heating && this.vehicleClimaterActive,
+ tooltipContent: t("climating"),
+ iconComponent: ClimaterIcon,
+ testId: "vehicle-status-climater",
+ },
+ {
+ id: "vehicleWelcome",
+ visible: !this.heating && this.vehicleWelcomeActive,
+ tooltipContent: t("welcome"),
+ iconComponent: WelcomeIcon,
+ testId: "vehicle-status-welcome",
+ },
+ {
+ id: "awaitingAuthorization",
+ visible: !this.heating && this.chargerStatusReason === REASON_AUTH,
+ tooltipContent: t("awaitingAuthorization"),
+ iconComponent: RfidWaitIcon,
+ testId: "vehicle-status-awaiting-authorization",
+ },
+ {
+ id: "disconnectRequired",
+ visible: !this.heating && this.chargerStatusReason === REASON_DISCONNECT,
+ tooltipContent: t("disconnectRequired"),
+ iconComponent: ReconnectIcon,
+ itemClass: "text-warning",
+ testId: "vehicle-status-disconnect-required",
+ },
+ {
+ id: "smartCost",
+ visible: this.smartCostLimit !== null,
+ tooltipContent: this.getSmartCostTooltip(),
+ iconComponent: this.smartCostPrice ? DynamicPriceIcon : "shopicon-regular-eco1",
+ itemClass: this.smartCostDisabled
+ ? "opacity-25"
+ : this.smartCostActive
+ ? "text-primary"
+ : "",
+ testId: "vehicle-status-smartcost",
+ clickable: true,
+ clickHandler: () => this.openLoadpointSettings(),
+ },
+ {
+ id: "smartFeedInPriority",
+ visible: this.smartFeedInPriorityActive || this.smartFeedInPriorityNextStart,
+ tooltipContent: this.getSmartFeedInPriorityTooltip(),
+ iconComponent: SunPauseIcon,
+ itemClass: this.smartFeedInPriorityDisabled
+ ? "opacity-25"
+ : this.smartFeedInPriorityActive
+ ? "text-warning"
+ : "",
+ testId: "vehicle-status-smartfeedinpriority",
+ clickable: true,
+ clickHandler: () => this.openLoadpointSettings(),
+ },
+ {
+ id: "batteryBoost",
+ visible: this.batteryBoostActive,
+ tooltipContent: t("batteryBoost"),
+ iconComponent: BatteryBoostIcon,
+ testId: "vehicle-status-batteryboost",
+ clickable: true,
+ clickHandler: () => this.openLoadpointSettings(),
+ },
+ {
+ id: "planActive",
+ visible: this.planProjectedEnd && this.planActive && !this.chargingPlanDisabled,
+ content: this.planProjectedEnd
+ ? this.fmtAbsoluteDate(new Date(this.planProjectedEnd))
+ : "",
+ tooltipContent: this.planTimeUnreachable
+ ? this.$t("main.targetCharge.notReachableInTime", {
+ overrun: this.fmtDuration(this.planOverrun, true, "h"),
+ })
+ : t("targetChargeActive", {
+ duration: this.planProjectedEnd
+ ? this.fmtDurationToTime(new Date(this.planProjectedEnd))
+ : "",
+ }),
+ iconComponent: PlanEndIcon,
+ itemClass: this.planTimeUnreachable ? "text-warning" : "text-primary",
+ testId: "vehicle-status-planactive",
+ clickable: true,
+ clickHandler: () => this.openPlanModal(),
+ },
+ {
+ id: "planStart",
+ visible:
+ this.planProjectedStart && !this.planActive && !this.chargingPlanDisabled,
+ content: this.planProjectedStart
+ ? this.fmtAbsoluteDate(new Date(this.planProjectedStart))
+ : "",
+ tooltipContent: t("targetChargePlanned", {
+ duration: this.planProjectedStart
+ ? this.fmtDurationToTime(new Date(this.planProjectedStart))
+ : "",
+ }),
+ iconComponent: PlanStartIcon,
+ testId: "vehicle-status-planstart",
+ clickable: true,
+ clickHandler: () => this.openPlanModal(),
+ },
+ ];
+
+ return items.filter((item) => item.visible);
},
},
mounted() {
- this.updatePlanStartTooltip();
- this.updatePlanActiveTooltip();
- this.updateMinSocTooltip();
- this.updatePhaseTooltip();
- this.updatePvTooltip();
- this.updateVehicleClimaterTooltip();
- this.updateVehicleWelcomeTooltip();
- this.updateSmartCostTooltip();
- this.updateVehicleLimitTooltip();
- this.updateTempLimitTooltip();
- this.updateAwaitingAuthorizationTooltip();
- this.updateDisconnectRequiredTooltip();
- this.updateDurations();
-
- this.interval = setInterval(this.updateDurations, 1000 * 60);
- this.updateBatteryBoostTooltip();
+ this.interval = setInterval(() => {
+ // Force reactivity update for time-dependent content
+ this.$forceUpdate();
+ }, 1000 * 60);
},
beforeUnmount() {
if (this.interval) {
@@ -599,167 +403,38 @@ export default defineComponent({
}
},
methods: {
- updateDurations() {
- if (this.planProjectedStart) {
- this.planProjectedStartDuration = this.fmtDurationToTime(
- new Date(this.planProjectedStart)
- );
- }
- if (this.planProjectedEnd) {
- this.planProjectedEndDuration = this.fmtDurationToTime(
- new Date(this.planProjectedEnd)
- );
- }
- if (this.smartCostNextStart) {
- this.smartCostNextStartDuration = this.fmtDurationToTime(
- new Date(this.smartCostNextStart)
- );
- }
- },
openLoadpointSettings() {
this.$emit("open-loadpoint-settings");
},
openMinSocSettings() {
- this.minSocTooltip?.hide();
this.$emit("open-minsoc-settings");
},
- planStartClicked() {
- this.planStartTooltip?.hide();
+ openPlanModal() {
this.$emit("open-plan-modal");
},
- planActiveClicked() {
- this.planActiveTooltip?.hide();
- this.$emit("open-plan-modal");
- },
- vehicleLimitClicked() {
- if (this.vehicleLimitWarning) {
- this.vehicleLimitTooltip?.hide();
- this.$emit("open-plan-modal");
+ getSmartCostTooltip() {
+ const prefix = `main.vehicleStatus.${this.smartCostPrice ? "cheap" : "clean"}`;
+ if (this.smartCostNowVisible) {
+ return this.$t(`${prefix}EnergyCharging`);
}
- },
- smartCostClicked() {
- this.openLoadpointSettings();
- this.smartCostTooltip?.hide();
- },
- batteryBoostClicked() {
- this.batteryBoostTooltip?.hide();
- this.openLoadpointSettings();
- },
- updatePvTooltip() {
- this.pvTooltip = this.updateTooltip(
- this.pvTooltip,
- this.pvTimerContent,
- this.$refs["pvTimer"]
- );
- },
- updatePhaseTooltip() {
- this.phaseTooltip = this.updateTooltip(
- this.phaseTooltip,
- this.phaseTimerContent,
- this.$refs["phaseTimer"]
- );
- },
- updatePlanStartTooltip() {
- this.planStartTooltip = this.updateTooltip(
- this.planStartTooltip,
- this.planStartTooltipContent,
- this.$refs["planStart"],
- true
- );
- },
- updatePlanActiveTooltip() {
- this.planActiveTooltip = this.updateTooltip(
- this.planActiveTooltip,
- this.planActiveTooltipContent,
- this.$refs["planActive"],
- true
- );
- },
- updateMinSocTooltip() {
- this.minSocTooltip = this.updateTooltip(
- this.minSocTooltip,
- this.minSocTooltipContent,
- this.$refs["minSoc"],
- true
- );
- },
- updateVehicleClimaterTooltip() {
- this.vehicleClimaterTooltip = this.updateTooltip(
- this.vehicleClimaterTooltip,
- this.vehicleClimaterTooltipContent,
- this.$refs["vehicleClimater"]
- );
- },
- updateBatteryBoostTooltip() {
- this.batteryBoostTooltip = this.updateTooltip(
- this.batteryBoostTooltip,
- this.batteryBoostTooltipContent,
- this.$refs["batteryBoost"]
- );
- },
- updateVehicleWelcomeTooltip() {
- this.vehicleWelcomeTooltip = this.updateTooltip(
- this.vehicleWelcomeTooltip,
- this.vehicleWelcomeTooltipContent,
- this.$refs["vehicleWelcome"]
- );
- },
- updateSmartCostTooltip() {
- this.smartCostTooltip = this.updateTooltip(
- this.smartCostTooltip,
- this.smartCostTooltipContent,
- this.$refs["smartCost"],
- true
- );
- },
- updateVehicleLimitTooltip() {
- this.vehicleLimitTooltip = this.updateTooltip(
- this.vehicleLimitTooltip,
- this.vehicleLimitTooltipContent,
- this.$refs["vehicleLimit"],
- true
- );
- },
- updateTempLimitTooltip() {
- this.tempLimitTooltip = this.updateTooltip(
- this.tempLimitTooltip,
- this.tempLimitTooltipContent,
- this.$refs["tempLimit"]
- );
- },
- updateAwaitingAuthorizationTooltip() {
- this.awaitingAuthorizationTooltip = this.updateTooltip(
- this.awaitingAuthorizationTooltip,
- this.awaitingAuthorizationTooltipContent,
- this.$refs["awaitingAuthorization"]
- );
- },
- updateDisconnectRequiredTooltip() {
- this.disconnectRequiredTooltip = this.updateTooltip(
- this.disconnectRequiredTooltip,
- this.disconnectRequiredTooltipContent,
- this.$refs["disconnectRequired"]
- );
- },
- updateTooltip(
- instance: Tooltip | null,
- content: string,
- ref: HTMLElement | undefined,
- hoverOnly = false
- ): Tooltip | null {
- if (!content || !ref) {
- if (instance) {
- instance.dispose();
- }
- return null;
+ if (this.smartCostNextStart) {
+ return this.$t(`${prefix}EnergyNextStart`, {
+ duration: this.fmtDurationToTime(new Date(this.smartCostNextStart)),
+ });
}
- let newInstance = instance;
- if (!newInstance) {
- const trigger = hoverOnly ? "hover" : "hover focus";
- newInstance = new Tooltip(ref, { title: " ", trigger });
+ return this.$t(`${prefix}EnergySet`);
+ },
+ getSmartFeedInPriorityTooltip() {
+ const prefix = "main.vehicleStatus.feedinPriority";
+ if (this.smartFeedInPriorityActive) {
+ return this.$t(`${prefix}Pausing`);
}
- newInstance.setContent({ ".tooltip-inner": content });
- return newInstance;
+ if (this.smartFeedInPriorityNextStart) {
+ return this.$t(`${prefix}NextStart`, {
+ duration: this.fmtDurationToTime(new Date(this.smartFeedInPriorityNextStart)),
+ });
+ }
+ return "";
},
},
});
@@ -769,26 +444,7 @@ export default defineComponent({
.charger-status {
padding-top: 2px;
}
-.entry {
- display: flex;
- align-items: center;
- flex-wrap: nowrap;
- text-wrap: nowrap;
- border: none;
- color: inherit;
- background: none;
- padding: 0;
- gap: 0.5rem;
- transition:
- color var(--evcc-transition-medium) linear,
- opacity var(--evcc-transition-medium) linear;
-}
-.phaseUp {
- transform: rotate(90deg);
-}
-.phaseDown {
- transform: rotate(-90deg);
-}
+
.tabular {
font-variant-numeric: tabular-nums;
}
diff --git a/assets/js/components/Vehicles/StatusItem.vue b/assets/js/components/Vehicles/StatusItem.vue
new file mode 100644
index 000000000..bd059dbd2
--- /dev/null
+++ b/assets/js/components/Vehicles/StatusItem.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
+
diff --git a/assets/js/components/Vehicles/Vehicle.vue b/assets/js/components/Vehicles/Vehicle.vue
index 251841209..d2bdfe30a 100644
--- a/assets/js/components/Vehicles/Vehicle.vue
+++ b/assets/js/components/Vehicles/Vehicle.vue
@@ -129,10 +129,14 @@ export default defineComponent({
smartCostNextStart: String,
smartCostLimit: Number,
smartCostType: String,
+ smartFeedInPriorityActive: Boolean,
+ smartFeedInPriorityNextStart: String,
+ smartFeedInPriorityLimit: Number,
socBasedCharging: Boolean,
socBasedPlanning: Boolean,
tariffCo2: Number,
tariffGrid: Number,
+ tariffFeedIn: Number,
vehicle: Object as PropType,
vehicleDetectionActive: Boolean,
vehicleName: String,
@@ -222,6 +226,9 @@ export default defineComponent({
smartCostDisabled() {
return this.chargingPlanDisabled;
},
+ smartFeedInPriorityDisabled() {
+ return this.chargingPlanDisabled;
+ },
},
watch: {
effectiveLimitSoc() {
diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts
index ded90e70c..fd14988d8 100644
--- a/assets/js/types/evcc.ts
+++ b/assets/js/types/evcc.ts
@@ -187,6 +187,7 @@ export interface Forecast {
co2?: ForecastSlot[];
solar?: SolarDetails;
planner?: ForecastSlot[];
+ feedin?: ForecastSlot[];
}
export interface SelectOption {
diff --git a/assets/js/utils/smartCostAvailable.ts b/assets/js/utils/smartCostAvailable.ts
deleted file mode 100644
index 6e71504fa..000000000
--- a/assets/js/utils/smartCostAvailable.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { SMART_COST_TYPE } from "@/types/evcc";
-
-export default function (smartCostType?: SMART_COST_TYPE) {
- return smartCostType && Object.values(SMART_COST_TYPE).includes(smartCostType);
-}
diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go
index 76620aac0..ac72074f1 100644
--- a/core/keys/loadpoint.go
+++ b/core/keys/loadpoint.go
@@ -42,8 +42,12 @@ const (
// smart charging
SmartCostActive = "smartCostActive" // smart cost active
- SmartCostLimit = "smartCostLimit" // smart cost limit
- SmartCostNextStart = "smartCostNextStart" // smart cost next start
+ SmartCostLimit = "smartCostLimit" // smart cost limit, fast charge when costs are below
+ SmartCostNextStart = "smartCostNextStart" // smart cost next start, time of next fast charging
+
+ SmartFeedInPriorityActive = "smartFeedInPriorityActive" // smart feed-in priority active
+ SmartFeedInPriorityLimit = "smartFeedInPriorityLimit" // smart feed-in priority limit, pause self-consumption when feed-in rates are above
+ SmartFeedInPriorityNextStart = "smartFeedInPriorityNextStart" // smart feed-in priority next start, time of next pause
// effective values
EffectivePriority = "effectivePriority" // effective priority
diff --git a/core/keys/site.go b/core/keys/site.go
index de780b2a6..45593e4d0 100644
--- a/core/keys/site.go
+++ b/core/keys/site.go
@@ -56,4 +56,8 @@ const (
// external battery control
BatteryModeExternal = "batteryModeExternal"
+
+ // smart charging
+ SmartCostAvailable = "smartCostAvailable" // smart cost available
+ SmartFeedInPriorityAvailable = "smartFeedInPriorityAvailable" // smart feed-in priority available
)
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 6ef271d2d..e0e4b81bd 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -107,15 +107,16 @@ 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
+ 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 consumption cost is below this value
+ smartFeedInPriorityLimit *float64 // prevent charging if feed-in cost is above this value
+ batteryBoost int // battery boost state
mode api.ChargeMode
enabled bool // Charger enabled state
@@ -332,6 +333,9 @@ func (lp *Loadpoint) restoreSettings() {
if v, err := lp.settings.Float(keys.SmartCostLimit); err == nil {
lp.SetSmartCostLimit(&v)
}
+ if v, err := lp.settings.Float(keys.SmartFeedInPriorityLimit); err == nil {
+ lp.SetSmartFeedInPriorityLimit(&v)
+ }
var thresholds loadpoint.ThresholdsConfig
if err := lp.settings.Json(keys.Thresholds, &thresholds); err == nil {
@@ -632,6 +636,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
lp.publish(keys.ChargerSinglePhase, lp.getChargerPhysicalPhases() == 1)
lp.publish(keys.PhasesActive, lp.ActivePhases())
lp.publish(keys.SmartCostLimit, lp.smartCostLimit)
+ lp.publish(keys.SmartFeedInPriorityLimit, lp.smartFeedInPriorityLimit)
lp.publishTimer(phaseTimer, 0, timerInactive)
lp.publishTimer(pvTimer, 0, timerInactive)
@@ -1781,17 +1786,16 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
}
// Update is the main control function. It reevaluates meters and charger state
-func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
+func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, consumption, feedin api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
// smart cost
- smartCostActive := lp.smartCostActive(rates)
+ smartCostActive, smartCostNextStart := lp.checkSmartLimit(lp.GetSmartCostLimit(), consumption, true)
lp.publish(keys.SmartCostActive, smartCostActive)
-
- var smartCostNextStart time.Time
- if !smartCostActive {
- smartCostNextStart = lp.smartCostNextStart(rates)
- }
lp.publish(keys.SmartCostNextStart, smartCostNextStart)
+ smartFeedInPriorityActive, smartFeedInPriorityNextStart := lp.checkSmartLimit(lp.GetSmartFeedInPriorityLimit(), feedin, false)
+ lp.publish(keys.SmartFeedInPriorityActive, smartFeedInPriorityActive)
+ lp.publish(keys.SmartFeedInPriorityNextStart, smartFeedInPriorityNextStart)
+
// long-running tasks
lp.processTasks()
@@ -1902,14 +1906,30 @@ func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rate
case mode == api.ModeMinPV || mode == api.ModePV:
// cheap tariff
if smartCostActive {
- rate, _ := rates.At(time.Now())
- lp.log.DEBUG.Printf("smart cost active: %.2f", rate.Value)
+ rate, _ := consumption.At(time.Now())
+ lp.log.DEBUG.Printf("smart consumption active: %.2f", rate.Value)
err = lp.fastCharging()
lp.resetPhaseTimer()
lp.elapsePVTimer() // let PV mode disable immediately afterwards
break
}
+ // attractive feedin
+ if smartFeedInPriorityActive {
+ rate, _ := feedin.At(time.Now())
+ lp.log.DEBUG.Printf("smart feed-in active: %.2f", rate.Value)
+
+ var targetCurrent float64
+ if mode == api.ModeMinPV {
+ targetCurrent = lp.GetMinCurrent()
+ }
+ err = lp.setLimit(targetCurrent)
+
+ lp.resetPhaseTimer()
+ lp.elapsePVTimer() // let PV mode disable immediately afterwards
+ break
+ }
+
targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBoostPower, batteryBuffered, batteryStart)
if targetCurrent == 0 && lp.vehicleClimateActive() {
diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go
index 6908ecbbe..93f539217 100644
--- a/core/loadpoint/api.go
+++ b/core/loadpoint/api.go
@@ -167,10 +167,14 @@ type API interface {
// smart grid charging
//
- // GetSmartChargingActive determines if smart charging is active
+ // GetSmartCostLimit return the smart cost limit
GetSmartCostLimit() *float64
// SetSmartCostLimit sets the smart cost limit
SetSmartCostLimit(limit *float64)
+ // GetSmartFeedInPriorityLimit return the smart feed-in limit
+ GetSmartFeedInPriorityLimit() *float64
+ // SetSmartFeedInPriorityLimit sets the smart feed-in limit
+ SetSmartFeedInPriorityLimit(limit *float64)
//
// power and energy
diff --git a/core/loadpoint/config.go b/core/loadpoint/config.go
index 6357ea3d2..3bc38d83d 100644
--- a/core/loadpoint/config.go
+++ b/core/loadpoint/config.go
@@ -17,18 +17,19 @@ 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"`
+ SmartCostLimit *float64 `json:"smartCostLimit"`
+ SmartFeedInPriorityLimit *float64 `json:"smartFeedInPriorityLimit"`
+ 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"`
@@ -56,6 +57,7 @@ func (payload DynamicConfig) Apply(lp API) error {
lp.SetTitle(payload.Title)
lp.SetPriority(payload.Priority)
lp.SetSmartCostLimit(payload.SmartCostLimit)
+ lp.SetSmartFeedInPriorityLimit(payload.SmartFeedInPriorityLimit)
lp.SetThresholds(payload.Thresholds)
lp.SetPlanEnergy(payload.PlanTime, time.Duration(payload.PlanPrecondition)*time.Second, payload.PlanEnergy)
lp.SetLimitEnergy(payload.LimitEnergy)
diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go
index e098a02cd..63c75498e 100644
--- a/core/loadpoint/mock.go
+++ b/core/loadpoint/mock.go
@@ -548,6 +548,20 @@ func (mr *MockAPIMockRecorder) GetSmartCostLimit() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).GetSmartCostLimit))
}
+// GetSmartFeedInPriorityLimit mocks base method.
+func (m *MockAPI) GetSmartFeedInPriorityLimit() *float64 {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetSmartFeedInPriorityLimit")
+ ret0, _ := ret[0].(*float64)
+ return ret0
+}
+
+// GetSmartFeedInPriorityLimit indicates an expected call of GetSmartFeedInPriorityLimit.
+func (mr *MockAPIMockRecorder) GetSmartFeedInPriorityLimit() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartFeedInPriorityLimit", reflect.TypeOf((*MockAPI)(nil).GetSmartFeedInPriorityLimit))
+}
+
// GetSocConfig mocks base method.
func (m *MockAPI) GetSocConfig() SocConfig {
m.ctrl.T.Helper()
@@ -908,6 +922,18 @@ func (mr *MockAPIMockRecorder) SetSmartCostLimit(limit any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).SetSmartCostLimit), limit)
}
+// SetSmartFeedInPriorityLimit mocks base method.
+func (m *MockAPI) SetSmartFeedInPriorityLimit(limit *float64) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "SetSmartFeedInPriorityLimit", limit)
+}
+
+// SetSmartFeedInPriorityLimit indicates an expected call of SetSmartFeedInPriorityLimit.
+func (mr *MockAPIMockRecorder) SetSmartFeedInPriorityLimit(limit any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSmartFeedInPriorityLimit", reflect.TypeOf((*MockAPI)(nil).SetSmartFeedInPriorityLimit), limit)
+}
+
// SetSocConfig mocks base method.
func (m *MockAPI) SetSocConfig(soc SocConfig) {
m.ctrl.T.Helper()
diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go
index 049021ca5..a45089461 100644
--- a/core/loadpoint_api.go
+++ b/core/loadpoint_api.go
@@ -608,7 +608,8 @@ func (lp *Loadpoint) GetChargePower() float64 {
// GetChargePowerFlexibility returns the flexible amount of current charging power
func (lp *Loadpoint) GetChargePowerFlexibility(rates api.Rates) float64 {
mode := lp.GetMode()
- if mode == api.ModeNow || !lp.charging() || lp.minSocNotReached() || lp.smartCostActive(rates) {
+ if mode == api.ModeNow || !lp.charging() || lp.minSocNotReached() ||
+ lp.smartLimitActive(lp.GetSmartCostLimit(), rates, true) {
return 0
}
@@ -807,6 +808,28 @@ func (lp *Loadpoint) SetSmartCostLimit(val *float64) {
}
}
+// GetSmartFeedInPriorityLimit gets the smart feed-in limit
+func (lp *Loadpoint) GetSmartFeedInPriorityLimit() *float64 {
+ lp.RLock()
+ defer lp.RUnlock()
+ return lp.smartFeedInPriorityLimit
+}
+
+// SetSmartFeedInPriorityLimit sets the smart cost feed-in
+func (lp *Loadpoint) SetSmartFeedInPriorityLimit(val *float64) {
+ lp.Lock()
+ defer lp.Unlock()
+
+ lp.log.DEBUG.Println("set smart feed-in limit:", printPtr("%.1f", val))
+
+ if !ptrValueEqual(lp.smartFeedInPriorityLimit, val) {
+ lp.smartFeedInPriorityLimit = val
+
+ lp.settings.SetFloatPtr(keys.SmartFeedInPriorityLimit, val)
+ lp.publish(keys.SmartFeedInPriorityLimit, val)
+ }
+}
+
// GetCircuit returns the assigned circuit
func (lp *Loadpoint) GetCircuit() api.Circuit {
lp.RLock()
diff --git a/core/loadpoint_smartcost.go b/core/loadpoint_smartcost.go
index 6c7f767de..5d794c3e0 100644
--- a/core/loadpoint_smartcost.go
+++ b/core/loadpoint_smartcost.go
@@ -6,22 +6,40 @@ import (
"github.com/evcc-io/evcc/api"
)
-func (lp *Loadpoint) smartCostActive(rates api.Rates) bool {
- rate, err := rates.At(time.Now())
- limit := lp.GetSmartCostLimit()
- return err == nil && limit != nil && rate.Value <= *limit
+// checkSmartLimit checks if current rate meets smart limit and returns next start time if not active.
+// checkBelow: true for rate <= limit, false for rate >= limit
+func (lp *Loadpoint) checkSmartLimit(limit *float64, rates api.Rates, checkBelow bool) (bool, time.Time) {
+ var nextStart time.Time
+
+ active := lp.smartLimitActive(limit, rates, checkBelow)
+ if !active {
+ nextStart = lp.smartLimitNextStart(limit, rates, checkBelow)
+ }
+
+ return active, nextStart
}
-// smartCostNextStart returns the next start time for a smart cost rate below the limit
-func (lp *Loadpoint) smartCostNextStart(rates api.Rates) time.Time {
- limit := lp.GetSmartCostLimit()
+func (lp *Loadpoint) smartLimitActive(limit *float64, rates api.Rates, checkBelow bool) bool {
+ rate, err := rates.At(time.Now())
+ if err != nil || limit == nil {
+ return false
+ }
+
+ if checkBelow {
+ return rate.Value <= *limit
+ }
+ return rate.Value >= *limit
+}
+
+// smartLimitNextStart returns the next start time when the smart limit condition will be met
+func (lp *Loadpoint) smartLimitNextStart(limit *float64, rates api.Rates, checkBelow bool) time.Time {
if limit == nil || rates == nil {
return time.Time{}
}
now := time.Now()
for _, slot := range rates {
- if slot.Start.After(now) && slot.Value <= *limit {
+ if slot.Start.After(now) && (checkBelow && slot.Value <= *limit || !checkBelow && slot.Value >= *limit) {
return slot.Start
}
}
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 68aa9347d..fe8afb9cf 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -183,7 +183,7 @@ func TestUpdatePowerZero(t *testing.T) {
}
lp.mode = tc.mode
- lp.Update(0, 0, nil, false, false, 0, nil, nil) // false,sitePower false,0
+ lp.Update(0, 0, nil, nil, false, false, 0, nil, nil) // false,sitePower false,0
ctrl.Finish()
}
@@ -428,7 +428,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
- lp.Update(500, 0, nil, false, false, 0, nil, nil)
+ lp.Update(500, 0, nil, nil, false, false, 0, nil, nil)
ctrl.Finish()
t.Log("charging above target - soc deactivates charger")
@@ -437,7 +437,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(false).Return(nil)
- lp.Update(500, 0, nil, false, false, 0, nil, nil)
+ lp.Update(500, 0, nil, nil, false, false, 0, nil, nil)
ctrl.Finish()
t.Log("deactivated charger changes status to B")
@@ -445,14 +445,14 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
vehicle.EXPECT().Soc().Return(95.0, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
- lp.Update(-500, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-500, 0, nil, nil, false, false, 0, nil, nil)
ctrl.Finish()
t.Log("soc has risen below target - soc update prevented by timer")
clock.Add(5 * time.Minute)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
- lp.Update(-500, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-500, 0, nil, nil, false, false, 0, nil, nil)
ctrl.Finish()
t.Log("soc has fallen below target - soc update timer expired")
@@ -462,7 +462,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
charger.EXPECT().Enable(true).Return(nil)
- lp.Update(-500, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-500, 0, nil, nil, false, false, 0, nil, nil)
ctrl.Finish()
}
@@ -497,14 +497,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
- lp.Update(500, 0, nil, false, false, 0, nil, nil)
+ lp.Update(500, 0, nil, nil, false, false, 0, nil, nil)
t.Log("switch off when disconnected")
clock.Add(5 * time.Minute)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusA, nil)
charger.EXPECT().Enable(false).Return(nil)
- lp.Update(-300, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-300, 0, nil, nil, false, false, 0, nil, nil)
if mode := lp.GetMode(); mode != api.ModeOff {
t.Error("unexpected mode", mode)
@@ -566,14 +566,14 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(0.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
- lp.Update(-1, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
t.Log("at 1:00h charging at 5 kWh")
clock.Add(time.Hour)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
- lp.Update(-1, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 5000.0)
t.Log("at 1:00h stop charging at 5 kWh")
@@ -581,7 +581,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
- lp.Update(-1, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 5000.0)
t.Log("at 1:00h restart charging at 5 kWh")
@@ -589,7 +589,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
- lp.Update(-1, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 5000.0)
t.Log("at 1:30h continue charging at 7.5 kWh")
@@ -597,7 +597,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(7.5, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
- lp.Update(-1, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 7500.0)
t.Log("at 2:00h stop charging at 10 kWh")
@@ -605,7 +605,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(10.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
- lp.Update(-1, 0, nil, false, false, 0, nil, nil)
+ lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 10000.0)
ctrl.Finish()
diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go
index 1905c1d2b..4c1ea4490 100644
--- a/core/loadpoint_vehicle_test.go
+++ b/core/loadpoint_vehicle_test.go
@@ -269,7 +269,7 @@ func TestReconnectVehicle(t *testing.T) {
// vehicle not updated yet
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)
- lp.Update(0, 0, nil, false, false, 0, nil, nil)
+ lp.Update(0, 0, nil, nil, false, false, 0, nil, nil)
ctrl.Finish()
// detection started
@@ -283,7 +283,7 @@ func TestReconnectVehicle(t *testing.T) {
// vehicle not updated yet
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil)
- lp.Update(0, 0, nil, false, false, 0, nil, nil)
+ lp.Update(0, 0, nil, nil, false, false, 0, nil, nil)
ctrl.Finish()
// vehicle detected
diff --git a/core/site.go b/core/site.go
index ea3a9fa37..de00de765 100644
--- a/core/site.go
+++ b/core/site.go
@@ -42,7 +42,7 @@ const standbyPower = 10 // consider less than 10W as charger in standby
// updater abstracts the Loadpoint implementation for testing
type updater interface {
loadpoint.API
- Update(sitePower, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
+ Update(sitePower, batteryBoostPower float64, consumption, feedin api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
}
// measurement is used as slice element for publishing structured data
@@ -871,13 +871,18 @@ func (site *Site) update(lp updater) {
site.log.DEBUG.Println("----")
// smart cost and battery mode handling
- rates, err := site.plannerRates()
+ consumption, err := site.tariffRates(api.TariffUsagePlanner)
if err != nil {
site.log.WARN.Println("planner:", err)
}
+ feedin, err := site.tariffRates(api.TariffUsageFeedIn)
+ if err != nil {
+ site.log.WARN.Println("feed-in:", err)
+ }
+
// update loadpoints
- totalChargePower := site.updateLoadpoints(rates)
+ totalChargePower := site.updateLoadpoints(consumption)
// update all circuits' power and currents
if site.circuit != nil {
@@ -894,13 +899,13 @@ func (site *Site) update(lp updater) {
flexiblePower = site.prioritizer.GetChargePowerFlexibility(lp)
}
- rate, err := rates.At(time.Now())
- if rates != nil && err != nil {
+ rate, err := consumption.At(time.Now())
+ if consumption != nil && err != nil {
msg := fmt.Sprintf("no matching rate for: %s", time.Now().Format(time.RFC3339))
- if len(rates) > 0 {
- msg += fmt.Sprintf(", %d rates (%s to %s)", len(rates),
- rates[0].Start.Local().Format(time.RFC3339),
- rates[len(rates)-1].End.Local().Format(time.RFC3339),
+ if len(consumption) > 0 {
+ msg += fmt.Sprintf(", %d consumption rates (%s to %s)", len(consumption),
+ consumption[0].Start.Local().Format(time.RFC3339),
+ consumption[len(consumption)-1].End.Local().Format(time.RFC3339),
)
}
@@ -923,8 +928,9 @@ func (site *Site) update(lp updater) {
greenShareHome := site.greenShare(0, homePower)
greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)
+ // TODO
lp.Update(
- sitePower, max(0, site.batteryPower), rates, batteryBuffered, batteryStart,
+ sitePower, max(0, site.batteryPower), consumption, feedin, batteryBuffered, batteryStart,
greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints),
)
@@ -962,6 +968,8 @@ func (site *Site) prepare() {
site.publish(keys.BatteryMode, site.batteryMode)
site.publish(keys.BatteryDischargeControl, site.batteryDischargeControl)
site.publish(keys.ResidualPower, site.GetResidualPower())
+ site.publish(keys.SmartCostAvailable, site.isDynamicTariff(api.TariffUsagePlanner))
+ site.publish(keys.SmartFeedInPriorityAvailable, site.isDynamicTariff(api.TariffUsageFeedIn))
site.publish(keys.Currency, site.tariffs.Currency)
if tariff := site.GetTariff(api.TariffUsagePlanner); tariff != nil {
diff --git a/core/site_battery.go b/core/site_battery.go
index 3ad41120d..61cede82f 100644
--- a/core/site_battery.go
+++ b/core/site_battery.go
@@ -106,8 +106,8 @@ func (site *Site) applyBatteryMode(mode api.BatteryMode) error {
return nil
}
-func (site *Site) plannerRates() (api.Rates, error) {
- tariff := site.GetTariff(api.TariffUsagePlanner)
+func (site *Site) tariffRates(usage api.TariffUsage) (api.Rates, error) {
+ tariff := site.GetTariff(usage)
if tariff == nil || tariff.Type() == api.TariffTypePriceStatic {
return nil, nil
}
diff --git a/core/site_tariffs.go b/core/site_tariffs.go
index 270b65a61..a91289929 100644
--- a/core/site_tariffs.go
+++ b/core/site_tariffs.go
@@ -167,3 +167,8 @@ func (site *Site) solarDetails(solar timeseries) solarDetails {
return res
}
+
+func (site *Site) isDynamicTariff(usage api.TariffUsage) bool {
+ tariff := site.GetTariff(usage)
+ return tariff != nil && tariff.Type() != api.TariffTypePriceStatic
+}
diff --git a/i18n/bg.json b/i18n/bg.json
index 5bbf499ba..9f386263b 100644
--- a/i18n/bg.json
+++ b/i18n/bg.json
@@ -944,7 +944,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} от {total}",
+ "activeHours": "{active} от {total}",
"activeHoursLabel": "Активни часове",
"applyToAll": "Приложи навсякъде?",
"batteryDescription": "Зарежда домашната батерия с енергия от мрежата.",
diff --git a/i18n/ca.json b/i18n/ca.json
index 3655f36b9..d91fdf277 100644
--- a/i18n/ca.json
+++ b/i18n/ca.json
@@ -300,7 +300,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} de {total}",
+ "activeHours": "{active} de {total}",
"activeHoursLabel": "Hores funcionant",
"co2Label": "Emissions de CO₂",
"co2Limit": "Límit de CO₂",
diff --git a/i18n/cs.json b/i18n/cs.json
index 796119e6b..fe89aa31d 100644
--- a/i18n/cs.json
+++ b/i18n/cs.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} z {total}",
+ "activeHours": "{active} z {total}",
"activeHoursLabel": "Aktivních hodin",
"applyToAll": "Aplikovat všude?",
"batteryDescription": "Nabíjí baterii FVE ze distribuční sítě.",
diff --git a/i18n/da.json b/i18n/da.json
index 26c1f921d..6d07123da 100644
--- a/i18n/da.json
+++ b/i18n/da.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} af {total}",
+ "activeHours": "{active} af {total}",
"activeHoursLabel": "Aktive timer",
"applyToAll": "Anvendes overalt?",
"batteryDescription": "Oplader hjemmebatteriet med energi fra elnettet.",
diff --git a/i18n/de.json b/i18n/de.json
index 5c453858f..1ef7451c3 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} von {total}",
+ "activeHours": "{active} von {total}",
"activeHoursLabel": "Aktive Stunden",
"applyToAll": "Überall anwenden?",
"batteryDescription": "Lädt die Hausbatterie aus dem Netz.",
diff --git a/i18n/el.json b/i18n/el.json
index 7b13a0745..2e03facd6 100644
--- a/i18n/el.json
+++ b/i18n/el.json
@@ -956,7 +956,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} από {total}",
+ "activeHours": "{active} από {total}",
"activeHoursLabel": "Ενεργές ώρες",
"applyToAll": "Εφαρμογή παντού;",
"batteryDescription": "Φορτίζει την οικιακή μπαταρία με ενέργεια από το δίκτυο.",
diff --git a/i18n/en.json b/i18n/en.json
index 1623071f7..27d89abcb 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -779,6 +779,8 @@
"connected": "Connected.",
"disconnectRequired": "Session terminated. Please reconnect.",
"disconnected": "Disconnected.",
+ "feedinPriorityNextStart": "High feed-in rates start in {duration}.",
+ "feedinPriorityPausing": "Solar charging paused to maximize feed-in.",
"finished": "Finished.",
"minCharge": "Minimum charging to {soc}.",
"pvDisable": "Not enough surplus. Pausing soon.",
@@ -957,7 +959,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} of {total}",
+ "activeHours": "{active} of {total}",
"activeHoursLabel": "Active hours",
"applyToAll": "Apply everywhere?",
"batteryDescription": "Charges the home battery with energy from the grid.",
@@ -974,6 +976,14 @@
"resetWarning": "There is no dynamic grid price or CO₂-source configured. However, there is still a limit of {limit}. Clean up your configuration?",
"saved": "Saved."
},
+ "smartFeedInPriority": {
+ "activeHoursLabel": "Paused hours",
+ "description": "Pauses charging during high prices to prioritize profitable grid feed-in.",
+ "priceLabel": "Feed-in rate",
+ "priceLimit": "Feed-in limit",
+ "resetWarning": "There is no dynamic feed-in tariff configured. However, there is still a limit of {limit}. Clean up your configuration?",
+ "title": "Feed-in Priority"
+ },
"startupError": {
"configFile": "Configuration file used:",
"configuration": "Config",
diff --git a/i18n/es.json b/i18n/es.json
index e063c6aee..3edbaabd9 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -723,7 +723,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} de {total}",
+ "activeHours": "{active} de {total}",
"activeHoursLabel": "Horas funcionando",
"applyToAll": "¿Aplicar en todas partes?",
"batteryDescription": "Carga la batería doméstica con la red eléctrica",
diff --git a/i18n/fi.json b/i18n/fi.json
index 7058197cd..efc492086 100644
--- a/i18n/fi.json
+++ b/i18n/fi.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} / {total}",
+ "activeHours": "{active} / {total}",
"activeHoursLabel": "Aktiiviset tunnit",
"applyToAll": "Käytä kaikkialla?",
"batteryDescription": "Lataa kodin akun sähköverkosta.",
diff --git a/i18n/fr.json b/i18n/fr.json
index 7b0ed2198..1a66c79ff 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} sur {total}",
+ "activeHours": "{active} sur {total}",
"activeHoursLabel": "Heures actives",
"applyToAll": "Appliquer partout ?",
"batteryDescription": "Charge la batterie domestique avec du courant du réseau.",
diff --git a/i18n/hr.json b/i18n/hr.json
index 4419ee5bb..3679f15ce 100644
--- a/i18n/hr.json
+++ b/i18n/hr.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} od {total}",
+ "activeHours": "{active} od {total}",
"activeHoursLabel": "Aktivno radno vrijeme",
"applyToAll": "Primijeniti svuda?",
"batteryDescription": "Puni kućnu bateriju energijom iz mreže.",
diff --git a/i18n/hu.json b/i18n/hu.json
index c3872436e..7adf54954 100644
--- a/i18n/hu.json
+++ b/i18n/hu.json
@@ -956,7 +956,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} / {total}",
+ "activeHours": "{active} / {total}",
"activeHoursLabel": "Aktív órák",
"applyToAll": "Minenhol alkalmazás?",
"batteryDescription": "Az otthoni töltéstároló töltése a hálózatról.",
diff --git a/i18n/it.json b/i18n/it.json
index 5a6d9e5f4..931255f52 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -944,7 +944,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} di {total}",
+ "activeHours": "{active} di {total}",
"activeHoursLabel": "Ore attive",
"applyToAll": "Applicare su tutti?",
"batteryDescription": "Carica la batteria di casa con la corrente della rete",
diff --git a/i18n/lb.json b/i18n/lb.json
index 387fddd79..8e26a5093 100644
--- a/i18n/lb.json
+++ b/i18n/lb.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} vun {total}",
+ "activeHours": "{active} vun {total}",
"activeHoursLabel": "Aktiv Stonnen",
"applyToAll": "Iwwerall uwennen?",
"batteryDescription": "Luet d'Hausbatterie aus dem Netz.",
diff --git a/i18n/lt.json b/i18n/lt.json
index 5f9b255d4..1aaad2df2 100644
--- a/i18n/lt.json
+++ b/i18n/lt.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} iš {total}",
+ "activeHours": "{active} iš {total}",
"activeHoursLabel": "Aktyvumo valandos",
"applyToAll": "Pritaikyti visur?",
"batteryDescription": "Įkrauna kaupiklį energija iš tinklo.",
diff --git a/i18n/nl.json b/i18n/nl.json
index b682e6d2c..2d1b253af 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} van {total}",
+ "activeHours": "{active} van {total}",
"activeHoursLabel": "Actieve uren",
"applyToAll": "Overal toepassen?",
"batteryDescription": "Laadt de huisbatterij op met stroom uit het net.",
diff --git a/i18n/no.json b/i18n/no.json
index 7dda10a87..a813a8de3 100644
--- a/i18n/no.json
+++ b/i18n/no.json
@@ -347,7 +347,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} av {total}",
+ "activeHours": "{active} av {total}",
"activeHoursLabel": "Aktive timer",
"co2Label": "CO₂-utslipp",
"co2Limit": "CO₂-grense",
diff --git a/i18n/pl.json b/i18n/pl.json
index f4635ab8a..047d16608 100644
--- a/i18n/pl.json
+++ b/i18n/pl.json
@@ -539,7 +539,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} z {total}",
+ "activeHours": "{active} z {total}",
"activeHoursLabel": "Aktywne godziny",
"applyToAll": "Aplikować wszędzie?",
"batteryDescription": "Ładuje akumulator domu energią z sieci.",
diff --git a/i18n/pt.json b/i18n/pt.json
index c50a3fad9..fea167cfc 100644
--- a/i18n/pt.json
+++ b/i18n/pt.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} de {total}",
+ "activeHours": "{active} de {total}",
"activeHoursLabel": "Horas ativas",
"applyToAll": "Aplicar em todo o lado?",
"batteryDescription": "Carrega a bateria doméstica com energia da Rede.",
diff --git a/i18n/ro.json b/i18n/ro.json
index a2c4af09d..f82e052ac 100644
--- a/i18n/ro.json
+++ b/i18n/ro.json
@@ -429,7 +429,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} din {total}",
+ "activeHours": "{active} din {total}",
"activeHoursLabel": "Ore de functionare",
"co2Label": "emisii CO₂",
"co2Limit": "limita CO₂",
diff --git a/i18n/sl.json b/i18n/sl.json
index 8bca6aee6..d50b97097 100644
--- a/i18n/sl.json
+++ b/i18n/sl.json
@@ -888,7 +888,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} od {total}",
+ "activeHours": "{active} od {total}",
"activeHoursLabel": "Aktivne ure",
"applyToAll": "Uporabi povsod?",
"batteryDescription": "Napolni domačo baterijo z energijo iz omrežja.",
diff --git a/i18n/sv.json b/i18n/sv.json
index 336698395..7afe547bc 100644
--- a/i18n/sv.json
+++ b/i18n/sv.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} av {total}",
+ "activeHours": "{active} av {total}",
"activeHoursLabel": "Aktiva timmar",
"applyToAll": "Tillämpa överallt?",
"batteryDescription": "Laddar batteriet med el från nätet.",
diff --git a/i18n/ta.json b/i18n/ta.json
index d2388c010..e5d58ce05 100644
--- a/i18n/ta.json
+++ b/i18n/ta.json
@@ -723,7 +723,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} இன் {total} \"",
+ "activeHours": "{active} இன் {total} \"",
"activeHoursLabel": "செயலில் நேரம்",
"applyToAll": "எல்லா இடங்களிலும் விண்ணப்பிக்கவா?",
"batteryDescription": "கட்டத்தில் இருந்து ஆற்றலுடன் வீட்டு பேட்டரியை வசூலிக்கிறது.",
diff --git a/i18n/tr.json b/i18n/tr.json
index a14f1ce76..653551267 100644
--- a/i18n/tr.json
+++ b/i18n/tr.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{total} saat içinde {charging}",
+ "activeHours": "{total} saat içinde {active}",
"activeHoursLabel": "Aktif saatler",
"applyToAll": "Heryerde uygulansın mı?",
"batteryDescription": "Ev enerji deposunu şebekeden doldurur.",
diff --git a/i18n/uk.json b/i18n/uk.json
index f914c46e0..4e92ae3e3 100644
--- a/i18n/uk.json
+++ b/i18n/uk.json
@@ -957,7 +957,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} зі {total}",
+ "activeHours": "{active} зі {total}",
"activeHoursLabel": "Активні години",
"applyToAll": "Застосовувати всюди?",
"batteryDescription": "Заряджає домашній акумулятор енергією з мережі.",
diff --git a/i18n/zh-Hans.json b/i18n/zh-Hans.json
index 1a5100eb9..943d0acb1 100644
--- a/i18n/zh-Hans.json
+++ b/i18n/zh-Hans.json
@@ -956,7 +956,7 @@
}
},
"smartCost": {
- "activeHours": "{charging} / {total}",
+ "activeHours": "{active} / {total}",
"activeHoursLabel": "有效时段",
"applyToAll": "应用于所有地方?",
"batteryDescription": "使用电网能源为住宅电池充电。",
diff --git a/server/http.go b/server/http.go
index 77302f9d3..161fa3ed2 100644
--- a/server/http.go
+++ b/server/http.go
@@ -10,6 +10,7 @@ import (
"github.com/evcc-io/evcc/api/globalconfig"
"github.com/evcc-io/evcc/core"
"github.com/evcc-io/evcc/core/keys"
+ "github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/server/assets"
"github.com/evcc-io/evcc/server/eebus"
@@ -127,6 +128,13 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
))
// site api
+ smartCostLimit := func(lp loadpoint.API, limit *float64) {
+ lp.SetSmartCostLimit(limit)
+ }
+ smartFeedInPriorityLimit := func(lp loadpoint.API, limit *float64) {
+ lp.SetSmartFeedInPriorityLimit(limit)
+ }
+
routes := map[string]route{
"health": {"GET", "/health", healthHandler(site)},
"buffersoc": {"POST", "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)},
@@ -138,8 +146,10 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
"batterymodedelete": {"DELETE", "/batterymode", updateBatteryMode(site)},
"prioritysoc": {"POST", "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)},
"residualpower": {"POST", "/residualpower/{value:-?[0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
- "smartcost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", updateSmartCostLimit(site)},
- "smartcostdelete": {"DELETE", "/smartcostlimit", updateSmartCostLimit(site)},
+ "smartcost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", updateSmartCostLimit(site, smartCostLimit)},
+ "smartcostdelete": {"DELETE", "/smartcostlimit", updateSmartCostLimit(site, smartCostLimit)},
+ "smartfeedin": {"POST", "/smartfeedinprioritylimit/{value:-?[0-9.]+}", updateSmartCostLimit(site, smartFeedInPriorityLimit)},
+ "smartfeedindelete": {"DELETE", "/smartfeedinprioritylimit", updateSmartCostLimit(site, smartFeedInPriorityLimit)},
"tariff": {"GET", "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
"sessions": {"GET", "/sessions", sessionHandler},
"updatesession": {"PUT", "/session/{id:[0-9]+}", updateSessionHandler},
@@ -177,29 +187,31 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
api := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id+1)).Subrouter()
routes := map[string]route{
- "mode": {"POST", "/mode/{value:[a-z]+}", handler(eapi.ChargeModeString, pass(lp.SetMode), lp.GetMode)},
- "limitsoc": {"POST", "/limitsoc/{value:[0-9]+}", intHandler(pass(lp.SetLimitSoc), lp.GetLimitSoc)},
- "limitenergy": {"POST", "/limitenergy/{value:[0-9.]+}", floatHandler(pass(lp.SetLimitEnergy), lp.GetLimitEnergy)},
- "mincurrent": {"POST", "/mincurrent/{value:[0-9.]+}", floatHandler(lp.SetMinCurrent, lp.GetMinCurrent)},
- "maxcurrent": {"POST", "/maxcurrent/{value:[0-9.]+}", floatHandler(lp.SetMaxCurrent, lp.GetMaxCurrent)},
- "phases": {"POST", "/phases/{value:[0-9]+}", intHandler(lp.SetPhasesConfigured, lp.GetPhasesConfigured)},
- "plan": {"GET", "/plan", planHandler(lp)},
- "staticPlanPreview": {"GET", "/plan/static/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.+-]+}", staticPlanPreviewHandler(lp)},
- "repeatingPlanPreview": {"GET", "/plan/repeating/preview/{soc:[0-9]+}/{weekdays:[0-6,]+}/{time:[0-2][0-9]:[0-5][0-9]}/{tz:[a-zA-Z0-9_./:-]+}", repeatingPlanPreviewHandler(lp)},
- "planenergy": {"POST", "/plan/energy/{value:[0-9.]+}/{time:[0-9TZ:.+-]+}", planEnergyHandler(lp)},
- "planenergy2": {"DELETE", "/plan/energy", planRemoveHandler(lp)},
- "vehicle": {"POST", "/vehicle/{name:[a-zA-Z0-9_.:-]+}", vehicleSelectHandler(site, lp)},
- "vehicle2": {"DELETE", "/vehicle", vehicleRemoveHandler(lp)},
- "vehicleDetect": {"PATCH", "/vehicle", vehicleDetectHandler(lp)},
- "remotedemand": {"POST", "/remotedemand/{demand:[a-z]+}/{source:[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
- "enableThreshold": {"POST", "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)},
- "enableDelay": {"POST", "/enable/delay/{value:[0-9]+}", durationHandler(pass(lp.SetEnableDelay), lp.GetEnableDelay)},
- "disableThreshold": {"POST", "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)},
- "disableDelay": {"POST", "/disable/delay/{value:[0-9]+}", durationHandler(pass(lp.SetDisableDelay), lp.GetDisableDelay)},
- "smartCost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", floatPtrHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
- "smartCostDelete": {"DELETE", "/smartcostlimit", floatPtrHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
- "priority": {"POST", "/priority/{value:[0-9]+}", intHandler(pass(lp.SetPriority), lp.GetPriority)},
- "batteryBoost": {"POST", "/batteryboost/{value:[01truefalse]+}", boolHandler(lp.SetBatteryBoost, func() bool { return lp.GetBatteryBoost() > 0 })},
+ "mode": {"POST", "/mode/{value:[a-z]+}", handler(eapi.ChargeModeString, pass(lp.SetMode), lp.GetMode)},
+ "limitsoc": {"POST", "/limitsoc/{value:[0-9]+}", intHandler(pass(lp.SetLimitSoc), lp.GetLimitSoc)},
+ "limitenergy": {"POST", "/limitenergy/{value:[0-9.]+}", floatHandler(pass(lp.SetLimitEnergy), lp.GetLimitEnergy)},
+ "mincurrent": {"POST", "/mincurrent/{value:[0-9.]+}", floatHandler(lp.SetMinCurrent, lp.GetMinCurrent)},
+ "maxcurrent": {"POST", "/maxcurrent/{value:[0-9.]+}", floatHandler(lp.SetMaxCurrent, lp.GetMaxCurrent)},
+ "phases": {"POST", "/phases/{value:[0-9]+}", intHandler(lp.SetPhasesConfigured, lp.GetPhasesConfigured)},
+ "plan": {"GET", "/plan", planHandler(lp)},
+ "staticPlanPreview": {"GET", "/plan/static/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.+-]+}", staticPlanPreviewHandler(lp)},
+ "repeatingPlanPreview": {"GET", "/plan/repeating/preview/{soc:[0-9]+}/{weekdays:[0-6,]+}/{time:[0-2][0-9]:[0-5][0-9]}/{tz:[a-zA-Z0-9_./:-]+}", repeatingPlanPreviewHandler(lp)},
+ "planenergy": {"POST", "/plan/energy/{value:[0-9.]+}/{time:[0-9TZ:.+-]+}", planEnergyHandler(lp)},
+ "planenergy2": {"DELETE", "/plan/energy", planRemoveHandler(lp)},
+ "vehicle": {"POST", "/vehicle/{name:[a-zA-Z0-9_.:-]+}", vehicleSelectHandler(site, lp)},
+ "vehicle2": {"DELETE", "/vehicle", vehicleRemoveHandler(lp)},
+ "vehicleDetect": {"PATCH", "/vehicle", vehicleDetectHandler(lp)},
+ "remotedemand": {"POST", "/remotedemand/{demand:[a-z]+}/{source:[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
+ "enableThreshold": {"POST", "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)},
+ "enableDelay": {"POST", "/enable/delay/{value:[0-9]+}", durationHandler(pass(lp.SetEnableDelay), lp.GetEnableDelay)},
+ "disableThreshold": {"POST", "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)},
+ "disableDelay": {"POST", "/disable/delay/{value:[0-9]+}", durationHandler(pass(lp.SetDisableDelay), lp.GetDisableDelay)},
+ "smartCost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", floatPtrHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
+ "smartCostDelete": {"DELETE", "/smartcostlimit", floatPtrHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
+ "smartFeedInPriority": {"POST", "/smartfeedinprioritylimit/{value:-?[0-9.]+}", floatPtrHandler(pass(lp.SetSmartFeedInPriorityLimit), lp.GetSmartFeedInPriorityLimit)},
+ "smartFeedInPriorityDelete": {"DELETE", "/smartfeedinprioritylimit", floatPtrHandler(pass(lp.SetSmartFeedInPriorityLimit), lp.GetSmartFeedInPriorityLimit)},
+ "priority": {"POST", "/priority/{value:[0-9]+}", intHandler(pass(lp.SetPriority), lp.GetPriority)},
+ "batteryBoost": {"POST", "/batteryboost/{value:[01truefalse]+}", boolHandler(lp.SetBatteryBoost, func() bool { return lp.GetBatteryBoost() > 0 })},
}
for _, r := range routes {
diff --git a/server/http_config_loadpoint_handler.go b/server/http_config_loadpoint_handler.go
index 5957b495d..baa45626b 100644
--- a/server/http_config_loadpoint_handler.go
+++ b/server/http_config_loadpoint_handler.go
@@ -29,20 +29,21 @@ 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(),
+ SmartCostLimit: lp.GetSmartCostLimit(),
+ SmartFeedInPriorityLimit: lp.GetSmartFeedInPriorityLimit(),
+ Thresholds: lp.GetThresholds(),
+ Soc: lp.GetSocConfig(),
+ PlanEnergy: planEnergy,
+ PlanTime: planTime,
+ PlanPrecondition: int64(planPrecondition.Seconds()),
+ LimitEnergy: lp.GetLimitEnergy(),
+ LimitSoc: lp.GetLimitSoc(),
}
}
diff --git a/server/http_site_handler.go b/server/http_site_handler.go
index c650fbb37..18f285447 100644
--- a/server/http_site_handler.go
+++ b/server/http_site_handler.go
@@ -12,6 +12,7 @@ import (
"time"
"github.com/evcc-io/evcc/api"
+ "github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/server/assets"
"github.com/evcc-io/evcc/util"
@@ -174,7 +175,7 @@ func getHandler[T any](get func() T) http.HandlerFunc {
}
// updateSmartCostLimit sets the smart cost limit globally
-func updateSmartCostLimit(site site.API) http.HandlerFunc {
+func updateSmartCostLimit(site site.API, setLimit func(loadpoint.API, *float64)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var val *float64
@@ -190,7 +191,7 @@ func updateSmartCostLimit(site site.API) http.HandlerFunc {
}
for _, lp := range site.Loadpoints() {
- lp.SetSmartCostLimit(val)
+ setLimit(lp, val)
}
jsonResult(w, val)
diff --git a/server/mqtt.go b/server/mqtt.go
index 3e004bcdc..30d5a6f5a 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -211,6 +211,11 @@ func (m *MQTT) listenSiteSetters(topic string, site site.API) error {
lp.SetSmartCostLimit(limit)
}
}))},
+ {"smartFeedInPriorityLimit", floatPtrSetter(pass(func(limit *float64) {
+ for _, lp := range site.Loadpoints() {
+ lp.SetSmartFeedInPriorityLimit(limit)
+ }
+ }))},
{"batteryGridChargeLimit", floatPtrSetter(pass(site.SetBatteryGridChargeLimit))},
{"batteryMode", ptrSetter(api.BatteryModeString, pass(func(m *api.BatteryMode) {
if m == nil {
@@ -241,6 +246,7 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.
{"enableDelay", durationSetter(pass(lp.SetEnableDelay))},
{"disableDelay", durationSetter(pass(lp.SetDisableDelay))},
{"smartCostLimit", floatPtrSetter(pass(lp.SetSmartCostLimit))},
+ {"smartFeedInPriorityLimit", floatPtrSetter(pass(lp.SetSmartFeedInPriorityLimit))},
{"batteryBoost", boolSetter(lp.SetBatteryBoost)},
{"planEnergy", func(payload string) error {
var plan struct {
diff --git a/tests/battery-settings.spec.ts b/tests/battery-settings.spec.ts
index 6fd3ead50..cd7c85bd4 100644
--- a/tests/battery-settings.spec.ts
+++ b/tests/battery-settings.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
-import { enableExperimental } from "./utils";
+import { enableExperimental, expectModalVisible, expectModalHidden } from "./utils";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
@@ -41,18 +41,21 @@ test.describe("battery settings", async () => {
await enableExperimental(page);
await page.getByTestId("topnavigation-button").click();
await page.getByTestId("topnavigation-battery").click();
+ const modal = page.getByTestId("battery-settings-modal");
+ await expectModalVisible(modal);
- await page.getByRole("link", { name: "Grid charging 🧪" }).click();
- await page.getByLabel("Price limit").selectOption({ label: "≤ 50.0 ct/kWh" });
- await expect(page.getByTestId("battery-settings-modal")).toContainText("5.0 ct – 50.0 ct");
+ await modal.getByRole("link", { name: "Grid charging 🧪" }).click();
+ await modal.getByLabel("Price limit").selectOption({ label: "≤ 50.0 ct/kWh" });
+ await expect(modal).toContainText("5.0 ct – 50.0 ct");
await page.getByRole("button", { name: "Close" }).click();
- await expect(page.getByTestId("battery-settings-modal")).not.toBeVisible();
+ await expectModalHidden(modal);
await page.getByTestId("energyflow").click();
await page.getByRole("button", { name: "grid charging active (≤ 50.0 ct)" }).click();
- await expect(page.getByTestId("battery-settings-modal")).toBeVisible();
- await page.getByLabel("Price limit").selectOption({ label: "≤ -10.0 ct/kWh" });
- await page.getByRole("button", { name: "Close" }).click();
- await expect(page.getByTestId("battery-settings-modal")).not.toBeVisible();
+ await expectModalVisible(modal);
+ await modal.getByLabel("Price limit").selectOption({ label: "≤ -10.0 ct/kWh" });
+ await modal.getByRole("button", { name: "Close" }).click();
+ await expectModalHidden(modal);
+ await expect(modal).not.toBeVisible();
await expect(page.getByRole("button", { name: "grid charging when ≤ -10.0 ct" })).toBeVisible();
});
});
diff --git a/tests/smart-feedin.evcc.yaml b/tests/smart-feedin.evcc.yaml
new file mode 100755
index 000000000..6e3498dd4
--- /dev/null
+++ b/tests/smart-feedin.evcc.yaml
@@ -0,0 +1,45 @@
+interval: 0.1s
+
+site:
+ title: Smart Feed-in
+ meters:
+ grid: grid
+
+loadpoints:
+ - title: LP1
+ charger: charger1
+ mode: pv
+ - title: LP2
+ charger: charger2
+ mode: pv
+
+meters:
+ - name: grid
+ type: template
+ template: demo-meter
+ power: -2000
+
+chargers:
+ - name: charger1
+ type: template
+ template: demo-charger
+ status: C
+ power: 2000
+ - name: charger2
+ type: template
+ template: demo-charger
+ status: B
+ power: 0
+
+tariffs:
+ currency: EUR
+ feedin:
+ type: fixed
+ price: 0.2 # EUR/kWh
+ zones:
+ - hours: 0-5
+ price: 0.4
+ - hours: 5-6
+ price: 0.6
+ - hours: 18-0
+ price: 0.4
diff --git a/tests/smart-feedin.spec.ts b/tests/smart-feedin.spec.ts
new file mode 100644
index 000000000..9916fdb15
--- /dev/null
+++ b/tests/smart-feedin.spec.ts
@@ -0,0 +1,74 @@
+import { test, expect } from "@playwright/test";
+import { start, stop, baseUrl } from "./evcc";
+import { expectModalHidden, expectModalVisible } from "./utils";
+
+const CONFIG = "smart-feedin.evcc.yaml";
+
+test.use({ baseURL: baseUrl() });
+
+test.beforeAll(async () => {
+ await start(CONFIG);
+});
+
+test.afterAll(async () => {
+ await stop();
+});
+
+test.describe("smart feed-in priority", async () => {
+ test("no limit, normal charging", async ({ page }) => {
+ await page.goto("/");
+ await expect(page.getByTestId("vehicle-status-smartfeedinpriority")).not.toBeVisible();
+ });
+
+ test("feed-in above threshold, pause charging", async ({ page }) => {
+ await page.goto("/");
+ const lp = page.getByTestId("loadpoint").first();
+
+ // set limit
+ await lp.getByTestId("loadpoint-settings-button").nth(1).click();
+ const modal = page.getByTestId("loadpoint-settings-modal").first();
+ await expectModalVisible(modal);
+ await modal.getByLabel("Feed-in limit").selectOption("≥ 10.0 ct/kWh");
+ await modal.getByLabel("Close").click();
+ await expectModalHidden(modal);
+
+ // check status
+ await expect(lp.getByTestId("vehicle-status-smartfeedinpriority")).toBeVisible();
+ await expect(lp.getByTestId("vehicle-status-smartfeedinpriority")).toHaveText(/≥ 10\.0 ct/);
+
+ // remove limit
+ await lp.getByTestId("loadpoint-settings-button").nth(1).click();
+ await expectModalVisible(modal);
+ await modal.getByLabel("Feed-in limit").selectOption("none");
+ await modal.getByLabel("Close").click();
+ await expectModalHidden(modal);
+
+ // check status
+ await expect(lp.getByTestId("vehicle-status-smartfeedinpriority")).not.toBeVisible();
+ });
+
+ test("feed-in apply to all loadpoints", async ({ page }) => {
+ await page.goto("/");
+ const lp1 = page.getByTestId("loadpoint").first();
+ const lp2 = page.getByTestId("loadpoint").last();
+ await lp1.getByTestId("loadpoint-settings-button").nth(1).click();
+ const modal1 = page.getByTestId("loadpoint-settings-modal").first();
+ await expectModalVisible(modal1);
+ await modal1.getByLabel("Feed-in limit").selectOption("≥ 10.0 ct/kWh");
+ await modal1.getByRole("button", { name: "Apply everywhere?" }).click();
+ await modal1.getByLabel("Close").click();
+ await expectModalHidden(modal1);
+
+ // check lp1, lp2 status
+ await expect(lp1.getByTestId("vehicle-status-smartfeedinpriority")).toBeVisible();
+ await expect(lp1.getByTestId("vehicle-status-smartfeedinpriority")).toHaveText(/≥ 10\.0 ct/);
+ await expect(lp2.getByTestId("vehicle-status-smartfeedinpriority")).toBeVisible();
+ await expect(lp2.getByTestId("vehicle-status-smartfeedinpriority")).toHaveText(/≥ 10\.0 ct/);
+
+ // check lp2 setting
+ await lp2.getByTestId("loadpoint-settings-button").nth(1).click();
+ const modal2 = page.getByTestId("loadpoint-settings-modal").last();
+ await expectModalVisible(modal2);
+ await expect(modal2.getByLabel("Feed-in limit")).toHaveValue("0.1");
+ });
+});