Repository: evcc-io/evcc
Pull request #5965 (2023-01-31 14:24:50)

Honor advanced setting in templates defaults.yaml
`evcc configure` currently asks values for params that have `advanced: true` set in `defaults.yaml` even when it runs in `standard` mode where it shouldn't ask for any param defined as advanced. This PR fixes this.

Also simplify code that overwrites param values with defaults as discussed in https://github.com/evcc-io/evcc/pull/5824


Patch:
diff --git a/assets/css/app.css b/assets/css/app.css
index 4fb8591a..5d830fb3 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -183,6 +183,14 @@ a:hover {
 	border-color: var(--evcc-accent3) !important;
 }
 
+.btn-xs {
+	--bs-btn-padding-y: 0;
+	--bs-btn-padding-x: 0.5rem;
+	--bs-btn-font-size: 0.7rem;
+	--bs-btn-border-radius: 0.25rem;
+	--bs-btn-line-height: 2;
+}
+
 .text-evcc {
 	color: var(--evcc-dark-green);
 }
diff --git a/assets/index.html b/assets/index.html
index dddc01c4..c3c8f5a7 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -5,7 +5,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="description" content="evcc - Sonne tanken ☀️🚘" />
     <meta name="apple-mobile-web-app-capable" content="yes" />
-    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="default" />
     <meta name="apple-mobile-web-app-title" content="evcc" />
     <meta name="application-name" content="evcc" />
 
diff --git a/assets/js/components/ChargingSessionModal.vue b/assets/js/components/ChargingSessionModal.vue
index bf3ef198..dd75beba 100644
--- a/assets/js/components/ChargingSessionModal.vue
+++ b/assets/js/components/ChargingSessionModal.vue
@@ -7,7 +7,7 @@
 			role="dialog"
 			aria-hidden="true"
 		>
-			<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+			<div class="modal-dialog modal-dialog-centered" role="document">
 				<div v-if="session" class="modal-content">
 					<div class="modal-header">
 						<h5>{{ $t("session.title") }}</h5>
@@ -125,7 +125,7 @@
 			role="dialog"
 			aria-hidden="true"
 		>
-			<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+			<div class="modal-dialog modal-dialog-centered" role="document">
 				<div v-if="session" class="modal-content">
 					<div class="modal-header">
 						<h5>{{ $t("sessions.reallyDelete") }}</h5>
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index a7fc58ed..7a263659 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -128,6 +128,7 @@ import Visualization from "./Visualization.vue";
 import EnergyflowEntry from "./EnergyflowEntry.vue";
 import formatter from "../../mixins/formatter";
 import AnimatedNumber from "../AnimatedNumber.vue";
