Repository: evcc-io/evcc
Pull request #8302 (2023-06-04 10:43:42)

Add ISO15118 vehicle template
- Fixes https://github.com/evcc-io/evcc/issues/8300
- Also fixes template schema


DerAndereAndi: I have overwritten the help text, using the default strings to clarify when these values in this template can be used

Patch:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 5b6c099d..cde0ccd7 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -44,15 +44,31 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
-      # - uses: actions/setup-go@v4
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
           go-version: ^1.20
           cache-key-prefix: build-cache-
         id: go
 
+      - uses: actions/setup-node@v3
+        with:
+          node-version: "18"
+          cache: "npm"
+
+      - name: Install UI
+        run: make install-ui
+
+      - name: Build UI
+        run: make ui
+
       - name: Build
-        run: mkdir dist && touch dist/empty && make build
+        run: make build
+
+      - name: Upload
+        uses: actions/upload-artifact@v3
+        with:
+          name: evcc-binary
+          path: ./evcc
 
   test:
     name: Test
@@ -92,7 +108,7 @@ jobs:
           version: latest
           args: --timeout 5m
 
-  build-ui:
+  ui:
     name: UI
     runs-on: ubuntu-latest
 
@@ -104,7 +120,7 @@ jobs:
           node-version: "18"
           cache: "npm"
 
-      - name: Install tools
+      - name: Install
         run: make install-ui
 
       - name: Lint
@@ -113,12 +129,48 @@ jobs:
       - name: Test
         run: make test-ui
 
-      - name: Build
-        run: make ui
+      - name: Porcelain
+        run: |
+          test -z "$(git status --porcelain)" || (git status; git diff; false)
+
+  integration:
+    name: Integration
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/setup-node@v3
+        with:
+          node-version: "18"
+          cache: "npm"
+
+      - name: Install npm
+        run: npm ci
+
+      - run: npx playwright install --with-deps chromium
+
+      - name: Wait for Build
+        uses: lewagon/wait-on-check-action@v1.3.1
+        with:
+          ref: ${{ github.event.pull_request.head.sha || github.sha }}
+          check-name: Build
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          wait-interval: 10
+
+      - uses: actions/download-artifact@v3
+        with:
+          name: evcc-binary
+
+      - name: Make executable
+        run: chmod +x evcc
+
+      - name: Run tests
+        run: npx playwright test
 
-      - name: Cache dist
-        uses: actions/cache/save@v3
-        id: cache-dist
+      - uses: actions/upload-artifact@v3
+        if: always()
         with:
-          path: dist
-          key: ${{ runner.os }}-${{ github.sha }}-dist
+          name: playwright-report
+          path: playwright-report/
+          retention-days: 30
diff --git a/.gitignore b/.gitignore
index 9da0cd4c..5d5fc691 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ __debug_bin
 !util/templates/**/*.yaml
 !package*.json
 !evcc.dist.yaml
+!tests/**/*.evcc.yaml
 evcc
 evcc.exe
 evcc_*
