Repository: evcc-io/evcc
Pull request #9244 (2023-08-04 13:22:45)

Support Go 1.21
- [x] depends on https://github.com/golang/go/issues/61857


Patch:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 25fdb98c..1c418150 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -1,5 +1,8 @@
 name: Default
 
+env:
+  GO_VERSION: ^1.21
+
 on:
   push:
     branches:
@@ -18,7 +21,7 @@ jobs:
       # - uses: actions/setup-go@v4
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
           cache-key-prefix: clean-cache-
         id: go
 
@@ -46,7 +49,7 @@ jobs:
 
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
           cache-key-prefix: build-cache-
         id: go
 
@@ -70,7 +73,7 @@ jobs:
       # - uses: actions/setup-go@v4
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
           cache-key-prefix: test-cache-
         id: go
 
@@ -86,7 +89,7 @@ jobs:
 
       - uses: actions/setup-go@v4
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
           cache: false # avoid cache thrashing
         id: go
 
@@ -142,7 +145,7 @@ jobs:
 
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
           cache-key-prefix: integration-cache-
         id: go
 
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 14a4c00f..9b6aa8be 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -1,5 +1,8 @@
 name: Deploy updated templates
 
+env:
+  GO_VERSION: ^1.21
+
 on:
   push:
     branches: [master]
@@ -16,7 +19,7 @@ jobs:
 
       - uses: actions/setup-go@v4
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
         id: go
 
       - name: Build docs
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index b8eac161..2c2b6458 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -1,5 +1,8 @@
 name: Nightly Build
 
+env:
+  GO_VERSION: ^1.21
+
 on:
   schedule: # runs on the default branch: master
     - cron: "0 2 * * *" # run at 2 AM UTC
@@ -81,7 +84,7 @@ jobs:
 
       - uses: actions/setup-go@v4
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
         id: go
 
       - name: Install Cloudsmith CLI
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1b6304cc..c50b37cc 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,5 +1,8 @@
 name: Release
 
+env:
+  GO_VERSION: ^1.21
+
 on:
   push:
     tags:
@@ -7,7 +10,7 @@ on:
 
 jobs:
   call-build-workflow:
-    if: "startsWith(github.ref, 'refs/tags')"
+    if: startsWith(github.ref, 'refs/tags')
     uses: evcc-io/evcc/.github/workflows/default.yml@master
 
   docker:
@@ -60,7 +63,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v4
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
         id: go
 
       - name: Install Cloudsmith CLI
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
index 0f30c3c2..b535b37a 100644
--- a/.github/workflows/website.yml
+++ b/.github/workflows/website.yml
@@ -1,5 +1,8 @@
 name: Deploy data to website
 
+env:
+  GO_VERSION: ^1.21
+
 on:
   push:
     branches: [master]
@@ -16,7 +19,7 @@ jobs:
 
       - uses: actions/setup-go@v4
         with:
-          go-version: ^1.20
+          go-version: ${{ env.GO_VERSION }}
         id: go
 
       - name: Build docs
diff --git a/.golangci.yml b/.golangci.yml
index 04d04a68..58250661 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,5 +1,5 @@
 run:
-  go: 1.20
+  go: "1.21"
 issues:
   exclude:
     - .regActualCurrent. is unused
diff --git a/Dockerfile b/Dockerfile
index 801b9790..41d8a394 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,7 +20,7 @@ RUN make ui
 
 
 # STEP 2 build executable binary
-FROM --platform=$BUILDPLATFORM golang:1.20-alpine as builder
+FROM --platform=$BUILDPLATFORM golang:1.21-alpine as builder
 
 # Install git + SSL ca certificates.
 # Git is required for fetching the dependencies.
diff --git a/Makefile b/Makefile
index 8b7527e9..c8a26dcf 100644
--- a/Makefile
+++ b/Makefile
@@ -35,7 +35,7 @@ clean::
 	rm -rf dist/
 
 install::
-	go install $$(go list -f '{{join .Imports " "}}' tools.go)
+	go install $$(go list -e -f '{{join .Imports " "}}' tools.go)
 
 install-ui::
 	npm ci
diff --git a/README.md b/README.md
index 07892908..d7515d6a 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ You'll find everything you need in our [documentation](https://docs.evcc.io/) (G
 
 ## Contribute
 
-To build evcc from source, [Go][1] 1.20 and [Node][2] 18 are required.
+To build evcc from source, [Go][1] 1.21 and [Node][2] 18 are required.
 
 Build and run go backend. The UI becomes available at http://127.0.0.1:7070/
 