+import settings from "../../settings";
 
 export default {
 	name: "Energyflow",
@@ -189,6 +190,10 @@ export default {
 	},
 	mounted() {
 		window.addEventListener("resize", this.updateHeight);
+		// height must be calculated in case of initially open details
+		if (settings.energyflowDetails) {
+			setTimeout(this.toggleDetails, 50);
+		}
 	},
 	unmounted() {
 		window.removeEventListener("resize", this.updateHeight);
@@ -200,6 +205,7 @@ export default {
 		toggleDetails: function () {
 			this.updateHeight();
 			this.detailsOpen = !this.detailsOpen;
+			settings.energyflowDetails = this.detailsOpen;
 		},
 		updateHeight: function () {
 			this.detailsCompleteHeight = this.$refs.detailsInner.offsetHeight;
diff --git a/assets/js/components/GlobalSettingsModal.vue b/assets/js/components/GlobalSettingsModal.vue
index 4d7dbf91..b04c0534 100644
--- a/assets/js/components/GlobalSettingsModal.vue
+++ b/assets/js/components/GlobalSettingsModal.vue
@@ -8,7 +8,7 @@
 			role="dialog"
 			aria-hidden="true"
 		>
-			<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+			<div class="modal-dialog modal-dialog-centered" role="document">
 				<div class="modal-content">
 					<div class="modal-header">
 						<h5 class="modal-title">{{ $t("settings.title") }}</h5>
diff --git a/assets/js/components/LoadpointSettingsModal.vue b/assets/js/components/LoadpointSettingsModal.vue
index ca88b369..9b44edc2 100644
--- a/assets/js/components/LoadpointSettingsModal.vue
+++ b/assets/js/components/LoadpointSettingsModal.vue
@@ -8,7 +8,7 @@
 			role="dialog"
 			aria-hidden="true"
 		>
-			<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+			<div class="modal-dialog modal-dialog-centered" role="document">
 				<div class="modal-content">
 					<div class="modal-header">
 						<h5 class="modal-title">
diff --git a/assets/js/components/Savings.vue b/assets/js/components/Savings.vue
index e6165014..50113163 100644
--- a/assets/js/components/Savings.vue
+++ b/assets/js/components/Savings.vue
@@ -23,10 +23,7 @@
 				role="dialog"
 				aria-hidden="true"
 			>
-				<div
-					class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"
-					role="document"
-				>
+				<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
 					<div class="modal-content">
 						<div class="modal-header">
 							<h5 class="modal-title">
diff --git a/assets/js/components/TargetCharge.vue b/assets/js/components/TargetCharge.vue
index 7a58721b..4be6c15e 100644
--- a/assets/js/components/TargetCharge.vue
+++ b/assets/js/components/TargetCharge.vue
@@ -26,10 +26,7 @@
 				role="dialog"
 				aria-hidden="true"
 			>
-				<div
-					class="modal-dialog modal-dialog-centered modal-dialog-scrollable"
-					role="document"
-				>
+				<div class="modal-dialog modal-dialog-centered" role="document">
 					<div class="modal-content">
 						<div class="modal-header">
 							<h5 class="modal-title">
diff --git a/assets/js/components/TargetChargePlan.vue b/assets/js/components/TargetChargePlan.vue
index 7de39a2e..ad0626ef 100644
--- a/assets/js/components/TargetChargePlan.vue
+++ b/assets/js/components/TargetChargePlan.vue
@@ -25,7 +25,6 @@
 				:class="{ active: isActive(index), toLate: slot.toLate }"
 				@touchstart="activeIndex = index"
 				@mouseenter="activeIndex = index"
-				@touchmove="touchmove"
 				@touchend="activeIndex = null"
 				@mouseleave="activeIndex = null"
 				@mouseup="activeIndex = null"
@@ -160,17 +159,6 @@ export default {
 				height: price === undefined ? "100%" : `${(100 / this.maxPrice) * price}%`,
 			};
 		},
-		touchmove(e) {
-			let $el = document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY);
-			if (!$el) return;
-			if (!$el.classList.contains("slot")) {
-				$el = $el.parent;
-			}
-			if (!$el) return;
-			const index = $el.getAttribute("data-index");
-			if (!index) return;
-			this.activeIndex = index * 1;
-		},
 		convertDates(list) {
 			if (!list?.length) {
 				return [];
diff --git a/assets/js/components/Version.vue b/assets/js/components/Version.vue
index dfe8bbe5..4d3ed861 100644
--- a/assets/js/components/Version.vue
+++ b/assets/js/components/Version.vue
@@ -40,10 +40,7 @@
 				role="dialog"
 				aria-hidden="true"
 			>
-				<div
-					class="modal-dialog modal-dialog-centered modal-dialog-scrollable"
-					role="document"
-				>
+				<div class="modal-dialog modal-dialog-centered" role="document">
 					<div class="modal-content">
 						<div class="modal-header">
 							<h5 class="modal-title">{{ $t("footer.version.modalTitle") }}</h5>
diff --git a/assets/js/settings.js b/assets/js/settings.js
index db655b16..195676bf 100644
--- a/assets/js/settings.js
+++ b/assets/js/settings.js
@@ -3,6 +3,7 @@ import { reactive, watch } from "vue";
 const SETTINGS_LOCALE = "settings_locale";
 const SETTINGS_THEME = "settings_theme";
 const SETTINGS_UNIT = "settings_unit";
+const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
 
 function read(key) {
   return window.localStorage[key];
@@ -22,15 +23,27 @@ function save(key) {
   };
 }
 
+function readBool(key) {
+  return read(key) === "true";
+}
+
+function saveBool(key) {
+  return (value) => {
+    save(key)(value ? "true" : "false");
+  };
+}
+
 const settings = reactive({
   telemetry: null,
   locale: read(SETTINGS_LOCALE),
   theme: read(SETTINGS_THEME),
   unit: read(SETTINGS_UNIT),
+  energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
 });
 
 watch(() => settings.locale, save(SETTINGS_LOCALE));
 watch(() => settings.theme, save(SETTINGS_THEME));
 watch(() => settings.unit, save(SETTINGS_UNIT));
+watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
 
 export default settings;
diff --git a/assets/js/views/ChargingSessions.vue b/assets/js/views/ChargingSessions.vue
index 28926b1a..86a0d5f5 100644
--- a/assets/js/views/ChargingSessions.vue
+++ b/assets/js/views/ChargingSessions.vue
@@ -12,10 +12,10 @@
 
 		<div class="row">
 			<main class="col-12">
-				<div class="mb-4">
+				<div class="d-flex justify-content-start mb-4">
 					<a
 						class="btn btn-outline-secondary text-nowrap my-2"
-						:href="`./api/sessions?format=csv&amp;lang=${$i18n.locale}`"
+						:href="csvHrefLink()"
 						download="sessions.csv"
 					>
 						{{ $t("sessions.downloadCsv") }}
@@ -23,97 +23,96 @@
 				</div>
 
 				<div v-for="group in sessionsByMonthAndLoadpoint" :key="group.month">
-					<div class="mx-2">
-						<div class="d-flex align-items-baseline my-5">
-							<h2 class="me-4 mb-0">
-								{{ formatGroupHeadline(group.month) }}
-							</h2>
-						</div>
+					<div class="d-flex align-items-center my-5">
+						<h2 class="me-4 mb-0">
+							{{ formatGroupHeadline(group.month) }}
+						</h2>
+						<a
+							class="btn btn-xs btn-outline-secondary text-nowrap"
+							:href="csvHrefLink(group.month)"
+							download="sessions.csv"
+						>
+							CSV
+						</a>
+					</div>
 
-						<div v-for="loadpoint in group.loadpoints" :key="loadpoint.name">
-							<div class="d-flex align-items-baseline mb-3">
-								<h3 class="me-4 mb-0">
-									{{ loadpoint.name }}
-								</h3>
-								<div class="large">{{ fmtKWh(loadpoint.total) }}</div>
-							</div>
+					<div v-for="loadpoint in group.loadpoints" :key="loadpoint.name">
+						<div class="d-flex align-items-baseline mb-3">
+							<h3 class="me-4 mb-0">
+								{{ loadpoint.name }}
+							</h3>
+							<div class="large">{{ fmtKWh(loadpoint.total) }}</div>
+						</div>
 
-							<ul class="breakdown text-gray d-sm-flex flex-sm-wrap ps-0 mb-2">
-								<li
-									v-for="(vehicle, id) in groupedKWh(
-										'vehicle',
-										loadpoint.sessions
-									)"
-									:key="id"
-									class="breakdown-item"
-								>
-									{{ vehicle.name }}: {{ fmtKWh(vehicle.energy) }}
-								</li>
-							</ul>
-							<div class="table-responsive my-3">
-								<table class="table">
-									<thead>
-										<tr>
-											<th scope="col">{{ $t("sessions.vehicle") }}</th>
-											<th scope="col" class="text-end ps-sm-4 pe-md-5">
-												{{ $t("sessions.energy") }}
-											</th>
-											<th scope="col" class="ps-3 ps-md-4 ps-md-5">
-												{{ $t("sessions.date") }}
-											</th>
-										</tr>
-									</thead>
-									<tbody>
-										<tr
-											v-for="(session, id) in loadpoint.sessions"
-											:key="id"
-											role="button"
-											@click="showDetails(session.id)"
+						<ul class="breakdown text-gray d-sm-flex flex-sm-wrap ps-0 mb-2">
+							<li
+								v-for="(vehicle, id) in groupedKWh('vehicle', loadpoint.sessions)"
+								:key="id"
+								class="breakdown-item"
+							>
+								{{ vehicle.name }}: {{ fmtKWh(vehicle.energy) }}
+							</li>
+						</ul>
+						<div class="table-responsive my-3">
+							<table class="table">
+								<thead>
+									<tr>
+										<th scope="col">{{ $t("sessions.vehicle") }}</th>
+										<th scope="col" class="text-end ps-sm-4 pe-md-5">
+											{{ $t("sessions.energy") }}
+										</th>
+										<th scope="col" class="ps-3 ps-md-4 ps-md-5">
+											{{ $t("sessions.date") }}
+										</th>
+									</tr>
+								</thead>
+								<tbody>
+									<tr
+										v-for="(session, id) in loadpoint.sessions"
+										:key="id"
+										role="button"
+										@click="showDetails(session.id)"
+									>
+										<td class="align-middle">
+											{{ session.vehicle }}
+										</td>
+										<td
+											class="text-nowrap text-end ps-sm-4 pe-md-5 align-middle"
 										>
-											<td class="align-middle">
-												{{ session.vehicle }}
-											</td>
-											<td
-												class="text-nowrap text-end ps-sm-4 pe-md-5 align-middle"
-											>
-												{{ fmtKWh(session.chargedEnergy * 1e3) }}
-											</td>
-											<td class="text-nowrap ps-3 ps-md-4 ps-md-5">
-												<span class="d-block d-sm-none">
-													{{
-														fmtFullDateTime(
-															new Date(session.created),
-															true
-														)
-													}}
-													<br />
-													{{
-														fmtFullDateTime(
-															new Date(session.finished),
-															true
-														)
-													}}
-												</span>
-												<span class="d-none d-sm-block">
-													{{
-														fmtFullDateTime(
-															new Date(session.created),
-															false
-														)
-													}}
-													<br />
-													{{
-														fmtFullDateTime(
-															new Date(session.finished),
-															false
-														)
-													}}
-												</span>
-											</td>
-										</tr>
-									</tbody>
-								</table>
-							</div>
+											{{ fmtKWh(session.chargedEnergy * 1e3) }}
+										</td>
+										<td class="text-nowrap ps-3 ps-md-4 ps-md-5">
+											<span class="d-block d-sm-none">
+												{{
+													fmtFullDateTime(new Date(session.created), true)
+												}}
+												<br />
+												{{
+													fmtFullDateTime(
+														new Date(session.finished),
+														true
+													)
+												}}
+											</span>
+											<span class="d-none d-sm-block">
+												{{
+													fmtFullDateTime(
+														new Date(session.created),
+														false
+													)
+												}}
+												<br />
+												{{
+													fmtFullDateTime(
+														new Date(session.finished),
+														false
+													)
+												}}
+											</span>
+										</td>
+									</tr>
+								</tbody>
+							</table>
 						</div>
 					</div>
 				</div>
@@ -131,7 +130,6 @@
 import Modal from "bootstrap/js/dist/modal";
 import TopNavigation from "../components/TopNavigation.vue";
 import "@h2d2/shopicons/es/bold/arrowback";
-import "@h2d2/shopicons/es/regular/trash";
 import formatter from "../mixins/formatter";
 import api from "../api";
 import store from "../store";
@@ -187,7 +185,7 @@ export default {
 		groupByMonth(sessions) {
 			return sessions.reduce((groups, session) => {
 				const date = new Date(session.finished);
-				const month = `${date.getFullYear()}.${date.getMonth()}`;
+				const month = `${date.getFullYear()}.${date.getMonth() + 1}`;
 				if (!groups[month]) groups[month] = [];
 				groups[month].push(session);
 				return groups;
@@ -219,7 +217,7 @@ export default {
 		formatGroupHeadline(group) {
 			const date = new Date();
 			const [year, month] = group.split(".");
-			date.setMonth(month);
+			date.setMonth(month - 1);
 			date.setFullYear(year);
 			return this.fmtMonthYear(date);
 		},
@@ -228,6 +226,18 @@ export default {
 			const modal = Modal.getOrCreateInstance(document.getElementById("sessionDetailsModal"));
 			modal.show();
 		},
+		csvHrefLink(groupKey) {
+			var url = `./api/sessions?format=csv&lang=${this.$i18n.locale}`;
+			if (groupKey) {
+				const [year, month] = groupKey.split(".");
+				url += `&year=${year}`;
+
+				if (month) {
+					url += `&month=${month}`;
+				}
+			}
+			return url;
+		},
 	},
 };
 </script>
diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go
index 87249086..1da400d0 100644
--- a/cmd/configure/helper.go
+++ b/cmd/configure/helper.go
@@ -418,11 +418,11 @@ func (c *CmdConfigure) processParams(templateItem *templates.Template, deviceCat
 			}
 
 		default:
-			if param.Advanced && !c.advancedMode || param.Deprecated {
+			if param.IsAdvanced() && !c.advancedMode || param.IsDeprecated() {
 				continue
 			}
 
-			if param.Hidden && param.Default != "" {
+			if param.IsHidden() && param.Default != "" {
 				additionalConfig[param.Name] = param.Default
 				continue
 			}
@@ -495,8 +495,8 @@ func (c *CmdConfigure) processInputConfig(param templates.Param) string {
 		help:         help,
 		valueType:    param.ValueType,
 		validValues:  param.ValidValues,
-		mask:         param.Mask,
-		required:     param.Required,
+		mask:         param.IsMask(),
+		required:     param.IsRequired(),
 	})
 
 	if param.ValueType == templates.ParamValueTypeBool && value == "true" {
diff --git a/server/http_session_handler.go b/server/http_session_handler.go
index f8172db3..b83fb3a7 100644
--- a/server/http_session_handler.go
+++ b/server/http_session_handler.go
@@ -4,7 +4,9 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
+	"strconv"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/db"
@@ -14,9 +16,9 @@ import (
 	"golang.org/x/text/language"
 )
 
-func csvResult(ctx context.Context, w http.ResponseWriter, res any) {
+func csvResult(ctx context.Context, w http.ResponseWriter, res any, filename string) {
 	w.Header().Set("Content-Type", "text/csv")
-	w.Header().Set("Content-Disposition", `attachment; filename="sessions.csv"`)
+	w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`.csv"`)
 
 	if ww, ok := res.(api.CsvWriter); ok {
 		_ = ww.WriteCsv(ctx, w)
@@ -33,8 +35,31 @@ func sessionHandler(w http.ResponseWriter, r *http.Request) {
 	}
 
 	var res db.Sessions
+	year := r.URL.Query().Get("year")
+	month := r.URL.Query().Get("month")
 
-	if txn := dbserver.Instance.Table("sessions").Where("charged_kwh>=0.05").Order("created desc").Find(&res); txn.Error != nil {
+	filename := "session"
+
+	fmtYear := "%"
+	fmtMonth := "%"
+
+	if year != "" {
+		fmtYear = year
+		filename += "-" + fmtYear
+
+		if month != "" {
+			iMonth, err := strconv.Atoi(month)
+			if err != nil {
+				jsonError(w, http.StatusInternalServerError, err)
+				return
+			}
+			fmtMonth = fmt.Sprintf("%02d", iMonth)
+			filename += "." + fmtMonth
+		}
+	}
+
+	whereQuery := "charged_kwh>=0.05 and strftime('%Y', created) like ? and strftime('%m', created) like ?"
+	if txn := dbserver.Instance.Where(whereQuery, fmtYear, fmtMonth).Order("created desc").Find(&res); txn.Error != nil {
 		jsonError(w, http.StatusInternalServerError, txn.Error)
 		return
 	}
@@ -50,7 +75,7 @@ func sessionHandler(w http.ResponseWriter, r *http.Request) {
 		}
 
 		ctx := context.WithValue(context.Background(), locale.Locale, lang)
-		csvResult(ctx, w, &res)
+		csvResult(ctx, w, &res, filename)
 		return
 	}
 
diff --git a/tariff/fixed.go b/tariff/fixed.go
index 11f88ea9..3ca0f222 100644
--- a/tariff/fixed.go
+++ b/tariff/fixed.go
@@ -44,9 +44,6 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
 	t := &Fixed{
 		unit:  cc.Currency,
 		clock: clock.New(),
-		zones: []fixed.Zone{
-			{Price: cc.Price}, // full week is implicit
-		},
 	}
 
 	for _, z := range cc.Zones {
@@ -79,6 +76,11 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
 
 	sort.Sort(t.zones)
 
+	// prepend catch-all zone
+	t.zones = append([]fixed.Zone{
+		{Price: cc.Price}, // full week is implicit
+	}, t.zones...)
+
 	return t, nil
 }
 
diff --git a/tariff/fixed/zone.go b/tariff/fixed/zone.go
index efbf1c52..f857559f 100644
--- a/tariff/fixed/zone.go
+++ b/tariff/fixed/zone.go
@@ -1,8 +1,6 @@
 package fixed
 
 import (
-	"sort"
-
 	"golang.org/x/exp/slices"
 )
 
@@ -39,7 +37,6 @@ func (r Zones) ForDay(day Day) Zones {
 		}
 	}
 
-	sort.Sort(zones)
 	return zones
 }
 
diff --git a/templates/definition/charger/homematic.yaml b/templates/definition/charger/homematic.yaml
index db81b4c5..6626533c 100644
--- a/templates/definition/charger/homematic.yaml
+++ b/templates/definition/charger/homematic.yaml
@@ -1,9 +1,14 @@
 template: homematic
 products:
-  - brand: Homematic IP
+  - brand: Homematic / Homematic IP
 group: switchsockets
 params:
   - name: host
+  - name: port
+    default: 2010
+    description:
+      en: XML-RPC server port number (BidCos-Wired=2000, BidCos-RF=2001, HmIP=2010)
+      de: XML-RPC-Server Port-Nummer (BidCos-Wired=2000, BidCos-RF=2001, HmIP=2010)
   - name: device
     description:
       de: Geräteadresse/Seriennummer
@@ -27,8 +32,8 @@ params:
     required: true
     advanced: true
     description:
-      en: Meter channel number (HMIP-PSM=6, HMIP-FSM+HMIP-FSM16=5)
-      de: Kanalnummer des Power-Meters (HMIP-PSM=6, HMIP-FSM+HMIP-FSM16=5)
+      en: Meter channel number (HMIP-PSM=6, HMIP-FSM+HMIP-FSM16=5, HM=2)
+      de: Kanalnummer des Power-Meters (HMIP-PSM=6, HMIP-FSM+HMIP-FSM16=5, HM=2)
     help:
       en: Homematic meter channel number like shown in the CCU web user interface.
       de: Kanalnummer des Messwertkanals, wie im CCU Webfrontend angezeigt.
@@ -38,14 +43,14 @@ params:
     required: true
     advanced: true
     description:
-      en: Switch/Actor channel number (HMIP-PSM=3, HMIP-FSM+HMIP-FSM16=2)
-      de: Kanalnummer der schaltbaren Steckdose (HMIP-PSM=3, HMIP-FSM+HMIP-FSM16=2)
+      en: Switch/Actor channel number (HMIP-PSM=3, HMIP-FSM+HMIP-FSM16=2, HM=1)
+      de: Kanalnummer der schaltbaren Steckdose (HMIP-PSM=3, HMIP-FSM+HMIP-FSM16=2, HM=1)
     help:
       en: Homematic switch actor channel number like shown in the CCU web user interface.
       de: Kanalnummer der schaltbaren Steckdose, wie im CCU Webfrontend angezeigt.
 render: |
   type: homematic
-  uri: {{ .host }}:2010
+  uri: {{ .host }}:{{ .port }}
   device: {{ .device }}
   meterchannel: {{ .meterchannel }}
   switchchannel: {{ .switchchannel }}
diff --git a/templates/definition/defaults.yaml b/templates/definition/defaults.yaml
index b3d17ff5..190441cd 100644
--- a/templates/definition/defaults.yaml
+++ b/templates/definition/defaults.yaml
@@ -359,7 +359,6 @@ presets:
     params:
       - name: ski
       - name: ip
-        required: false
     render: |
       {{define "eebus-meter"}}
       type: eebus
@@ -371,7 +370,6 @@ presets:
     params:
       - name: ski
       - name: ip
-        required: false
     render: |
       {{define "eebus-no-meter"}}
       type: eebus
diff --git a/templates/docs/charger/etrel-duo_0.yaml b/templates/docs/charger/etrel-duo_0.yaml
index b67c292f..8fea9d87 100644
--- a/templates/docs/charger/etrel-duo_0.yaml
+++ b/templates/docs/charger/etrel-duo_0.yaml
@@ -7,6 +7,11 @@ description: |
   Die Wallbox muss sich im "Power" Modus befinden.
 render:
   - default: |
+      type: template
+      template: etrel-duo
+      host: 192.0.2.2 # IP-Adresse oder Hostname
+      port: 502 # Port # Optional
+    advanced: |
       type: template
       template: etrel-duo
       connector: 1 # Optional
diff --git a/templates/docs/charger/homematic_0.yaml b/templates/docs/charger/homematic_0.yaml
index a1f7e83d..1834573b 100644
--- a/templates/docs/charger/homematic_0.yaml
+++ b/templates/docs/charger/homematic_0.yaml
@@ -1,11 +1,12 @@
 product:
-  brand: Homematic IP
+  brand: Homematic / Homematic IP
   group: Schaltbare Steckdosen
 render:
   - default: |
       type: template
       template: homematic
       host: 192.0.2.2 # IP-Adresse oder Hostname
+      port: 2010 # Port # Optional
       device: '0001EE89AAD848' # Homematic Geräte Id, wie im CCU Webfrontend angezeigt.
       standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
       user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
@@ -14,6 +15,7 @@ render:
       type: template
       template: homematic
       host: 192.0.2.2 # IP-Adresse oder Hostname
+      port: 2010 # Port # Optional
       device: '0001EE89AAD848' # Homematic Geräte Id, wie im CCU Webfrontend angezeigt.
       standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
       user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
diff --git a/templates/docs/charger/openwb_0.yaml b/templates/docs/charger/openwb_0.yaml
index e7a74f86..a6a4bff7 100644
--- a/templates/docs/charger/openwb_0.yaml
+++ b/templates/docs/charger/openwb_0.yaml
@@ -8,7 +8,6 @@ render:
       type: template
       template: openwb
       host: 192.0.2.2 # IP-Adresse oder Hostname
-      connector: 1 # Optional
     advanced: |
       type: template
       template: openwb
diff --git a/templates/docs/charger/pracht-alpha_0.yaml b/templates/docs/charger/pracht-alpha_0.yaml
index 23bf27e3..ad1f9030 100644
--- a/templates/docs/charger/pracht-alpha_0.yaml
+++ b/templates/docs/charger/pracht-alpha_0.yaml
@@ -20,6 +20,28 @@ render:
       host: 192.0.2.2 # Hostname
       port: 502 # Port
 
+      # Modbus TCP
+      modbus: tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+    advanced: |
+      type: template
+      template: pracht-alpha      
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
diff --git a/templates/docs/meter/sma-energy-meter_0.yaml b/templates/docs/meter/sma-energy-meter_0.yaml
index 594adb3c..fa990512 100644
--- a/templates/docs/meter/sma-energy-meter_0.yaml
+++ b/templates/docs/meter/sma-energy-meter_0.yaml
@@ -8,6 +8,11 @@ render:
       template: sma-energy-meter
       usage: grid
       host: 192.0.2.2 # IP-Adresse oder Hostname
+    advanced: |
+      type: template
+      template: sma-energy-meter
+      usage: grid
+      host: 192.0.2.2 # IP-Adresse oder Hostname
       interface: eth0 # Netzwerkinterface # Optional
   - usage: pv
     default: |
@@ -15,4 +20,9 @@ render:
       template: sma-energy-meter
       usage: pv
       host: 192.0.2.2 # IP-Adresse oder Hostname
+    advanced: |
+      type: template
+      template: sma-energy-meter
+      usage: pv
+      host: 192.0.2.2 # IP-Adresse oder Hostname
       interface: eth0 # Netzwerkinterface # Optional
diff --git a/templates/docs/meter/sma-home-manager_0.yaml b/templates/docs/meter/sma-home-manager_0.yaml
index fba43dac..f692f0d7 100644
--- a/templates/docs/meter/sma-home-manager_0.yaml
+++ b/templates/docs/meter/sma-home-manager_0.yaml
@@ -9,4 +9,9 @@ render:
       template: sma-home-manager
       usage: grid
       host: 192.0.2.2 # IP-Adresse oder Hostname
+    advanced: |
+      type: template
+      template: sma-home-manager
+      usage: grid
+      host: 192.0.2.2 # IP-Adresse oder Hostname
       interface: eth0 # Netzwerkinterface # Optional
diff --git a/templates/docs/meter/solaredge-hybrid_0.yaml b/templates/docs/meter/solaredge-hybrid_0.yaml
index e85af42a..b7dd471e 100644
--- a/templates/docs/meter/solaredge-hybrid_0.yaml
+++ b/templates/docs/meter/solaredge-hybrid_0.yaml
@@ -28,7 +28,6 @@ render:
       id: 1
       host: 192.0.2.2 # Hostname
       port: 1502 # Port
-      timeout: 10s # Optional
     advanced: |
       type: template
       template: solaredge-hybrid
@@ -78,7 +77,6 @@ render:
       id: 1
       host: 192.0.2.2 # Hostname
       port: 1502 # Port
-      timeout: 10s # Optional
     advanced: |
       type: template
       template: solaredge-hybrid
@@ -128,7 +126,6 @@ render:
       id: 1
       host: 192.0.2.2 # Hostname
       port: 1502 # Port
-      timeout: 10s # Optional
     advanced: |
       type: template
       template: solaredge-hybrid
diff --git a/templates/docs/meter/solaredge-inverter_0.yaml b/templates/docs/meter/solaredge-inverter_0.yaml
index 027fa418..24a7308f 100644
--- a/templates/docs/meter/solaredge-inverter_0.yaml
+++ b/templates/docs/meter/solaredge-inverter_0.yaml
@@ -23,6 +23,29 @@ render:
       host: 192.0.2.2 # Hostname
       port: 1502 # Port
 
+      # Modbus TCP
+      modbus: tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 1502 # Port
+    advanced: |
+      type: template
+      template: solaredge-inverter
+      usage: grid      
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 1502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -48,6 +71,29 @@ render:
       host: 192.0.2.2 # Hostname
       port: 1502 # Port
 
+      # Modbus TCP
+      modbus: tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 1502 # Port
+    advanced: |
+      type: template
+      template: solaredge-inverter
+      usage: pv      
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 1502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index 9379826e..a7639f39 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -51,7 +51,7 @@
  ],
  "SmartPlugs": [
   "AVM",
-  "Homematic IP",
+  "Homematic / Homematic IP",
   "myStrom",
   "Shelly",
   "Tasmota",
diff --git a/util/templates/config.go b/util/templates/config.go
index 37fdc440..455b7281 100644
--- a/util/templates/config.go
+++ b/util/templates/config.go
@@ -38,7 +38,7 @@ func (c *ConfigDefaults) LoadDefaults() {
 	for k := range c.Modbus.Types {
 		for i, p := range c.Modbus.Types[k].Params {
 			// if this is a reference, get the referenced values and then overwrite it with the values defined here
-			if p.Reference {
+			if p.IsReference() {
 				finalName := p.Name
 				referencedItemName := p.Name
 				if p.Referencename != "" {
diff --git a/util/templates/documentation.tpl b/util/templates/documentation.tpl
index 59e49486..56ca124e 100644
--- a/util/templates/documentation.tpl
+++ b/util/templates/documentation.tpl
@@ -28,7 +28,7 @@ render:
       {{- range $.Params }}
       {{- if eq .Name "modbus" -}}
 {{ $.Modbus | indent 6 -}}
-      {{- else if ne .Advanced true }}
+      {{- else if ne .IsAdvanced true }}
       {{ .Name }}:
       {{- if len .Value }} {{ .Value }} {{- end }}
       {{- if ne (len .Values) 0 }}
@@ -36,7 +36,7 @@ render:
         - {{ . }}
       {{ end }}
       {{- end }}
-      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if ne .Required true }} # Optional{{ end }}
+      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if eq .IsRequired false }} # Optional{{ end }}
       {{- end -}}
       {{- end -}}
 {{- if $.AdvancedParams }}
@@ -55,7 +55,7 @@ render:
         - {{ . }}
       {{- end }}
       {{- end }}
-      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if ne .Required true }} # Optional{{ end }}
+      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if ne .IsRequired true }} # Optional{{ end }}
       {{- end -}}
       {{- end -}}
 {{ end }}
@@ -67,7 +67,7 @@ render:
       {{- range $.Params }}
       {{- if eq .Name "modbus" -}}
 {{ $.Modbus | indent 6 -}}
-      {{- else if ne .Advanced true }}
+      {{- else if ne .IsAdvanced true }}
       {{ .Name }}:
       {{- if len .Value }} {{ .Value }} {{- end }}
       {{- if ne (len .Values) 0 }}
@@ -75,7 +75,7 @@ render:
         - {{ . }}
       {{- end }}
       {{- end }}
-      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if ne .Required true }} # Optional{{ end }}
+      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if ne .IsRequired true }} # Optional{{ end }}
       {{- end -}}
       {{- end -}}
 {{- if $.AdvancedParams }}
@@ -93,7 +93,7 @@ render:
         - {{ . }}
       {{- end }}
       {{- end }}
-      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if ne .Required true }} # Optional{{ end }}
+      {{- if .Help.DE }} # {{ .Help.DE }}{{ end }}{{ if ne .IsRequired true }} # Optional{{ end }}
       {{- end -}}
       {{- end -}}
 {{- end }}
