Repository: evcc-io/evcc
Pull request #2209 (2022-01-05 15:43:29)

Allow to define supported phases at vehicle level
References https://github.com/evcc-io/evcc/issues/1894#issuecomment-1005748238

Any vehicle using the `embed` helper can now define `phases` for supported phases. Leave empty for current behaviour.

DerAndereAndi: Das müsste dann auch in die Templates rein, gib mir bitte Bescheid wenn das soweit ist.
premultiply: Ich denke hier wäre maxPhases wohl der treffendere Name. 1 geht immer. Ansonsten ist es vom LP abhängig. Damit wäre besser abschätzbar was bei einer Umschaltung passieren wird und ob sie überhaupt sinnvoll ist.

Theoretisch bräuchte man auch noch maxCurrent (ggf. auch noch pro Phasenkonfiguration) am Vehicle den man mit dem maxCurrent und minCurrent des LPs abgleicht um sinnlose Schaltereien zu vermeiden und "Leistungslöcher" zu erkennen.

Es ist komplex und erfordert einen stimmigen Algorithmus.
Chris591: > Ich denke hier wäre maxPhases wohl der treffendere Name. 1 geht immer. Ansonsten ist es vom LP abhängig. Damit wäre besser abschätzbar was bei einer Umschaltung passieren wird und ob sie überhaupt sinnvoll ist.
> 
> Theoretisch bräuchte man auch noch maxCurrent (ggf. auch noch pro Phasenkonfiguration) am Vehicle den man mit dem maxCurrent und minCurrent des LPs abgleicht um sinnlose Schaltereien zu vermeiden und "Leistungslöcher" zu erkennen.
> 
> Es ist komplex und erfordert einen stimmigen Algorithmus.

max hört sich gut an aber das ist ja eigentlich überall ein MAX oder? gibt es Autos die vielleicht nur mit 2/3 Phasen laden können?

Gibt es ein Auto das die Phasen unterschiedlich belastet wenn es mehrere zu verfügung hat?

Sonst stimmt es schon wir müssen nicht 3 Phasen an einem Auto Konfigurieren oder am LP da es ja sowieso das Maximum ist was geht aber es gibt Autos mit 2 oder nur einer Phase. Also finde ich es ok das man auch 3 konfigurieren kann was aber der Default für viele sein sollte.
ThiemoSt: Ich kenn es von meiner Alfen Wallbox wenn diese auf 3phasig steht und ein Bekannter mit seinem Skoda Hybrid (unterstützt nur 1phasig) lädt das diese dann auch nicht umschaltet. Es bleibt bei 3phasig, man erkennt es nur in den Strömen der einzelnen Phasen. 
andig: > Ich kenn es von meiner Alfen Wallbox wenn diese auf 3phasig steht und ein Bekannter mit seinem Skoda Hybrid (unterstützt nur 1phasig) lädt das diese dann auch nicht umschaltet. Es bleibt bei 3phasig, man erkennt es nur in den Strömen der einzelnen Phasen.

Das sollte bei EVCC heute schon so sein. Falls nein bitte Ticket mit Logfile.

Patch:
diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml
index 2951b53e..ac36affd 100644
--- a/.github/workflows/rebase.yml
+++ b/.github/workflows/rebase.yml
@@ -12,13 +12,13 @@ jobs:
       github.event.issue.pull_request != ''
       && contains(github.event.comment.body, '/rebase')
       && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
-
     steps:
     - name: Checkout the latest code
       uses: actions/checkout@v2
       with:
+        token: ${{ secrets.GITHUB_TOKEN }}
         fetch-depth: 0
     - name: Automatic Rebase
-      uses: cirrus-actions/rebase@1.4
+      uses: cirrus-actions/rebase@1.5
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 00000000..b581f29a
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,18 @@
+name: 'Stale issue handler'
+on:
+  workflow_dispatch:
+  schedule:
+    - cron: '0 0 * * *'
+
+jobs:
+  stale:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@main
+        id: stale
+        with:
+          days-before-stale: 20
+          days-before-close: 5
+          exempt-issue-labels: 'blocked,must,should,keep,pinned'
+      - name: Print outputs
+        run: echo ${{ join(steps.stale.outputs.*, ',') }}
diff --git a/Makefile b/Makefile
index d7f6542c..f2c8d9fa 100644
--- a/Makefile
+++ b/Makefile
@@ -91,12 +91,12 @@ publish-images:
 
 # gokrazy image
 prepare-image:
-	go get github.com/gokrazy/tools/cmd/gokr-packer@latest
+	go install github.com/gokrazy/tools/cmd/gokr-packer@latest
 	mkdir -p flags/github.com/gokrazy/breakglass
 	echo "-forward=private-network" > flags/github.com/gokrazy/breakglass/flags.txt
 	mkdir -p buildflags/github.com/evcc-io/evcc
 	echo "$(BUILD_TAGS),gokrazy" > buildflags/github.com/evcc-io/evcc/buildflags.txt
-	echo "-ldflags=$(LD_FLAGS)" >> buildflags/github.com/evcc-io/evcc/buildflags.txt
+	echo "$(BUILD_ARGS)" >> buildflags/github.com/evcc-io/evcc/buildflags.txt
 
 image:
 	gokr-packer -overwrite=$(IMAGE_FILE) -target_storage_bytes=1258299392 $(IMAGE_OPTIONS)