diff --git a/charger/easee/log.go b/charger/easee/log.go
index 7ed46673..7b1829b7 100644
--- a/charger/easee/log.go
+++ b/charger/easee/log.go
@@ -2,10 +2,10 @@ package easee
 
 import (
 	"fmt"
+	"slices"
 	"strings"
 
 	"github.com/philippseith/signalr"
-	"golang.org/x/exp/slices"
 )
 
 // Logger is a simple logger interface
diff --git a/charger/warp2.go b/charger/warp2.go
index b4d108f2..fd46709c 100644
--- a/charger/warp2.go
+++ b/charger/warp2.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"slices"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -11,7 +12,6 @@ import (
 	"github.com/evcc-io/evcc/provider"
 	"github.com/evcc-io/evcc/provider/mqtt"
 	"github.com/evcc-io/evcc/util"
-	"golang.org/x/exp/slices"
 )
 
 // Warp2 is the Warp charger v2 firmware implementation
diff --git a/cmd/configure/flow.go b/cmd/configure/flow.go
index ca5933ae..e1e7d75e 100644
--- a/cmd/configure/flow.go
+++ b/cmd/configure/flow.go
@@ -3,9 +3,9 @@ package configure
 import (
 	"errors"
 	"fmt"
+	"slices"
 
 	"github.com/evcc-io/evcc/util/templates"
-	"golang.org/x/exp/slices"
 )
 
 // configureDeviceGuidedSetup lets the user choose a device that is set to support guided setup
diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go
index 8de52276..d3a33ef8 100644
--- a/cmd/configure/helper.go
+++ b/cmd/configure/helper.go
@@ -2,6 +2,7 @@ package configure
 
 import (
 	"fmt"
+	"slices"
 	"sort"
 	"strings"
 
@@ -10,7 +11,6 @@ import (
 	"github.com/evcc-io/evcc/util/sponsor"
 	"github.com/evcc-io/evcc/util/templates"
 	stripmd "github.com/writeas/go-strip-markdown/v2"
-	"golang.org/x/exp/slices"
 	"gopkg.in/yaml.v3"
 )
 
diff --git a/cmd/configure/main.go b/cmd/configure/main.go
index d7f59b96..31d095a1 100644
--- a/cmd/configure/main.go
+++ b/cmd/configure/main.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -18,7 +19,6 @@ import (
 	"github.com/evcc-io/evcc/util/templates"
 	"github.com/nicksnyder/go-i18n/v2/i18n"
 	"golang.org/x/exp/maps"
-	"golang.org/x/exp/slices"
 	"golang.org/x/text/language"
 )
 
diff --git a/cmd/configure/survey.go b/cmd/configure/survey.go
index ab17e06b..a57bd6aa 100644
--- a/cmd/configure/survey.go
+++ b/cmd/configure/survey.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -13,7 +14,6 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util/templates"
 	stripmd "github.com/writeas/go-strip-markdown/v2"
-	"golang.org/x/exp/slices"
 )
 
 // surveyAskOne asks the user for input
@@ -22,7 +22,6 @@ func (c *CmdConfigure) surveyAskOne(p survey.Prompt, response interface{}, opts
 		icons.Question.Text = ""
 	}))
 	err := survey.AskOne(p, response, opts...)