@@ -32,3 +33,6 @@ fly.toml
 vendor/*
 QEMU_EFI.fd
 asset-stats.html
+/test-results/
+/playwright-report/
+/playwright/.cache/
diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue
index 57745dd6..53aa89d1 100644
--- a/assets/js/components/Energyflow/Visualization.vue
+++ b/assets/js/components/Energyflow/Visualization.vue
@@ -1,5 +1,9 @@
 <template>
-	<div class="visualization" :class="{ 'visualization--ready': visualizationReady }">
+	<div
+		data-testid="visualization"
+		class="visualization"
+		:class="{ 'visualization--ready': visualizationReady }"
+	>
 		<div class="label-scale d-flex">
 			<div class="d-flex justify-content-start flex-grow-1">
 				<LabelBar v-bind="labelBarProps('top', 'pvProduction')">
diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue
index dbc0fc07..0a03afb2 100644
--- a/assets/js/components/Loadpoints.vue
+++ b/assets/js/components/Loadpoints.vue
@@ -11,6 +11,7 @@
 				<Loadpoint
 					v-bind="loadpoint"
 					:id="index + 1"
+					data-testid="loadpoint"
 					:vehicles="vehicles"
 					:smartCostLimit="smartCostLimit"
 					:smartCostType="smartCostType"
diff --git a/charger/daheimladen-mb.go b/charger/daheimladen-mb.go
index cf5773ce..be25a478 100644
--- a/charger/daheimladen-mb.go
+++ b/charger/daheimladen-mb.go
@@ -103,7 +103,9 @@ func NewDaheimLadenMB(uri string, id uint8) (api.Charger, error) {
 	if err != nil {
 		return nil, fmt.Errorf("failsafe timeout: %w", err)
 	}
-	go wb.heartbeat(time.Duration(binary.BigEndian.Uint16(b)/2) * time.Second)
+	if u := binary.BigEndian.Uint16(b); u > 0 {
+		go wb.heartbeat(time.Duration(u/2) * time.Second)
+	}
 
 	return wb, err
 }
diff --git a/charger/obo.go b/charger/obo.go
new file mode 100644
index 00000000..f96da84d
--- /dev/null
+++ b/charger/obo.go
@@ -0,0 +1,136 @@
+package charger
+
+import (
+	"encoding/binary"
+	"fmt"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+)
+
+// Obo charger implementation
+type Obo struct {
+	log  *util.Logger
+	conn *modbus.Connection
+}
+
+const (
+	oboRegEnable     = 5
+	oboRegAmpsConfig = 6
+	oboRegStatus     = 11
+	oboRegTimeout    = 28
+)
+
+func init() {
+	registry.Add("obo", NewOboFromConfig)
+}
+
+// NewOboFromConfig creates a OBO Bettermann charger from generic config
+func NewOboFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := modbus.Settings{
+		Baudrate: 19200,
+		Comset:   "8E1",
+		ID:       101,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewObo(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+}
+
+// NewObo creates OBO Bettermann charger
+func NewObo(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	log := util.NewLogger("obo")
+	conn.Logger(log.TRACE)
+
+	wb := &Obo{
+		log:  log,
+		conn: conn,
+	}
+
+	// get failsafe timeout from charger
+	b, err := conn.ReadHoldingRegisters(oboRegTimeout, 1)
+	if err != nil {
+		return nil, fmt.Errorf("failsafe timeout: %w", err)
+	}
+	if u := binary.BigEndian.Uint16(b); u > 0 {
+		go wb.heartbeat(time.Duration(u/2) * time.Millisecond)
+	}
+
+	// lightshow
+	// go func() {
+	// 	conn.WriteSingleRegister(3, 1)
+	// 	for {
+	// 		for i := 0; i < 3; i++ {
+	// 			u := rand.Int31n(256)
+	// 			conn.WriteSingleRegister(uint16(i), uint16(u))
+	// 		}
+	// 		time.Sleep(10 * time.Millisecond)
+	// 	}
+	// }()
+	// time.Sleep(10 * time.Second)
+
+	return wb, nil
+}
+
+func (wb *Obo) heartbeat(timeout time.Duration) {
+	for range time.Tick(timeout) {
+		if _, err := wb.conn.ReadHoldingRegisters(dlRegSafeCurrent, 1); err != nil {
+			wb.log.ERROR.Println("heartbeat:", err)
+		}
+	}
+}
+
+// Status implements the api.Charger interface
+func (wb *Obo) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadHoldingRegisters(oboRegStatus, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	switch u := binary.BigEndian.Uint16(b); u {
+	case 0, 1, 2:
+		// A..C
+		return api.ChargeStatus(string('A' + rune(u))), nil
+	default:
+		// D, F
+		return api.StatusNone, fmt.Errorf("invalid status: %d", u)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Obo) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(oboRegEnable, 1)
+	if err != nil {
+		return false, err
+	}
+
+	return binary.BigEndian.Uint16(b) > 0, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *Obo) Enable(enable bool) error {
+	var u uint16
+	if enable {
+		u = 1
+	}
+
+	_, err := wb.conn.WriteSingleRegister(oboRegEnable, u)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Obo) MaxCurrent(current int64) error {
+	_, err := wb.conn.WriteSingleRegister(oboRegAmpsConfig, uint16(current))
+	return err
+}
diff --git a/charger/webasto-next.go b/charger/webasto-next.go
index 03c5f149..ef1076bd 100644
--- a/charger/webasto-next.go
+++ b/charger/webasto-next.go
@@ -97,8 +97,9 @@ func NewWebastoNext(uri string, id uint8) (api.Charger, error) {
 	if err != nil {
 		return nil, fmt.Errorf("failsafe timeout: %w", err)
 	}
-
-	go wb.heartbeat(time.Duration(binary.BigEndian.Uint16(b)/2) * time.Second)
+	if u := binary.BigEndian.Uint16(b); u > 0 {
+		go wb.heartbeat(time.Duration(u/2) * time.Second)
+	}
 
 	return wb, err
 }
diff --git a/i18n/ca.toml b/i18n/ca.toml
index 4ac037ce..29113f01 100644
--- a/i18n/ca.toml
+++ b/i18n/ca.toml
@@ -243,7 +243,7 @@ cleanEnergyCharging = "Energia neta disponible. Carregant..."
 climating = "Precondicionament detectat."
 connected = "Connectat."
 disconnected = "Desconnectat."
-guard = "Començant a {remaining}..."
+guard = "Protecció de commutació activa durant {remaining}"
 minCharge = "Càrrega mínima fins a {soc}%."
 pvDisable = "Excedent insuficient. Pausa en {remaining}..."
 pvEnable = "Excedent disponible. Començar en {remaining}..."
diff --git a/i18n/cs.toml b/i18n/cs.toml
index bd6f1fc5..71212886 100644
--- a/i18n/cs.toml
+++ b/i18n/cs.toml
@@ -1,6 +1,7 @@
 [batterySettings]
 batteryLevel = "Nabití baterie"
 capacity = "{energy} z {total}"
+legendBottomName = "dom má přednost"
 legendBottomSubline = "nepoužito pro dobíjení"
 legendMiddleName = "nejdříve vozidlo"
 legendMiddleSubline = "domácnost druhá"
@@ -49,12 +50,16 @@ tabTitle = "Moje údaje"
 
 [footer.sponsor]
 becomeSponsor = "Staň se sponzorem"
+confetti = "Připraven na konfety?"
+confettiPromise = "Budou nálepky a digitální konfety"
+sticker = "... nebo evcc nálepky?"
 supportUs = "Našim clem je udělat ze soláru standard. Pomozte EVCC zaplacením tolik, kolik pro vás znamená."
 thanks = "Děkujeme, {sponsor}! Tvůj příspěvěk pomáhá vyvíjet evcc i nadále."
 titleNoSponsor = "Podpořte nás"
 titleSponsor = "Jste podporovatel"
 
 [footer.telemetry]
+optIn = "Chci přispět svými daty."
 optInMoreDetails = "Více informací {0}."
 optInMoreDetailsLink = "zde"
 optInSponsorship = "Sponzorování vyžadováno"
@@ -77,3 +82,46 @@ activeHoursLabel = "Aktivně hodin"
 co2Label = "CO₂ emise"
 co2Limit = "CO₂ limit"
 costLimitDescription = "Umožňuje dočasné rychlé dobíjení v solárním režimu"
+modalTitle = "Smart Grid Nabíjení"
+none = "žádné"
+priceLabel = "Cena energie"
+priceLimit = "Cenový strop"
+
+[header]
+about = "O"
+blog = "Blog"
+docs = "Dokumentace"
+github = "GitHub"
+login = "Přihlášení k vozidlu"
+needHelp = "Potřebujete poradit?"
+sessions = "Průběhy nabíjení"
+settings = "Nastavení"
+
+[help]
+discussionsButton = "GitHub diskuse"
+documentationButton = "Dokumentace"
+issueButton = "Nahlásit bug"
+issueDescription = "Našli jste podivné nebo nesprávné chování?"
+modalTitle = "Protřebujete pomoc?"
+primaryActions = "Něco nefunguje, jak má? Tohle jsou dobrá místa, kde získat pomoc."
+restartButton = "Restartovat"
+restartDescription = "Zkusili jste to restartovat?"
+secondaryActions = "Stále se vám nedaří vyřešit váš problém? Zde jsou některé další možnosti."
+
+[help.restart]
+cancel = "Zrušit"
+confirm = "Ano, restartovat!"
+description = "Za normálních okolností by restartování nemělo být nutné. Pokud potřebujete pravidelně restartovat evcc, zvažte prosím nahlášení bugu."
+disclaimer = "Poznámka: evcc se ukončí a spoléhá na to, že operační systém restartuje službu."
+modalTitle = "Opravdu chcete restartovat?"
+
+[main]
+vehicles = "Parkoviště"
+
+[main.chargingPlan]
+activeLabel = "{time}"
+arrivalTab = "Příchod"
+departureTab = "Odchod"
+modalTitle = "Plán nabíjení"
+none = "Žádný"
+title = "Plán"
diff --git a/i18n/de.toml b/i18n/de.toml
index 0c7e819e..5d7cb088 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -9,7 +9,7 @@ legendTitle = "Wie soll Sonnenstrom genutzt werden?"
 legendTopAutostart = "startet automatisch"
 legendTopName = "batterieunterstütztes Laden"
 legendTopSubline = "ohne Unterbrechungen"
-modalTitle = "Batterie Einstellungen"
+modalTitle = "Batterieeinstellungen"
 note = "Hinweis: Nur relevant im PV Modus. Das Ladeverhalten wird entsprechend angepasst."
 
 [batterySettings.bufferStart]
@@ -136,7 +136,7 @@ batteryTooltip = "{energy} von {total} ({soc})"
 gridImport = "Netzbezug"
 homePower = "Verbrauch"
 loadpoints = "Ladepunkt | Ladepunkt | {count} Ladepunkte"
-noEnergy = "Kein Messwerte"
+noEnergy = "Keine Messwerte"
 pvExport = "Einspeisung"
 pvProduction = "Erzeugung"
 selfConsumption = "Eigenverbrauch"
diff --git a/i18n/fi.toml b/i18n/fi.toml
index 5c8077d1..0f556068 100644
--- a/i18n/fi.toml
+++ b/i18n/fi.toml
@@ -245,7 +245,7 @@ cleanEnergyCharging = "Puhdasta energiaa saataville. Lataa..."
 climating = "Esilämmitys/-viilennys havaittu."
 connected = "Yhdistetty."
 disconnected = "Irroitettu."
-guard = "Alkaa {remaining}…"
+guard = "Suojaus aktiivinen: {remaining}…"
 minCharge = "Ladataan minimissään {soc}%."
 pvDisable = "Ei tarpeeksi ylijäämää. Tauko {remaining}…"
 pvEnable = "Ylijäämää saatavilla. Aloitus {remaining}…"
diff --git a/i18n/hr.toml b/i18n/hr.toml
index 3b57ff31..e8bb2ba8 100644
--- a/i18n/hr.toml
+++ b/i18n/hr.toml
@@ -1,3 +1,23 @@
+[batterySettings]
+batteryLevel = "Razina baterije"
+capacity = "{energy} od {total}"
+legendBottomName = "Kućni prioritet"
+legendBottomSubline = "ne koristi se za punjenje"
+legendMiddleName = "vozilo na prvom mjestu"
+legendMiddleSubline = "Kućna sekunda"
+legendTitle = "Kako bi se trebala koristiti solarna energija?"
+legendTopAutostart = "pokreće se automatski"
+legendTopName = "punjenje podržano baterijom"
+legendTopSubline = "bez prekida"
+modalTitle = "Postavke baterije"
+note = "Napomena: Ove postavke utječu samo na solarni način. Ponašanje punjenja prilagođava se u skladu s tim."
+
+[batterySettings.bufferStart]
+full = "kad je skoro pun"
+low = "kad je donekle pun"
+medium = "kad je napola pun"
+never = "samo s dovoljno viška"
+
 [footer]
 
 [footer.community]
@@ -74,9 +94,28 @@ blog = "Blog"
 docs = "Dokumentacija"
 github = "GitHub"
 login = "Prijave vozila"
+needHelp = "Trebate pomoć?"
 sessions = "Napajanja"
 settings = "Postavke"
 
+[help]
+discussionsButton = "GitHub rasprave"
+documentationButton = "Dokumentacija"
+issueButton = "Prijavi grešku"
+issueDescription = "Pronašao čudno ili pogrešno ponašanje?"
+modalTitle = "Trebate pomoć?"
+primaryActions = "Nešto ne radi onako kako bi trebalo? Ovo su dobra mjesta za pomoć."
+restartButton = "Restart"
+restartDescription = "Jesi li ga pokušao ugasiti i ponovno uključiti?"
+secondaryActions = "Još uvijek ne možete riješiti svoj problem? Evo još nekih teških opcija."
+
+[help.restart]
+cancel = "Otkazati"
+confirm = "Da, ponovno pokreni!"
+description = "U normalnim okolnostima ponovno pokretanje ne bi trebalo biti potrebno. Razmislite o podnošenju greške ako trebate redovito ponovno pokretati evcc."
+disclaimer = "Napomena: evcc će prekinuti i osloniti se na operativni sustav za ponovno pokretanje usluge."
+modalTitle = "Jeste li sigurni da želite ponovno pokrenuti?"
+
 [main]
 vehicles = "Parkiralište"
 
@@ -104,10 +143,14 @@ pvProduction = "Produkcija"
 selfConsumption = "Samopotrošnja"
 
 [main.loadpoint]
+avgPrice = "Ø Cijena"
 charged = "Napunjeno"
+co2 = "Ø CO₂"
 duration = "Trajanje"
 fallbackName = "Mjesto punjenja"
 power = "Energija"
+price = "Σ Cijena"
+solar = "Solarno"
 
 [main.loadpointSettings]
 currents = "Struja punjenja"
@@ -203,7 +246,7 @@ cleanEnergyCharging = "Čista energija dostupna. Punjenje..."
 climating = "Otkriveno predkondicioniranje."
 connected = "Povezano."
 disconnected = "Nepovezano."
-guard = "Počinje za {remaining}…"
+guard = "Promjena zaštite aktivna za {remaining}."
 minCharge = "Minimalno napajanje do {soc} %."
 pvDisable = "Nema dovoljno viška. Zaustavlja se za {remaining}…"
 pvEnable = "Dostupan višak. Počinje za {remaining}…"
@@ -226,20 +269,28 @@ reload = "Ponovo učitati?"
 
 [session]
 cancel = "Odustani"
+co2 = "CO₂"
+date = "Razdoblje"
 delete = "Izbriši"
 finished = "Gotovo"
+meter = "Mjerač"
 meterstart = "Početak mjerača"
 meterstop = "Zaustavljanje mjerača"
 odometer = "Kilometraža"
+price = "Cijena"
 started = "Počelo"
 title = "Sesije punjenja"
 
 [sessions]
-date = "Razdoblje"
+avgPrice = "Ø Cijena"
+co2 = "Ø CO₂"
+date = "Gotovo"
 downloadCsv = "Preuzmi kao CSV"
 energy = "Napojeno"
 loadpoint = "Mjesto napajanja"
+price = "Σ Cijena"
 reallyDelete = "Želite li stvarno izbrisati ovu sesiju?"
+solar = "Solarno"
 title = "Napajanja"
 vehicle = "Vozilo"
 
diff --git a/i18n/nl.toml b/i18n/nl.toml
index 83b6a12c..f8d57bf0 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -1,3 +1,16 @@
+[batterySettings]
+batteryLevel = "Batterijniveau"
+capacity = "{energy} van {total}"
+legendBottomName = "prioriteit huis"
+legendBottomSubline = "niet gebruikt voor opladen"
+legendMiddleName = "eerst voertuig"
+legendTitle = "Hoe moet zonne-energie gebruikt worden?"
+legendTopAutostart = "start automatisch"
+legendTopName = "batterij-ondersteund opladen"
+legendTopSubline = "zonder onderbrekingen"
+modalTitle = "Batterij-instellingen"
+note = "Opgelet: Deze instellingen beïnvloeden enkel de zonnemodus. Oplaadgedrag wordt overeenkomstig aangepast."
+
 [footer]
 
 [footer.community]
diff --git a/i18n/sl.toml b/i18n/sl.toml
index 9ed07dc5..1dde908c 100644
--- a/i18n/sl.toml
+++ b/i18n/sl.toml
@@ -111,6 +111,9 @@ secondaryActions = "Še vedno ne morete rešiti svoje težave? Tukaj je nekaj za
 [help.restart]
 cancel = "Prekliči"
 confirm = "Da, ponovno zaženi!"
+description = "V normalnih okoliščinah ponovni zagon ne bi smel biti potreben. Prosimo, razmislite o prijavi napake, če morate redno ponovno zaganjati evcc."
+disclaimer = "Opomba: evcc bo prenehal delovati in se bo za ponovni zagon storitve zanašal na operacijski sistem."
+modalTitle = "Ali ste prepričani, da želite znova zagnati?"
 
 [main]
 vehicles = "Parkiranje"
@@ -139,10 +142,14 @@ pvProduction = "Proizvodnja"
 selfConsumption = "Samoporaba"
 
 [main.loadpoint]
+avgPrice = "Ø Cena"
 charged = "Napolnjeno"
+co2 = "Ø CO₂"
 duration = "Trajanje"
 fallbackName = "Polnilno mesto"
 power = "Moč"
+price = "Σ Cena"
+solar = "Sončna energija"
 
 [main.loadpointSettings]
 currents = "Polnilni tok"
@@ -238,7 +245,7 @@ cleanEnergyCharging = "Na voljo je čista energija. Polnjenje..."
 climating = "Zaznano predkondicioniranje."
 connected = "Povezan."
 disconnected = "Odklopljen."
-guard = "Začne se čez {remaining} ..."
+guard = "Zaščita preklopa je aktivna za {remaining}."
 minCharge = "Minimalno polnjenje na {soc}%."
 pvDisable = "Premalo presežka. Prekinitev čez {remaining}..."
 pvEnable = "Na voljo so presežki. Začetek čez {remaining}..."
@@ -261,20 +268,28 @@ reload = "Ponovno naloži?"
 
 [session]
 cancel = "Prekliči"
+co2 = "CO₂"
+date = "Obdobje"
 delete = "Izbriši"
 finished = "Končano"
+meter = "Merilnik"
 meterstart = "Začetek merilnika"
 meterstop = "Zaključek merilnika"
 odometer = "Število prevoženih kilometrov"
+price = "Cena"
 started = "Začetek"
 title = "Seja polnjenja"
 
 [sessions]
-date = "Obdobje"
+avgPrice = "Ø Cena"
+co2 = "Ø CO₂"
+date = "Končano"
 downloadCsv = "Prenesi kot CSV"
 energy = "Napolnjeno"
 loadpoint = "Polnilno mesto"
+price = "Σ Cena"
 reallyDelete = "Ali res želite izbrisati to sejo?"
+solar = "Sončna energija"
 title = "Seje polnjenja"
 vehicle = "Vozilo"
 
diff --git a/package-lock.json b/package-lock.json
index e0df2122..c0797a59 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
         "@babel/core": "^7.17.2",
         "@h2d2/shopicons": "^1.2.0",
         "@histoire/plugin-vue": "^0.16.1",
+        "@playwright/test": "^1.34.3",
         "@popperjs/core": "^2.11.5",
         "@vitejs/plugin-legacy": "^4.0.2",
         "@vitejs/plugin-vue": "^4.1.0",
@@ -2356,6 +2357,24 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@playwright/test": {
+      "version": "1.34.3",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz",
+      "integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==",
+      "dependencies": {
+        "@types/node": "*",
+        "playwright-core": "1.34.3"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.2"
+      }
+    },
     "node_modules/@polka/url": {
       "version": "1.0.0-next.21",
       "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
@@ -6124,6 +6143,17 @@
       "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz",
       "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w=="
     },
+    "node_modules/playwright-core": {
+      "version": "1.34.3",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
+      "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.4.23",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
diff --git a/package.json b/package.json
index 8f9c2698..376006a4 100644
--- a/package.json
+++ b/package.json
@@ -7,13 +7,15 @@
     "test": "TZ=Europe/Berlin vitest",
     "lint": "prettier assets './**/*.{yml,yaml,toml}' --write && eslint assets/**/*.js assets/**/*.vue --fix",
     "dev": "vite",
