Repository: evcc-io/evcc
Pull request #11469 (2024-01-04 10:00:38)

Kostal: fix battery control
Refs https://github.com/evcc-io/evcc/discussions/11389#discussioncomment-8011119

premultiply: Lass uns einfach noch einen Switch ins Template einbauen dass man das umschalten kann - wenn es überhaupt erstmal jetzt so funktioniert.
andig: Gerne- dann aber (ausnahmsweise) mal mit LE als Standard? Baust Du es ein?

Patch:
diff --git a/assets/css/app.css b/assets/css/app.css
index 4bf4818f..3511d755 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -78,6 +78,7 @@
 	--evcc-accent2: var(--evcc-dark-green);
 	--evcc-accent3: var(--evcc-darker-green);
 	--bs-primary: var(--evcc-dark-green);
+	--bs-border-color-translucent: rgba(255, 255, 255, 0.175);
 }
 
 html {
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index b5317d08..c4bac273 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -2,6 +2,7 @@
 	<div
 		class="energyflow cursor-pointer position-relative"
 		:class="{ 'energyflow--open': detailsOpen }"
+		data-testid="energyflow"
 		@click="toggleDetails"
 	>
 		<div class="row">
@@ -78,6 +79,7 @@
 							:details="batterySoc"
 							:detailsFmt="batteryFmt"
 							detailsClickable
+							data-testid="energyflow-entry-batterydischarge"
 							@details-clicked="openBatterySettingsModal"
 						/>
 						<EnergyflowEntry
@@ -87,8 +89,9 @@
 							:powerInKw="powerInKw"
 							:details="detailsValue(tariffGrid, tariffCo2)"
 							:detailsFmt="detailsFmt"
-							:detailsClickable="smartCostAvailable"
+							:detailsClickable="gridModalAvailable"
 							:detailsTooltip="detailsTooltip(tariffGrid, tariffCo2)"
+							data-testid="energyflow-entry-gridimport"
 							@details-clicked="openGridSettingsModal"
 						/>
 					</div>
@@ -157,8 +160,6 @@
 				</div>
 			</div>
 		</div>
-		<GridSettingsModal v-bind="gridSettings" />
-		<BatterySettingsModal v-bind="batterySettings" />
 	</div>
 </template>
 
@@ -167,13 +168,12 @@ import "@h2d2/shopicons/es/filled/square";
 import Modal from "bootstrap/js/dist/modal";
 import Visualization from "./Visualization.vue";
 import EnergyflowEntry from "./EnergyflowEntry.vue";
-import GridSettingsModal from "../GridSettingsModal.vue";
 import formatter from "../../mixins/formatter";
 import AnimatedNumber from "../AnimatedNumber.vue";
 import settings from "../../settings";
-import { CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE } from "../../units";
+import { CO2_TYPE } from "../../units";
 import collector from "../../mixins/collector";
-import BatterySettingsModal from "../BatterySettingsModal.vue";
+import gridModalAvailable from "../../utils/gridModalAvailable";
 
 export default {
 	name: "Energyflow",
@@ -181,8 +181,6 @@ export default {
 		Visualization,
 		EnergyflowEntry,
 		AnimatedNumber,
-		GridSettingsModal,
-		BatterySettingsModal,
 	},
 	mixins: [formatter, collector],
 	props: {
@@ -217,8 +215,8 @@ export default {
 		return { detailsOpen: false, detailsCompleteHeight: null, gridSettingsModal: null };
 	},
 	computed: {
-		smartCostAvailable: function () {
-			return [CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE].includes(this.smartCostType);
+		gridModalAvailable: function () {
+			return gridModalAvailable(this.smartCostType);
 		},
 		gridImport: function () {
 			return Math.max(0, this.gridPower);
@@ -288,27 +286,15 @@ export default {
 		batteryFmt() {
 			return (soc) => `${Math.round(soc)}%`;
 		},
-		gridSettings() {
-			return this.collectProps(GridSettingsModal);
-		},
-		batterySettings() {
-			return this.collectProps(BatterySettingsModal);
-		},
 		co2Available() {
 			return this.smartCostType === CO2_TYPE;
 		},
 	},
 	mounted() {
-		this.gridSettingsModal = Modal.getOrCreateInstance(
-			document.querySelector("#gridSettingsModal")
-		);
-		this.batterySettingsModal = Modal.getOrCreateInstance(
-			document.querySelector("#batterySettingsModal")
-		);
 		window.addEventListener("resize", this.updateHeight);
 		// height must be calculated in case of initially open details
 		if (settings.energyflowDetails) {
-			setTimeout(this.toggleDetails, 50);
+			setTimeout(this.toggleDetails, 100);
 		}
 	},
 	unmounted() {
@@ -349,10 +335,14 @@ export default {
 			this.detailsCompleteHeight = this.$refs.detailsInner.offsetHeight;
 		},
 		openGridSettingsModal() {
-			this.gridSettingsModal.show();
+			const modal = Modal.getOrCreateInstance(document.getElementById("gridSettingsModal"));
+			modal.show();
 		},
 		openBatterySettingsModal() {
-			this.batterySettingsModal.show();
+			const modal = Modal.getOrCreateInstance(
+				document.getElementById("batterySettingsModal")
+			);
+			modal.show();
 		},
 	},
 };
diff --git a/assets/js/components/Energyflow/EnergyflowEntry.vue b/assets/js/components/Energyflow/EnergyflowEntry.vue
index 33b3bff3..0a055aef 100644
--- a/assets/js/components/Energyflow/EnergyflowEntry.vue
+++ b/assets/js/components/Energyflow/EnergyflowEntry.vue
@@ -13,6 +13,7 @@
 				ref="details"
 				class="fw-normal"
 				:class="{ 'text-decoration-underline': detailsClickable }"
+				data-testid="energyflow-entry-details"
 				data-bs-toggle="tooltip"
 				:tabindex="detailsClickable ? 0 : undefined"
 				@click="detailsClicked"
diff --git a/assets/js/components/TopNavigation.vue b/assets/js/components/TopNavigation.vue
index 4783087b..9ba99d7b 100644
--- a/assets/js/components/TopNavigation.vue
+++ b/assets/js/components/TopNavigation.vue
@@ -26,18 +26,32 @@
 					{{ $t("header.sessions") }}
 				</router-link>
 			</li>
+			<li><hr class="dropdown-divider" /></li>
 			<li>
 				<button type="button" class="dropdown-item" @click="openSettingsModal">
 					<span
 						v-if="sponsorTokenExpires"
 						class="d-inline-block p-1 rounded-circle bg-danger border border-light rounded-circle"
 					></span>
-					{{ $t("header.settings") }}
+					{{ $t("settings.title") }}
+				</button>
+			</li>
+			<li v-if="batteryModalAvailable">
+				<button type="button" class="dropdown-item" @click="openBatterySettingsModal">
+					{{ $t("batterySettings.modalTitle") }}
+				</button>
+			</li>
+			<li v-if="gridModalAvailable">
+				<button type="button" class="dropdown-item" @click="openGridSettingsModal">
+					{{ $t("gridSettings.modalTitle") }}
 				</button>
 			</li>
 			<li v-if="$hiddenFeatures()">
-				<router-link class="dropdown-item" to="/config"> Configuration 🧪 </router-link>
+				<router-link class="dropdown-item" to="/config">
+					Device Configuration 🧪
+				</router-link>
 			</li>
+			<li><hr class="dropdown-divider" /></li>
 			<template v-if="providerLogins.length > 0">
 				<li><hr class="dropdown-divider" /></li>
 				<li>
@@ -73,8 +87,6 @@
 				</a>
 			</li>
 		</ul>
-		<GlobalSettingsModal v-bind="globalSettingsModalProps" />
-		<HelpModal />
 	</div>
 </template>
 
@@ -85,15 +97,13 @@ import "@h2d2/shopicons/es/regular/gift";
 import "@h2d2/shopicons/es/regular/moonstars";
 import "@h2d2/shopicons/es/regular/menu";
 import "@h2d2/shopicons/es/regular/newtab";
-import GlobalSettingsModal from "./GlobalSettingsModal.vue";
-import HelpModal from "./HelpModal.vue";
 import collector from "../mixins/collector";
+import gridModalAvailable from "../utils/gridModalAvailable";
 
 import baseAPI from "../baseapi";
 
 export default {
 	name: "TopNavigation",
-	components: { GlobalSettingsModal, HelpModal },
 	mixins: [collector],
 	props: {
 		vehicleLogins: {
@@ -104,11 +114,10 @@ export default {
 		},
 		sponsor: String,
 		sponsorTokenExpires: Number,
+		batteryConfigured: Boolean,
+		smartCostType: String,
 	},
 	computed: {
-		globalSettingsModalProps: function () {
-			return this.collectProps(GlobalSettingsModal);
-		},
 		logoutCount() {
 			return this.providerLogins.filter((login) => !login.loggedIn).length;
 		},
@@ -126,6 +135,12 @@ export default {
 		showBadge() {
 			return this.loginRequired || this.sponsorTokenExpires;
 		},
+		batteryModalAvailable() {
+			return this.batteryConfigured;
+		},
+		gridModalAvailable: function () {
+			return gridModalAvailable(this.smartCostType);
+		},
 	},
 	mounted() {
 		const $el = document.getElementById("topNavigatonDropdown");
@@ -155,6 +170,16 @@ export default {
 			const modal = Modal.getOrCreateInstance(document.getElementById("helpModal"));
 			modal.show();
 		},
+		openBatterySettingsModal() {
+			const modal = Modal.getOrCreateInstance(
+				document.getElementById("batterySettingsModal")
+			);
+			modal.show();
+		},
+		openGridSettingsModal() {
+			const modal = Modal.getOrCreateInstance(document.getElementById("gridSettingsModal"));
+			modal.show();
+		},
 	},
 };
 </script>
diff --git a/assets/js/mixins/collector.js b/assets/js/mixins/collector.js
index b84b893d..1a3b1bef 100644
--- a/assets/js/mixins/collector.js
+++ b/assets/js/mixins/collector.js
@@ -1,9 +1,14 @@
 export default {
   methods: {
     // collect all target component properties from current instance
-    collectProps: function (component) {
+    collectProps: function (component, state) {
       let data = {};
       for (var p in component.props) {
+        // check in optional state
+        if (state && p in state) {
+          data[p] = state[p];
+        }
+        // check in current instance
         if (p in this) {
           data[p] = this[p];
         }
diff --git a/assets/js/utils/gridModalAvailable.js b/assets/js/utils/gridModalAvailable.js
new file mode 100644
index 00000000..a3d02435
--- /dev/null
+++ b/assets/js/utils/gridModalAvailable.js
@@ -0,0 +1,5 @@
+import { CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE } from "../units";
+
+export default function (smartCostType) {
+  return [CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE].includes(smartCostType);
+}
diff --git a/assets/js/views/App.vue b/assets/js/views/App.vue
index b99c7bc5..1074f6b0 100644
--- a/assets/js/views/App.vue
+++ b/assets/js/views/App.vue
@@ -1,14 +1,27 @@
 <template>
 	<div class="app">
 		<router-view :notifications="notifications" :offline="offline"></router-view>
+
+		<GlobalSettingsModal v-bind="globalSettingsProps" />
+		<BatterySettingsModal v-if="batteryModalAvailabe" v-bind="batterySettingsProps" />
+		<GridSettingsModal v-if="gridModalAvailable" v-bind="gridSettingsProps" />
+		<HelpModal />
 	</div>
 </template>
 
 <script>
 import store from "../store";
+import GlobalSettingsModal from "../components/GlobalSettingsModal.vue";
+import BatterySettingsModal from "../components/BatterySettingsModal.vue";
+import GridSettingsModal from "../components/GridSettingsModal.vue";
+import HelpModal from "../components/HelpModal.vue";
+import collector from "../mixins/collector";
+import gridModalAvailable from "../utils/gridModalAvailable";
 
 export default {
 	name: "App",
+	components: { GlobalSettingsModal, HelpModal, BatterySettingsModal, GridSettingsModal },
+	mixins: [collector],
 	props: {
 		notifications: Array,
 		offline: Boolean,
@@ -20,6 +33,23 @@ export default {
 		const siteTitle = store.state.siteTitle;
 		return { title: siteTitle ? `${siteTitle} | evcc` : "evcc" };
 	},
+	computed: {
+		gridModalAvailable: function () {
+			return gridModalAvailable(store.state.smartCostType);
+		},
+		batteryModalAvailabe: function () {
+			return store.state.batteryConfigured;
+		},
+		globalSettingsProps: function () {
+			return this.collectProps(GlobalSettingsModal, store.state);
+		},
+		batterySettingsProps() {
+			return this.collectProps(BatterySettingsModal, store.state);
+		},
+		gridSettingsProps() {
+			return this.collectProps(GridSettingsModal, store.state);
+		},
+	},
 	mounted: function () {
 		this.connect();
 		document.addEventListener("visibilitychange", this.pageVisibilityChanged, false);
diff --git a/charger/easee.go b/charger/easee.go
index 2b2b5b75..00b807f9 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -377,7 +377,12 @@ func (c *Easee) ProductUpdate(i json.RawMessage) {
 
 // ChargerUpdate implements the signalr receiver
 func (c *Easee) ChargerUpdate(i json.RawMessage) {
-	// c.observe("ChargerUpdate", i)
+	c.log.TRACE.Printf("ChargerUpdate: %s", i)
+}
+
+// SubscribeToMyProduct implements the signalr receiver
+func (c *Easee) SubscribeToMyProduct(i json.RawMessage) {
+	c.log.TRACE.Printf("SubscribeToMyProduct: %s", i)
 }
 
 // CommandResponse implements the signalr receiver
diff --git a/core/site.go b/core/site.go
index b41146c4..612e1ef2 100644
--- a/core/site.go
+++ b/core/site.go
@@ -807,7 +807,7 @@ func (site *Site) update(lp Updater) {
 	}
 
 	if batMode := site.GetBatteryMode(); site.batteryDischargeControl {
-		if mode := site.determineBatteryMode(site.Loadpoints()); mode != batMode {
+		if mode := site.determineBatteryMode(site.Loadpoints(), smartCostActive); mode != batMode {
 			if err := site.updateBatteryMode(mode); err != nil {
 				site.log.ERROR.Println("battery mode:", err)
 			}
diff --git a/core/site_battery.go b/core/site_battery.go
index fc26bb41..c0eb71f8 100644
--- a/core/site_battery.go
+++ b/core/site_battery.go
@@ -30,9 +30,9 @@ func (site *Site) SetBatteryMode(batMode api.BatteryMode) {
 	}
 }
 
-func (site *Site) determineBatteryMode(loadpoints []loadpoint.API) api.BatteryMode {
+func (site *Site) determineBatteryMode(loadpoints []loadpoint.API, smartCostActive bool) api.BatteryMode {
 	for _, lp := range loadpoints {
-		if lp.GetStatus() == api.StatusC && (lp.GetMode() == api.ModeNow || lp.GetPlanActive()) {
+		if lp.GetStatus() == api.StatusC && (smartCostActive || lp.GetMode() == api.ModeNow || lp.GetPlanActive()) {
 			return api.BatteryHold
 		}
 	}
diff --git a/i18n/de.toml b/i18n/de.toml
index 68c65a73..e92c0620 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -140,7 +140,6 @@ github = "GitHub"
 login = "Fahrzeug-Logins"
 needHelp = "Hilfe benötigt?"
 sessions = "Ladevorgänge"
-settings = "Einstellungen"
 
 [help]
 discussionsButton = "GitHub Discussions"
@@ -384,7 +383,7 @@ allVehicles = "Alle Fahrzeuge"
 filter = "Filtern"
 
 [settings]
-title = "Einstellungen"
+title = "Allgemeine Einstellungen"
 
 [settings.hiddenFeatures]
 label = "Experimentell"
diff --git a/i18n/en.toml b/i18n/en.toml
index c0610edc..0dc3ea7c 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -13,7 +13,7 @@ legendTitle = "How should solar energy be used?"
 legendTopAutostart = "starts automatically"
 legendTopName = "battery supported charging"
 legendTopSubline = "without interruptions"
-modalTitle = "Battery settings"
+modalTitle = "Battery Settings"
 
 [batterySettings.bufferStart]
 full = "when nearly full"
@@ -134,10 +134,9 @@ about = "About"
 blog = "Blog"
 docs = "Documentation"
 github = "GitHub"
-login = "Vehicle logins"
+login = "Vehicle Logins"
 needHelp = "Need help?"
-sessions = "Charging sessions"
-settings = "Settings"
+sessions = "Charging Sessions"
 
 [help]
 discussionsButton = "GitHub discussions"
@@ -381,7 +380,7 @@ allVehicles = "all vehicles"
 filter = "Filter"
 
 [settings]
-title = "Settings"
+title = "General Settings"
 
 [settings.hiddenFeatures]
 label = "Experimental"
diff --git a/provider/go.go b/provider/go.go
index be534d71..ecf13871 100644
--- a/provider/go.go
+++ b/provider/go.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"fmt"
+	"reflect"
 
 	"github.com/evcc-io/evcc/provider/golang"
 	"github.com/evcc-io/evcc/util"
@@ -167,7 +168,7 @@ func (p *Go) evaluate() (res any, err error) {
 		return nil, err
 	}
 
-	if v.IsNil() {
+	if (v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface) && v.IsNil() {
 		return nil, nil
 	}
 
diff --git a/tariff/fixed.go b/tariff/fixed.go
index 0c1a41b4..71f5753a 100644
--- a/tariff/fixed.go
+++ b/tariff/fixed.go
@@ -39,7 +39,7 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
 
 	t := &Fixed{
 		clock:   clock.New(),
-		dynamic: len(cc.Zones) > 1,
+		dynamic: len(cc.Zones) >= 1,
 	}
 
 	for _, z := range cc.Zones {
diff --git a/templates/definition/meter/kostal-plenticore.yaml b/templates/definition/meter/kostal-plenticore.yaml
index 9d8a73ed..00a476c7 100644
--- a/templates/definition/meter/kostal-plenticore.yaml
+++ b/templates/definition/meter/kostal-plenticore.yaml
@@ -14,10 +14,8 @@ requirements:
   description:
     de: |
       Nur ein System kann und darf auf den Wechselrichter zugreifen!
-      Für die optionale Batteriesteuerung muss Modbus Bytereihenfolge auf BigEndian (Sunspec) konfiguriert sein.
     en: |
-      Only one system may access the inverter!
-      For optional battery control, Modbus byte order must be set to BigEndian (Sunspec).
+      Only a single system may access the inverter!
 params:
   - name: usage
     choice: ["pv", "battery"]
@@ -26,6 +24,13 @@ params:
     choice: ["tcpip"]
     id: 71
     port: 1502
+  - name: endianness
+    description:
+      de: Byte-Reihenfolge (Little/Big)
+      en: Endianness (Little/Big)
+    validvalues: ["big", "little"]
+    default: little
+    advanced: true
   - name: capacity
     advanced: true
   # battery control
@@ -79,7 +84,7 @@ render: |
       register:
         address: 1042 # limit soc
         type: writemultiple
-        decode: float32
+        decode: {{ if (eq .endianness "big") }}float32{{ else }}float32s{{ end }}
   capacity: {{ .capacity }} # kWh
   minsoc: {{ .minsoc }} # %
   maxsoc: {{ .maxsoc }} # %
diff --git a/templates/definition/meter/openems.yaml b/templates/definition/meter/openems.yaml
index 6617572f..521ab574 100644
--- a/templates/definition/meter/openems.yaml
+++ b/templates/definition/meter/openems.yaml
@@ -30,7 +30,7 @@ render: |
       type: basic
       user: x
       password: {{ .password }}
-    jq: .value
+    jq: (.value // 0)
   {{- if eq .usage "battery" }}
   soc:
     source: http
@@ -39,7 +39,7 @@ render: |
       type: basic
       user: x
       password: {{ .password }}
-    jq: .value
+    jq: (.value // 0)
   {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/sofarsolar-g3.yaml b/templates/definition/meter/sofarsolar-g3.yaml
index ad15fa59..f333d51c 100644
--- a/templates/definition/meter/sofarsolar-g3.yaml
+++ b/templates/definition/meter/sofarsolar-g3.yaml
@@ -12,7 +12,7 @@ products:
   - brand: SofarSolar
     description:
       generic: SOFAR 5…24KTL-G3
-capabilities: ["battery-control"]
+# capabilities: ["battery-control"]
 requirements:
   description:
     de: Es wird dringend empfohlen die Verbindung über einen LSE-3 Logger Stick herzustellen. Andere Verbindungswege (z.B. LSW-3 oder via RS485) sind u.U. instabil und benötigen Verzögerungen (delay) > 0s.
@@ -155,23 +155,22 @@ render: |
       address: 0x0608 # SOC_Bat1
       type: holding
       decode: uint16
-  limitsoc:
-    source: go
-    script:
-      int(100-limitSoc)
-    out:
-    - name: dod
-      type: int
-      config:
-        source: modbus
-        {{- include "modbus" . | indent 6 }}
-        delay: {{ .delay }}
-        register:
-          address: 0x104D # BatConfig_Depth_of_Discharge
-          type: writemultiple
-          decode: uint16
-        scale: 100
+  # limitsoc:
+  #   source: go
+  #   script:
+  #     int(100-limitSoc)
+  #   out:
+  #   - name: dod
+  #     type: int
+  #     config:
+  #       source: modbus
+  #       {- include "modbus" . | indent 6 }
+  #       delay: { .delay }
+  #       register:
+  #         address: 0x104D # BatConfig_Depth_of_Discharge
+  #         type: writemultiple
+  #         decode: uint16
   capacity: {{ .capacity }} # kWh
-  minsoc: {{ .minsoc }} # %
-  maxsoc: {{ .maxsoc }} # %
+  # minsoc: {{ .minsoc }} # %
+  # maxsoc: {{ .maxsoc }} # %
   {{- end }}
diff --git a/templates/definition/meter/solax.yaml b/templates/definition/meter/solax.yaml
index eff1c4f9..5d5ef3d6 100644
--- a/templates/definition/meter/solax.yaml
+++ b/templates/definition/meter/solax.yaml
@@ -81,46 +81,35 @@ render: |
     switch:
     - case: 1 # normal
       set:
-        source: sequence
+        source: const
+        value: 0 # self use mode
         set:
-        - source: const
-          value: 0 # self use mode
-          set:
-            source: modbus
-            {{- include "modbus" . | indent 10 }}
-            register:
-              address: 0x001F # SolarChargeUseMode
-              type: writesingle
-              decode: uint16
-        - source: const
-          value: 0 # Stop force charge & discharge
-          set:
-            source: modbus
-            {{- include "modbus" . | indent 10 }}
-            register:
-              address: 0x0020 # Manual mode
-              type: writesingle
-              decode: uint16
+          source: modbus
+          {{- include "modbus" . | indent 8 }}
+          register:
+            address: 0x001F # SolarChargeUseMode
+            type: writesingle
+            decode: uint16
     - case: 2 # hold
       set:
         source: sequence
         set:
         - source: const
-          value: 2 # backup mode
+          value: 0 # Stop force charge & discharge
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
             register:
-              address: 0x001F # SolarChargeUseMode
+              address: 0x0020 # Manual mode
               type: writesingle
               decode: uint16
         - source: const
-          value: 0 # Stop force charge & discharge
+          value: 3 # manual mode
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
             register:
-              address: 0x0020 # Manual mode
+              address: 0x001F # SolarChargeUseMode
               type: writesingle
               decode: uint16
     - case: 3 # charge
@@ -128,21 +117,21 @@ render: |
         source: sequence
         set:
         - source: const
-          value: 3 # manual mode
+          value: 1 # Force charge
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
             register:
-              address: 0x001F # SolarChargeUseMode
+              address: 0x0020 # Manual mode
               type: writesingle
               decode: uint16
         - source: const
-          value: 1 # Force charge
+          value: 3 # manual mode
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
             register:
-              address: 0x0020 # Manual mode
+              address: 0x001F # SolarChargeUseMode
               type: writesingle
               decode: uint16
   {{- if .capacity }}


Test patch:
diff --git a/core/site_battery_test.go b/core/site_battery_test.go
index 859b7898..6cd68636 100644
--- a/core/site_battery_test.go
+++ b/core/site_battery_test.go
@@ -44,7 +44,7 @@ func TestDetermineBatteryMode(t *testing.T) {
 
 		loadpoints := []loadpoint.API{lp}
 
-		mode := s.determineBatteryMode(loadpoints)
+		mode := s.determineBatteryMode(loadpoints, false)
 		assert.Equal(t, tc.expBatMode, mode, tc)
 	}
 }
diff --git a/tests/limits.spec.js b/tests/limits.spec.js
index 3786c2f5..8082254c 100644
--- a/tests/limits.spec.js
+++ b/tests/limits.spec.js
@@ -1,5 +1,5 @@
 const { test, expect } = require("@playwright/test");
-const { start, stop, restart } = require("./evcc");
+const { start, stop } = require("./evcc");
 const { startSimulator, stopSimulator, SIMULATOR_URL } = require("./simulator");
 
 const CONFIG = "simulator.evcc.yaml";
diff --git a/tests/modals.spec.js b/tests/modals.spec.js
new file mode 100644
index 00000000..2eeac459
--- /dev/null
+++ b/tests/modals.spec.js
@@ -0,0 +1,103 @@
+const { test, expect } = require("@playwright/test");
+const { start, stop } = require("./evcc");
+const { startSimulator, stopSimulator } = require("./simulator");
+
+const BASICS_CONFIG = "basics.evcc.yaml";
+const SIMULATOR_CONFIG = "simulator.evcc.yaml";
+
+test.describe("Basics", async () => {
+  test.beforeAll(async () => {
+    await start(BASICS_CONFIG);
+  });
+
+  test.afterAll(async () => {
+    await stop();
+  });
+
+  test("Menu options. No battery and grid.", async ({ page }) => {
+    await page.goto("/");
+
+    await page.getByTestId("topnavigation-button").click();
+    await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
+    await expect(page.getByRole("button", { name: "Battery Settings" })).not.toBeVisible();
+    await expect(page.getByRole("button", { name: "Smart Grid Charging" })).not.toBeVisible();
+    await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
+  });
+
+  test("Need help?", async ({ page }) => {
+    await page.goto("/");
+
+    await page.getByTestId("topnavigation-button").click();
+    await page.getByRole("button", { name: "Need help?" }).click();
+
+    await expect(page.getByRole("heading", { name: "Need help?" })).toBeVisible();
+  });
+
+  test("General Settings", async ({ page }) => {
+    await page.goto("/");
+    await page.getByTestId("topnavigation-button").click();
+    await page.getByRole("button", { name: "General Settings" }).click();
+
+    await expect(page.getByRole("heading", { name: "General Settings" })).toBeVisible();
+  });
+});
+
+test.describe("Advanced", async () => {
+  test.beforeAll(async () => {
+    await start(SIMULATOR_CONFIG);
+    await startSimulator();
+  });
+
+  test.afterAll(async () => {
+    await stopSimulator();
+    await stop();
+  });
+
+  test("Menu options. All available.", async ({ page }) => {
+    await page.goto("/");
+
+    await page.getByTestId("topnavigation-button").click();
+    await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
+    await expect(page.getByRole("button", { name: "Battery Settings" })).toBeVisible();
+    await expect(page.getByRole("button", { name: "Smart Grid Charging" })).toBeVisible();
+    await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
+  });
+
+  test("Battery Settings from top navigation", async ({ page }) => {
+    await page.goto("/");
+    await page.getByTestId("topnavigation-button").click();
+    await page.getByRole("button", { name: "Battery Settings" }).click();
+
+    await expect(page.getByRole("heading", { name: "Battery Settings" })).toBeVisible();
+  });
+
+  test("Battery Settings from energyflow", async ({ page }) => {
+    await page.goto("/");
+    await page.getByTestId("energyflow").click();
+    await page
+      .getByTestId("energyflow-entry-batterydischarge")
+      .getByTestId("energyflow-entry-details")
+      .click();
+
+    await expect(page.getByRole("heading", { name: "Battery Settings" })).toBeVisible();
+  });
+
+  test("Smart Grid Charging from top navigation", async ({ page }) => {
+    await page.goto("/");
+    await page.getByTestId("topnavigation-button").click();
+    await page.getByRole("button", { name: "Smart Grid Charging" }).click();
+
+    await expect(page.getByRole("heading", { name: "Smart Grid Charging" })).toBeVisible();
+  });
+
+  test("Smart Grid Charging from energyflow", async ({ page }) => {
+    await page.goto("/");
+    await page.getByTestId("energyflow").click();
+    await page
+      .getByTestId("energyflow-entry-gridimport")
+      .getByTestId("energyflow-entry-details")
+      .click();
+
+    await expect(page.getByRole("heading", { name: "Smart Grid Charging" })).toBeVisible();
+  });
+});
diff --git a/tests/simulator.evcc.yaml b/tests/simulator.evcc.yaml
index ae867265..06935f98 100755
--- a/tests/simulator.evcc.yaml
+++ b/tests/simulator.evcc.yaml
@@ -95,4 +95,7 @@ tariffs:
   currency: EUR
   grid:
     type: fixed
-    price: 0.40
+    price: 0.4 # EUR/kWh
+    zones:
+      - hours: 1-6
+        price: 0.2