Repository: evcc-io/evcc (go)

Issue:
Einstellungsmenü nicht durchgängig identisch
Die neuen Einstellungen für "Batterie" und "smartes Netzladen" sind nicht durchgängig verfügbar.
Sie fehlen bei den Ansichten "Ladevorgänge" und "device configuration".

![image](https://github.com/evcc-io/evcc/assets/99199865/d4d3678d-4763-4ce3-a82c-38494f9307b7)

![image](https://github.com/evcc-io/evcc/assets/99199865/4ef174a3-553d-41eb-8860-43ff4d6b1138)


Pull request description:
Fix: battery/grid settings links on sessons and config page
fixes #11536

Patch:
diff --git a/assets/js/views/ChargingSessions.vue b/assets/js/views/ChargingSessions.vue
index 13fca909..fd228c87 100644
--- a/assets/js/views/ChargingSessions.vue
+++ b/assets/js/views/ChargingSessions.vue
@@ -8,7 +8,7 @@
 				<div size="s" class="mx-2 flex-grow-0 flex-shrink-0 fw-normal">/</div>
 				<span class="text-truncate">{{ $t("sessions.title") }}</span>
 			</h1>
-			<TopNavigation />
+			<TopNavigation v-bind="topNavigation" />
 		</header>
 
 		<div class="row">
@@ -263,6 +263,7 @@ import CustomSelect from "../components/CustomSelect.vue";
 import ChargingSessionModal from "../components/ChargingSessionModal.vue";
 import breakpoint from "../mixins/breakpoint";
 import settings from "../settings";
+import collector from "../mixins/collector";
 
 const COLUMNS_PER_BREAKPOINT = {
 	xs: 1,
@@ -276,7 +277,7 @@ const COLUMNS_PER_BREAKPOINT = {
 export default {
 	name: "ChargingSessions",
 	components: { TopNavigation, ChargingSessionModal, CustomSelect },
-	mixins: [formatter, breakpoint],
+	mixins: [formatter, breakpoint, collector],
 	props: {
 		notifications: Array,
 		month: { type: Number, default: () => new Date().getMonth() + 1 },
@@ -295,6 +296,10 @@ export default {
 		return { title: `${this.$t("sessions.title")} | evcc` };
 	},
 	computed: {
+		topNavigation: function () {
+			const vehicleLogins = store.state.auth ? store.state.auth.vehicles : {};
+			return { vehicleLogins, ...this.collectProps(TopNavigation, store.state) };
+		},
 		currentSessions() {
 			const sessionsWithDefaults = this.sessions.map((session) => {
 				const loadpoint = session.loadpoint || this.$t("main.loadpoint.fallbackName");
diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue
index 115f7070..71c63362 100644
--- a/assets/js/views/Config.vue
+++ b/assets/js/views/Config.vue
@@ -7,7 +7,7 @@
 				</router-link>
 				Configuration 🧪
 			</h1>
-			<TopNavigation />
+			<TopNavigation v-bind="topNavigation" />
 		</header>
 
 		<div class="alert alert-danger mb-5" role="alert">
@@ -189,10 +189,13 @@ import Modal from "bootstrap/js/dist/modal";
 import api from "../api";
 import VehicleIcon from "../components/VehicleIcon";
 import VehicleModal from "../components/Config/VehicleModal.vue";
+import store from "../store";
+import collector from "../mixins/collector";
 
 export default {
 	name: "Config",
 	components: { TopNavigation, VehicleIcon, VehicleModal },
+	mixins: [collector],
 	data() {
 		return {
 			vehicles: [],
@@ -203,6 +206,12 @@ export default {
 			vehicleId: undefined,
 		};
 	},
+	computed: {
+		topNavigation: function () {
+			const vehicleLogins = store.state.auth ? store.state.auth.vehicles : {};
+			return { vehicleLogins, ...this.collectProps(TopNavigation, store.state) };
+		},
+	},
 	mounted() {
 		this.loadVehicles();
 		this.loadLoadpoints();
diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go
index 224d5aa0..86fb4da9 100644
--- a/charger/openwb-2.0.go
+++ b/charger/openwb-2.0.go
@@ -11,9 +11,10 @@ import (
 
 // OpenWB20 charger implementation
 type OpenWB20 struct {
-	conn *modbus.Connection
-	curr uint16
-	base uint16
+	conn    *modbus.Connection
+	enabled bool
+	curr    uint16
+	base    uint16
 }
 
 const (
@@ -106,12 +107,7 @@ func (wb *OpenWB20) Status() (api.ChargeStatus, error) {
 
 // Enabled implements the api.Charger interface
 func (wb *OpenWB20) Enabled() (bool, error) {
-	b, err := wb.conn.ReadInputRegisters(wb.base+openwbRegActualAmps, 1)
-	if err != nil {
-		return false, err
-	}
-
-	return binary.BigEndian.Uint16(b) == 1, nil
+	return verifyEnabled(wb, wb.enabled)
 }
 
 func (wb *OpenWB20) setCurrent(u uint16) error {
@@ -125,8 +121,11 @@ func (wb *OpenWB20) Enable(enable bool) error {
 	if enable {
 		u = wb.curr
 	}
-
-	return wb.setCurrent(u)
+	err := wb.setCurrent(u)
+	if err == nil {
+		wb.enabled = enable
+	}
+	return err
 }
 
 // MaxCurrent implements the api.Charger interface
diff --git a/templates/definition/meter/solarman.yaml b/templates/definition/meter/solarman.yaml
index a5bb6860..55afbcf9 100644
--- a/templates/definition/meter/solarman.yaml
+++ b/templates/definition/meter/solarman.yaml
@@ -4,15 +4,6 @@ products:
   - brand: IGEN Tech
     description:
       generic: Solarman Logger
-  - brand: Deye
-    description:
-      generic: SUN series micro inverter
-  - brand: Bosswerk
-    description:
-      generic: MI series micro inverter
-  - brand: Ginlong
-    description:
-      generic: Solis inverter
 params:
   - name: usage
     choice: ["pv"]
diff --git a/templates/definition/meter/solis-hybrid.yaml b/templates/definition/meter/solis-hybrid.yaml
new file mode 100644
index 00000000..9d2a09af
--- /dev/null
+++ b/templates/definition/meter/solis-hybrid.yaml
@@ -0,0 +1,122 @@
+template: solis-hybrid
+products:
+  - brand: Ginlong
+    description:
+      generic: Solis Hybrid Inverter
+params:
+  - name: usage
+    choice: ["grid", "pv", "battery"]
+    allinone: true
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 9600
+    id: 1
+  - name: capacity
+    advanced: true
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33263 # Meter total active power
+      type: input
+      decode: int32
+    scale: -1
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33283 # Meter total active energy from grid
+      type: input
+      decode: uint32
+    scale: 0.01
+  currents:
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33252 # Meter ac current A
+      type: input
+      decode: uint16
+    scale: 0.01
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33254 # Meter ac current B
+      type: input
+      decode: uint16
+    scale: 0.01
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33256 # Meter ac current C
+      type: input
+      decode: uint16
+    scale: 0.01
+  powers:
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33257 # Meter active power A
+      type: input
+      decode: int32
+    scale: -1
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33259 # Meter active power B
+      type: input
+      decode: int32
+    scale: -1
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33261 # Meter active power C
+      type: input
+      decode: int32
+    scale: -1
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      type: input
+      address: 33057 # Total DC output power (PV Power)
+      decode: uint32
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33029 # Total energy generation
+      type: input
+      decode: uint32
+  {{- end }}
+  {{- if eq .usage "battery" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      type: input
+      address: 33149 # Battery power
+      decode: int32
+    scale: -1      
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33165 # Battery total discharge energy
+      type: input
+      decode: uint32
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33139 # Battery capacity SOC
+      type: input
+      decode: uint16
+  {{- if .capacity }}
+  capacity: {{ .capacity }} # kWh
+  {{- end }}
+  {{- end }}
diff --git a/templates/definition/meter/solis.yaml b/templates/definition/meter/solis.yaml
new file mode 100644
index 00000000..a34a9f28
--- /dev/null
+++ b/templates/definition/meter/solis.yaml
@@ -0,0 +1,48 @@
+template: solis
+products:
+  - brand: Ginlong
+    description:
+      generic: Solis Inverter
+params:
+  - name: usage
+    choice: ["grid", "pv"]
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 9600
+    id: 1
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 3262 # Meter Total P
+      type: input
+      decode: int32
+    scale: -1
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 3282 # Meter grid import active energy
+      type: input
+      decode: uint32
+    scale: 0.01
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      type: input
+      address: 3004 # Active power
+      decode: uint32
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 3008 # Total energy
+      type: input
+      decode: uint32
+  {{- end }}


Test patch:
diff --git a/tests/modals.spec.js b/tests/modals.spec.js
index 2eeac459..59f19195 100644
--- a/tests/modals.spec.js
+++ b/tests/modals.spec.js
@@ -5,6 +5,8 @@ const { startSimulator, stopSimulator } = require("./simulator");
 const BASICS_CONFIG = "basics.evcc.yaml";
 const SIMULATOR_CONFIG = "simulator.evcc.yaml";
 
+const UI_ROUTES = ["/", "/#/sessions", "/#/config"];
+
 test.describe("Basics", async () => {
   test.beforeAll(async () => {
     await start(BASICS_CONFIG);
@@ -15,13 +17,15 @@ test.describe("Basics", async () => {
   });
 
   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();
+    for (const route of UI_ROUTES) {
+      await page.goto(route);
+
+      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 }) => {
@@ -54,13 +58,15 @@ test.describe("Advanced", async () => {
   });
 
   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();
+    for (const route of UI_ROUTES) {
+      await page.goto(route);
+
+      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 }) => {