-    "histoire": "histoire dev"
+    "histoire": "histoire dev",
+    "playwright": "playwright test --ui"
   },
   "main": "index.js",
   "dependencies": {
     "@babel/core": "^7.17.2",
     "@h2d2/shopicons": "^1.2.0",
     "@histoire/plugin-vue": "^0.16.1",
+    "@playwright/test": "^1.34.3",
     "@popperjs/core": "^2.11.5",
     "@vitejs/plugin-legacy": "^4.0.2",
     "@vitejs/plugin-vue": "^4.1.0",
diff --git a/playwright.config.js b/playwright.config.js
new file mode 100644
index 00000000..443a018c
--- /dev/null
+++ b/playwright.config.js
@@ -0,0 +1,32 @@
+const { defineConfig, devices } = require("@playwright/test");
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ */
+module.exports = defineConfig({
+  testDir: "./tests",
+  /* Run tests in files in parallel */
+  fullyParallel: false,
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!process.env.CI,
+  /* Retry on CI only */
+  retries: process.env.CI ? 2 : 0,
+  /* Opt out of parallel tests on CI. */
+  workers: process.env.CI ? 1 : undefined,
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: "html",
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: "http://127.0.0.1:7070",
+
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: "on-first-retry",
+  },
+  projects: [
+    {
+      name: "chromium",
+      use: { ...devices["Desktop Chrome"] },
+    },
+  ],
+});
diff --git a/provider/mqtt/client.go b/provider/mqtt/client.go
index d87ec994..df4eeace 100644
--- a/provider/mqtt/client.go
+++ b/provider/mqtt/client.go
@@ -135,9 +135,11 @@ func (m *Client) Listen(topic string, callback func(string)) {
 }
 
 // ListenSetter creates a /set listener that resets the payload after handling