-
 	if err != nil {
 		if err == terminal.InterruptErr {
 			fmt.Println(c.localizedString("Cancel"))
diff --git a/cmd/device.go b/cmd/device.go
index f4005c0e..da5be89c 100644
--- a/cmd/device.go
+++ b/cmd/device.go
@@ -1,13 +1,14 @@
 package cmd
 
 import (
+	"cmp"
 	"fmt"
+	"slices"
 	"strings"
 
 	"github.com/evcc-io/evcc/util/config"
 	"github.com/evcc-io/evcc/util/templates"
 	"github.com/spf13/cobra"
-	"golang.org/x/exp/slices"
 )
 
 // deviceCmd represents the device debug command
@@ -49,8 +50,8 @@ func runDevice(cmd *cobra.Command, args []string) {
 			fmt.Printf("%d. %s\n", d.ID, d.Type)
 
 			details := d.Details
-			slices.SortFunc(details, func(i, j config.ConfigDetail) bool {
-				return i.Key < j.Key
+			slices.SortFunc(details, func(i, j config.ConfigDetail) int {
+				return cmp.Compare(i.Key, j.Key)
 			})
 
 			for _, d := range details {
diff --git a/cmd/token.go b/cmd/token.go
index 9e8f5b6e..18ca3ade 100644
--- a/cmd/token.go
+++ b/cmd/token.go
@@ -2,12 +2,12 @@ package cmd
 
 import (
 	"fmt"
+	"slices"
 	"strings"
 
 	"github.com/evcc-io/evcc/util/config"
 	"github.com/samber/lo"
 	"github.com/spf13/cobra"
-	"golang.org/x/exp/slices"
 	"golang.org/x/oauth2"
 )
 
diff --git a/core/coordinator/adapter.go b/core/coordinator/adapter.go
index a9fbbec9..42ce2ad3 100644
--- a/core/coordinator/adapter.go
+++ b/core/coordinator/adapter.go
@@ -1,9 +1,10 @@
 package coordinator
 
 import (
+	"slices"
+
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/loadpoint"
-	"golang.org/x/exp/slices"
 )
 
 type adapter struct {
diff --git a/core/helper.go b/core/helper.go
index 1cc85f87..8d9bd31b 100644
--- a/core/helper.go
+++ b/core/helper.go
@@ -5,7 +5,6 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/samber/lo"
-	"golang.org/x/exp/constraints"
 )
 
 var (
@@ -47,18 +46,3 @@ func vehicleTitles(vehicles []api.Vehicle) []string {
 		return v.Title()
 	})
 }
-
-// max of slice values
-func max[T constraints.Ordered](s []T) T {
-	if len(s) == 0 {
-		var zero T
-		return zero
-	}
-	m := s[0]
-	for _, v := range s {
-		if v > m {
-			m = v
-		}
-	}
-	return m
-}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 91451130..e7cef270 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -775,7 +775,7 @@ func (lp *Loadpoint) setStatus(status api.ChargeStatus) {
 
 // remainingChargeEnergy returns missing energy amount in kWh if vehicle has a valid energy target
 func (lp *Loadpoint) remainingChargeEnergy() (float64, bool) {
-	return math.Max(0, lp.targetEnergy-lp.getChargedEnergy()/1e3),
+	return max(0, lp.targetEnergy-lp.getChargedEnergy()/1e3),
 		!lp.vehicleHasSoc() && lp.targetEnergy > 0
 }
 
@@ -903,8 +903,8 @@ func (lp *Loadpoint) effectiveCurrent() float64 {
 
 	// adjust actual current for vehicles like Zoe where it remains below target
 	if lp.chargeCurrents != nil {
-		cur := max(lp.chargeCurrents)
-		return math.Min(cur+2.0, lp.chargeCurrent)
+		cur := max(lp.chargeCurrents[0], lp.chargeCurrents[1], lp.chargeCurrents[2])
+		return min(cur+2.0, lp.chargeCurrent)
 	}
 
 	return lp.chargeCurrent
@@ -1122,7 +1122,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
 	effectiveCurrent := lp.effectiveCurrent()
 	activePhases := lp.activePhases()
 	deltaCurrent := powerToCurrent(-sitePower, activePhases)
-	targetCurrent := math.Max(effectiveCurrent+deltaCurrent, 0)
+	targetCurrent := max(effectiveCurrent+deltaCurrent, 0)
 
 	lp.log.DEBUG.Printf("pv charge current: %.3gA = %.3gA + %.3gA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, activePhases)
 
@@ -1198,7 +1198,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
 	lp.resetPVTimer()
 
 	// cap at maximum current
-	targetCurrent = math.Min(targetCurrent, maxCurrent)
+	targetCurrent = min(targetCurrent, maxCurrent)
 
 	return targetCurrent
 }
diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go
index c12c0592..0ef979ab 100644
--- a/core/loadpoint_api.go
+++ b/core/loadpoint_api.go
@@ -3,7 +3,6 @@ package core
 import (
 	"errors"
 	"fmt"
-	"math"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -316,7 +315,7 @@ func (lp *Loadpoint) GetChargePowerFlexibility() float64 {
 	}
 
 	// MinPV mode
-	return math.Max(0, lp.GetChargePower()-lp.GetMinPower())
+	return max(0, lp.GetChargePower()-lp.GetMinPower())
 }
 
 // GetMinCurrent returns the min loadpoint current
diff --git a/core/loadpoint_charger.go b/core/loadpoint_charger.go
index 607b9e8d..3ad488e8 100644
--- a/core/loadpoint_charger.go
+++ b/core/loadpoint_charger.go
@@ -1,8 +1,9 @@
 package core
 
 import (
+	"slices"
+
 	"github.com/evcc-io/evcc/api"
-	"golang.org/x/exp/slices"
 )
 
 // chargerHasFeature checks availability of charger feature
diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go
index 3b7d6a93..16f9ce72 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -1,8 +1,6 @@
 package core
 
 import (
-	"math"
-
 	"github.com/evcc-io/evcc/api"
 )
 
@@ -58,16 +56,6 @@ func (lp *Loadpoint) getMeasuredPhases() int {
 // assume 3p for switchable charger during startup
 const unknownPhases = 3
 
-func min(i ...int) int {
-	v := math.MaxInt
-	for _, i := range i {
-		if i < v {
-			v = i
-		}
-	}
-	return v
-}
-
 func expect(phases int) int {
 	if phases > 0 {
 		return phases
diff --git a/core/loadpoint_plan.go b/core/loadpoint_plan.go
index 88db4782..8a06d086 100644
--- a/core/loadpoint_plan.go
+++ b/core/loadpoint_plan.go
@@ -1,11 +1,11 @@
 package core
 
 import (
+	"slices"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/planner"
-	"golang.org/x/exp/slices"
 )
 
 const (
diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go
index dfcfb0f2..e3a696d0 100644
--- a/core/loadpoint_vehicle.go
+++ b/core/loadpoint_vehicle.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"regexp"
+	"slices"
 	"strings"
 	"time"
 
@@ -12,7 +13,6 @@ import (
 	"github.com/evcc-io/evcc/core/soc"
 	"github.com/evcc-io/evcc/provider"
 	"github.com/evcc-io/evcc/server/db/settings"
-	"golang.org/x/exp/slices"
 )
 
 const (
diff --git a/core/planner/planner.go b/core/planner/planner.go
index 842a3a16..5377c73b 100644
--- a/core/planner/planner.go
+++ b/core/planner/planner.go
@@ -1,13 +1,13 @@
 package planner
 
 import (
+	"slices"
 	"time"
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/jinzhu/copier"
-	"golang.org/x/exp/slices"
 )
 
 // Planner plans a series of charging slots for a given (variable) tariff
diff --git a/core/planner/sort.go b/core/planner/sort.go
index 08479084..fa0813bb 100644
--- a/core/planner/sort.go
+++ b/core/planner/sort.go
@@ -3,15 +3,18 @@ package planner
 import "github.com/evcc-io/evcc/api"
 
 // SortByTime is a sortFunc for slices.Sort
-func SortByTime(i, j api.Rate) bool {
-	return i.Start.Before(j.Start)
+func SortByTime(i, j api.Rate) int {
+	return i.Start.Compare(j.Start)
 }
 
 // sortByCost is a sortFunc for slices.Sort
-func sortByCost(i, j api.Rate) bool {
-	if i.Price == j.Price {
-		return i.Start.After(j.Start)
+func sortByCost(i, j api.Rate) int {
+	switch {
+	case i.Price < j.Price:
+		return -1
+	case i.Price > j.Price:
+		return +1
+	default:
+		return j.Start.Compare(i.Start)
 	}
-
-	return i.Price < j.Price
 }
diff --git a/core/site.go b/core/site.go
index 492b78c4..7de64880 100644
--- a/core/site.go
+++ b/core/site.go
@@ -411,7 +411,7 @@ func (site *Site) updateMeters() error {
 
 			if err == nil {
 				// ignore negative values which represent self-consumption
-				site.pvPower += math.Max(0, power)
+				site.pvPower += max(0, power)
 				if power < -500 {
 					site.log.WARN.Printf("pv %d power: %.0fW is negative - check configuration if sign is correct", i+1, power)
 				}
@@ -654,12 +654,12 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
 }
 
 func (site *Site) greenShare() float64 {
-	batteryDischarge := math.Max(0, site.batteryPower)
-	batteryCharge := -math.Min(0, site.batteryPower)
-	pvConsumption := math.Min(site.pvPower, site.pvPower+site.gridPower-batteryCharge)
+	batteryDischarge := max(0, site.batteryPower)
+	batteryCharge := -min(0, site.batteryPower)
+	pvConsumption := min(site.pvPower, site.pvPower+site.gridPower-batteryCharge)
 
-	gridImport := math.Max(0, site.gridPower)
-	selfConsumption := math.Max(0, batteryDischarge+pvConsumption+batteryCharge)
+	gridImport := max(0, site.gridPower)
+	selfConsumption := max(0, batteryDischarge+pvConsumption+batteryCharge)
 
 	share := selfConsumption / (gridImport + selfConsumption)
 
@@ -754,8 +754,8 @@ func (site *Site) update(lp Updater) {
 		lp.Update(sitePower, autoCharge, batteryBuffered, batteryStart, greenShare, site.effectivePrice(greenShare), site.effectiveCo2(greenShare))
 
 		// ignore negative pvPower values as that means it is not an energy source but consumption
-		homePower := site.gridPower + math.Max(0, site.pvPower) + site.batteryPower - totalChargePower
-		homePower = math.Max(homePower, 0)
+		homePower := site.gridPower + max(0, site.pvPower) + site.batteryPower - totalChargePower
+		homePower = max(homePower, 0)
 		site.publish("homePower", homePower)
 
 		site.Health.Update()
diff --git a/core/soc/estimator.go b/core/soc/estimator.go
index 3ff2cc9b..a159ef22 100644
--- a/core/soc/estimator.go
+++ b/core/soc/estimator.go
@@ -2,7 +2,6 @@ package soc
 
 import (
 	"errors"
-	"math"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -80,13 +79,12 @@ func (s *Estimator) RemainingChargeDuration(targetSoc int, chargePower float64)
 
 	// Zeit von vehicleSoc bis Reduktionspunkt (linear)
 	if s.vehicleSoc < rrp {
-		t1 = (math.Min(float64(targetSoc), rrp) - s.vehicleSoc) / minChargeSoc * s.virtualCapacity / chargePower
+		t1 = (min(float64(targetSoc), rrp) - s.vehicleSoc) / minChargeSoc * s.virtualCapacity / chargePower
 	}
 
 	// Zeit von Reduktionspunkt bis targetSoc (degressiv)
 	if float64(targetSoc) > rrp {
-		t2 = (float64(targetSoc) - math.Max(s.vehicleSoc, rrp)) / minChargeSoc * s.virtualCapacity / ((chargePower-s.minChargePower)/2 + s.minChargePower)
-
+		t2 = (float64(targetSoc) - max(s.vehicleSoc, rrp)) / minChargeSoc * s.virtualCapacity / ((chargePower-s.minChargePower)/2 + s.minChargePower)
 	}
 
 	return time.Duration(float64(time.Hour) * (t1 + t2)).Round(time.Second)
@@ -153,7 +151,7 @@ func (s *Estimator) Soc(chargedEnergy float64) (float64, error) {
 
 	if s.estimate && s.virtualCapacity > 0 {
 		socDelta := s.vehicleSoc - s.prevSoc
-		energyDelta := math.Max(chargedEnergy, 0) - s.prevChargedEnergy
+		energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy
 
 		if socDelta != 0 || energyDelta < 0 { // soc value change or unexpected energy reset
 			// compare ChargeState of vehicle and charger
@@ -191,10 +189,10 @@ func (s *Estimator) Soc(chargedEnergy float64) (float64, error) {
 			}
 
 			// sample charged energy at soc change, reset energy delta
-			s.prevChargedEnergy = math.Max(chargedEnergy, 0)
+			s.prevChargedEnergy = max(chargedEnergy, 0)
 			s.prevSoc = s.vehicleSoc
 		} else {
-			s.vehicleSoc = math.Min(*fetchedSoc+energyDelta/s.energyPerSocStep, 100)
+			s.vehicleSoc = min(*fetchedSoc+energyDelta/s.energyPerSocStep, 100)
 			s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.vehicleSoc, *fetchedSoc)
 		}
 	}
diff --git a/go.mod b/go.mod
index 233fc4a8..21c695a1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/evcc-io/evcc
 
-go 1.20
+go 1.21
 
 require (
 	github.com/AlecAivazis/survey/v2 v2.3.7
diff --git a/go.sum b/go.sum
index df6431b2..b677ca32 100644
--- a/go.sum
+++ b/go.sum
@@ -378,6 +378,7 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
@@ -425,6 +426,7 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -442,6 +444,7 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
 github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
+github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
 github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
 github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
@@ -548,6 +551,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -570,6 +574,7 @@ github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
 github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
@@ -880,6 +885,7 @@ github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeR
 github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
 github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
 github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg=
+github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
 github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@@ -994,6 +1000,7 @@ github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dk
 github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/simonvetter/modbus v1.6.0 h1:RDHJevtc7LDIVoHAbhDun8fy+QwnGe+ZU+sLm9ZZzjc=
+github.com/simonvetter/modbus v1.6.0/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd8q+GVRObPw=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -1003,8 +1010,10 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU=
+github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w=
+github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -1763,6 +1772,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
diff --git a/meter/tibber/client.go b/meter/tibber/client.go
index f28ba5c8..b5d464fd 100644
--- a/meter/tibber/client.go
+++ b/meter/tibber/client.go
@@ -3,11 +3,11 @@ package tibber
 import (
 	"context"
 	"fmt"
+	"slices"
 
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/shurcooL/graphql"
-	"golang.org/x/exp/slices"
 	"golang.org/x/oauth2"
 )
 
diff --git a/provider/calc.go b/provider/calc.go
index fafcbb16..6fd81c78 100644
--- a/provider/calc.go
+++ b/provider/calc.go
@@ -3,7 +3,6 @@ package provider
 import (
 	"errors"
 	"fmt"
-	"math"
 
 	"github.com/evcc-io/evcc/util"
 )
@@ -31,7 +30,7 @@ func NewCalcFromConfig(other map[string]interface{}) (Provider, error) {
 	}
 
 	o := &calcProvider{}
-	if i := int(math.Min(float64(len(cc.Add)), 1)) + int(math.Min(float64(len(cc.Mul)), 1)) + int(math.Min(float64(len(cc.Div)), 1)); i > 1 ||
+	if i := min(len(cc.Add), 1) + min(len(cc.Mul), 1) + min(len(cc.Div), 1); i > 1 ||
 		(len(cc.Add) > 0 && cc.Sign != nil) ||
 		(len(cc.Mul) > 0 && cc.Sign != nil) ||
 		(len(cc.Div) > 0 && cc.Sign != nil) {
diff --git a/server/db/settings/setting.go b/server/db/settings/setting.go
index 2d43d197..6dc2ad07 100644
--- a/server/db/settings/setting.go
+++ b/server/db/settings/setting.go
@@ -3,12 +3,12 @@ package settings
 import (
 	"encoding/json"
 	"errors"
+	"slices"
 	"strconv"
 	"sync/atomic"
 	"time"
 
 	"github.com/evcc-io/evcc/server/db"
-	"golang.org/x/exp/slices"
 )
 
 var ErrNotFound = errors.New("not found")
diff --git a/server/http_config_handler.go b/server/http_config_handler.go
index 577979a6..6495d50f 100644
--- a/server/http_config_handler.go
+++ b/server/http_config_handler.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"net/http"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -14,7 +15,6 @@ import (
 	"github.com/evcc-io/evcc/util/templates"
 	"github.com/evcc-io/evcc/vehicle"
 	"github.com/gorilla/mux"
-	"golang.org/x/exp/slices"
 )
 
 const (
@@ -76,8 +76,8 @@ func productsHandler(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	slices.SortFunc(res, func(a, b product) bool {
-		return strings.ToLower(a.Name) < strings.ToLower(b.Name)
+	slices.SortFunc(res, func(a, b product) int {
+		return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
 	})
 
 	jsonResult(w, res)
diff --git a/tariff/awattar.go b/tariff/awattar.go
index 6a2764b1..3949575e 100644
--- a/tariff/awattar.go
+++ b/tariff/awattar.go
@@ -2,6 +2,7 @@ package tariff
 
 import (
 	"fmt"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -11,7 +12,6 @@ import (
 	"github.com/evcc-io/evcc/tariff/awattar"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"golang.org/x/exp/slices"
 )
 
 type Awattar struct {
diff --git a/tariff/elering.go b/tariff/elering.go
index 7b73ac5b..99105168 100644
--- a/tariff/elering.go
+++ b/tariff/elering.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -13,7 +14,6 @@ import (
 	"github.com/evcc-io/evcc/tariff/elering"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"golang.org/x/exp/slices"
 )
 
 type Elering struct {
diff --git a/tariff/energinet.go b/tariff/energinet.go
index 24bc5874..d3ddaaf3 100644
--- a/tariff/energinet.go
+++ b/tariff/energinet.go
@@ -3,6 +3,7 @@ package tariff
 import (
 	"errors"
 	"fmt"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -12,7 +13,6 @@ import (
 	"github.com/evcc-io/evcc/tariff/energinet"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"golang.org/x/exp/slices"
 )
 
 type Energinet struct {
diff --git a/tariff/fixed/day.go b/tariff/fixed/day.go
index bbee9961..63d214d6 100644
--- a/tariff/fixed/day.go
+++ b/tariff/fixed/day.go
@@ -3,10 +3,9 @@ package fixed
 import (
 	"errors"
 	"fmt"
+	"slices"
 	"strconv"
 	"strings"
-
-	"golang.org/x/exp/slices"
 )
 
 //go:generate enumer -type Day
diff --git a/tariff/fixed/zone.go b/tariff/fixed/zone.go
index a65fe028..ba67c59b 100644
--- a/tariff/fixed/zone.go
+++ b/tariff/fixed/zone.go
@@ -1,7 +1,7 @@
 package fixed
 
 import (
-	"golang.org/x/exp/slices"
+	"slices"
 )
 
 type Zone struct {
diff --git a/tariff/gruenstromindex.go b/tariff/gruenstromindex.go
index db7bc1ea..5615a08d 100644
--- a/tariff/gruenstromindex.go
+++ b/tariff/gruenstromindex.go
@@ -3,6 +3,7 @@ package tariff
 import (
 	"errors"
 	"fmt"
+	"slices"
 	"sync"
 	"time"
 
@@ -10,7 +11,6 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"golang.org/x/exp/slices"
 )
 
 type GrünStromIndex struct {
diff --git a/tariff/octopus.go b/tariff/octopus.go
index f2d2f182..14867ef1 100644
--- a/tariff/octopus.go
+++ b/tariff/octopus.go
@@ -2,6 +2,7 @@ package tariff
 
 import (
 	"errors"
+	"slices"
 	"sync"
 	"time"
 
@@ -10,7 +11,6 @@ import (
 	"github.com/evcc-io/evcc/tariff/octopus"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"golang.org/x/exp/slices"
 )
 
 type Octopus struct {
diff --git a/tariff/tibber.go b/tariff/tibber.go
index 60d89399..2c5f39f7 100644
--- a/tariff/tibber.go
+++ b/tariff/tibber.go
@@ -3,6 +3,7 @@ package tariff
 import (
 	"context"
 	"errors"
+	"slices"
 	"sync"
 	"time"
 
@@ -12,7 +13,6 @@ import (
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/shurcooL/graphql"
-	"golang.org/x/exp/slices"
 )
 
 type Tibber struct {
diff --git a/util/goversion/constraint.go b/util/goversion/constraint.go
index 27a47955..c3192c94 100644
--- a/util/goversion/constraint.go
+++ b/util/goversion/constraint.go
@@ -1,8 +1,8 @@
 // Approach from https://github.com/theckman/goconstraint/
 
 // Package goversion should only be used as a blank import. If imported, it
-// will only compile if the Go runtime version is >= 1.20.
+// will only compile if the Go runtime version is >= 1.21.
 package goversion
 
-// This will fail to compile if the Go runtime version isn't >= 1.20.
-var _ = __EVCC_REQUIRES_GO_VERSION_1_20__
+// This will fail to compile if the Go runtime version isn't >= 1.21.
+var _ = __EVCC_REQUIRES_GO_VERSION_1_21__
diff --git a/util/goversion/version.go b/util/goversion/version.go
index e0200199..7e9b0022 100644
--- a/util/goversion/version.go
+++ b/util/goversion/version.go
@@ -1,5 +1,5 @@
-//go:build go1.20
+//go:build go1.21
 
 package goversion
 
-const __EVCC_REQUIRES_GO_VERSION_1_20__ = uint8(0)
+const __EVCC_REQUIRES_GO_VERSION_1_21__ = uint8(0)
diff --git a/util/pipe/limiter.go b/util/pipe/limiter.go
index 8b8772cd..c45ccd09 100644
--- a/util/pipe/limiter.go
+++ b/util/pipe/limiter.go
@@ -1,11 +1,11 @@
 package pipe
 
 import (
+	"slices"
 	"time"
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/util"
-	"golang.org/x/exp/slices"
 )
 
 // Piper is the interface that data flow plugins must implement
diff --git a/util/request/roundtrip.go b/util/request/roundtrip.go
index 74c22315..36aa5943 100644
--- a/util/request/roundtrip.go
+++ b/util/request/roundtrip.go
@@ -60,13 +60,6 @@ func NewTripper(log *util.Logger, base http.RoundTripper) http.RoundTripper {
 	return tripper
 }
 
-func min(a, b int) int {
-	if a < b {
-		return a
-	}
-	return b
-}
-
 // copy of http.drainBody
 func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
 	if b == nil || b == http.NoBody {
diff --git a/util/templates/generate/main.go b/util/templates/generate/main.go
index dc8f224f..910e5c5a 100644
--- a/util/templates/generate/main.go
+++ b/util/templates/generate/main.go
@@ -5,11 +5,11 @@ import (
 	"fmt"
 	"os"
 	"path"
+	"slices"
 	"sort"
 	"strings"
 
 	"github.com/evcc-io/evcc/util/templates"
-	"golang.org/x/exp/slices"
 )
 
 const (
diff --git a/util/templates/init.go b/util/templates/init.go
index 3bf4eca5..b62c6f3d 100644
--- a/util/templates/init.go
+++ b/util/templates/init.go
@@ -6,11 +6,11 @@ import (
 	"fmt"
 	"io/fs"
 	"path"
+	"slices"
 	"sync"
 	"text/template"
 
 	"github.com/evcc-io/evcc/templates/definition"
-	"golang.org/x/exp/slices"
 	"gopkg.in/yaml.v3"
 )
 
diff --git a/util/templates/template.go b/util/templates/template.go
index 5cb1d11f..ebee7dd0 100644
--- a/util/templates/template.go
+++ b/util/templates/template.go
@@ -4,12 +4,12 @@ import (
 	"bytes"
 	_ "embed"
 	"fmt"
+	"slices"
 	"strings"
 	"text/template"
 
 	"github.com/Masterminds/sprig/v3"
 	"github.com/evcc-io/evcc/util"
-	"golang.org/x/exp/slices"
 )
 
 // Template describes is a proxy device for use with cli and automated testing
diff --git a/vehicle/renault/provider.go b/vehicle/renault/provider.go
index cba683d6..2775594c 100644
--- a/vehicle/renault/provider.go
+++ b/vehicle/renault/provider.go
@@ -2,6 +2,7 @@ package renault
 
 import (
 	"net/http"
+	"slices"
 	"strings"
 	"time"
 
@@ -9,7 +10,6 @@ import (
 	"github.com/evcc-io/evcc/provider"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/evcc-io/evcc/vehicle/renault/kamereon"
-	"golang.org/x/exp/slices"
 )
 
 // Provider is an api.Vehicle implementation for PSA cars
diff --git a/vehicle/tronity.go b/vehicle/tronity.go
index e1565bce..72ed1cfe 100644
--- a/vehicle/tronity.go
+++ b/vehicle/tronity.go
@@ -21,6 +21,7 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"slices"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -30,7 +31,6 @@ import (
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/evcc-io/evcc/util/sponsor"
 	"github.com/evcc-io/evcc/vehicle/tronity"
-	"golang.org/x/exp/slices"
 	"golang.org/x/oauth2"
 )
 
@@ -117,7 +117,6 @@ func NewTronityFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 			return v.VIN
 		},
 	)
-
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/core/planner/planner_test.go b/core/planner/planner_test.go
index 79d3fb4a..97f83de6 100644
--- a/core/planner/planner_test.go
+++ b/core/planner/planner_test.go
@@ -1,6 +1,7 @@
 package planner
 
 import (
+	"slices"
 	"testing"
 	"time"
 
@@ -10,7 +11,6 @@ import (
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
-	"golang.org/x/exp/slices"
 )
 
 func rates(prices []float64, start time.Time, slotDuration time.Duration) api.Rates {
diff --git a/core/planner/sort_test.go b/core/planner/sort_test.go
index 466210f9..6059dd6a 100644
--- a/core/planner/sort_test.go
+++ b/core/planner/sort_test.go
@@ -1,13 +1,13 @@
 package planner
 
 import (
+	"slices"
 	"testing"
 	"time"
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/stretchr/testify/assert"
-	"golang.org/x/exp/slices"
 )
 
 func testRates(clock clock.Clock) api.Rates {
@@ -25,8 +25,8 @@ func testRates(clock clock.Clock) api.Rates {
 			Start: clock.Now().Add(time.Hour),
 		},
 	}
-
 }
+
 func TestRatesSortByTime(t *testing.T) {
 	clock := clock.NewMock()
 
diff --git a/util/templates/render_testing.go b/util/templates/render_testing.go
index afe7afdd..af96b7ee 100644
--- a/util/templates/render_testing.go
+++ b/util/templates/render_testing.go
@@ -2,10 +2,10 @@ package templates
 
 import (
 	"os"
+	"slices"
 	"testing"
 
 	"github.com/jinzhu/copier"
-	"golang.org/x/exp/slices"
 	"gopkg.in/yaml.v3"
 )