diff --git a/util/templates/template.go b/util/templates/template.go
index 2d47fa64..607f75a2 100644
--- a/util/templates/template.go
+++ b/util/templates/template.go
@@ -27,7 +27,7 @@ type Template struct {
 // GuidedSetupEnabled returns true if there are linked templates or >1 usage
 func (t *Template) GuidedSetupEnabled() bool {
 	_, p := t.ParamByName(ParamUsage)
-	return len(t.Linked) > 0 || (len(p.Choice) > 1 && p.AllInOne)
+	return len(t.Linked) > 0 || (len(p.Choice) > 1 && p.IsAllInOne())
 }
 
 // UpdateParamWithDefaults adds default values to specific param name entries
@@ -137,7 +137,9 @@ func (t *Template) ResolvePresets() error {
 		}
 
 		if i, _ := t.ParamByName(p.Name); i > -1 {
-			t.Params[i].OverwriteProperties(p)
+			// take the preset values as a base and overwrite it with param values
+			p.OverwriteProperties(t.Params[i])
+			t.Params[i] = p
 		} else {
 			t.Params = append(t.Params, p)
 		}
@@ -251,7 +253,7 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, lang str
 	// remove params with no values
 	var newParams []Param
 	for _, param := range t.Params {
-		if !param.Required {
+		if !param.IsRequired() {
 			switch param.ValueType {
 			case ParamValueTypeStringList:
 				if len(param.Values) == 0 {
@@ -315,7 +317,7 @@ func (t *Template) RenderResult(renderMode string, other map[string]interface{})
 			if !slices.Contains(predefinedTemplateProperties, out) {
 				return nil, values, fmt.Errorf("invalid key: %s", key)
 			}
-		} else if p.Deprecated {
+		} else if p.IsDeprecated() {
 			continue
 		} else {
 			out = p.Name
diff --git a/util/templates/template_documentation.go b/util/templates/template_documentation.go
index 8864b59b..572b5f4c 100644
--- a/util/templates/template_documentation.go
+++ b/util/templates/template_documentation.go
@@ -68,11 +68,11 @@ func (t *Template) RenderDocumentation(product Product, values map[string]interf
 	var hasAdvancedParam bool
 	var newParams []Param
 	for _, param := range t.Params {
-		if param.Deprecated || param.Name == ParamUsage {
+		if param.IsDeprecated() || param.Name == ParamUsage {
 			continue
 		}
 
-		if param.Advanced {
+		if param.IsAdvanced() {
 			hasAdvancedParam = true
 		}
 
diff --git a/util/templates/template_types.go b/util/templates/template_types.go
index 381f4752..db8f05df 100644
--- a/util/templates/template_types.go
+++ b/util/templates/template_types.go
@@ -3,8 +3,9 @@ package templates
 import (
 	"bufio"
 	"fmt"
-	"reflect"
 	"strings"
+
+	"github.com/imdario/mergo"
 )
 
 const (
@@ -171,16 +172,16 @@ type LinkedTemplate struct {
 // 3. defaults.yaml modbus section
 // 4. template
 type Param struct {
-	Reference     bool         // if this is references another param definition
+	Reference     *bool        // if this is references another param definition
 	Referencename string       // name of the referenced param if it is not identical to the defined name
 	Preset        string       // Reference a predefined set of params
 	Name          string       // Param name which is used for assigning defaults properties and referencing in render
 	Description   TextLanguage // language specific titles (presented in UI instead of Name)
-	Required      bool         // cli if the user has to provide a non empty value
-	Mask          bool         // cli if the value should be masked, e.g. for passwords
-	Advanced      bool         // cli if the user does not need to be asked. Requires a "Default" to be defined.
-	Hidden        bool         // cli if the parameter should not be presented in the cli, the default value be assigned
-	Deprecated    bool         // if the parameter is deprecated and thus should not be presented in the cli or docs
+	Required      *bool        // cli if the user has to provide a non empty value
+	Mask          *bool        // cli if the value should be masked, e.g. for passwords
+	Advanced      *bool        // cli if the user does not need to be asked. Requires a "Default" to be defined.
+	Hidden        *bool        // cli if the parameter should not be presented in the cli, the default value be assigned
+	Deprecated    *bool        // if the parameter is deprecated and thus should not be presented in the cli or docs
 	Default       string       // default value if no user value is provided in the configuration
 	Example       string       // cli example value
 	Help          TextLanguage // cli configuration help
@@ -190,7 +191,7 @@ type Param struct {
 	ValueType     string       // string representation of the value type, "string" is default
 	ValidValues   []string     // list of valid values the user can provide
 	Choice        []string     // defines a set of choices, e.g. "grid", "pv", "battery", "charge" for "usage"
-	AllInOne      bool         // defines if the defined usages can all be present in a single device
+	AllInOne      *bool        // defines if the defined usages can all be present in a single device
 	Requirements  Requirements // requirements for this param to be usable, only supported via ValueType "bool"
 
 	Baudrate int    // device specific default for modbus RS485 baudrate
@@ -213,49 +214,39 @@ func (p *Param) DefaultValue(renderMode string) interface{} {
 	return p.Default
 }
 
-// overwrite specific properties by using values from another param
-//
-// always overwrites if not provided empty: description, valuetype, default, mask, required
-//
-// only overwrite if not provided empty and empty in param: help, example, requirements
+// OverwriteProperties merges properties from parameter definition
 func (p *Param) OverwriteProperties(withParam Param) {
-	// always overwrite if defined
-	p.Description.Update(withParam.Description, true)
-
-	if len(p.Usages) == 0 {
-		p.Usages = withParam.Usages
-	}
-
-	if withParam.ValueType != "" {
-		p.ValueType = withParam.ValueType
+	if err := mergo.Merge(p, &withParam); err != nil {
+		panic(err)
 	}
+}
 
-	if withParam.Required {
-		p.Required = withParam.Required
-	}
+func (p *Param) IsReference() bool {
+	return p.Reference != nil && *p.Reference
+}
 
-	if withParam.Default != "" {
-		p.Default = withParam.Default
-	}
+func (p *Param) IsAdvanced() bool {
+	return p.Advanced != nil && *p.Advanced
+}
 
-	if withParam.Mask {
-		p.Mask = withParam.Mask
-	}
+func (p *Param) IsHidden() bool {
+	return p.Hidden != nil && *p.Hidden
+}
 
-	// only set if empty
-	p.Help.Update(withParam.Help, false)
+func (p *Param) IsMask() bool {
+	return p.Mask != nil && *p.Mask
+}
 
-	if p.Example == "" && withParam.Example != "" {
-		p.Example = withParam.Example
-	}
+func (p *Param) IsRequired() bool {
+	return p.Required != nil && *p.Required
+}
 
-	if p.ValidValues == nil && withParam.ValidValues != nil {
-		p.ValidValues = withParam.ValidValues
-	}
+func (p *Param) IsDeprecated() bool {
+	return p.Deprecated != nil && *p.Deprecated
+}
 
-	if reflect.DeepEqual(p.Requirements, Requirements{}) {
-		p.Requirements = withParam.Requirements
-	}
+func (p *Param) IsAllInOne() bool {
+	return p.AllInOne != nil && *p.AllInOne
 }
 
 // Product contains naming information about a product a template supports
diff --git a/vehicle/vag/vwidentity/endpoint.go b/vehicle/vag/vwidentity/endpoint.go
index 652ca3d0..08d126ad 100644
--- a/vehicle/vag/vwidentity/endpoint.go
+++ b/vehicle/vag/vwidentity/endpoint.go
@@ -148,11 +148,7 @@ func (v *Service) Login(uri, user, password string) (url.Values, error) {
 			}
 
 			if u := resp.Request.URL.Query().Get("updated"); err == nil && u != "" {
-				if resp, err = v.postTos(resp.Request.URL.String()); err == nil {
-					resp.Body.Close()
-				} else {
-					err = fmt.Errorf("updated ToS: %w", err)
-				}
+				err = errors.New("terms of service updated- please open app or website and confirm")
 			}
 		}
 	}
@@ -171,23 +167,3 @@ func (v *Service) Login(uri, user, password string) (url.Values, error) {
 
 	return nil, err
 }
-
-func (v *Service) postTos(uri string) (*http.Response, error) {
-	var vars FormVars
-	resp, err := v.Get(uri)
-	if err == nil {
-		vars, err = FormValues(resp.Body, "form#emailPasswordForm")
-	}
-
-	if err == nil {
-		data := make(url.Values)
-		for k, v := range vars.Inputs {
-			data.Set(k, v)
-		}
-
-		uri := BaseURL + vars.Action
-		resp, err = v.PostForm(uri, data)
-	}
-
-	return resp, err
-}


Test patch:
diff --git a/tariff/fixed_test.go b/tariff/fixed_test.go
index 0f98349d..3a181c12 100644
--- a/tariff/fixed_test.go
+++ b/tariff/fixed_test.go
@@ -33,3 +33,46 @@ func TestFixed(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, expect, rates)
 }
+
+func TestFixedSplitZones(t *testing.T) {
+	at, err := NewFixedFromConfig(map[string]interface{}{
+		"price": 0.5,
+		"zones": []struct {
+			Price float64
+			Hours string
+		}{
+			{0.1, "0-5:30,21-0"},
+		},
+	})
+	assert.NoError(t, err)
+
+	tf := at.(*Fixed)
+	tf.clock = clock.NewMock()
+
+	var expect api.Rates
+	for i := 0; i < 7; i++ {
+		dayStart := carbon.FromStdTime(tf.clock.Now()).StartOfDay().AddDays(i)
+
+		expect = append(expect,
+			api.Rate{
+				Price: 0.1,
+				Start: dayStart.ToStdTime(),
+				End:   dayStart.AddHours(5).AddMinutes(30).ToStdTime(),
+			},
+			api.Rate{
+				Price: 0.5,
+				Start: dayStart.AddHours(5).AddMinutes(30).ToStdTime(),
+				End:   dayStart.AddHours(21).ToStdTime(),
+			},
+			api.Rate{
+				Price: 0.1,
+				Start: dayStart.AddHours(21).ToStdTime(),
+				End:   dayStart.AddDay().ToStdTime(),
+			},
+		)
+	}
+
+	rates, err := tf.Rates()
+	assert.NoError(t, err)
+	assert.Equal(t, expect, rates)
+}