-func (m *Client) ListenSetter(topic string, callback func(string)) {
+func (m *Client) ListenSetter(topic string, callback func(string) error) {
 	m.Listen(topic, func(payload string) {
-		callback(payload)
+		if err := callback(payload); err != nil {
+			m.log.ERROR.Printf("set %s: %v", strings.TrimSuffix(topic, "/set"), err)
+		}
 		if err := m.Publish(topic, true, ""); err != nil {
 			m.log.ERROR.Printf("clear: %s: %v", topic, err)
 		}
diff --git a/server/mqtt.go b/server/mqtt.go
index 1d57216d..dcc35464 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -118,65 +118,65 @@ func (m *MQTT) publish(topic string, retained bool, payload interface{}) {
 }
 
 func (m *MQTT) listenSetters(topic string, site site.API, lp loadpoint.API) {
-	m.Handler.ListenSetter(topic+"/mode/set", func(payload string) {
-		lp.SetMode(api.ChargeMode(payload))
+	m.Handler.ListenSetter(topic+"/mode/set", func(payload string) error {
+		mode, err := api.ChargeModeString(payload)
+		if err == nil {
+			lp.SetMode(mode)
+		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/minSoc/set", func(payload string) {
-		if soc, err := strconv.Atoi(payload); err == nil {
+	m.Handler.ListenSetter(topic+"/minSoc/set", func(payload string) error {
+		soc, err := strconv.Atoi(payload)
+		if err == nil {
 			lp.SetMinSoc(soc)
-		} else {
-			m.log.ERROR.Printf("set %s: %v", topic+"/minSoc", err)
 		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/targetEnergy/set", func(payload string) {
-		if val, err := parseFloat(payload); err == nil {
+	m.Handler.ListenSetter(topic+"/targetEnergy/set", func(payload string) error {
+		val, err := parseFloat(payload)
+		if err == nil {
 			lp.SetTargetEnergy(val)
-		} else {
-			m.log.ERROR.Printf("set %s: %v", topic+"/targetEnergy", err)
 		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/targetSoc/set", func(payload string) {
-		if soc, err := strconv.Atoi(payload); err == nil {
+	m.Handler.ListenSetter(topic+"/targetSoc/set", func(payload string) error {
+		soc, err := strconv.Atoi(payload)
+		if err == nil {
 			lp.SetTargetSoc(soc)
-		} else {
-			m.log.ERROR.Printf("set %s: %v", topic+"/targetSoc", err)
 		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/targetTime/set", func(payload string) {
+	m.Handler.ListenSetter(topic+"/targetTime/set", func(payload string) error {
 		val, err := time.Parse(time.RFC3339, payload)
 		if err == nil {
 			err = lp.SetTargetTime(val)
 		} else if string(payload) == "null" {
 			err = lp.SetTargetTime(time.Time{})
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", topic+"/targetTime", err)
-		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/minCurrent/set", func(payload string) {
-		if current, err := parseFloat(payload); err == nil {
+	m.Handler.ListenSetter(topic+"/minCurrent/set", func(payload string) error {
+		current, err := parseFloat(payload)
+		if err == nil {
 			lp.SetMinCurrent(current)
-		} else {
-			m.log.ERROR.Printf("set %s: %v", topic+"/minCurrent", err)
 		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/maxCurrent/set", func(payload string) {
-		if current, err := parseFloat(payload); err == nil {
+	m.Handler.ListenSetter(topic+"/maxCurrent/set", func(payload string) error {
+		current, err := parseFloat(payload)
+		if err == nil {
 			lp.SetMaxCurrent(current)
-		} else {
-			m.log.ERROR.Printf("set %s: %v", topic+"/maxCurrent", err)
 		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/phases/set", func(payload string) {
+	m.Handler.ListenSetter(topic+"/phases/set", func(payload string) error {
 		phases, err := strconv.Atoi(payload)
 		if err == nil {
 			err = lp.SetPhases(phases)
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", topic+"/phases", err)
-		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/vehicle/set", func(payload string) {
+	m.Handler.ListenSetter(topic+"/vehicle/set", func(payload string) error {
 		vehicle, err := strconv.Atoi(payload)
 		if err == nil {
 			if vehicle > 0 {
@@ -189,23 +189,21 @@ func (m *MQTT) listenSetters(topic string, site site.API, lp loadpoint.API) {
 				lp.SetVehicle(nil)
 			}
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", topic+"/vehicle", err)
-		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/enableThreshold/set", func(payload string) {
-		if threshold, err := parseFloat(payload); err == nil {
+	m.Handler.ListenSetter(topic+"/enableThreshold/set", func(payload string) error {
+		threshold, err := parseFloat(payload)
+		if err == nil {
 			lp.SetEnableThreshold(threshold)
-		} else {
-			m.log.ERROR.Printf("set %s: %v", topic+"/enableThreshold", err)
 		}
+		return err
 	})
-	m.Handler.ListenSetter(topic+"/disableThreshold/set", func(payload string) {
-		if threshold, err := parseFloat(payload); err == nil {
+	m.Handler.ListenSetter(topic+"/disableThreshold/set", func(payload string) error {
+		threshold, err := parseFloat(payload)
+		if err == nil {
 			lp.SetDisableThreshold(threshold)
-		} else {
-			m.log.ERROR.Printf("set %s: %v", topic+"/disableThreshold", err)
 		}
+		return err
 	})
 }
 
@@ -216,54 +214,44 @@ func (m *MQTT) Run(site site.API, in <-chan util.Param) {
 	m.publish(topic, true, "online")
 
 	// site setters
-	m.Handler.ListenSetter(m.root+"/site/prioritySoc/set", func(payload string) {
+	m.Handler.ListenSetter(m.root+"/site/prioritySoc/set", func(payload string) error {
 		val, err := parseFloat(payload)
 		if err == nil {
 			err = site.SetPrioritySoc(val)
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", m.root+"/site/prioritySoc", err)
-		}
+		return err
 	})
 
-	m.Handler.ListenSetter(m.root+"/site/bufferSoc/set", func(payload string) {
+	m.Handler.ListenSetter(m.root+"/site/bufferSoc/set", func(payload string) error {
 		val, err := parseFloat(payload)
 		if err == nil {
 			err = site.SetBufferSoc(val)
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", m.root+"/site/bufferSoc", err)
-		}
+		return err
 	})
 
-	m.Handler.ListenSetter(m.root+"/site/bufferStartSoc/set", func(payload string) {
+	m.Handler.ListenSetter(m.root+"/site/bufferStartSoc/set", func(payload string) error {
 		val, err := parseFloat(payload)
 		if err == nil {
 			err = site.SetBufferStartSoc(val)
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", m.root+"/site/bufferStartSoc", err)
-		}
+		return err
 	})
 
-	m.Handler.ListenSetter(m.root+"/site/residualPower/set", func(payload string) {
+	m.Handler.ListenSetter(m.root+"/site/residualPower/set", func(payload string) error {
 		val, err := parseFloat(payload)
 		if err == nil {
 			err = site.SetResidualPower(val)
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", m.root+"/site/residualPower", err)
-		}
+		return err
 	})
 
-	m.Handler.ListenSetter(m.root+"/site/smartcostlimit/set", func(payload string) {
+	m.Handler.ListenSetter(m.root+"/site/smartCostLimit/set", func(payload string) error {
 		val, err := parseFloat(payload)
 		if err == nil {
 			err = site.SetSmartCostLimit(val)
 		}
-		if err != nil {
-			m.log.ERROR.Printf("set %s: %v", m.root+"/site/smartcostlimit", err)
-		}
+		return err
 	})
 
 	// number of loadpoints
diff --git a/templates/README.md b/templates/README.md
index 31bd45df..825969fc 100644
--- a/templates/README.md
+++ b/templates/README.md
@@ -172,9 +172,9 @@ Example Use Case: With SMA Home Manager, there can be a SMA Energy Meter used fo
 
 `example` provides an example value, so the user can get an idea of what is expected and what to look out for
 
-### `valuetype`
+### `type`
 
-`valuetype` allows to define the value type to let the CLI verify the user provided content
+`type` allows to define the value type to let the CLI verify the user provided content
 
 **Possible values**:
 
diff --git a/templates/definition/charger/homematic.yaml b/templates/definition/charger/homematic.yaml
index 353fbd34..b90508a2 100644
--- a/templates/definition/charger/homematic.yaml
+++ b/templates/definition/charger/homematic.yaml
@@ -51,12 +51,12 @@ render: |
   type: homematic
   uri: {{ .host }}:{{ .port }}
   device: {{ .device }}
-  {{ if ne .user "" }}
+  {{- if .user }}
   user: {{ .user }}
-  {{ end }}
-  {{ if ne .password "" }}
+  {{- end }}
+  {{- if .password }}
   password: {{ .password }}
-  {{ end }}
+  {{- end }}
   meterchannel: {{ .meterchannel }}
   switchchannel: {{ .switchchannel }}
   {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/keba-udp.yaml b/templates/definition/charger/keba-udp.yaml
index 74988c9e..04a28b17 100644
--- a/templates/definition/charger/keba-udp.yaml
+++ b/templates/definition/charger/keba-udp.yaml
@@ -27,10 +27,10 @@ params:
 render: |
   type: keba-udp
   uri: {{ .host }}
-  {{ if ne .rfid "" }}
+  {{- if .rfid }}
   rfid:
     tag: {{ .rfid }}
-  {{ end }}
-  {{ if ne .serial "" }}
+  {{- end }}
+  {{- if .serial }}
   serial: {{ .serial }}
-  {{ end }}
+  {{- end }}
diff --git a/templates/definition/charger/obo.yaml b/templates/definition/charger/obo.yaml
new file mode 100644
index 00000000..ebbda791
--- /dev/null
+++ b/templates/definition/charger/obo.yaml
@@ -0,0 +1,14 @@
+template: obo
+products:
+  - brand: OBO Bettermann
+    description:
+      generic: Ion
+params:
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 19200
+    comset: 8E1
+    id: 101
+render: |
+  type: obo
+  {{- include "modbus" . }}
diff --git a/templates/definition/charger/shelly.yaml b/templates/definition/charger/shelly.yaml
index 700ba02c..366cc2b2 100644
--- a/templates/definition/charger/shelly.yaml
+++ b/templates/definition/charger/shelly.yaml
@@ -13,7 +13,11 @@ params:
 render: |
   type: shelly
   uri: http://{{ .host }}
-  {{ if ne .user "" }}user: {{ .user }}{{ end }}
-  {{ if ne .password "" }}password: {{ .password }}{{ end }}
+  {{- if .user }}
+  user: {{ .user }}
+  {{- end }}
+  {{- if .password }}
+  password: {{ .password }}
+  {{- end }}
   channel: {{ .channel }}  # shelly device relay channel
   {{ include "switchsocket" . }}
diff --git a/templates/definition/charger/tasmota.yaml b/templates/definition/charger/tasmota.yaml
index 39f621fa..fe3e2d46 100644
--- a/templates/definition/charger/tasmota.yaml
+++ b/templates/definition/charger/tasmota.yaml
@@ -26,11 +26,11 @@ params:
 render: |
   type: tasmota
   uri: http://{{ .host }}
-  {{ if ne .user "" }}
+  {{- if .user }}
   user: {{ .user }}
-  {{ end }}
-  {{ if ne .password "" }}
+  {{- end }}
+  {{- if .password }}
   password: {{ .password }}
-  {{ end }}
+  {{- end }}
   channel: {{ .channel }}  # relay channel (allowed range: 1-8)
   {{ include "switchsocket" . }}
diff --git a/templates/definition/common-schema.json b/templates/definition/common-schema.json
index 94e68e16..d2de2fe3 100644
--- a/templates/definition/common-schema.json
+++ b/templates/definition/common-schema.json
@@ -194,7 +194,7 @@
         "deprecated": {
           "type": "boolean"
         },
-        "valuetype": {
+        "type": {
           "type": "string",
           "enum": [
             "string",
@@ -288,7 +288,7 @@
         "default": {
           "minLength": 1
         },
-        "valuetype": {
+        "type": {
           "type": "string",
           "enum": [
             "string",
diff --git a/templates/definition/meter/alpha-ess-smile.yaml b/templates/definition/meter/alpha-ess-smile.yaml
index bac7a635..4a81d6de 100644
--- a/templates/definition/meter/alpha-ess-smile.yaml
+++ b/templates/definition/meter/alpha-ess-smile.yaml
@@ -133,7 +133,7 @@ render: |
       type: holding
       decode: uint16
     scale: 0.1
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/deye-hybrid.yaml b/templates/definition/meter/deye-hybrid.yaml
index 3e0eb570..d5fdffd1 100644
--- a/templates/definition/meter/deye-hybrid.yaml
+++ b/templates/definition/meter/deye-hybrid.yaml
@@ -23,7 +23,7 @@ render: |
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 625 # Grid side total power 
+      address: 625 # Grid side total power
       type: holding
       decode: int16
   energy:
@@ -41,21 +41,21 @@ render: |
         address: 613 # "Out-of-grid - current A"
         type: holding
         decode: int16
-      scale: 0.01 
+      scale: 0.01
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       register: # manual non-sunspec register configuration
         address: 614 # "Out-of-grid - current B"
         type: holding
         decode: int16
-      scale: 0.01 
+      scale: 0.01
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       register: # manual non-sunspec register configuration
         address: 615 # "Out-of-grid - current C"
         type: holding
         decode: int16
-      scale: 0.01       
+      scale: 0.01
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
@@ -109,7 +109,7 @@ render: |
       address: 588 # "battery capacity"
       type: holding
       decode: int16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   energy:
diff --git a/templates/definition/meter/e3dc.yaml b/templates/definition/meter/e3dc.yaml
index 81fd011e..80614fde 100644
--- a/templates/definition/meter/e3dc.yaml
+++ b/templates/definition/meter/e3dc.yaml
@@ -55,7 +55,7 @@ render: |
       address: 40082 # Batterie-SOC in %
       type: holding
       decode: uint16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/enphase.yaml b/templates/definition/meter/enphase.yaml
index 4b7a5117..b4b832ab 100644
--- a/templates/definition/meter/enphase.yaml
+++ b/templates/definition/meter/enphase.yaml
@@ -5,19 +5,64 @@ products:
       generic: IQ Envoy
 params:
   - name: usage
-    choice: ["pv"]
+    choice: ["grid", "pv"]
   - name: host
     required: true
+  - name: token
+    help:
+      en: "Required if Envoy Firmware D7.x.xxx. Token is valid for one year. Instructions for obtaining a token via web UI: https://enphase.com/download/accessing-iq-gateway-local-apis-or-local-ui-token-based-authentication"
+      de: "Ab Envoy Firmware D7.x.xxx notwendig. Token ist ein Jahr gültig. Anleitung (Obtaining a token via web UI): https://enphase.com/download/accessing-iq-gateway-local-apis-or-local-ui-token-based-authentication"
 render: |
   type: custom
+  {{- if eq .usage "grid" }}
   power:
     source: http
     uri: http://{{ .host }}/production.json
     method: GET
-    jq: .production[0].wNow
+    {{- if .token }}
+    auth:
+      type: bearer
+      password: {{ .token }}
+    insecure: true
+    {{- end }}
+    jq: .consumption[] | select(.measurementType == "net-consumption").wNow
+    scale: 1
   energy:
     source: http
     uri: http://{{ .host }}/production.json
     method: GET
-    jq: .production[0].whLifetime
+    {{- if .token }}
+    auth:
+      type: bearer
+      password: {{ .token }}
+    insecure: true
+    {{- end }}
+    jq: .consumption[] | select(.measurementType == "net-consumption").whLifetime
     scale: 0.001
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: http
+    uri: http://{{ .host }}/production.json
+    method: GET
+    {{- if .token }}
+    auth:
+      type: bearer
+      password: {{ .token }}
+    insecure: true
+    {{- end }}
+    jq: .production[] | select(.measurementType == "production").wNow
+    scale: 1
+  energy:
+    source: http
+    uri: http://{{ .host }}/production.json
+    method: GET
+    {{- if .token }}
+    auth:
+      type: bearer
+      password: {{ .token }}
+    insecure: true
+    {{- end }}
+    jq: .production[] | select(.measurementType == "production").whLifetime
+    scale: 0.001
+  {{- end }}
diff --git a/templates/definition/meter/fox-ess-ethernet.yaml b/templates/definition/meter/fox-ess-h1-ethernet.yaml
similarity index 92%
rename from templates/definition/meter/fox-ess-ethernet.yaml
rename to templates/definition/meter/fox-ess-h1-ethernet.yaml
index 988b0c0f..bea9b585 100644
--- a/templates/definition/meter/fox-ess-ethernet.yaml
+++ b/templates/definition/meter/fox-ess-h1-ethernet.yaml
@@ -1,8 +1,9 @@
-template: fox-ess-ethernet
+template: fox-ess-h1
+covers: ["fox-ess-ethernet"]
 products:
   - brand: FoxESS
     description:
-      generic: FoxESS via Ethernet
+      generic: H1 via Ethernet
 params:
   - name: usage
     choice: ["pv", "battery"]
diff --git a/templates/definition/meter/fox-ess-h3-ethernet.yaml b/templates/definition/meter/fox-ess-h3-ethernet.yaml
new file mode 100644
index 00000000..bf86968b
--- /dev/null
+++ b/templates/definition/meter/fox-ess-h3-ethernet.yaml
@@ -0,0 +1,49 @@
+template: fox-ess-h3
+products:
+  - brand: FoxESS
+    description:
+      generic: H3 via Ethernet
+params:
+  - name: usage
+    choice: ["pv", "battery"]
+  - name: modbus
+    choice: ["tcpip"]
+render: |
+  type: custom
+  {{- if eq .usage "pv" }}
+  power:
+    source: calc
+    add:
+    - source: modbus
+      {{- include "modbus" . | indent 4 }}
+      # register details
+      register:
+        address: 31002 # PV1
+        type: holding
+        decode: int16
+    - source: modbus
+      {{- include "modbus" . | indent 4 }}
+      # register details
+      register:
+        address: 31005 # PV2
+        type: holding
+        decode: int16
+  {{- end }}
+  {{- if eq .usage "battery" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    # register details
+    register:
+      address: 31036 # Battery Charge/Discharge
+      type: holding
+      decode: int16
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    # register details
+    register:
+      address: 31038 # Soc
+      type: holding
+      decode: int16
+  {{- end }}
diff --git a/templates/definition/meter/fronius-gen24.yaml b/templates/definition/meter/fronius-gen24.yaml
index 9f300d4b..e4291da0 100644
--- a/templates/definition/meter/fronius-gen24.yaml
+++ b/templates/definition/meter/fronius-gen24.yaml
@@ -75,7 +75,7 @@ render: |
     id: 1
     model: sunspec
     value: ChargeState
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/fronius-solarapi-v1.yaml b/templates/definition/meter/fronius-solarapi-v1.yaml
index 72c2b77c..643f2eb0 100644
--- a/templates/definition/meter/fronius-solarapi-v1.yaml
+++ b/templates/definition/meter/fronius-solarapi-v1.yaml
@@ -27,7 +27,7 @@ render: |
     source: http
     uri: http://{{ .host }}/solar_api/v1/GetPowerFlowRealtimeData.fcgi
     jq: .Body.Data.Inverters."1".SOC
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml
index 04672560..8804b3f6 100644
--- a/templates/definition/meter/goodwe-hybrid.yaml
+++ b/templates/definition/meter/goodwe-hybrid.yaml
@@ -84,7 +84,7 @@ render: |
       address: 37007 # SOC First group battery capacity
       type: holding
       decode: uint16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   energy:
diff --git a/templates/definition/meter/growatt-hybrid-tlxh.yaml b/templates/definition/meter/growatt-hybrid-tlxh.yaml
index 838c5651..71bc44c5 100644
--- a/templates/definition/meter/growatt-hybrid-tlxh.yaml
+++ b/templates/definition/meter/growatt-hybrid-tlxh.yaml
@@ -84,7 +84,7 @@ render: |
       address: 3171 # SOC
       type: input
       decode: uint16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   energy:
diff --git a/templates/definition/meter/growatt-hybrid.yaml b/templates/definition/meter/growatt-hybrid.yaml
index 71e9ccf9..42177682 100644
--- a/templates/definition/meter/growatt-hybrid.yaml
+++ b/templates/definition/meter/growatt-hybrid.yaml
@@ -82,7 +82,7 @@ render: |
       address: 1014 # SOC
       type: input
       decode: uint16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   energy:
diff --git a/templates/definition/meter/homematic.yaml b/templates/definition/meter/homematic.yaml
index b023eb7b..40ba5ccd 100644
--- a/templates/definition/meter/homematic.yaml
+++ b/templates/definition/meter/homematic.yaml
@@ -35,12 +35,12 @@ params:
 render: |
   type: homematic
   usage: {{ .usage }}
-  uri: {{ .host }}:{{- if (eq .usage "grid")}}2001{{ else }}2010{{end}}
+  uri: {{ .host }}:{{- if (eq .usage "grid") }}2001{{ else }}2010{{ end }}
   device: {{ .device }}
-  meterchannel: {{ if (eq .usage "grid")}}1{{ else }}{{ .meterchannel }}{{end}}
-  {{ if ne .user "" }}
+  meterchannel: {{ if (eq .usage "grid") }}1{{ else }}{{ .meterchannel }}{{ end }}
+  {{- if .user }}
   user: {{ .user }}
-  {{ end }}
-  {{ if ne .password "" }}
+  {{- end }}
+  {{- if .password }}
   password: {{ .password }}
-  {{ end }}
+  {{- end }}
diff --git a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
index 51470462..1c49a190 100644
--- a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
+++ b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
@@ -139,7 +139,7 @@ render: |
       type: holding
       decode: uint32
     scale: 0.01
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/huawei-sun2000-rs485.yaml b/templates/definition/meter/huawei-sun2000-rs485.yaml
index b1c4f53b..e3b31fbf 100644
--- a/templates/definition/meter/huawei-sun2000-rs485.yaml
+++ b/templates/definition/meter/huawei-sun2000-rs485.yaml
@@ -116,7 +116,7 @@ render: |
       type: holding
       decode: uint32
     scale: 0.01
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/kostal-piko.yaml b/templates/definition/meter/kostal-piko.yaml
index 517989df..ef68a3e1 100644
--- a/templates/definition/meter/kostal-piko.yaml
+++ b/templates/definition/meter/kostal-piko.yaml
@@ -38,7 +38,7 @@ render: |
     uri: http://{{ .host }}/api/dxs.json?dxsEntries=33556229 # Battery SOC
     #   | ----------------------------- Bat SOC% --------- |
     jq: (.dxsEntries[] | select(.dxsId==33556229) | .value )
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end -}}
diff --git a/templates/definition/meter/kostal-plenticore.yaml b/templates/definition/meter/kostal-plenticore.yaml
index 99b712a8..a8c1baa6 100644
--- a/templates/definition/meter/kostal-plenticore.yaml
+++ b/templates/definition/meter/kostal-plenticore.yaml
@@ -44,7 +44,7 @@ render: |
   model: sunspec
   power: 802:W # sunspec model 802 battery
   soc: 802:SoC # sunspec model 802 battery
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/lg-ess-home-8-10.yaml b/templates/definition/meter/lg-ess-home-8-10.yaml
index 264c6df9..26883a86 100644
--- a/templates/definition/meter/lg-ess-home-8-10.yaml
+++ b/templates/definition/meter/lg-ess-home-8-10.yaml
@@ -22,6 +22,6 @@ render: |
   # uri and password are only required once if multiple lgess usages are defined
   uri: https://{{ .host }}
   password: {{ .registration }}
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/openems.yaml b/templates/definition/meter/openems.yaml
index 07bcddaf..6617572f 100644
--- a/templates/definition/meter/openems.yaml
+++ b/templates/definition/meter/openems.yaml
@@ -40,7 +40,7 @@ render: |
       user: x
       password: {{ .password }}
     jq: .value
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/rct-power.yaml b/templates/definition/meter/rct-power.yaml
index cd5dae84..e777e9c3 100644
--- a/templates/definition/meter/rct-power.yaml
+++ b/templates/definition/meter/rct-power.yaml
@@ -15,6 +15,6 @@ render: |
   uri: {{ .host }}
   usage: {{ .usage }}
   cache: 2s
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/senec-home.yaml b/templates/definition/meter/senec-home.yaml
index 340b1b3e..4bcedc82 100644
--- a/templates/definition/meter/senec-home.yaml
+++ b/templates/definition/meter/senec-home.yaml
@@ -42,7 +42,7 @@ render: |
     jq: .ENERGY.GUI_BAT_DATA_FUEL_CHARGE | sub("fl_"; "")
     unpack: hex
     decode: float32
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/shelly-1pm.yaml b/templates/definition/meter/shelly-1pm.yaml
index f6514060..fe63a45e 100644
--- a/templates/definition/meter/shelly-1pm.yaml
+++ b/templates/definition/meter/shelly-1pm.yaml
@@ -15,6 +15,10 @@ params:
 render: |
   type: shelly
   uri: http://{{ .host }}  # shelly device ip address (local)
-  {{ if ne .user "" }}user: {{ .user }}{{ end }}
-  {{ if ne .password "" }}password: {{ .password }}{{ end }}
+  {{- if .user }}
+  user: {{ .user }}
+  {{- end }}
+  {{- if .password }}
+  password: {{ .password }}
+  {{- end }}
   channel: {{ .channel }}  # shelly device relay channel
diff --git a/templates/definition/meter/shelly-pro-3em.yaml b/templates/definition/meter/shelly-pro-3em.yaml
index 231b3f84..15b493bb 100644
--- a/templates/definition/meter/shelly-pro-3em.yaml
+++ b/templates/definition/meter/shelly-pro-3em.yaml
@@ -13,6 +13,10 @@ params:
 render: |
   type: shelly-energymeter
   uri: http://{{ .host }}  # shelly device ip address (local)
-  {{ if ne .user "" }}user: {{ .user }}{{ end }}
-  {{ if ne .password "" }}password: {{ .password }}{{ end }}
+  {{- if .user }}
+  user: {{ .user }}
+  {{- end }}
+  {{- if .password }}
+  password: {{ .password }}
+  {{- end }}
   channel: 0  # shelly device relay channel
diff --git a/templates/definition/meter/sma-datamanager.yaml b/templates/definition/meter/sma-datamanager.yaml
index 3a4570d3..e80f6616 100644
--- a/templates/definition/meter/sma-datamanager.yaml
+++ b/templates/definition/meter/sma-datamanager.yaml
@@ -83,7 +83,7 @@ render: |
       address: 30845 # Battery Soc, %
       type: holding
       decode: uint32nan
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/sma-energymeter.yaml b/templates/definition/meter/sma-energymeter.yaml
index 1cae6eae..fa7d6840 100644
--- a/templates/definition/meter/sma-energymeter.yaml
+++ b/templates/definition/meter/sma-energymeter.yaml
@@ -11,7 +11,7 @@ params:
 render: |
   type: sma
   uri: {{ .host }}
-  {{- if ne .interface "" }}
+  {{- if .interface }}
   interface: {{ .interface }}
   {{- end }}
   {{- if eq .usage "pv" }}
diff --git a/templates/definition/meter/sma-homemanager.yaml b/templates/definition/meter/sma-homemanager.yaml
index 57071969..5a3d168f 100644
--- a/templates/definition/meter/sma-homemanager.yaml
+++ b/templates/definition/meter/sma-homemanager.yaml
@@ -29,6 +29,6 @@ params:
 render: |
   type: sma
   uri: {{ .host }}
-  {{- if ne .interface "" }}
+  {{- if .interface }}
   interface: {{ .interface }}
   {{- end }}
diff --git a/templates/definition/meter/sma-hybrid.yaml b/templates/definition/meter/sma-hybrid.yaml
index c7e529e0..c0f153cf 100644
--- a/templates/definition/meter/sma-hybrid.yaml
+++ b/templates/definition/meter/sma-hybrid.yaml
@@ -60,7 +60,7 @@ render: |
       address: 30845 # SMA Modbus Profile: Battery state of charge
       type: holding
       decode: uint32nan
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/sma-inverter.yaml b/templates/definition/meter/sma-inverter.yaml
index 8b75ba6e..958d78d6 100644
--- a/templates/definition/meter/sma-inverter.yaml
+++ b/templates/definition/meter/sma-inverter.yaml
@@ -18,9 +18,9 @@ params:
 render: |
   type: sma
   uri: {{ .host }} # IP address or hostname
-  {{- if ne .password "" }}
+  {{- if .password }}
   password: {{ .password }} # optional
   {{- end }}
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/sofarsolar-g3.yaml b/templates/definition/meter/sofarsolar-g3.yaml
index ada3e74c..a0131648 100644
--- a/templates/definition/meter/sofarsolar-g3.yaml
+++ b/templates/definition/meter/sofarsolar-g3.yaml
@@ -144,7 +144,7 @@ render: |
       type: holding
       decode: uint32
     scale: 0.1
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/sofarsolar.yaml b/templates/definition/meter/sofarsolar.yaml
index c17dd6eb..62b57426 100644
--- a/templates/definition/meter/sofarsolar.yaml
+++ b/templates/definition/meter/sofarsolar.yaml
@@ -91,7 +91,7 @@ render: |
       address: 0x0210 # The residual capacity of battery
       type: holding
       decode: uint16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml
index d1ae3bea..871f1219 100644
--- a/templates/definition/meter/solaredge-hybrid.yaml
+++ b/templates/definition/meter/solaredge-hybrid.yaml
@@ -63,7 +63,7 @@ render: |
       address: 62852 # Battery 1 State of Energy (SOE)
       type: holding
       decode: float32s
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/solarmax-maxstorage.yaml b/templates/definition/meter/solarmax-maxstorage.yaml
index fbe84038..dcee6621 100644
--- a/templates/definition/meter/solarmax-maxstorage.yaml
+++ b/templates/definition/meter/solarmax-maxstorage.yaml
@@ -47,7 +47,7 @@ render: |
       address: 122 # Batterie Soc
       type: input
       decode: int16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/solarwatt-myreserve-matrix.yaml b/templates/definition/meter/solarwatt-myreserve-matrix.yaml
index 960cc212..6cf187e7 100644
--- a/templates/definition/meter/solarwatt-myreserve-matrix.yaml
+++ b/templates/definition/meter/solarwatt-myreserve-matrix.yaml
@@ -29,7 +29,7 @@ render: |
     source: http
     uri: http://{{ .host }}:{{ .port }}/
     jq: .SData.SoC
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/solarwatt.yaml b/templates/definition/meter/solarwatt.yaml
index b30f37c7..8b18d26b 100644
--- a/templates/definition/meter/solarwatt.yaml
+++ b/templates/definition/meter/solarwatt.yaml
@@ -49,7 +49,7 @@ render: |
     source: http
     uri: http://{{ .host }}/rest/kiwigrid/wizard/devices # EnergyManager
     jq: .result.items[] | select(.deviceModel[].deviceClass == "com.kiwigrid.devices.location.Location" ) | .tagValues.WorkReleased.value / 1000
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/solax-hybrid-cloud.yaml b/templates/definition/meter/solax-hybrid-cloud.yaml
index a3060cbd..81c4cc13 100644
--- a/templates/definition/meter/solax-hybrid-cloud.yaml
+++ b/templates/definition/meter/solax-hybrid-cloud.yaml
@@ -70,7 +70,7 @@ render: |
     uri: https://www.solaxcloud.com/proxyApp/proxy/api/getRealtimeInfo.do?tokenId={{.tokenid}}&sn={{.serial}}
     jq: .result.soc  # Solax API inverter.DC.battery.energy.SOC
     cache: 2m30s
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/solax.yaml b/templates/definition/meter/solax.yaml
index f58dc015..cc833a6d 100644
--- a/templates/definition/meter/solax.yaml
+++ b/templates/definition/meter/solax.yaml
@@ -75,7 +75,7 @@ render: |
       address: 28 # 0x001C Battery Capacity
       type: input
       decode: uint16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/sonnenbatterie.yaml b/templates/definition/meter/sonnenbatterie.yaml
index 2d9b2c22..0b5f489b 100644
--- a/templates/definition/meter/sonnenbatterie.yaml
+++ b/templates/definition/meter/sonnenbatterie.yaml
@@ -31,7 +31,7 @@ render: |
     source: http
     uri: http://{{ .host }}:{{ .port }}/api/v1/status
     jq: .USOC
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/sungrow-hybrid.yaml b/templates/definition/meter/sungrow-hybrid.yaml
index 3ce1ee48..afd8dc70 100644
--- a/templates/definition/meter/sungrow-hybrid.yaml
+++ b/templates/definition/meter/sungrow-hybrid.yaml
@@ -97,7 +97,7 @@ render: |
       type: input
       decode: int16
     scale: 0.1
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/sunspec-hybrid.yaml b/templates/definition/meter/sunspec-hybrid.yaml
index 78ca3ea3..d8de484b 100644
--- a/templates/definition/meter/sunspec-hybrid.yaml
+++ b/templates/definition/meter/sunspec-hybrid.yaml
@@ -93,7 +93,7 @@ render: |
     {{- include "modbus" . | indent 2 }}
     model: sunspec
     value: ChargeState
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/tibber-pulse.yaml b/templates/definition/meter/tibber-pulse.yaml
index 10c53184..8e91b87e 100644
--- a/templates/definition/meter/tibber-pulse.yaml
+++ b/templates/definition/meter/tibber-pulse.yaml
@@ -15,6 +15,6 @@ params:
 render: |
   type: tibber-pulse
   token: {{ .token }}
-  {{- if ne .homeid "" }}
+  {{- if .homeid }}
   homeid: {{ .homeid }}
   {{- end }}
diff --git a/templates/definition/meter/varta.yaml b/templates/definition/meter/varta.yaml
index fd74c020..e3c5d1ff 100644
--- a/templates/definition/meter/varta.yaml
+++ b/templates/definition/meter/varta.yaml
@@ -49,7 +49,7 @@ render: |
       address: 1068 # SOC
       type: holding
       decode: int16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/meter/victron-energy.yaml b/templates/definition/meter/victron-energy.yaml
index 4e74f19a..822525d4 100644
--- a/templates/definition/meter/victron-energy.yaml
+++ b/templates/definition/meter/victron-energy.yaml
@@ -110,7 +110,7 @@ render: |
       address: 843 # Soc
       type: input
       decode: uint16
-  {{- if ne .capacity "" }}
+  {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
   {{- end }}
diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml
index 4ec09e5a..b1ae369c 100644
--- a/templates/definition/vehicle/fiat.yaml
+++ b/templates/definition/vehicle/fiat.yaml
@@ -11,7 +11,7 @@ params:
 render: |
   type: fiat
   {{ include "vehicle-base" . }}
-  {{- if ne .pin "" }}
+  {{- if .pin }}
   pin: {{ .pin }} # mandatory to deep refresh Soc
   {{- end }}
   {{ include "vehicle-identify" . }}
diff --git a/templates/definition/vehicle/iso15118.yaml b/templates/definition/vehicle/iso15118.yaml
new file mode 100644
index 00000000..16cddf1c
--- /dev/null
+++ b/templates/definition/vehicle/iso15118.yaml
@@ -0,0 +1,32 @@
+template: iso15118
+products:
+  - description:
+      generic: ISO15118
+group: generic
+requirements:
+  description:
+    de: Nur unterstützt wenn das Fahrzeug den Ladestand (Soc) an die verbundene Wallbox übermitteln kann.
+    en: Only supported if the vehicle can provided the state of charge (Soc) to the connected wallbox.
+params:
+  - name: title
+  - name: capacity
+  - name: phases
+    advanced: true
+  - name: icon
+    default: car
+    advanced: true
+  - preset: vehicle-identify
+render: |
+  type: custom
+  features: ["offline"]
+  {{- if .title }}
+  title: {{ .title }}
+  {{- end }}
+  {{- if .icon }}
+  icon: {{ .icon }}
+  {{- end }}
+  capacity: {{ .capacity }} # kWh
+  {{- if .phases }}
+  phases: {{ .phases }}
+  {{- end }}
+  {{ include "vehicle-identify" . }}
diff --git a/templates/definition/vehicle/mazda2mqtt.yaml b/templates/definition/vehicle/mazda2mqtt.yaml
index ad8c64ef..0e25f81a 100644
--- a/templates/definition/vehicle/mazda2mqtt.yaml
+++ b/templates/definition/vehicle/mazda2mqtt.yaml
@@ -5,12 +5,15 @@ products:
 group: generic
 requirements:
   description:
-    en: Required MQTT broker configuration and a mazda2mqtt installation https://github.com/C64Axel/mazda2mqtt
-    de: Voraussetzung ist ein konfigurierter MQTT Broker und eine mazda2mqtt Installation https://github.com/C64Axel/mazda2mqtt
+    en: Required MQTT broker configuration and a mazda2mqtt installation https://github.com/C64Axel/mazda2mqtt.
+    de: Voraussetzung ist ein konfigurierter MQTT Broker und eine mazda2mqtt Installation https://github.com/C64Axel/mazda2mqtt.
 params:
   - name: title
   - name: vin
     required: true
+    help:
+      de: Erforderlich
+      en: Required
   - name: capacity
   - name: phases
     advanced: true
diff --git a/templates/definition/vehicle/niu-e-scooter.yaml b/templates/definition/vehicle/niu-e-scooter.yaml
index 30bdaf18..1654bad5 100644
--- a/templates/definition/vehicle/niu-e-scooter.yaml
+++ b/templates/definition/vehicle/niu-e-scooter.yaml
@@ -26,7 +26,7 @@ render: |
   {{- if .title }}
   title: {{ .title }}
   {{- end }}
-  {{- if ne .icon "" }}
+  {{- if .icon }}
   icon: {{ .icon }}
   {{- end }}
   user: {{ .user }} # NIU app user
diff --git a/templates/definition/vehicle/offline.yaml b/templates/definition/vehicle/offline.yaml
index 700f395e..135fe4d4 100644
--- a/templates/definition/vehicle/offline.yaml
+++ b/templates/definition/vehicle/offline.yaml
@@ -26,7 +26,7 @@ render: |
   {{- if .title }}
   title: {{ .title }}
   {{- end }}
-  {{- if ne .icon "" }}
+  {{- if .icon }}
   icon: {{ .icon }}
   {{- end }}
   capacity: {{ .capacity }} # kWh
@@ -37,18 +37,18 @@ render: |
     source: js
     script: 0;
   onIdentify:
-  {{- if (ne .mode "") }}
+  {{- if .mode }}
     mode: {{ .mode }}
   {{- end }}
     minSoc: 0 # fixed - no Soc available
     targetSoc: 100 # fixed - no Soc available
-  {{- if (ne .minCurrent "") }}
+  {{- if .minCurrent }}
     minCurrent: {{ .minCurrent }}
   {{- end }}
-  {{- if (ne .maxCurrent "") }}
+  {{- if .maxCurrent }}
     maxCurrent: {{ .maxCurrent }}
   {{- end }}
-  {{- if ne (len .identifiers) 0 }}
+  {{- if (len .identifiers) }}
   identifiers:
   {{-   range .identifiers }}
   - {{ . }}
diff --git a/templates/definition/vehicle/ovms.yaml b/templates/definition/vehicle/ovms.yaml
index 23702179..c11222c3 100644
--- a/templates/definition/vehicle/ovms.yaml
+++ b/templates/definition/vehicle/ovms.yaml
@@ -29,7 +29,7 @@ render: |
   {{- if .title }}
   title: {{ .title }}
   {{- end }}
-  {{- if ne .icon "" }}
+  {{- if .icon }}
   icon: {{ .icon }}
   {{- end }}
   user: {{ .user }}
diff --git a/templates/definition/vehicle/tesla.yaml b/templates/definition/vehicle/tesla.yaml
index 1b5c9394..1ef527c1 100644
--- a/templates/definition/vehicle/tesla.yaml
+++ b/templates/definition/vehicle/tesla.yaml
@@ -43,7 +43,7 @@ render: |
   {{- if .title }}
   title: {{ .title }}
   {{- end }}
-  {{- if ne .icon "" }}
+  {{- if .icon }}
   icon: {{ .icon }}
   {{- end }}
   tokens:
@@ -53,7 +53,7 @@ render: |
   {{- if .phases }}
   phases: {{ .phases }}
   {{- end }}
-  {{- if ne .vin "" }}
+  {{- if .vin }}
   vin: {{ .vin }}
   {{- end }}
   {{ include "vehicle-identify" . }}
diff --git a/templates/definition/vehicle/tronity.yaml b/templates/definition/vehicle/tronity.yaml
index 5adc14a0..c31e7158 100644
--- a/templates/definition/vehicle/tronity.yaml
+++ b/templates/definition/vehicle/tronity.yaml
@@ -36,7 +36,7 @@ render: |
   {{- if .title }}
   title: {{ .title }}
   {{- end }}
-  {{- if ne .icon "" }}
+  {{- if .icon }}
   icon: {{ .icon }}
   {{- end }}
   credentials:
@@ -46,7 +46,7 @@ render: |
   {{- if .phases }}
   phases: {{ .phases }}
   {{- end }}
-  {{- if ne .vin "" }}
+  {{- if .vin }}
   vin: {{ .vin }}
   {{- end }}
   {{ include "vehicle-identify" . }}
diff --git a/templates/docs/charger/obo_0.yaml b/templates/docs/charger/obo_0.yaml
new file mode 100644
index 00000000..f7ac3318
--- /dev/null
+++ b/templates/docs/charger/obo_0.yaml
@@ -0,0 +1,20 @@
+product:
+  brand: OBO Bettermann
+  description: Ion
+render:
+  - default: |
+      type: template
+      template: obo
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 101
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 19200 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8E1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 101
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
diff --git a/templates/docs/meter/enphase_0.yaml b/templates/docs/meter/enphase_0.yaml
index 56bb9b54..09000212 100644
--- a/templates/docs/meter/enphase_0.yaml
+++ b/templates/docs/meter/enphase_0.yaml
@@ -2,9 +2,17 @@ product:
   brand: Enphase
   description: IQ Envoy
 render:
+  - usage: grid
+    default: |
+      type: template
+      template: enphase
+      usage: grid
+      host: 192.0.2.2 # IP-Adresse oder Hostname
+      token: # Ab Envoy Firmware D7.x.xxx notwendig. Token ist ein Jahr gültig. Anleitung (Obtaining a token via web UI): https://enphase.com/download/accessing-iq-gateway-local-apis-or-local-ui-token-based-authentication (Optional)
   - usage: pv
     default: |
       type: template
       template: enphase
       usage: pv
       host: 192.0.2.2 # IP-Adresse oder Hostname
+      token: # Ab Envoy Firmware D7.x.xxx notwendig. Token ist ein Jahr gültig. Anleitung (Obtaining a token via web UI): https://enphase.com/download/accessing-iq-gateway-local-apis-or-local-ui-token-based-authentication (Optional)
diff --git a/templates/docs/meter/fox-ess-ethernet_0.yaml b/templates/docs/meter/fox-ess-h1_0.yaml
similarity index 79%
rename from templates/docs/meter/fox-ess-ethernet_0.yaml
rename to templates/docs/meter/fox-ess-h1_0.yaml
index 8c3407ff..b1690ddf 100644
--- a/templates/docs/meter/fox-ess-ethernet_0.yaml
+++ b/templates/docs/meter/fox-ess-h1_0.yaml
@@ -1,11 +1,11 @@
 product:
   brand: FoxESS
-  description: FoxESS via Ethernet
+  description: H1 via Ethernet
 render:
   - usage: pv
     default: |
       type: template
-      template: fox-ess-ethernet
+      template: fox-ess-h1
       usage: pv
 
       # Modbus TCP
@@ -16,7 +16,7 @@ render:
   - usage: battery
     default: |
       type: template
-      template: fox-ess-ethernet
+      template: fox-ess-h1
       usage: battery
 
       # Modbus TCP
diff --git a/templates/docs/meter/fox-ess-h3_0.yaml b/templates/docs/meter/fox-ess-h3_0.yaml
new file mode 100644
index 00000000..0489a9ef
--- /dev/null
+++ b/templates/docs/meter/fox-ess-h3_0.yaml
@@ -0,0 +1,26 @@
+product:
+  brand: FoxESS
+  description: H3 via Ethernet
+render:
+  - usage: pv
+    default: |
+      type: template
+      template: fox-ess-h3
+      usage: pv
+
+      # Modbus TCP
+      modbus: tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+  - usage: battery
+    default: |
+      type: template
+      template: fox-ess-h3
+      usage: battery
+
+      # Modbus TCP
+      modbus: tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
diff --git a/templates/docs/vehicle/iso15118_0.yaml b/templates/docs/vehicle/iso15118_0.yaml
new file mode 100644
index 00000000..8a3be6f4
--- /dev/null
+++ b/templates/docs/vehicle/iso15118_0.yaml
@@ -0,0 +1,25 @@
+product:
+  description: ISO15118
+  group: Generische Unterstützung
+description: |
+  Nur unterstützt wenn das Fahrzeug den Ladestand (Soc) an die verbundene Wallbox übermitteln kann.
+render:
+  - default: |
+      type: template
+      template: iso15118
+      title: # Wird in der Benutzeroberfläche angezeigt (Optional)
+      capacity: 50 # Akkukapazität in kWh (Optional)
+    advanced: |
+      type: template
+      template: iso15118
+      title: # Wird in der Benutzeroberfläche angezeigt (Optional)
+      capacity: 50 # Akkukapazität in kWh (Optional)
+      phases: 3 # Die maximale Anzahl der Phasen welche genutzt werden können (Optional)
+      icon: car # Icon in der Benutzeroberfläche (Optional)
+      mode: # Möglich sind Off, Now, MinPV und PV, oder leer wenn keiner definiert werden soll (Optional)
+      minSoc: 25 # Ladung mit maximaler Geschwindigkeit bis zu dem angegeben Ladestand unabhängig PV-Erzeugung, wenn der Lademodus nicht auf 'Aus' steht (Optional)
+      targetSoc: 80 # Bis zu welchem Ladestand (Soc) soll das Fahrzeug geladen werden (Optional)
+      minCurrent: 6 # Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
+      maxCurrent: 16 # Definiert die maximale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
+      identifiers: # Kann meist erst später eingetragen werden, siehe: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox (Optional)
+      priority: # Priorität des Ladepunktes oder Fahrzeugs in Relation zu anderen Ladepunkten oder Fahrzeugen für die Zuweisung von PV-Energie (Optional)
diff --git a/templates/docs/vehicle/mazda2mqtt_0.yaml b/templates/docs/vehicle/mazda2mqtt_0.yaml
index b6424cfa..27e1a5ed 100644
--- a/templates/docs/vehicle/mazda2mqtt_0.yaml
+++ b/templates/docs/vehicle/mazda2mqtt_0.yaml
@@ -2,19 +2,19 @@ product:
   description: mazda2mqtt
   group: Generische Unterstützung
 description: |
-  Voraussetzung ist ein konfigurierter MQTT Broker und eine mazda2mqtt Installation https://github.com/C64Axel/mazda2mqtt
+  Voraussetzung ist ein konfigurierter MQTT Broker und eine mazda2mqtt Installation https://github.com/C64Axel/mazda2mqtt.
 render:
   - default: |
       type: template
       template: mazda2mqtt
       title: # Wird in der Benutzeroberfläche angezeigt (Optional)
-      vin: W... # Erforderlich, wenn mehrere Fahrzeuge des Herstellers vorhanden sind
+      vin: W... # Erforderlich
       capacity: 50 # Akkukapazität in kWh (Optional)
     advanced: |
       type: template
       template: mazda2mqtt
       title: # Wird in der Benutzeroberfläche angezeigt (Optional)
-      vin: W... # Erforderlich, wenn mehrere Fahrzeuge des Herstellers vorhanden sind
+      vin: W... # Erforderlich
       capacity: 50 # Akkukapazität in kWh (Optional)
       phases: 3 # Die maximale Anzahl der Phasen welche genutzt werden können (Optional)
       icon: car # Icon in der Benutzeroberfläche (Optional)
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index 3b36acea..092397b8 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -35,6 +35,7 @@
   "LadeFoxx",
   "Mennekes",
   "NRGKick",
+  "OBO Bettermann",
   "OpenEVSE",
   "openWB",
   "Optec",
@@ -83,6 +84,7 @@
   "DZG",
   "E3/DC",
   "Eastron",
+  "Enphase",
   "FENECON",
   "FoxESS",
   "Fronius",
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index eafb1eef..4308b0f4 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -173,7 +173,7 @@ params:
       en: Minimum state of charge (Soc) in %
     help:
       de: Ladung mit maximaler Geschwindigkeit bis zu dem angegeben Ladestand unabhängig PV-Erzeugung, wenn der Lademodus nicht auf 'Aus' steht
-      en: Immediate charging with maximum power up to the defined state of charge independently from solar production if the charge mode is not set to 'OFF'
+      en: Immediate charging with maximum power up to the defined state of charge independently from solar production if the charge mode is not set to 'Off'
     example: 25
     type: number
   - name: targetsoc


Test patch:
diff --git a/tests/basics.evcc.yaml b/tests/basics.evcc.yaml
new file mode 100755
index 00000000..99832e63
--- /dev/null
+++ b/tests/basics.evcc.yaml
@@ -0,0 +1,34 @@
+site:
+  title: Hello World
+  meters:
+    grid: grid
+
+meters:
+  - name: grid
+    type: custom
+    power:
+      source: js
+      script: |
+        1000
+
+loadpoints:
+  - title: Carport
+    charger: charger
+
+chargers:
+  - name: charger
+    type: custom
+    enable:
+      source: js
+      script:
+    enabled:
+      source: js
+      script: |
+        false
+    status:
+      source: js
+      script: |
+        "B"
+    maxcurrent:
+      source: js
+      script:
diff --git a/tests/basics.spec.js b/tests/basics.spec.js
new file mode 100644
index 00000000..a2a6cf44
--- /dev/null
+++ b/tests/basics.spec.js
@@ -0,0 +1,40 @@
+const { test, expect } = require("@playwright/test");
+const { execEvcc } = require("./utils");
+
+let server;
+
+test.beforeAll(() => {
+  server = execEvcc("basics.evcc.yaml");
+});
+
+test.afterAll(() => {
+  server.kill();
+});
+
+test.beforeEach(async ({ page }) => {
+  await page.goto("/");
+});
+
+test.describe("main screen", async () => {
+  test("site title", async ({ page }) => {
+    await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
+  });
+
+  test("visualization", async ({ page }) => {
+    const locator = page.getByTestId("visualization");
+    await expect(locator).toBeVisible();
+    await expect(locator).toContainText("1,000 W");
+  });
+
+  test("one loadpoint", async ({ page }) => {
+    await expect(page.getByTestId("loadpoint")).toHaveCount(1);
+  });
+
+  test("loadpoint title", async ({ page }) => {
+    await expect(page.getByRole("heading", { name: "Carport" })).toBeVisible();
+  });
+
+  test("guest vehicle", async ({ page }) => {
+    await expect(page.getByRole("heading", { name: "Guest vehicle" })).toBeVisible();
+  });
+});
diff --git a/tests/utils.js b/tests/utils.js
new file mode 100644
index 00000000..04a3b926
--- /dev/null
+++ b/tests/utils.js
@@ -0,0 +1,7 @@
+import { exec } from "child_process";
+
+export function execEvcc(config) {
+  const server = exec(`./evcc --config tests/${config}`);
+  server.stdout.pipe(process.stdout);
+  return server;
+}