diff --git a/README.md b/README.md
index e0b3f9b0..171a91be 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# evcc
+# evcc 🚘☀️
 
 [![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/evcc-io/evcc)
 [![Build Status](https://github.com/evcc-io/evcc/workflows/Build/badge.svg)](https://github.com/evcc-io/evcc/actions?query=workflow%3ABuild)
@@ -7,15 +7,16 @@
 [![Pulls from Docker Hub](https://img.shields.io/docker/pulls/andig/evcc.svg)](https://hub.docker.com/r/andig/evcc)
 [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=48YVXXA7BDNC2)
 
-EVCC is an extensible EV Charge Controller with PV integration implemented in [Go][2]. Featured in [PV magazine](https://www.pv-magazine.de/2021/01/15/selbst-ist-der-groeoenlandhof-wallbox-ladesteuerung-selbst-gebaut/).
+Evcc is an extensible EV Charge Controller with PV integration implemented in [Go][2]. Featured in [PV magazine](https://www.pv-magazine.de/2021/01/15/selbst-ist-der-groeoenlandhof-wallbox-ladesteuerung-selbst-gebaut/).
 
+![Screenshot](docs/screenshot.png)
 
 ## Features
 
 - simple and clean user interface
 - multiple [chargers](https://docs.evcc.io/docs/devices/chargers):
   - Open source: [openWB](https://openwb.de/), [EVSEWifi](https://www.evse-wifi.de) (includes smartWB)
-  - Other commercial: ABL eMH1, Alfen Eve, cFos PowerBrain, Daheimladen, go-eCharger, Heidelberg Energy Control, Innogy eBox, KEBA/BMW, NRGkick, Wallbe, Mobile Charger Connect, EEBUS (experimental)
+  - Other commercial: ABL eMH1, Alfen Eve, cFos PowerBrain, Daheimladen, go-eCharger, Heidelberg Energy Control, Innogy eBox, KEBA/BMW, NRGkick, PC Electric (includes Garo), Wallbe, Mobile Charger Connect, EEBUS (experimental)
   - Build-your-own: Phoenix (includes ESL Walli), [SimpleEVSE](https://www.evse-wifi.de/produkt-schlagwort/simple-evse-wb/)
   - Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link
 - multiple [meters](https://docs.evcc.io/docs/devices/meters): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall, LG ESS HOME
@@ -26,11 +27,9 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
 - granular charge power control down to mA steps with supported chargers (labeled by e.g. smartWB als [OLC](https://board.evse-wifi.de/viewtopic.php?f=16&t=187))
 - REST and MQTT [APIs](https://docs.evcc.io/docs/reference/api) for integration with home automation systems (e.g. [HomeAssistant](https://github.com/evcc-io/evcc-hassio-addon))
 
-![Screenshot](docs/screenshot.png)
-
-## Documentation
+## Getting Started
 
-Documentation is available in german language at [https://docs.evcc.io/](https://docs.evcc.io/)
+You'll find everything you need in our [documentation](https://docs.evcc.io/) (German).
 
 ## Build
 
@@ -42,10 +41,10 @@ make
 
 ## Sponsorship
 
-EVCC believes in open source software. We're committed to provide best in class EV charging experience.
-Maintaining EVCC consumes time and effort. With the vast amount of different devices to support, we depend on community and vendor support to keep EVCC alive.
+Evcc believes in open source software. We're committed to provide best in class EV charging experience.
+Maintaining evcc consumes time and effort. With the vast amount of different devices to support, we depend on community and vendor support to keep EVCC alive.
 
-While EVCC is open source, we would also like to encourage vendors to provide open source hardware devices, public documentation and support open source projects like hours that provide additional value to otherwised closed hardware. Where this is not the case, EVCC requires "sponsor token" to finance ongoing development and support of evcc.
+While evcc is open source, we would also like to encourage vendors to provide open source hardware devices, public documentation and support open source projects like hours that provide additional value to otherwised closed hardware. Where this is not the case, EVCC requires "sponsor token" to finance ongoing development and support of evcc.
 
 The personal sponsor token requires a [Github Sponsorship](https://github.com/sponsors/andig) and can be requested at [cloud.evcc.io](https://cloud.evcc.io/). A sponsor token is valid for one year and can be renewed any time with active sponsorship.
 
@@ -53,18 +52,18 @@ The personal sponsor token requires a [Github Sponsorship](https://github.com/sp
 
 <img src="docs/logo.png" align="right" width="150" />
 
-EVCC is heavily inspired by [OpenWB][1]. However, in 2019, I found OpenWB's architecture slightly intimidating with everything basically global state and heavily relying on shell scripting. On the other side, especially the scripting aspect is one that contributes to [OpenWB's][1] flexibility.
+Evcc is heavily inspired by [OpenWB][1]. However, in 2019, I found OpenWB's architecture slightly intimidating with everything basically global state and heavily relying on shell scripting. On the other side, especially the scripting aspect is one that contributes to OpenWB's flexibility.
 
-Hence, for a simplified and stricter implementation of an EV charge controller, the design goals for EVCC were:
+Hence, for a simplified and stricter implementation of an EV charge controller, the design goals for evcc were:
 
 - typed language with ability for systematic testing - achieved by using [Go][2]
 - structured configuration - supports YAML-based [config file](evcc.dist.yaml)
-- avoidance of feature bloat, simple and clean UI - utilizes [Bootstrap][4]
-- containerized operation beyond Raspberry Pi - provide multi-arch [Docker Image][5]
+- avoidance of feature bloat, simple and clean UI - utilizes [Vue.js][4] and [Bootstrap][5]
+- containerized operation beyond Raspberry Pi - provide multi-arch [Docker Image][6]
 
 [1]: https://github.com/snaptec/openWB
 [2]: https://golang.org
 [3]: https://nodejs.org/
-[4]: https://getbootstrap.org
-[5]: https://hub.docker.com/repository/docker/andig/evcc
-[6]: https://github.com/volkszaehler/mbmd
+[4]: https://vuejs.org
+[5]: https://getbootstrap.org
+[6]: https://hub.docker.com/repository/docker/andig/evcc
diff --git a/api/api.go b/api/api.go
index 767e0b04..3bb77c42 100644
--- a/api/api.go
+++ b/api/api.go
@@ -9,7 +9,7 @@ import (
 	"github.com/fatih/structs"
 )
 
-//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery
+//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,VehiclePhases,ChargeRater,Battery
 
 // ChargeMode are charge modes modeled after OpenWB
 type ChargeMode string
@@ -170,6 +170,11 @@ type VehiclePosition interface {
 	Position() (float64, float64, error)
 }
 
+// VehiclePhases returns the number of supported phases
+type VehiclePhases interface {
+	Phases() int
+}
+
 // VehicleStartCharge starts the charging session on the vehicle side
 type VehicleStartCharge interface {
 	StartCharge() error
diff --git a/charger/pcelectric.go b/charger/pcelectric.go
new file mode 100644
index 00000000..1e1ff846
--- /dev/null
+++ b/charger/pcelectric.go
@@ -0,0 +1,279 @@
+package charger
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/charger/pcelectric"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+)
+
+// PCElectric charger implementation
+type PCElectric struct {
+	*request.Helper
+	log *util.Logger
+
+	uri        string // http://garo2216247:8080/servlet
+	slaveIndex int    // 0 = Master, 1..n Slave
+	meter      string // <CENTRAL100|CENTRAL101|INTERNAL|EXTERNAL|TWIN>
+
+	lbmode       bool // true/false (wird automatisch bestimmt)
+	serialNumber int  // 1234567
+}
+
+func init() {
+	registry.Add("garo", NewPCElectricFromConfig)
+	registry.Add("pcelectric", NewPCElectricFromConfig)
+}
+
+//go:generate go run ../cmd/tools/decorate.go -f decoratePCE -b *PCElectric -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
+
+// NewPCElectricFromConfig creates a PCElectric charger from generic config
+func NewPCElectricFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := struct {
+		URI        string
+		SlaveIndex int
+		Meter      string
+	}{
+		Meter: "CENTRAL100",
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	wb, err := NewPCElectric(util.DefaultScheme(cc.URI, "http"), cc.SlaveIndex, cc.Meter)
+	if err == nil && wb.slaveIndex == 0 { // Nur Master hat den Zähler...leider
+		var res pcelectric.MeterInfo
+		if err := wb.GetJSON(wb.meter, &res); err == nil && res.MeterSerial != "" {
+			return decoratePCE(wb, wb.currentPower, wb.totalEnergy, wb.currents), nil
+		}
+
+		wb.meter = ""
+	}
+
+	return wb, err
+}
+
+// NewPCElectric creates PCElectric charger
+func NewPCElectric(uri string, slaveIndex int, meter string) (*PCElectric, error) {
+	log := util.NewLogger("pce")
+	uri = strings.TrimSuffix(strings.TrimRight(uri, "/"), "/servlet") + "/servlet/rest/chargebox"
+
+	wb := &PCElectric{
+		Helper:     request.NewHelper(log),
+		log:        log,
+		uri:        uri,
+		slaveIndex: slaveIndex,
+		meter:      strings.ToUpper(meter),
+	}
+
+	// Nur Master: lb Config auslesen.
+	// Ohne Loadbalancer: Steuerung über currentlimit
+	// Mit Loadbalander: Steuerung über loadBalancingFuse
+	var lbconfig pcelectric.LbConfig
+	uri = fmt.Sprintf("%s/lbconfig/false", wb.uri)
+	if err := wb.GetJSON(uri, &lbconfig); err == nil {
+		wb.lbmode = lbconfig.MasterLoadBalanced
+		wb.serialNumber = lbconfig.Slaves[wb.slaveIndex].SerialNumber
+		log.DEBUG.Printf("lbmode: %t  serial: %d ", wb.lbmode, wb.serialNumber)
+	}
+
+	return wb, nil
+}
+
+// Status implements the api.Charger interface
+func (wb *PCElectric) Status() (api.ChargeStatus, error) {
+	var chargeStatus int
+	var sessionStartTime int64
+
+	if wb.slaveIndex == 0 {
+		var status pcelectric.Status
+
+		uri := fmt.Sprintf("%s/status", wb.uri)
+		if err := wb.GetJSON(uri, &status); err != nil {
+			return api.StatusNone, err
+		}
+		chargeStatus = status.ChargeStatus
+		sessionStartTime = status.SessionStartTime
+	} else {
+		var status pcelectric.SlaveStatus
+
+		uri := fmt.Sprintf("%s/slaves/false", wb.uri)
+		if err := wb.GetJSON(uri, &status); err != nil {
+			return api.StatusNone, err
+		}
+		if wb.slaveIndex >= len(status) {
+			return api.StatusNone, nil
+		}
+		chargeStatus = status[wb.slaveIndex].ChargeStatus
+		sessionStartTime = status[wb.slaveIndex].SessionStartTime
+	}
+	wb.log.DEBUG.Printf("chargeStatus: %d", chargeStatus)
+
+	var res api.ChargeStatus
+	switch chargeStatus {
+	case 0x00, 0x10: // notconnected
+		res = api.StatusA
+	case 0x30: // connected
+		res = api.StatusB
+	case 0x40: // charging
+		res = api.StatusC
+	case 0x42, // chargepaused
+		0x50, // chargefinished
+		0x60: // chargecancelled
+		res = api.StatusB
+	case 0x90: // unavailable
+		if sessionStartTime > 0 {
+			res = api.StatusB
+		} else {
+			res = api.StatusF
+		}
+	case 0x95, // dcfault
+		0x96, // dchardwarefault
+		0x9A, // cpfault
+		0x9B: // cpshorted
+		res = api.StatusE
+	case 0x70, // overheat
+		0x80, // criticaltemperature
+		0x91, // reserved
+		0x9C, // remotedisabled
+		0x9D, // dlmfault
+		0xA0, // cablefault
+		0xA1,
+		0xA2, // lockingfault
+		0xA3,
+		0xA4, // contactorfault
+		0xA8, // rcdfault
+		0xF0, // wait
+		0xF1: // ventfault
+		res = api.StatusF
+	default: // generalfault
+		res = api.StatusF
+	}
+
+	return res, nil
+}
+
+// Enabled implements the api.Charger interface
+func (wb *PCElectric) Enabled() (bool, error) {
+	var res pcelectric.Status
+	uri := fmt.Sprintf("%s/status", wb.uri)
+	err := wb.GetJSON(uri, &res)
+	if err == nil && res.PowerMode == "ON" {
+		return true, err
+	}
+	return false, err
+}
+
+// Enable implements the api.Charger interface
+func (wb *PCElectric) Enable(enable bool) error {
+	if wb.slaveIndex > 0 {
+		return nil //Slave wird immer mit dem Master geschaltet!
+	}
+
+	// Master Only !!
+	mode := "ALWAYS_OFF"
+	if enable {
+		mode = "ALWAYS_ON"
+	}
+
+	uri := fmt.Sprintf("%s/mode/%s", wb.uri, mode)
+	req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding)
+	if err == nil {
+		_, err = wb.DoBody(req)
+	}
+
+	return err
+}
+
+func (wb *PCElectric) MinCurrent(current int64) error {
+	data := pcelectric.MinCurrentLimitStruct{
+		{
+			MinCurrentLimit: int(current), // default=6
+			SerialNumber:    wb.serialNumber,
+			TwinSerial:      -1,
+		},
+	}
+
+	uri := fmt.Sprintf("%s/mincurrentlimit", wb.uri)
+	req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
+	if err == nil {
+		_, err = wb.DoBody(req)
+	}
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *PCElectric) MaxCurrent(current int64) error {
+	if wb.slaveIndex > 0 {
+		return nil //Slave wird immer mit dem Master geschaltet!
+	}
+
+	// Ohne Loadbalancer Regelung über currentlimit:
+	if !wb.lbmode {
+		data := pcelectric.ReducedIntervals{
+			ReducedIntervalsEnabled: true,
+			ReducedCurrentIntervals: []pcelectric.ReducedCurrentInterval{
+				{
+					SchemaId:    1,
+					Start:       "00:00:00",
+					Stop:        "24:00:00",
+					Weekday:     8,
+					ChargeLimit: int(current),
+				},
+			},
+		}
+
+		uri := fmt.Sprintf("%s/currentlimit", wb.uri)
+		req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
+		if err == nil {
+			_, err = wb.DoBody(req)
+		}
+
+		return err
+	}
+
+	// Mit Loadbalancer Regelung über lbconfig/LoadBalancingFuse
+	var data pcelectric.LbConfigShort
+	uri := fmt.Sprintf("%s/lbconfig/false", wb.uri)
+	if err := wb.GetJSON(uri, &data); err != nil {
+		return err
+	}
+
+	data.LoadBalancingFuse = int(current)
+
+	uri = fmt.Sprintf("%s/lbconfig", wb.uri)
+	req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
+	if err == nil {
+		_, err = wb.DoBody(req)
+	}
+
+	return err
+}
+
+// CurrentPower implements the api.Meter interface W
+func (wb *PCElectric) currentPower() (float64, error) {
+	l1, l2, l3, err := wb.currents()
+	return 230 * (l1 + l2 + l3), err
+}
+
+// TotalEnergy implements the api.MeterEnergy interface kwh
+func (wb *PCElectric) totalEnergy() (float64, error) {
+	var res pcelectric.MeterInfo
+	uri := fmt.Sprintf("%s/meterinfo/%s", wb.uri, wb.meter)
+	err := wb.GetJSON(uri, &res)
+	return float64(res.AccEnergy) / 1e3, err
+}
+
+// Currents implements the api.MeterCurrents interface A
+func (wb *PCElectric) currents() (float64, float64, float64, error) {
+	var res pcelectric.MeterInfo
+	uri := fmt.Sprintf("%s/meterinfo/%s", wb.uri, wb.meter)
+	err := wb.GetJSON(uri, &res)
+	return float64(res.Phase1Current) / 10, float64(res.Phase2Current) / 10, float64(res.Phase3Current) / 10, err
+}
diff --git a/charger/pcelectric/types.go b/charger/pcelectric/types.go
new file mode 100644
index 00000000..884a47bb
--- /dev/null
+++ b/charger/pcelectric/types.go
@@ -0,0 +1,244 @@
+package pcelectric
+
+// /servlet/rest/chargebox/status
+
+type Status struct {
+	SerialNumber           int64  //  2216247,
+	Connector              string //  "NOT_CONNECTED",
+	Mode                   string //  "ALWAYS_ON",
+	CurrentLimit           int    //  11,
+	FactoryCurrentLimit    int    //  32,
+	SwitchCurrentLimit     int    //  32,
+	PowerMode              string //  "OFF",
+	CurrentChargingCurrent int    //  -1,
+	CurrentChargingPower   int    //  -1,
+	AccSessionEnergy       int    //  0,
+	SessionStartTime       int64  //  1641489661842
+	ChargeboxTime          string //  "22:25"
+	AccSessionMillis       int64  //  0,
+	LatestReading          int    //  0,
+	ChargeStatus           int    //  0,
+	NrOfPhases             int    //  1,
+	MainCharger            struct {
+		Reference              string //  "Garage",
+		SerialNumber           int    //  2216247,
+		LastContact            int64  //  1640781615305,
+		Online                 bool   //  false,
+		LoadBalanced           bool   //  true,
+		Phase                  int    //  16,
+		ProductId              int    //  121,
+		MeterStatus            int    //  1,
+		MeterSerial            string //  "",
+		ChargeStatus           int    //  0,
+		PilotLevel             int    //  6,
+		AccEnergy              int    //  -1,
+		Connector              string //  "NOT_CONNECTED",
+		AccSessionEnergy       int    //  0,
+		SessionStartValue      int64  //  -1,
+		AccSessionMillis       int64  //  0,
+		CurrentChargingCurrent int    //  -1,
+		CurrentChargingPower   int    //  0,
+		NrOfPhases             int    //  1,
+		TwinSerial             int    //  -1,
+	}
+	TwinCharger interface{}
+}
+
+// /servlet/rest/chargebox/slaves/false
+type SlaveStatus []struct {
+	Reference              string //  "Garage",
+	SerialNumber           int    //  2216247,
+	LastContact            int64  //  1640781615305,
+	Online                 bool   //  false,
+	LoadBalanced           bool   //  true,
+	Phase                  int    //  16,
+	ProductId              int    //  121,
+	MeterStatus            int    //  1,
+	MeterSerial            string //  "",
+	ChargeStatus           int    //  0,
+	PilotLevel             int    //  6,
+	AccEnergy              int    //  -1,
+	FirmwareVersion        int    //  7
+	FirmwareRevision       int    //  9
+	WifiCardStatus         int    //  2
+	Connector              string //  "NOT_CONNECTED",
+	AccSessionEnergy       int    //  0,
+	SessionStartValue      int    //  -1,
+	AccSessionMillis       int64  //  0,
+	SessionStartTime       int64  //  1641136092090
+	CurrentChargingCurrent int    //  -1,
+	CurrentChargingPower   int    //  0,
+	NrOfPhases             int    //  1,
+	TwinSerial             int    //  -1,
+	CableLockMode          int    //  0
+	MinCurrentLimit        int    //  6
+	DipSwitchSettings      int    //  8188
+}
+
+type ReducedIntervals struct {
+	ReducedIntervalsEnabled bool                     `json:"reducedIntervalsEnabled"`
+	ReducedCurrentIntervals []ReducedCurrentInterval `json:"reducedCurrentIntervals,omitempty"`
+}
+
+type ReducedCurrentInterval struct {
+	SchemaId    int    `json:"schemaId"`
+	Start       string `json:"start"`
+	Stop        string `json:"stop"`
+	Weekday     int    `json:"weekday"`
+	ChargeLimit int    `json:"chargeLimit"`
+}
+
+// /servlet/rest/chargebox/meterinfo/CENTRAL100
+
+type MeterInfo struct {
+	Success         int    // 0,
+	AccEnergy       int64  // 169100,
+	Phase1Current   int    // 158,
+	Phase2Current   int    // 157,
+	Phase3Current   int    // 156,
+	Phase1InstPower int    // 3,
+	Phase2InstPower int    // 3,
+	Phase3InstPower int    // 3,
+	ReadTime        int64  // 1640783279023,
+	GridNetType     string // "UNKNOWN",
+	MeterSerial     string // "116223V",
+	Type            int    // 341,
+	ApparentPower   int    // 9
+}
+
+// /servlet/rest/chargebox/lbconfig/false
+type LbConfigShort struct {
+	LoadBalancingFuse     int  `json:"loadBalancingFuse"`     // 32
+	LoadBalancingPower    int  `json:"loadBalancingPower"`    // 0
+	LoadBalancingFuse101  int  `json:"loadBalancingFuse101"`  // 32
+	LoadBalancingPower101 int  `json:"loadBalancingPower101"` // 0
+	MasterPhase           int  `json:"masterPhase"`           // 16
+	MasterLoadBalanced    bool `json:"masterLoadBalanced"`    // true
+}
+
+type LbConfig struct {
+	LoadBalancingFuse     int  // 16
+	LoadBalancingPower    int  // 0
+	LoadBalancingFuse101  int  // 16
+	LoadBalancingPower101 int  // 0
+	MasterPhase           int  // 16
+	MasterLoadBalanced    bool // true
+	Slaves                []struct {
+		Reference              string // "Garage"
+		SerialNumber           int    // 2216247
+		LastContact            int64  // 1640970181816
+		Online                 bool   // false
+		LoadBalanced           bool   // true
+		Phase                  int    // 16
+		ProductId              int    // 121
+		MeterStatus            int    //	1
+		MeterSerial            string // ""
+		ChargeStatus           int    // 144
+		PilotLevel             int    // 6
+		AccEnergy              int    // -1
+		FirmwareVersion        int    // 7
+		FirmwareRevision       int    // 9
+		WifiCardStatus         int    // 2
+		Connector              string // "UNAVAILABLE"
+		AccSessionEnergy       int    // 0
+		SessionStartValue      int    // -1
+		AccSessionMillis       int64  // 174723
+		SessionStartTime       int64  // 1640957660208
+		CurrentChargingCurrent int    // -1
+		CurrentChargingPower   int    // 0
+		NrOfPhases             int    // 1
+		TwinSerial             int    // -1
+		CableLockMode          int    // 0
+		MinCurrentLimit        int    // 6
+		DipSwitchSettings      int    // 8188
+	}
+}
+type Config struct {
+	OcppConnected           bool   // false
+	MaxChargeCurrent        int    // 32
+	ProductId               string // "81"
+	ProgramVersion          string // "7.9"
+	FirmwareVersion         int    // 7
+	FirmwareRevision        int    // 9
+	SmallFirmwareVersion    int    // 2
+	SmallFirmwareRevision   int    // 15
+	LargeFirmwareVersion    int    // 7
+	LargeFirmwareRevision   int    // 9
+	LbVersion2              bool   // true
+	SerialNumber            int    // 2216247
+	MeterSerialNumber       string // ""
+	MeterType               int    // 0
+	FactoryChargeLimit      int    // 32
+	SwitchChargeLimit       int    // 32
+	RfidReaderPresent       bool   // true
+	RfidMode                string // "RFID_WIFI"
+	PowerbackupStatus       int    // 2
+	LastTemperature         int    // 25
+	WarningTemperature      int    // 65
+	CutoffTemperature       int    // 70
+	ReducedIntervalsEnabled bool   // true
+	ReducedCurrentIntervals []struct {
+		SchemaId    int    // 1
+		Start       string // "00:00:00"
+		Stop        string // "00:00:00"
+		Weekday     int    // 8
+		ChargeLimit int    // 6
+	}
+	SoftwareVersion      int    // 185
+	AvailableVersion     int    // 185
+	UpdateUrl            string // "http%3A%2F%2F85.11.39.104%2Fchargebox_185.tgz"
+	NetworkMode          int    // 1
+	NetworkType          int    // 0
+	NetworkSSID          string // "SchlockeNet"
+	WebNetworkPassword   string // ""
+	NetworkAPChannel     int    // 6
+	EthNetworkMode       int    // 0
+	GcConfigTimestamp    int64  // null
+	GcloudActivated      bool   // false
+	GcActivatedFrom      int64  // null
+	EthGateway           string // ""
+	EthDNS               string // ""
+	EthIP                string // ""
+	EthMask              int    // 24
+	LocalLoadBalanced    bool   // false
+	GroupLoadBalanced    bool   // true
+	GroupLoadBalanced101 bool   // false
+	EnergyReportEnabled  bool   // false
+	Master               bool   // true
+	Timezone             string // null
+	SlaveList            []struct {
+		Reference              string // Garage
+		SerialNumber           int    // 2216247
+		LastContact            int64  // 1640960502344
+		Online                 bool   // false
+		LoadBalanced           bool   // true
+		Phase                  int    // 16
+		ProductId              int    // 121
+		MeterStatus            int    // 1
+		MeterSerial            string // ""
+		ChargeStatus           int    // 144
+		PilotLevel             int    // 6
+		AccEnergy              int    // -1
+		FirmwareVersion        int    // 7
+		FirmwareRevision       int    // 9
+		WifiCardStatus         int    // 2
+		Connector              string // "UNAVAILABLE"
+		AccSessionEnergy       int    //	0
+		SessionStartValue      int    // -1
+		AccSessionMillis       int    // 5635
+		SessionStartTime       int64  // 1640957660208
+		CurrentChargingCurrent int    // -1
+		CurrentChargingPower   int    // 0
+		NrOfPhases             int    // 1
+		TwinSerial             int    // -1
+		CableLockMode          int    // 0
+		MinCurrentLimit        int    // 6
+		DipSwitchSettings      int    // 8188
+	}
+}
+
+type MinCurrentLimitStruct []struct {
+	MinCurrentLimit int `json:"minCurrentLimit"`
+	SerialNumber    int `json:"serialNumber"`
+	TwinSerial      int `json:"twinSerial"` // -1
+}
diff --git a/charger/pcelectric_decorators.go b/charger/pcelectric_decorators.go
new file mode 100644
index 00000000..0e4b6c53
--- /dev/null
+++ b/charger/pcelectric_decorators.go
@@ -0,0 +1,137 @@
+package charger
+
+// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
+
+import (
+	"github.com/evcc-io/evcc/api"
+)
+
+func decoratePCE(base *PCElectric, meter func() (float64, error), meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error)) api.Charger {
+	switch {
+	case meter == nil && meterCurrent == nil && meterEnergy == nil:
+		return base
+
+	case meter != nil && meterCurrent == nil && meterEnergy == nil:
+		return &struct {
+			*PCElectric
+			api.Meter
+		}{
+			PCElectric: base,
+			Meter: &decoratePCEMeterImpl{
+				meter: meter,
+			},
+		}
+
+	case meter == nil && meterCurrent == nil && meterEnergy != nil:
+		return &struct {
+			*PCElectric
+			api.MeterEnergy
+		}{
+			PCElectric: base,
+			MeterEnergy: &decoratePCEMeterEnergyImpl{
+				meterEnergy: meterEnergy,
+			},
+		}
+
+	case meter != nil && meterCurrent == nil && meterEnergy != nil:
+		return &struct {
+			*PCElectric
+			api.Meter
+			api.MeterEnergy
+		}{
+			PCElectric: base,
+			Meter: &decoratePCEMeterImpl{
+				meter: meter,
+			},
+			MeterEnergy: &decoratePCEMeterEnergyImpl{
+				meterEnergy: meterEnergy,
+			},
+		}
+
+	case meter == nil && meterCurrent != nil && meterEnergy == nil:
+		return &struct {
+			*PCElectric
+			api.MeterCurrent
+		}{
+			PCElectric: base,
+			MeterCurrent: &decoratePCEMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+		}
+
+	case meter != nil && meterCurrent != nil && meterEnergy == nil:
+		return &struct {
+			*PCElectric
+			api.Meter
+			api.MeterCurrent
+		}{
+			PCElectric: base,
+			Meter: &decoratePCEMeterImpl{
+				meter: meter,
+			},
+			MeterCurrent: &decoratePCEMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+		}
+
+	case meter == nil && meterCurrent != nil && meterEnergy != nil:
+		return &struct {
+			*PCElectric
+			api.MeterCurrent
+			api.MeterEnergy
+		}{
+			PCElectric: base,
+			MeterCurrent: &decoratePCEMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+			MeterEnergy: &decoratePCEMeterEnergyImpl{
+				meterEnergy: meterEnergy,
+			},
+		}
+
+	case meter != nil && meterCurrent != nil && meterEnergy != nil:
+		return &struct {
+			*PCElectric
+			api.Meter
+			api.MeterCurrent
+			api.MeterEnergy
+		}{
+			PCElectric: base,
+			Meter: &decoratePCEMeterImpl{
+				meter: meter,
+			},
+			MeterCurrent: &decoratePCEMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+			MeterEnergy: &decoratePCEMeterEnergyImpl{
+				meterEnergy: meterEnergy,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decoratePCEMeterImpl struct {
+	meter func() (float64, error)
+}
+
+func (impl *decoratePCEMeterImpl) CurrentPower() (float64, error) {
+	return impl.meter()
+}
+
+type decoratePCEMeterCurrentImpl struct {
+	meterCurrent func() (float64, float64, float64, error)
+}
+
+func (impl *decoratePCEMeterCurrentImpl) Currents() (float64, float64, float64, error) {
+	return impl.meterCurrent()
+}
+
+type decoratePCEMeterEnergyImpl struct {
+	meterEnergy func() (float64, error)
+}
+
+func (impl *decoratePCEMeterEnergyImpl) TotalEnergy() (float64, error) {
+	return impl.meterEnergy()
+}
diff --git a/cmd/demo.yaml b/cmd/demo.yaml
index bea85d1d..e8263425 100644
--- a/cmd/demo.yaml
+++ b/cmd/demo.yaml
@@ -1,24 +1,21 @@
 log: info
 
+interval: 3s
+
 javascript:
 - vm: shared
   script: |
     state = {
-      residualpower: 200,
-      pvpower: -3000,
-      batterypower: 200,
-      gridpower: -2000,
+      residualpower: 300,
+      batterypower: 0,
+      pvpower: 6200,
+      gridpower: 0,
       chargepower: 0,
       maxcurrent: 0,
       battery: 63, // car
     };
-    function get() {
-      console.log("state:", JSON.stringify(state));
-    }
-    function set() {
-      console.log(param+":", val);
-      console.log("state:", JSON.stringify(state));
-    }
+    function log() { console.log("state:", JSON.stringify(state)); }
+    function set() { console.log("set:", param+"="+val); }
 
 meters:
 - name: grid
@@ -26,14 +23,31 @@ meters:
   power:
     type: js
     vm: shared
-    script: state.gridpower = state.pvpower + state.chargepower + state.residualpower + state.batterypower;
+    script: |
+      state.pvpower += 200*(Math.random()-0.5);
+      state.chargepower = 0;
+      if (state.enabled) {
+        state.chargepower = state.maxcurrent * 3 * 230;
+        state.batterypower = 0;
+      }
+      state.gridpower = -state.pvpower + state.chargepower + state.residualpower + state.batterypower;
+      // if (state.gridpower < 0) {
+      //   state.batterypower = -state.gridpower;
+      //   if (state.batterypower > 1000) {
+      //     state.batterypower = 1000;
+      //   }
+      //   state.gridpower += state.batterypower;
+      // } else {
+      //   state.batterypower = 0;
+      // }
+      state.gridpower;
 
 - name: pv
   type: custom
   power:
     type: js
     vm: shared
-    script: state.pvpower += 100*Math.random();
+    script: state.pvpower
 
 - name: battery
   type: custom
@@ -62,10 +76,7 @@ chargers:
   enable:
     type: js
     vm: shared
-    script: |
-      set();
-      state.enabled = val;
-      if (state.enabled) state.chargepower = state.maxcurrent * 230; else state.chargepower = 0;
+    script: state.enabled = val;
   enabled:
     type: js
     vm: shared
@@ -79,10 +90,7 @@ chargers:
   maxcurrent:
     type: js
     vm: shared
-    script: |
-      set();
-      state.maxcurrent = val;
-      if (state.enabled) state.chargepower = state.maxcurrent * 230;
+    script: state.maxcurrent = val;
 
 vehicles:
 - name: demo
diff --git a/cmd/dump.go b/cmd/dump.go
index 080cc949..fb6dd499 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -69,7 +69,7 @@ func runDump(cmd *cobra.Command, args []string) {
 	} else {
 		for id, name := range site.Meters.BatteryMetersRef {
 			if name != "" {
-				d.DumpWithHeader(fmt.Sprintf("Battery %d: %s", id, name), cp.Meter(name))
+				d.DumpWithHeader(fmt.Sprintf("battery %d: %s", id, name), cp.Meter(name))
 			}
 		}
 	}
diff --git a/cmd/root.go b/cmd/root.go
index f1f0ecc5..4d266d38 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -226,8 +226,8 @@ func run(cmd *cobra.Command, args []string) {
 	pushChan := configureMessengers(conf.Messaging, cache)
 
 	// set channels
-	site.Prepare(valueChan, pushChan)
 	site.DumpConfig()
+	site.Prepare(valueChan, pushChan)
 
 	stopC := make(chan struct{})
 	exitC := make(chan struct{})
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 547ccfc8..a01b40ed 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -48,6 +48,9 @@ const (
 	settleDuration        = 100 * time.Millisecond
 )
 
+// elapsed is the time an expired timer will be set to
+var elapsed = time.Unix(0, 1)
+
 // PollConfig defines the vehicle polling mode and interval
 type PollConfig struct {
 	Mode     string        `mapstructure:"mode"`     // polling mode charging (default), connected, always
@@ -219,8 +222,9 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
 	lp.configureChargerType(lp.charger)
 
 	// ensure 1p setup for switchable charger (https://github.com/evcc-io/evcc/issues/1572)
-	if _, ok := lp.charger.(api.ChargePhases); ok {
-		lp.setPhases(1)
+	if _, ok := lp.charger.(api.ChargePhases); ok && lp.Phases != 0 {
+		lp.log.WARN.Printf("ignoring phases config (%dp) for switchable charger", lp.Phases)
+		lp.setPhases(0)
 	}
 
 	// allow target charge handler to access loadpoint
@@ -349,7 +353,10 @@ func (lp *LoadPoint) evChargeStopHandler() {
 	lp.socUpdated = time.Time{}
 
 	// reset pv enable/disable timer
-	lp.resetPVTimerIfRunning()
+	// https://github.com/evcc-io/evcc/issues/2289
+	if !lp.pvTimer.Equal(elapsed) {
+		lp.resetPVTimerIfRunning()
+	}
 }
 
 // evVehicleConnectHandler sends external start event
@@ -550,9 +557,10 @@ func (lp *LoadPoint) syncCharger() {
 }
 
 // setLimit applies charger current limits and enables/disables accordingly
-func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) {
+func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) error {
 	// set current
 	if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.GetMinCurrent() {
+		var err error
 		if charger, ok := lp.charger.(api.ChargerEx); ok {
 			err = charger.MaxCurrentMillis(chargeCurrent)
 		} else {
@@ -560,17 +568,17 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) {
 			err = lp.charger.MaxCurrent(int64(chargeCurrent))
 		}
 
-		if err == nil {
-			lp.log.DEBUG.Printf("max charge current: %.3gA", chargeCurrent)
-			lp.chargeCurrent = chargeCurrent
-			lp.bus.Publish(evChargeCurrent, chargeCurrent)
-		} else {
-			err = fmt.Errorf("max charge current %.3g: %w", chargeCurrent, err)
+		if err != nil {
+			return fmt.Errorf("max charge current %.3gA: %w", chargeCurrent, err)
 		}
+
+		lp.log.DEBUG.Printf("max charge current: %.3gA", chargeCurrent)
+		lp.chargeCurrent = chargeCurrent
+		lp.bus.Publish(evChargeCurrent, chargeCurrent)
 	}
 
 	// set enabled/disabled
-	if enabled := chargeCurrent >= lp.GetMinCurrent(); enabled != lp.enabled && err == nil {
+	if enabled := chargeCurrent >= lp.GetMinCurrent(); enabled != lp.enabled {
 		if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 && !force {
 			lp.log.DEBUG.Printf("charger %s: contactor delay %v", status[enabled], remaining)
 			return nil
@@ -585,27 +593,27 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) {
 		// 	}
 		// }
 
-		if err = lp.charger.Enable(enabled); err == nil {
-			lp.log.DEBUG.Printf("charger %s", status[enabled])
-			lp.enabled = enabled
-			lp.guardUpdated = lp.clock.Now()
+		if err := lp.charger.Enable(enabled); err != nil {
+			return fmt.Errorf("charger %s: %w", status[enabled], err)
+		}
 
-			lp.bus.Publish(evChargeCurrent, chargeCurrent)
+		lp.log.DEBUG.Printf("charger %s", status[enabled])
+		lp.enabled = enabled
+		lp.guardUpdated = lp.clock.Now()
 
-			// wake up vehicle
-			// TODO https://github.com/evcc-io/evcc/discussions/1929
-			// if car, ok := lp.vehicle.(api.VehicleStartCharge); enabled && ok {
-			// 	// log but don't propagate
-			// 	if err := car.StartCharge(); err != nil {
-			// 		lp.log.ERROR.Printf("vehicle remote charge start: %v", err)
-			// 	}
-			// }
-		} else {
-			err = fmt.Errorf("charger %s: %w", status[enabled], err)
-		}
+		lp.bus.Publish(evChargeCurrent, chargeCurrent)
+
+		// wake up vehicle
+		// TODO https://github.com/evcc-io/evcc/discussions/1929
+		// if car, ok := lp.vehicle.(api.VehicleStartCharge); enabled && ok {
+		// 	// log but don't propagate
+		// 	if err := car.StartCharge(); err != nil {
+		// 		lp.log.ERROR.Printf("vehicle remote charge start: %v", err)
+		// 	}
+		// }
 	}
 
-	return err
+	return nil
 }
 
 // connected returns the EVs connection state
@@ -765,6 +773,8 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
 		lp.publish("vehicleCapacity", lp.vehicle.Capacity())
 
 		lp.applyAction(vehicle.OnIdentified())
+
+		lp.setVehiclePhases()
 	} else {
 		lp.socEstimator = nil
 
@@ -877,8 +887,11 @@ func (lp *LoadPoint) effectiveCurrent() float64 {
 
 // elapsePVTimer puts the pv enable/disable timer into elapsed state
 func (lp *LoadPoint) elapsePVTimer() {
-	lp.pvTimer = lp.clock.Now().Add(-lp.Disable.Delay)
-	lp.guardUpdated = lp.clock.Now().Add(-lp.GuardDuration)
+	lp.log.DEBUG.Printf("pv timer elapse")
+
+	lp.pvTimer = elapsed
+	lp.guardUpdated = elapsed
+
 	lp.publishTimer(pvTimer, 0, timerInactive)
 }
 
@@ -898,6 +911,25 @@ func (lp *LoadPoint) resetPVTimerIfRunning(typ ...string) {
 	lp.publishTimer(pvTimer, 0, timerInactive)
 }
 
+// setVehiclePhases sets the expected active phases by the vehicle
+func (lp *LoadPoint) setVehiclePhases() {
+	if v, ok := lp.vehicle.(api.VehiclePhases); ok {
+		if phases := v.Phases(); phases > 0 {
+			lp.log.DEBUG.Printf("vehicle phases: %dp", phases)
+
+			lp.Lock()
+			defer lp.Unlock()
+
+			if phases > lp.Phases {
+				phases = lp.Phases
+			}
+
+			lp.activePhases = phases
+			lp.publish("activePhases", lp.activePhases)
+		}
+	}
+}
+
 // scalePhasesIfAvailable scales if api.ChargePhases is available
 func (lp *LoadPoint) scalePhasesIfAvailable(phases int) error {
 	err := lp.scalePhases(phases)
@@ -916,12 +948,16 @@ func (lp *LoadPoint) setPhases(phases int) {
 		lp.Phases = phases
 		lp.publish("phases", lp.Phases)
 
-		// When scaling down, charger will temporarily disable. During this time, activePhases will not be updated
-		// since all currents are zero. This will lead to inconsistent state when scaling is triggered again
-		// (1p configured vs 3p active). Update activePhases to reflect the current state of the charger.
 		if phases < lp.activePhases {
+			// When scaling down, charger will temporarily disable. During this time, activePhases will not be updated
+			// since all currents are zero. This will lead to inconsistent state when scaling is triggered again
+			// (1p configured vs 3p active). Update activePhases to reflect the current state of the charger.
 			lp.activePhases = phases
 			lp.publish("activePhases", lp.activePhases)
+		} else {
+			// When scaling up, charger will offer more phases than vehicle can use.
+			// Adjust to enable subsequent PV restart at lower powers than full 3p.
+			lp.setVehiclePhases()
 		}
 	}
 }
@@ -964,21 +1000,18 @@ func (lp *LoadPoint) scalePhases(phases int) error {
 
 // pvScalePhases switches phases if necessary and returns if switch occurred
 func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float64) bool {
-	// correct charger state inconsistency (https://github.com/evcc-io/evcc/issues/1572)
 	phases := lp.GetPhases()
 
-	// if more active phases observed than configured, update internal state accordingly
+	// observed phase state inconsistency (https://github.com/evcc-io/evcc/issues/1572, https://github.com/evcc-io/evcc/issues/2230)
 	if phases < lp.activePhases {
-		lp.log.WARN.Printf("inconsistent phases: %dp configured < %dp observed active, updating internal state to 3p", phases, lp.activePhases)
-		phases = 3
-		lp.setPhases(3)
+		lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, lp.activePhases)
 	}
 
 	var waiting bool
 	targetCurrent := availablePower / Voltage / float64(lp.activePhases)
 
 	// scale down phases
-	if targetCurrent < minCurrent && phases > 1 && lp.activePhases > 1 {
+	if targetCurrent < minCurrent && (phases == 0 || phases == 3) && lp.activePhases > 1 {
 		lp.log.DEBUG.Printf("available power below %dp min threshold of %.0fW", lp.activePhases, float64(lp.activePhases)*Voltage*minCurrent)
 
 		if lp.phaseTimer.IsZero() {
@@ -1004,7 +1037,7 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
 	}
 
 	// scale up phases
-	if min3pCurrent := powerToCurrent(availablePower, 3); min3pCurrent >= minCurrent && phases == 1 {
+	if min3pCurrent := powerToCurrent(availablePower, 3); min3pCurrent >= minCurrent && (phases == 0 || phases == 1) {
 		lp.log.DEBUG.Printf("available power above 3p min threshold of %.0fW", 3*Voltage*minCurrent)
 
 		if lp.phaseTimer.IsZero() {
diff --git a/core/site.go b/core/site.go
index f2158faa..4228e7e1 100644
--- a/core/site.go
+++ b/core/site.go
@@ -149,7 +149,7 @@ func meterCapabilities(name string, meter interface{}) string {
 	_, currents := meter.(api.MeterCurrent)
 
 	name += ":"
-	return fmt.Sprintf("    %-8s power %s energy %s currents %s",
+	return fmt.Sprintf("    %-10s power %s energy %s currents %s",
 		name,
 		presence[power],
 		presence[energy],
@@ -159,28 +159,23 @@ func meterCapabilities(name string, meter interface{}) string {
 
 // DumpConfig site configuration
 func (site *Site) DumpConfig() {
-	site.publish("siteTitle", site.Title)
-
 	site.log.INFO.Println("site config:")
-	site.log.INFO.Printf("  meters:    grid %s pv %s battery %s",
+	site.log.INFO.Printf("  meters:      grid %s pv %s battery %s",
 		presence[site.gridMeter != nil],
 		presence[len(site.pvMeters) > 0],
 		presence[len(site.batteryMeters) > 0],
 	)
 
-	site.publish("gridConfigured", site.gridMeter != nil)
 	if site.gridMeter != nil {
 		site.log.INFO.Println(meterCapabilities("grid", site.gridMeter))
 	}
 
-	site.publish("pvConfigured", len(site.pvMeters) > 0)
 	if len(site.pvMeters) > 0 {
 		for i, pv := range site.pvMeters {
 			site.log.INFO.Println(meterCapabilities(fmt.Sprintf("pv %d", i), pv))
 		}
 	}
 
-	site.publish("batteryConfigured", len(site.batteryMeters) > 0)
 	if len(site.batteryMeters) > 0 {
 		for i, battery := range site.batteryMeters {
 			_, ok := battery.(api.Battery)
@@ -188,45 +183,40 @@ func (site *Site) DumpConfig() {
 				meterCapabilities(fmt.Sprintf("battery %d", i), battery),
 				fmt.Sprintf("soc %s", presence[ok]),
 			)
-
-			if ok {
-				site.publish("prioritySoC", site.PrioritySoC)
-			}
 		}
 	}
 
 	for i, lp := range site.loadpoints {
 		lp.log.INFO.Printf("loadpoint %d:", i+1)
-
-		lp.log.INFO.Printf("  mode:      %s", lp.GetMode())
+		lp.log.INFO.Printf("  mode:        %s", lp.GetMode())
 
 		_, power := lp.charger.(api.Meter)
 		_, energy := lp.charger.(api.MeterEnergy)
 		_, currents := lp.charger.(api.MeterCurrent)
-		_, timer := lp.charger.(api.ChargeTimer)
+		_, phases := lp.charger.(api.ChargePhases)
 
-		lp.log.INFO.Printf("  charger:   power %s energy %s currents %s timer %s",
+		lp.log.INFO.Printf("  charger:     power %s energy %s currents %s phases %s",
 			presence[power],
 			presence[energy],
 			presence[currents],
-			presence[timer],
+			presence[phases],
 		)
 
-		lp.log.INFO.Printf("  meters:    charge %s", presence[lp.HasChargeMeter()])
+		lp.log.INFO.Printf("  meters:      charge %s", presence[lp.HasChargeMeter()])
 
 		lp.publish("chargeConfigured", lp.HasChargeMeter())
 		if lp.HasChargeMeter() {
 			lp.log.INFO.Printf(meterCapabilities("charge", lp.chargeMeter))
 		}
 
-		lp.log.INFO.Printf("  vehicles:  %s", presence[len(lp.vehicles) > 0])
+		lp.log.INFO.Printf("  vehicles:    %s", presence[len(lp.vehicles) > 0])
 
 		for i, v := range lp.vehicles {
 			_, rng := v.(api.VehicleRange)
 			_, finish := v.(api.VehicleFinishTimer)
 			_, status := v.(api.ChargeState)
 			_, climate := v.(api.VehicleClimater)
-			lp.log.INFO.Printf("    car %d:   range %s finish %s status %s climate %s",
+			lp.log.INFO.Printf("    vehicle %d: range %s finish %s status %s climate %s",
 				i, presence[rng], presence[finish], presence[status], presence[climate],
 			)
 		}
@@ -278,6 +268,8 @@ func (site *Site) updateMeters() error {
 		return err
 	}
 
+	err := retryMeter("grid", site.gridMeter, &site.gridPower)
+
 	if len(site.pvMeters) > 0 {
 		site.pvPower = 0
 
@@ -300,8 +292,6 @@ func (site *Site) updateMeters() error {
 		site.publish("pvPower", site.pvPower)
 	}
 
-	err := retryMeter("grid", site.gridMeter, &site.gridPower)
-
 	if len(site.batteryMeters) > 0 {
 		site.batteryPower = 0
 
@@ -431,6 +421,13 @@ func (site *Site) update(lp Updater) {
 
 // prepare publishes initial values
 func (site *Site) prepare() {
+	site.publish("siteTitle", site.Title)
+
+	site.publish("gridConfigured", site.gridMeter != nil)
+	site.publish("pvConfigured", len(site.pvMeters) > 0)
+	site.publish("batteryConfigured", len(site.batteryMeters) > 0)
+	site.publish("prioritySoC", site.PrioritySoC)
+
 	site.publish("currency", site.tariffs.Currency.String())
 	site.publish("savingsSince", site.savings.Since().Unix())
 }
diff --git a/mock/mock_api.go b/mock/mock_api.go
index 60e518c1..6ca0847f 100644
--- a/mock/mock_api.go
+++ b/mock/mock_api.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery)
+// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,VehiclePhases,ChargeRater,Battery)
 
 // Package mock is a generated GoMock package.
 package mock
@@ -375,6 +375,43 @@ func (mr *MockVehicleMockRecorder) Title() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Title", reflect.TypeOf((*MockVehicle)(nil).Title))
 }
 
+// MockVehiclePhases is a mock of VehiclePhases interface.
+type MockVehiclePhases struct {
+	ctrl     *gomock.Controller
+	recorder *MockVehiclePhasesMockRecorder
+}
+
+// MockVehiclePhasesMockRecorder is the mock recorder for MockVehiclePhases.
+type MockVehiclePhasesMockRecorder struct {
+	mock *MockVehiclePhases
+}
+
+// NewMockVehiclePhases creates a new mock instance.
+func NewMockVehiclePhases(ctrl *gomock.Controller) *MockVehiclePhases {
+	mock := &MockVehiclePhases{ctrl: ctrl}
+	mock.recorder = &MockVehiclePhasesMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockVehiclePhases) EXPECT() *MockVehiclePhasesMockRecorder {
+	return m.recorder
+}
+
+// Phases mocks base method.
+func (m *MockVehiclePhases) Phases() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Phases")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// Phases indicates an expected call of Phases.
+func (mr *MockVehiclePhasesMockRecorder) Phases() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases", reflect.TypeOf((*MockVehiclePhases)(nil).Phases))
+}
+
 // MockChargeRater is a mock of ChargeRater interface.
 type MockChargeRater struct {
 	ctrl     *gomock.Controller
diff --git a/server/mqtt.go b/server/mqtt.go
index 7669f75d..5126cea4 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -104,8 +104,7 @@ func (m *MQTT) Run(site site.API, in <-chan util.Param) {
 
 	// site setters
 	m.Handler.ListenSetter(fmt.Sprintf("%s/site/prioritySoC/set", m.root), func(payload string) {
-		soc, err := strconv.Atoi(payload)
-		if err == nil {
+		if soc, err := strconv.Atoi(payload); err == nil {
 			_ = site.SetPrioritySoC(float64(soc))
 		}
 	})
diff --git a/vehicle/embed.go b/vehicle/embed.go
index bfc878eb..71f9d2a1 100644
--- a/vehicle/embed.go
+++ b/vehicle/embed.go
@@ -5,6 +5,7 @@ import "github.com/evcc-io/evcc/api"
 type embed struct {
 	Title_       string           `mapstructure:"title"`
 	Capacity_    int64            `mapstructure:"capacity"`
+	Phases_      int              `mapstructure:"phases"`
 	Identifiers_ []string         `mapstructure:"identifiers"`
 	OnIdentify   api.ActionConfig `mapstructure:"onIdentify"`
 }
@@ -19,6 +20,11 @@ func (v *embed) Capacity() int64 {
 	return v.Capacity_
 }
 
+// Phases returns the phases used by the vehicle
+func (v *embed) Phases() int {
+	return v.Phases_
+}
+
 // Identifiers implements the api.Identifier interface
 func (v *embed) Identifiers() []string {
 	return v.Identifiers_
diff --git a/vehicle/ovms.go b/vehicle/ovms.go
index 969fecd1..3f574e32 100644
--- a/vehicle/ovms.go
+++ b/vehicle/ovms.go
@@ -15,16 +15,16 @@ import (
 )
 
 type ovmsStatusResponse struct {
-	Odometer string `json:"odometer"`
+	Odometer float64 `json:"odometer,string"`
 }
 
 type ovmsChargeResponse struct {
-	ChargeEtrFull    string `json:"charge_etr_full"`
-	ChargeState      string `json:"chargestate"`
-	ChargePortOpen   int    `json:"cp_dooropen"`
-	EstimatedRange   string `json:"estimatedrange"`
-	MessageAgeServer int    `json:"m_msgage_s"`
-	Soc              string `json:"soc"`
+	ChargeEtrFull    int64   `json:"charge_etr_full,string"`
+	ChargeState      string  `json:"chargestate"`
+	ChargePortOpen   int     `json:"cp_dooropen"`
+	EstimatedRange   string  `json:"estimatedrange"`
+	MessageAgeServer int     `json:"m_msgage_s"`
+	Soc              float64 `json:"soc,string"`
 }
 
 type ovmsConnectResponse struct {
@@ -167,7 +167,7 @@ func (v *Ovms) SoC() (float64, error) {
 	res, err := v.chargeG()
 
 	if res, ok := res.(ovmsChargeResponse); err == nil && ok {
-		return strconv.ParseFloat(res.Soc, 64)
+		return res.Soc, nil
 	}
 
 	return 0, err
@@ -212,10 +212,7 @@ func (v *Ovms) Odometer() (float64, error) {
 	res, err := v.statusG()
 
 	if res, ok := res.(ovmsStatusResponse); err == nil && ok {
-		odometer, err := strconv.ParseFloat(res.Odometer, 64)
-		if err == nil {
-			return odometer / 10, nil
-		}
+		return res.Odometer / 10, nil
 	}
 
 	return 0, err
@@ -228,10 +225,7 @@ func (v *Ovms) FinishTime() (time.Time, error) {
 	res, err := v.chargeG()
 
 	if res, ok := res.(ovmsChargeResponse); err == nil && ok {
-		cef, err := strconv.ParseInt(res.ChargeEtrFull, 0, 64)
-		if err == nil {
-			return time.Now().Add(time.Duration(cef) * time.Minute), nil
-		}
+		return time.Now().Add(time.Duration(res.ChargeEtrFull) * time.Minute), nil
 	}
 
 	return time.Time{}, err


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index d64e9198..1928081e 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -882,15 +882,15 @@ func TestScalePhases(t *testing.T) {
 			lp.phaseTimer = lp.clock.Now().Add(-dt)
 		}},
 
-		// error states from 1p/3p misconfig
-		{"1/3->1, enough power", 1, 3, 1 * Voltage * maxA, 3, false, nil},
-		{"1/3->1, kickoff, correct phase setting", 1, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
+		// error states from 1p/3p misconfig - no correction for time being (stay at 1p)
+		{"1/3->1, enough power", 1, 3, 1 * Voltage * maxA, 1, false, nil},
+		{"1/3->1, kickoff, correct phase setting", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
 			lp.phaseTimer = time.Time{}
 		}},
-		{"1/3->1, timer running, correct phase setting", 1, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
+		{"1/3->1, timer running, correct phase setting", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
 			lp.phaseTimer = lp.clock.Now()
 		}},
-		{"1/3->1, switch executed", 1, 3, 1 * Voltage * maxA, 1, true, func(lp *LoadPoint) {
+		{"1/3->1, switch not executed", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
 			lp.phaseTimer = lp.clock.Now().Add(-dt)
 		}},
 	}
@@ -932,3 +932,50 @@ func TestScalePhases(t *testing.T) {
 		}
 	}
 }
+
+func TestVehiclePhases(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	vehicle := &struct {
+		*mock.MockVehicle
+		*mock.MockVehiclePhases
+	}{
+		mock.NewMockVehicle(ctrl),
+		mock.NewMockVehiclePhases(ctrl),
+	}
+
+	tc := []struct {
+		phases, activePhases, vehiclePhases int
+		res                                 int
+	}{
+		{1, 1, 0, 1}, // leave as-is
+		{3, 1, 0, 1}, // leave as-is
+		{3, 3, 0, 3}, // leave as-is
+		{1, 1, 1, 1}, // leave as-is
+		{3, 1, 1, 1}, // leave as-is
+		{3, 3, 1, 1}, // limit to 1p
+		{1, 1, 2, 1}, // leave as-is
+		{3, 1, 2, 2}, // limit to 2p
+		{3, 3, 2, 2}, // limit to 2p
+		{1, 1, 3, 1}, // leave as-is
+		{3, 1, 3, 3}, // limit to 3p
+		{3, 3, 3, 3}, // leave as-is
+	}
+
+	for _, tc := range tc {
+		t.Logf("%+v", tc)
+
+		lp := &LoadPoint{
+			log:          util.NewLogger("foo"),
+			vehicle:      vehicle,
+			Phases:       tc.phases,
+			activePhases: tc.activePhases,
+		}
+
+		vehicle.MockVehiclePhases.EXPECT().Phases().Return(tc.vehiclePhases)
+
+		lp.setVehiclePhases()
+		if lp.activePhases != tc.res {
+			t.Errorf("expected %v, got %v", tc.res, lp.activePhases)
+		}
+	}
+}