Repository: andig/evcc
Pull request #1213 (2021-07-13 09:53:33)

Add 1p3p support
This PR brings 1p3p support, i.e. phase switching, to evcc. The PR contains:

- support for Easee and OpenWB (enable using `phases: true`)
- mqtt and rest api

The `custom` charger is currently not supported as to not run any risk of damaging vehicles by home-grown hardware.

andig: /rebase
andig: /rebase
simonstamm: @andig Just tested it but unfortunately I get the following error with `circuit` and without that in the configuration:

```[main  ] FATAL 2021/08/17 12:38:45 cannot create charger 'charger': cannot create charger 'easee': cannot determine circuit id, found: [{121841 126057 1 1 16 false 0} {121847 126057 2 2 16 false 0}]

[main  ] FATAL 2021/08/17 12:40:52 cannot create charger 'charger': cannot create charger 'easee': 1 error(s) decoding:```

Looks like the changes from #1295 has not been rebased into that branch here.
andig: /rebase
simonstamm: @andig I have the feeling that another /rebase will not solve the merge conflicts. :-)
andig: squash + rebase did ;)
simonstamm: Building for armv7 does not work (amd64 works):

```
 > [node  6/10] RUN make install-ui:
#27 0.349 npm ci
[...]
#27 204.4 > deasync@0.1.21 install /build/node_modules/deasync
#27 204.4 > node ./build.js
#27 204.4
#27 207.3 gyp ERR! find Python
#27 207.3 gyp ERR! find Python Python is not set from command line or npm configuration
#27 207.3 gyp ERR! find Python Python is not set from environment variable PYTHON
#27 207.3 gyp ERR! find Python checking if "python" can be used
#27 207.3 gyp ERR! find Python - "python" is not in PATH or produced an error
#27 207.3 gyp ERR! find Python checking if "python2" can be used
#27 207.3 gyp ERR! find Python - "python2" is not in PATH or produced an error
#27 207.3 gyp ERR! find Python checking if "python3" can be used
#27 207.3 gyp ERR! find Python - "python3" is not in PATH or produced an error
#27 207.3 gyp ERR! find Python
#27 207.3 gyp ERR! find Python **********************************************************
#27 207.3 gyp ERR! find Python You need to install the latest version of Python.
#27 207.3 gyp ERR! find Python Node-gyp should be able to find and use Python. If not,
#27 207.3 gyp ERR! find Python you can try one of the following options:
#27 207.3 gyp ERR! find Python - Use the switch --python="/path/to/pythonexecutable"
#27 207.3 gyp ERR! find Python   (accepted by both node-gyp and npm)
#27 207.3 gyp ERR! find Python - Set the environment variable PYTHON
#27 207.3 gyp ERR! find Python - Set the npm configuration variable python:
#27 207.3 gyp ERR! find Python   npm config set python "/path/to/pythonexecutable"
#27 207.3 gyp ERR! find Python For more information consult the documentation at:
#27 207.3 gyp ERR! find Python https://github.com/nodejs/node-gyp#installation
#27 207.3 gyp ERR! find Python **********************************************************
#27 207.3 gyp ERR! find Python
#27 207.3 gyp ERR! configure error
#27 207.3 gyp ERR! stack Error: Could not find any Python installation to use
#27 207.3 gyp ERR! stack     at PythonFinder.fail (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:307:47)
#27 207.3 gyp ERR! stack     at PythonFinder.runChecks (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:136:21)
#27 207.3 gyp ERR! stack     at PythonFinder.<anonymous> (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:179:16)
#27 207.3 gyp ERR! stack     at PythonFinder.execFileCallback (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:271:16)
#27 207.3 gyp ERR! stack     at exithandler (child_process.js:397:5)
#27 207.3 gyp ERR! stack     at ChildProcess.errorhandler (child_process.js:409:5)
#27 207.4 gyp ERR! stack     at ChildProcess.emit (events.js:400:28)
#27 207.4 gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)
#27 207.4 gyp ERR! stack     at onErrorNT (internal/child_process.js:467:16)
#27 207.4 gyp ERR! stack     at processTicksAndRejections (internal/process/task_queues.js:82:21)
#27 207.4 gyp ERR! System Linux 5.10.25-linuxkit
#27 207.4 gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
#27 207.4 gyp ERR! cwd /build/node_modules/deasync
#27 207.4 gyp ERR! node -v v14.17.5
#27 207.4 gyp ERR! node-gyp -v v5.1.0
#27 207.4 gyp ERR! not ok
#27 207.4 Build failed
#27 207.5 npm ERR! code ELIFECYCLE
#27 207.5 npm ERR! errno 1
#27 207.6 npm ERR! deasync@0.1.21 install: `node ./build.js`
#27 207.6 npm ERR! Exit status 1
#27 207.6 npm ERR!
#27 207.6 npm ERR! Failed at the deasync@0.1.21 install script.
#27 207.6 npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
#27 207.8
#27 207.8 npm ERR! A complete log of this run can be found in:
#27 207.8 npm ERR!     /root/.npm/_logs/2021-08-17T17_16_18_723Z-debug.log
#27 207.9 make: *** [Makefile:36: install-ui] Error 1
------
error: failed to solve: rpc error: code = Unknown desc = executor failed running [/bin/sh -c make install-ui]: exit code: 2
```
andig: Readme gelesen?
simonstamm: Ohje, jetzt übersehe ich was offensichtliches... :-)
andig:     make build
simonstamm: Ja, so hab ichs letztes Mal auch getestet. Mich wunderte nur, dass ein:

```
docker buildx build --platform linux/amd64 --tag evcc:1p3pv2 .
```

funktioniert, aber ein

```
docker buildx build --platform linux/arm/v7 --tag evcc:1p3pv2 .
```

mit dem oben genannten Fehler abbricht. Zum eigentlichen: Ggf. fehlt mir nun eine Konfiguration, aber beim loadpoint ist der `pvTimer` ja private, sodass ich jetzt auf dem 1. Blick nicht sehe, ob ich da was zusätzlich konfigurieren muss. Der pvScalePhases timer fehlt:

```
[lp-1  ] INFO 2021/08/18 07:07:48 vehicle updated: unknown -> Tesla Model S
[lp-1  ] INFO 2021/08/18 07:07:48 loadpoint 1:
[lp-1  ] INFO 2021/08/18 07:07:48   mode:      minpv
[lp-1  ] INFO 2021/08/18 07:07:48   charger:   power ✓ energy ✗ currents ✓ timer ✗
[lp-1  ] INFO 2021/08/18 07:07:48   meters:    charge ✓
[lp-1  ] INFO 2021/08/18 07:07:48     charge:  power ✓ energy ✗ currents ✗
[lp-1  ] INFO 2021/08/18 07:07:48   vehicles:  ✓
[lp-1  ] INFO 2021/08/18 07:07:48     car 0:   range ✓ finish ✓ status ✓ climate ✗
[lp-1  ] DEBUG 2021/08/18 07:07:48 charge power: 0W
[lp-1  ] DEBUG 2021/08/18 07:07:48 charger status: B
[lp-1  ] DEBUG 2021/08/18 07:07:49 vehicle soc: 57%
[lp-1  ] DEBUG 2021/08/18 07:07:49 vehicle range: 253km
[lp-1  ] DEBUG 2021/08/18 07:07:49 max charge current: 0.0A = 0.0A + -1.3A (300W @ 1p)
[lp-1  ] DEBUG 2021/08/18 07:07:49 !!pvScalePhases available power -300 for target current -1.3 @ 1p/1p
[lp-1  ] DEBUG 2021/08/18 07:07:49 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/18 07:07:49 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/18 07:07:49 max charge current: 6
[lp-1  ] DEBUG 2021/08/18 07:07:49 charger enable
[lp-1  ] ERROR 2021/08/18 07:07:49 vehicle remote charge start: charging
[lp-1  ] DEBUG 2021/08/18 07:07:58 charge power: 53W
[lp-1  ] DEBUG 2021/08/18 07:07:58 charger status: C
[lp-1  ] INFO 2021/08/18 07:07:58 start charging ->
[lp-1  ] DEBUG 2021/08/18 07:07:58 vehicle soc: 57%
[lp-1  ] DEBUG 2021/08/18 07:07:58 vehicle range: 253km
[lp-1  ] DEBUG 2021/08/18 07:07:58 max charge current: 4.6A = 6.0A + -1.4A (325W @ 1p)
[lp-1  ] DEBUG 2021/08/18 07:07:58 !!pvScalePhases available power -272 for target current -1.2 @ 1p/1p
[lp-1  ] DEBUG 2021/08/18 07:07:58 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/18 07:07:58 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/18 07:08:08 charge power: 400W
[lp-1  ] DEBUG 2021/08/18 07:08:08 charger status: C
[lp-1  ] DEBUG 2021/08/18 07:08:08 vehicle soc: 57%
[lp-1  ] DEBUG 2021/08/18 07:08:08 vehicle range: 253km
[lp-1  ] DEBUG 2021/08/18 07:08:08 max charge current: 0.0A = 6.0A + -7.4A (1693W @ 1p)
[lp-1  ] DEBUG 2021/08/18 07:08:08 !!pvScalePhases available power -1293 for target current -5.6 @ 1p/1p
[lp-1  ] DEBUG 2021/08/18 07:08:08 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/18 07:08:08 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/18 07:08:18 charge power: 3494W
[lp-1  ] DEBUG 2021/08/18 07:08:18 detected phases: 3p (6.0A @ 3494W)
[lp-1  ] DEBUG 2021/08/18 07:08:18 charger status: C
[lp-1  ] DEBUG 2021/08/18 07:08:18 vehicle soc: 57%
[lp-1  ] DEBUG 2021/08/18 07:08:18 vehicle range: 253km
[lp-1  ] DEBUG 2021/08/18 07:08:18 max charge current: 0.5A = 6.0A + -5.5A (3762W @ 3p)
[lp-1  ] DEBUG 2021/08/18 07:08:18 !!pvScalePhases available power -268 for target current -0.4 @ 3p/1p
[lp-1  ] DEBUG 2021/08/18 07:08:18 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/18 07:08:18 pv max charge current: 6A
```
andig: @simonstamm läuft der PR jetzt bei Dir?
andig: /rebase
simonstamm: @andig Leider noch nicht. Er startet zwar mit einer Phase, switched dann aber wieder auf die drei Phasen trotz fehlendes PV-Überschusses:

```
[lp-1  ] INFO 2021/08/19 07:31:25 vehicle updated: unknown -> Tesla Model S
[lp-1  ] INFO 2021/08/19 07:31:25 loadpoint 1:
[lp-1  ] INFO 2021/08/19 07:31:25   mode:      minpv
[lp-1  ] INFO 2021/08/19 07:31:25   charger:   power ✓ energy ✗ currents ✓ timer ✗
[lp-1  ] INFO 2021/08/19 07:31:25   meters:    charge ✓
[lp-1  ] INFO 2021/08/19 07:31:25     charge:  power ✓ energy ✗ currents ✗
[lp-1  ] INFO 2021/08/19 07:31:25   vehicles:  ✓
[lp-1  ] INFO 2021/08/19 07:31:25     car 0:   range ✓ finish ✓ status ✓ climate ✗
[lp-1  ] DEBUG 2021/08/19 07:31:25 charge power: 0W
[lp-1  ] DEBUG 2021/08/19 07:31:25 charger status: B
[lp-1  ] WARN 2021/08/19 07:31:25 updating soc failed: 408 Request Timeout
[lp-1  ] ERROR 2021/08/19 07:31:25 vehicle: 408 Request Timeout
[lp-1  ] DEBUG 2021/08/19 07:31:25 max charge current: 0.0A = 0.0A + -1.2A (284W @ 1p)
[lp-1  ] DEBUG 2021/08/19 07:31:25 !!pvScalePhases available power -284 for target current -1.2 @ 1p/1p
[lp-1  ] DEBUG 2021/08/19 07:31:25 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 07:31:25 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 07:31:26 max charge current: 6
[lp-1  ] DEBUG 2021/08/19 07:31:26 charger enable
[lp-1  ] ERROR 2021/08/19 07:31:34 vehicle remote charge start: charging
[lp-1  ] DEBUG 2021/08/19 07:31:35 charge power: 0W
[lp-1  ] DEBUG 2021/08/19 07:31:35 charger status: C
[lp-1  ] INFO 2021/08/19 07:31:35 start charging ->
[lp-1  ] WARN 2021/08/19 07:31:35 updating soc failed: 408 Request Timeout
[lp-1  ] ERROR 2021/08/19 07:31:35 vehicle: 408 Request Timeout
[lp-1  ] DEBUG 2021/08/19 07:31:35 max charge current: 4.7A = 6.0A + -1.3A (296W @ 1p)
[lp-1  ] DEBUG 2021/08/19 07:31:35 !!pvScalePhases available power -296 for target current -1.3 @ 1p/1p
[lp-1  ] DEBUG 2021/08/19 07:31:35 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 07:31:35 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 07:31:45 charge power: 411W
[lp-1  ] DEBUG 2021/08/19 07:31:45 charger status: C
[lp-1  ] WARN 2021/08/19 07:31:45 updating soc failed: 408 Request Timeout
[lp-1  ] ERROR 2021/08/19 07:31:45 vehicle: 408 Request Timeout
[lp-1  ] DEBUG 2021/08/19 07:31:45 max charge current: 3.6A = 6.0A + -2.4A (563W @ 1p)
[lp-1  ] DEBUG 2021/08/19 07:31:45 !!pvScalePhases available power -152 for target current -0.7 @ 1p/1p
[lp-1  ] DEBUG 2021/08/19 07:31:45 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 07:31:45 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 07:31:55 charge power: 1714W
[lp-1  ] DEBUG 2021/08/19 07:31:55 detected phases: 1p (6.0A @ 1714W)
[lp-1  ] DEBUG 2021/08/19 07:31:55 charger status: C
[lp-1  ] WARN 2021/08/19 07:31:55 updating soc failed: 408 Request Timeout
[lp-1  ] ERROR 2021/08/19 07:31:55 vehicle: 408 Request Timeout
[lp-1  ] DEBUG 2021/08/19 07:31:55 max charge current: 0.0A = 6.0A + -13.6A (3123W @ 1p)
[lp-1  ] DEBUG 2021/08/19 07:31:55 !!pvScalePhases available power -1409 for target current -6.1 @ 1p/1p
[lp-1  ] DEBUG 2021/08/19 07:31:55 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 07:31:55 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 07:32:05 charge power: 3518W
[lp-1  ] DEBUG 2021/08/19 07:32:05 detected phases: 3p (6.0A @ 3518W)
[lp-1  ] DEBUG 2021/08/19 07:32:05 charger status: C
[lp-1  ] WARN 2021/08/19 07:32:05 updating soc failed: 408 Request Timeout
[lp-1  ] ERROR 2021/08/19 07:32:05 vehicle: 408 Request Timeout
[lp-1  ] DEBUG 2021/08/19 07:32:05 max charge current: 0.5A = 6.0A + -5.5A (3814W @ 3p)
[lp-1  ] DEBUG 2021/08/19 07:32:05 !!pvScalePhases available power -296 for target current -0.4 @ 3p/1p
[lp-1  ] DEBUG 2021/08/19 07:32:05 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 07:32:05 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 07:32:15 charge power: 3518W
[lp-1  ] DEBUG 2021/08/19 07:32:15 detected phases: 3p (6.0A @ 3518W)
[lp-1  ] DEBUG 2021/08/19 07:32:15 charger status: C
[lp-1  ] WARN 2021/08/19 07:32:15 updating soc failed: 408 Request Timeout
[lp-1  ] ERROR 2021/08/19 07:32:15 vehicle: 408 Request Timeout
[lp-1  ] DEBUG 2021/08/19 07:32:15 max charge current: 0.5A = 6.0A + -5.5A (3808W @ 3p)
[lp-1  ] DEBUG 2021/08/19 07:32:15 !!pvScalePhases available power -290 for target current -0.4 @ 3p/1p
[lp-1  ] DEBUG 2021/08/19 07:32:15 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 07:32:15 pv max charge current: 6A
```
andig: @simonstamm das Logfile sieht leider unvollständig aus, insbesondere fehlt das ein/ausschalten völlig- ist das wirklich alles?
andig: @simonstamm was war egtl. Mit Deinem Auto los? Sieht aus wie ein Tesla der überhaupt nicht mehr aufwachen wollte obwohl er an der Ladestation hängt und lädt? Oder kein Empfang beim Auto? Ist etwas seltsam.
simonstamm: @andig Das ist aktuell ein total nerviger Bug mit dem MCU2-Retrofit... seitdem ich das Upgrade gemacht habe von der MCU1 auf die MCU2 braucht er manchmal 5 Minuten, bis er WLAN/3G aufbaut. Davon nicht irritieren lassen. Haben einige das Problem, Service Center wimmelt einen aber nur ab mit: Muss das Softwareteam lösen. Vielleicht ist es bis zum nächsten Jahr endlich gelöst.

Hier nochmal ein kompletter Auszug Ende zu Ende:

```
➜  evcc git:(feature/1p3pv2) git pull
Current branch feature/1p3pv2 is up to date.
➜  evcc git:(feature/1p3pv2) make build
Version: 0.62 2021-08-19_06:29:50
go build -v -tags=release -ldflags='-X github.com/andig/evcc/server.Version=0.62 -X github.com/andig/evcc/server.Commit=6f41528 -s -w'
➜  evcc git:(feature/1p3pv2) ./evcc
[lp-1  ] INFO 2021/08/19 08:30:20 vehicle updated: unknown -> Tesla Model S
[lp-1  ] INFO 2021/08/19 08:30:20 loadpoint 1:
[lp-1  ] INFO 2021/08/19 08:30:20   mode:      minpv
[lp-1  ] INFO 2021/08/19 08:30:20   charger:   power ✓ energy ✗ currents ✓ timer ✗
[lp-1  ] INFO 2021/08/19 08:30:20   meters:    charge ✓
[lp-1  ] INFO 2021/08/19 08:30:20     charge:  power ✓ energy ✗ currents ✗
[lp-1  ] INFO 2021/08/19 08:30:20   vehicles:  ✓
[lp-1  ] INFO 2021/08/19 08:30:20     car 0:   range ✓ finish ✓ status ✓ climate ✗
[lp-1  ] DEBUG 2021/08/19 08:30:20 charge power: 0W
[lp-1  ] DEBUG 2021/08/19 08:30:20 charger status: B
[lp-1  ] DEBUG 2021/08/19 08:30:21 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:30:21 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:30:21 max charge current: 0.0A = 0.0A + -1.0A (239W @ 1p)
[lp-1  ] DEBUG 2021/08/19 08:30:21 !!pvScalePhases available power -239 for target current -1.0 @ 1p/1p
[lp-1  ] DEBUG 2021/08/19 08:30:21 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:30:21 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 08:30:21 max charge current: 6
[lp-1  ] DEBUG 2021/08/19 08:30:21 charger enable
[lp-1  ] ERROR 2021/08/19 08:30:22 vehicle remote charge start: charging
[lp-1  ] DEBUG 2021/08/19 08:30:30 charge power: 0W
[lp-1  ] DEBUG 2021/08/19 08:30:30 charger status: C
[lp-1  ] INFO 2021/08/19 08:30:30 start charging ->
[lp-1  ] DEBUG 2021/08/19 08:30:30 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:30:30 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:30:30 max charge current: 5.0A = 6.0A + -1.0A (239W @ 1p)
[lp-1  ] DEBUG 2021/08/19 08:30:30 !!pvScalePhases available power -239 for target current -1.0 @ 1p/1p
[lp-1  ] DEBUG 2021/08/19 08:30:30 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:30:30 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 08:30:40 charge power: 728W
[lp-1  ] DEBUG 2021/08/19 08:30:40 detected phases: 1p (6.0A @ 728W)
[lp-1  ] DEBUG 2021/08/19 08:30:40 charger status: C
[lp-1  ] DEBUG 2021/08/19 08:30:40 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:30:40 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:30:40 max charge current: 3.8A = 6.0A + -2.2A (498W @ 1p)
[lp-1  ] DEBUG 2021/08/19 08:30:40 !!pvScalePhases available power 230 for target current 1.0 @ 1p/1p
[lp-1  ] DEBUG 2021/08/19 08:30:40 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:30:40 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 08:30:50 charge power: 3507W
[lp-1  ] DEBUG 2021/08/19 08:30:50 detected phases: 3p (6.0A @ 3507W)
[lp-1  ] DEBUG 2021/08/19 08:30:50 charger status: C
[lp-1  ] DEBUG 2021/08/19 08:30:50 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:30:50 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:30:50 max charge current: 0.6A = 6.0A + -5.4A (3700W @ 3p)
[lp-1  ] DEBUG 2021/08/19 08:30:50 !!pvScalePhases available power -193 for target current -0.3 @ 3p/1p
[lp-1  ] DEBUG 2021/08/19 08:30:50 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:30:50 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 08:31:00 charge power: 3507W
[lp-1  ] DEBUG 2021/08/19 08:31:00 detected phases: 3p (6.0A @ 3507W)
[lp-1  ] DEBUG 2021/08/19 08:31:00 charger status: C
[lp-1  ] DEBUG 2021/08/19 08:31:00 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:31:00 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:31:00 max charge current: 0.6A = 6.0A + -5.4A (3712W @ 3p)
[lp-1  ] DEBUG 2021/08/19 08:31:00 !!pvScalePhases available power -205 for target current -0.3 @ 3p/1p
[lp-1  ] DEBUG 2021/08/19 08:31:00 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:31:00 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 08:31:10 charge power: 3507W
[lp-1  ] DEBUG 2021/08/19 08:31:10 detected phases: 3p (6.0A @ 3507W)
[lp-1  ] DEBUG 2021/08/19 08:31:10 charger status: C
[lp-1  ] DEBUG 2021/08/19 08:31:10 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:31:10 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:31:10 max charge current: 0.7A = 6.0A + -5.3A (3686W @ 3p)
[lp-1  ] DEBUG 2021/08/19 08:31:10 !!pvScalePhases available power -179 for target current -0.3 @ 3p/1p
[lp-1  ] DEBUG 2021/08/19 08:31:10 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:31:10 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 08:31:20 charge power: 3507W
[lp-1  ] DEBUG 2021/08/19 08:31:20 detected phases: 3p (6.0A @ 3507W)
[lp-1  ] DEBUG 2021/08/19 08:31:20 charger status: C
[lp-1  ] DEBUG 2021/08/19 08:31:20 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:31:20 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:31:20 max charge current: 0.7A = 6.0A + -5.3A (3684W @ 3p)
[lp-1  ] DEBUG 2021/08/19 08:31:20 !!pvScalePhases available power -177 for target current -0.3 @ 3p/1p
[lp-1  ] DEBUG 2021/08/19 08:31:20 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:31:20 pv max charge current: 6A
[lp-1  ] DEBUG 2021/08/19 08:31:30 charge power: 3507W
[lp-1  ] DEBUG 2021/08/19 08:31:30 detected phases: 3p (6.0A @ 3507W)
[lp-1  ] DEBUG 2021/08/19 08:31:30 charger status: C
[lp-1  ] DEBUG 2021/08/19 08:31:30 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:31:30 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:31:30 max charge current: 0.7A = 6.0A + -5.3A (3691W @ 3p)
[lp-1  ] DEBUG 2021/08/19 08:31:30 !!pvScalePhases available power -184 for target current -0.3 @ 3p/1p
[lp-1  ] DEBUG 2021/08/19 08:31:30 !!pvScalePhases timer empty
[lp-1  ] DEBUG 2021/08/19 08:31:30 pv max charge current: 6A
[lp-1  ] INFO 2021/08/19 08:31:33 set charge mode: off
[lp-1  ] DEBUG 2021/08/19 08:31:33 charge power: 3507W
[lp-1  ] DEBUG 2021/08/19 08:31:33 detected phases: 3p (6.0A @ 3507W)
[lp-1  ] DEBUG 2021/08/19 08:31:33 charger status: C
[lp-1  ] DEBUG 2021/08/19 08:31:33 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:31:33 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:31:33 charger disable
[lp-1  ] DEBUG 2021/08/19 08:31:40 charge power: 2118W
[lp-1  ] DEBUG 2021/08/19 08:31:40 detected phases: 2p (6.0A @ 2118W)
[lp-1  ] DEBUG 2021/08/19 08:31:40 charger status: B
[lp-1  ] INFO 2021/08/19 08:31:40 stop charging <-
[lp-1  ] DEBUG 2021/08/19 08:31:40 vehicle soc: 55%
[lp-1  ] DEBUG 2021/08/19 08:31:40 vehicle range: 244km
[lp-1  ] DEBUG 2021/08/19 08:31:50 charge power: 0W
[lp-1  ] DEBUG 2021/08/19 08:31:50 charger status: B
^C%
➜  evcc git:(feature/1p3pv2)
```
simonstamm: Wobei da etwas in evcc selber glaub noch nicht richtig gehandled wird. Hatte gerade wieder die ganze Zeit den 408 Request Timeout, obwohl das Auto über die App erreichbar war. Dann kurz evcc beendet, direkt wieder gestartet und dann wurde auch der SoC richtig angezeigt & abgefragt. 🤔 
andig: > Wobei da etwas in evcc selber glaub noch nicht richtig gehandled wird. Hatte gerade wieder die ganze Zeit den 408 Request Timeout, obwohl das Auto über die App erreichbar war. Dann kurz evcc beendet, direkt wieder gestartet und dann wurde auch der SoC richtig angezeigt & abgefragt. 🤔 Fairerweise ist das mit der MCU2-Unerreichbarkeit auch aktuell eher ein Edge Case.

Gerne neues Ticket mit Trace Log. Prinzipiell fragen wir nicht ständig ab sondern es wird gecacht- da kann für 15min mal der gleiche Zustand kommen. Bei 408 wird für 2min versucht das, Auto zu erreichen. Wenn das nicht klappt ist die 408 nach den 2min aber genau der Fehlerzustand der dann auch für die nächsten 15min Cachezeit stehen bleibt. Richtig falsch wäre es nur wenn er zu Beginn die 2 Minuten retry nicht machen würde.
andig: Bei dir fehlt völlig dieser Teil hier: https://github.com/andig/evcc/pull/1213/files#diff-c456526d4bd9022a488ce32de208b40d86016e2c40b0c81a2dffe557c186377dR919

Das Logfile kann egtl. auch weiter nicht vollständig sein. So kommen wir nicht weiter- mir fehlt hier die Sichtbarkeit was wirklich passiert. Könntest Du mal mit dem Debugger rein schauen wann/warum er die Phasen umschaltet?

Alternativ: ich komme mit Laptop bei Dir vorbei und wir schauen das live an ;)
simonstamm: Stimmt, der Teil fehlt tatsächlich. Kannst gerne ab 11:30 Uhr rumkommen, bin im Homeoffice. ;)
andig: Du hast Mail, sonst meld Dich gerne mal bei cpuidle@gmx.de
simonstamm: Hast du meine Mails bekommen?
simonstamm: @andig In https://github.com/andig/evcc/blob/6f41528c7d4cb16ff060a3fa4035a1b2bd6955ac/evcc.dist.yaml#L98 wird suggeriert, dass der Default 3 Phasen sind. Nach https://github.com/andig/evcc/blob/6f41528c7d4cb16ff060a3fa4035a1b2bd6955ac/core/loadpoint.go#L236 ist es aber nur eine. Da ich "phases" in der Config nicht gesetzt habe, ist er von 1 ausgegangen und dadurch niemals durch https://github.com/andig/evcc/blob/6f41528c7d4cb16ff060a3fa4035a1b2bd6955ac/core/loadpoint.go#L894 gekommen. :)

Weiß jetzt nicht, was dir lieber ist. Default auf 3 oder den Kommentar in der Config anpassen.
simonstamm: Funktioniert nun prima, er ist automatisch auf 1 Phase gewechselt, weil der PV-Überschuss nicht ausreicht:

![Bildschirmfoto 2021-08-19 um 13 38 37](https://user-images.githubusercontent.com/940371/130062397-592714fc-1107-4171-893b-5717a64d00a7.png)

Aufgrund des bewölktem Himmels kann ich gerade nur nicht die scale up Phase validieren.
andig: > Funktioniert nun prima,

Und was hast Du jetzt geändert?

> wenngleich er dann den Ladevorgang einmal unterbricht.

Das ist klar. Die Umschaltung geht nicht ohne Unterbrechung und "now" passiert immer mit 3p.
andig: Vielleicht sollten wir es einfach mal mergen und schauen obs dann mehr Feedback gibt?
simonstamm: > Und was hast Du jetzt geändert?

Wie im Kommentar davor quasi geschrieben: Ich habe`phases: 3` in die `evcc.yaml` beim Loadpoint hinzugefügt.

> Vielleicht sollten wir es einfach mal mergen und schauen obs dann mehr Feedback gibt?

Jap, bin ich dafür.
andig: > Wie im Kommentar davor quasi geschrieben: Ich habephases: 3 in die evcc.yaml beim Loadpoint hinzugefügt (obwohl es eigentlich der default sein sollte laut evcc.yaml.dist).

Der ist mir durchgerutscht. Ich denke die meisten Boxen sind 3p angeschlossen- dann würde ich das so ändern. Wenn evcc bei 1p/pv nicht startet wird ja schnell klar, woran das liegt...

Patch:
diff --git a/README.md b/README.md
index 9d661a5d..d8cef6c9 100644
--- a/README.md
+++ b/README.md
@@ -194,7 +194,7 @@ Available charger implementations are:
 - `mcc`: Mobile Charger Connect devices (Audi, Bentley, Porsche)
 - `nrgkick-bluetooth`: NRGkick chargers with Bluetooth connector (Linux only, not supported on Docker)
 - `nrgkick-connect`: NRGkick chargers with additional NRGkick Connect module
-- `openWB`: openWB chargers using openWB's MQTT interface
+- `openWB`: openWB chargers using openWB's MQTT interface (set `phases: true` to indicate if openWB is equipped with 1p3p capability- currently this cannot be auto detected)
 - `phoenix-em-eth`: chargers with Phoenix **EM**-CP-PP-**ETH** controllers
 - `phoenix-ev-eth`: chargers with Phoenix **EV**-CC-\*\*\*-**ETH** controllers (see [Preparation](#phoenix-emev-ethernet-controller-preparation-))
 - `phoenix-ev-ser`: chargers with Phoenix **EV**-CC-\*\*\*-**SER** serial controllers (Modbus RTU)
@@ -583,7 +583,9 @@ EVCC provides a REST and MQTT APIs.
 
 - `/api/state`: EVCC state (static configuration and dynamic state)
 - `/api/loadpoints/<id>/mode`: loadpoint charge mode (writable)
+- `/api/loadpoints/<id>/minsoc`: loadpoint minimum SoC (writable)
 - `/api/loadpoints/<id>/targetsoc`: loadpoint target SoC (writable)
+- `/api/loadpoints/<id>/phases`: loadpoint enabled phases (writable)
 
 Note: to modify writable settings perform a `POST` request appending the value as path segment.
 
@@ -601,6 +603,7 @@ The MQTT API follows the REST API's structure, with loadpoint ids starting at `0
 - `evcc/loadpoints/<id>/mode`: loadpoint charge mode (writable)
 - `evcc/loadpoints/<id>/minSoC`: loadpoint minimum SoC (writable)
 - `evcc/loadpoints/<id>/targetSoC`: loadpoint target SoC (writable)
+- `evcc/loadpoints/<id>/phases`: loadpoint enabled phases (writable)
 
 Note: to modify writable settings append `/set` to the topic for writing.
 
diff --git a/api/api.go b/api/api.go
index a8fd08d6..82226442 100644
--- a/api/api.go
+++ b/api/api.go
@@ -2,7 +2,7 @@ package api
 
 import "time"
 
-//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery
+//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery
 
 // ChargeMode are charge modes modeled after OpenWB
 type ChargeMode string
@@ -77,6 +77,11 @@ type ChargerEx interface {
 	MaxCurrentMillis(current float64) error
 }
 
+// ChargePhases provides 1p3p switching
+type ChargePhases interface {
+	Phases1p3p(phases int) error
+}
+
 // Diagnosis is a helper interface that allows to dump diagnostic data to console
 type Diagnosis interface {
 	Diagnose()
diff --git a/charger/easee.go b/charger/easee.go
index 68ff6d7d..23b31cc0 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -35,15 +35,17 @@ import (
 // Easee charger implementation
 type Easee struct {
 	*request.Helper
-	charger           string
-	site, circuit     int
-	status            easee.ChargerStatus
-	updated           time.Time
-	cache             time.Duration
-	lp                core.LoadPointAPI
-	lastSmartCharging bool
-	lastChargeMode    api.ChargeMode
-	log               *util.Logger
+	charger       string
+	site, circuit int
+	status        easee.ChargerStatus
+	updated       time.Time
+	cache         time.Duration
+	lp            core.LoadPointAPI
+	//lastSmartCharging bool
+	//lastChargeMode api.ChargeMode
+	log     *util.Logger
+	phases  int
+	current float64
 }
 
 func init() {
@@ -83,6 +85,7 @@ func NewEasee(user, password, charger string, circuit int, cache time.Duration)
 		circuit: circuit,
 		cache:   cache,
 		log:     log,
+		phases:  3,
 	}
 
 	ts, err := easee.TokenSource(log, user, password)
@@ -152,6 +155,7 @@ func (c *Easee) chargerDetails() (res easee.Site, err error) {
 	return res, err
 }
 
+/*
 func (c *Easee) syncSmartCharging() error {
 	if c.lp == nil {
 		return nil
@@ -194,6 +198,7 @@ func (c *Easee) syncSmartCharging() error {
 	}
 	return nil
 }
+*/
 
 func (c *Easee) state() (easee.ChargerStatus, error) {
 	if time.Since(c.updated) < c.cache {
@@ -204,7 +209,7 @@ func (c *Easee) state() (easee.ChargerStatus, error) {
 	req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding)
 	if err == nil {
 		if err = c.DoJSON(req, &c.status); err == nil {
-			err = c.syncSmartCharging()
+			// err = c.syncSmartCharging()
 			c.updated = time.Now()
 		}
 	}
@@ -285,27 +290,40 @@ var _ api.ChargerEx = (*Easee)(nil)
 
 // MaxCurrentMillis implements the api.ChargerEx interface
 func (c *Easee) MaxCurrentMillis(current float64) error {
-	cur := int(current)
+	var current23 float64
+	if c.phases > 1 {
+		current23 = current
+	}
+
 	data := easee.CircuitSettings{
-		DynamicCircuitCurrentP1: &cur,
-		DynamicCircuitCurrentP2: &cur,
-		DynamicCircuitCurrentP3: &cur,
+		DynamicCircuitCurrentP1: &current,
+		DynamicCircuitCurrentP2: &current23,
+		DynamicCircuitCurrentP3: &current23,
 	}
 
 	uri := fmt.Sprintf("%s/sites/%d/circuits/%d/settings", easee.API, c.site, c.circuit)
 	resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data))
 	if err == nil {
 		resp.Body.Close()
-	}
 
-	c.updated = time.Time{} // clear cache
+		c.updated = time.Time{} // clear cache
+		c.current = current
+	}
 
 	return err
 }
 
+var _ api.ChargePhases = (*Easee)(nil)
+
+// Phases1p3p implements the api.ChargePhases interface
+func (c *Easee) Phases1p3p(phases int) error {
+	c.phases = phases
+	return c.MaxCurrentMillis(c.current)
+}
+
 var _ api.Meter = (*Easee)(nil)
 
-// CurrentPower implements the api.Meter interface.
+// CurrentPower implements the api.Meter interface
 func (c *Easee) CurrentPower() (float64, error) {
 	res, err := c.state()
 	return 1e3 * res.TotalPower, err
diff --git a/charger/easee/message.go b/charger/easee/message.go
index 7cfa17d2..bdc805dd 100644
--- a/charger/easee/message.go
+++ b/charger/easee/message.go
@@ -136,14 +136,14 @@ type ChargerSettings struct {
 
 // CircuitSettings is the circuit settings type
 type CircuitSettings struct {
-	DynamicCircuitCurrentP1    *int  `json:"dynamicCircuitCurrentP1,omitempty"`
-	DynamicCircuitCurrentP2    *int  `json:"dynamicCircuitCurrentP2,omitempty"`
-	DynamicCircuitCurrentP3    *int  `json:"dynamicCircuitCurrentP3,omitempty"`
-	MaxCircuitCurrentP1        *int  `json:"maxCircuitCurrentP1,omitempty"`
-	MaxCircuitCurrentP2        *int  `json:"maxCircuitCurrentP2,omitempty"`
-	MaxCircuitCurrentP3        *int  `json:"maxCircuitCurrentP3,omitempty"`
-	EnableIdleCurrent          *bool `json:"enableIdleCurrent,omitempty"`
-	OfflineMaxCircuitCurrentP1 *int  `json:"offlineMaxCircuitCurrentP1,omitempty"`
-	OfflineMaxCircuitCurrentP2 *int  `json:"offlineMaxCircuitCurrentP2,omitempty"`
-	OfflineMaxCircuitCurrentP3 *int  `json:"offlineMaxCircuitCurrentP3,omitempty"`
+	DynamicCircuitCurrentP1    *float64 `json:"dynamicCircuitCurrentP1,omitempty"`
+	DynamicCircuitCurrentP2    *float64 `json:"dynamicCircuitCurrentP2,omitempty"`
+	DynamicCircuitCurrentP3    *float64 `json:"dynamicCircuitCurrentP3,omitempty"`
+	MaxCircuitCurrentP1        *int     `json:"maxCircuitCurrentP1,omitempty"`
+	MaxCircuitCurrentP2        *int     `json:"maxCircuitCurrentP2,omitempty"`
+	MaxCircuitCurrentP3        *int     `json:"maxCircuitCurrentP3,omitempty"`
+	EnableIdleCurrent          *bool    `json:"enableIdleCurrent,omitempty"`
+	OfflineMaxCircuitCurrentP1 *int     `json:"offlineMaxCircuitCurrentP1,omitempty"`
+	OfflineMaxCircuitCurrentP2 *int     `json:"offlineMaxCircuitCurrentP2,omitempty"`
+	OfflineMaxCircuitCurrentP3 *int     `json:"offlineMaxCircuitCurrentP3,omitempty"`
 }
diff --git a/charger/openwb.go b/charger/openwb.go
index 754e3031..c983e547 100644
--- a/charger/openwb.go
+++ b/charger/openwb.go
@@ -21,8 +21,11 @@ type OpenWB struct {
 	currentPowerG func() (float64, error)
 	totalEnergyG  func() (float64, error)
 	currentsG     []func() (float64, error)
+	phasesS       func(int64) error
 }
 
+// go:generate go run ../cmd/tools/decorate.go -f decorateOpenWB -b *OpenWB -r api.Charger -t "api.ChargePhases,Phases1p3p,func(int) (error)"
+
 // NewOpenWBFromConfig creates a new configurable charger
 func NewOpenWBFromConfig(other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
@@ -30,6 +33,7 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Charger, error) {
 		Topic       string
 		Timeout     time.Duration
 		ID          int
+		Phases      bool
 	}{
 		Topic:   openwb.RootTopic,
 		Timeout: openwb.Timeout,
@@ -42,11 +46,11 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Charger, error) {
 
 	log := util.NewLogger("openwb")
 
-	return NewOpenWB(log, cc.Config, cc.ID, cc.Topic, cc.Timeout)
+	return NewOpenWB(log, cc.Config, cc.ID, cc.Topic, cc.Phases, cc.Timeout)
 }
 
 // NewOpenWB creates a new configurable charger
-func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, timeout time.Duration) (*OpenWB, error) {
+func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, p1p3 bool, timeout time.Duration) (api.Charger, error) {
 	client, err := mqtt.RegisteredClientOrDefault(log, mqttconf)
 	if err != nil {
 		return nil, err
@@ -59,7 +63,7 @@ func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, tim
 
 	// getters
 	boolG := func(topic string) func() (bool, error) {
-		g := provider.NewMqtt(log, client, topic, 1, 0).BoolGetter()
+		g := provider.NewMqtt(log, client, topic, 1, timeout).BoolGetter()
 		return func() (val bool, err error) {
 			if val, err = g(); err == nil {
 				_, err = timer()
@@ -69,7 +73,7 @@ func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, tim
 	}
 
 	floatG := func(topic string) func() (float64, error) {
-		g := provider.NewMqtt(log, client, topic, 1, 0).FloatGetter()
+		g := provider.NewMqtt(log, client, topic, 1, timeout).FloatGetter()
 		return func() (val float64, err error) {
 			if val, err = g(); err == nil {
 				_, err = timer()
@@ -85,18 +89,18 @@ func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, tim
 	}
 
 	// adapt plugged/charging to status
-	plugged := boolG(fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.PluggedTopic))
-	charging := boolG(fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.ChargingTopic))
-	status := provider.NewOpenWBStatusProvider(plugged, charging).StringGetter
+	pluggedG := boolG(fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.PluggedTopic))
+	chargingG := boolG(fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.ChargingTopic))
+	statusG := provider.NewOpenWBStatusProvider(pluggedG, chargingG).StringGetter
 
 	// remaining getters
-	enabled := boolG(fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.EnabledTopic))
+	enabledG := boolG(fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.EnabledTopic))
 
 	// setters
-	enable := provider.NewMqtt(log, client,
+	enableS := provider.NewMqtt(log, client,
 		fmt.Sprintf("%s/set/lp%d/%s", topic, id, openwb.EnabledTopic),
 		1, timeout).BoolSetter("enable")
-	maxcurrent := provider.NewMqtt(log, client,
+	maxcurrentS := provider.NewMqtt(log, client,
 		fmt.Sprintf("%s/set/lp%d/%s", topic, id, openwb.MaxCurrentTopic),
 		1, timeout).IntSetter("maxcurrent")
 
@@ -110,19 +114,29 @@ func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, tim
 		currentsG = append(currentsG, current)
 	}
 
-	charger, err := NewConfigurable(status, enabled, enable, maxcurrent)
+	charger, err := NewConfigurable(statusG, enabledG, enableS, maxcurrentS)
 	if err != nil {
 		return nil, err
 	}
 
-	res := &OpenWB{
+	c := &OpenWB{
 		Charger:       charger,
 		currentPowerG: currentPowerG,
 		totalEnergyG:  totalEnergyG,
 		currentsG:     currentsG,
 	}
 
-	return res, nil
+	// optional capabilities
+	c.phasesS = provider.NewMqtt(log, client,
+		fmt.Sprintf("%s/set/isss/%s", topic, openwb.PhasesTopic),
+		1, timeout).IntSetter("phases")
+
+	var phases func(int) error
+	if p1p3 {
+		phases = c.phases
+	}
+
+	return decorateOpenWB(c, phases), nil
 }
 
 var _ api.Meter = (*OpenWB)(nil)
@@ -155,3 +169,8 @@ func (m *OpenWB) Currents() (float64, float64, float64, error) {
 
 	return currents[0], currents[1], currents[2], nil
 }
+
+// phases implements the api.ChargePhases interface
+func (c *OpenWB) phases(phases int) error {
+	return c.phasesS(int64(phases))
+}
diff --git a/charger/openwb/topics.go b/charger/openwb/topics.go
index 5fdc493a..060d2408 100644
--- a/charger/openwb/topics.go
+++ b/charger/openwb/topics.go
@@ -20,6 +20,7 @@ const (
 	// getter/setter
 	EnabledTopic    = "ChargePointEnabled"
 	MaxCurrentTopic = "DirectChargeAmps"
+	PhasesTopic     = "U1p3p"
 
 	// charge power
 	ChargePowerTopic       = "W"
diff --git a/charger/openwb_decorators.go b/charger/openwb_decorators.go
new file mode 100644
index 00000000..4f3459af
--- /dev/null
+++ b/charger/openwb_decorators.go
@@ -0,0 +1,35 @@
+package charger
+
+// Code generated by github.com/andig/cmd/tools/decorate.go. DO NOT EDIT.
+
+import (
+	"github.com/andig/evcc/api"
+)
+
+func decorateOpenWB(base *OpenWB, chargePhases func(int) error) api.Charger {
+	switch {
+	case chargePhases == nil:
+		return base
+
+	case chargePhases != nil:
+		return &struct {
+			*OpenWB
+			api.ChargePhases
+		}{
+			OpenWB: base,
+			ChargePhases: &decorateOpenWBChargePhasesImpl{
+				chargePhases: chargePhases,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateOpenWBChargePhasesImpl struct {
+	chargePhases func(int) error
+}
+
+func (impl *decorateOpenWBChargePhasesImpl) Phases1p3p(phases int) error {
+	return impl.chargePhases(phases)
+}
diff --git a/core/helper.go b/core/helper.go
index 1bd4cab3..0efeb53d 100644
--- a/core/helper.go
+++ b/core/helper.go
@@ -17,6 +17,9 @@ var (
 
 // powerToCurrent is a helper function to convert power to per-phase current
 func powerToCurrent(power float64, phases int64) float64 {
+	if Voltage == 0 {
+		panic("Voltage is not set")
+	}
 	return power / (float64(phases) * Voltage)
 }
 
diff --git a/core/loadpoint.go b/core/loadpoint.go
index bdacd3f3..7296100c 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -86,7 +86,7 @@ type LoadPoint struct {
 	Mode       api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
 
 	Title       string   `mapstructure:"title"`    // UI title
-	Phases      int64    `mapstructure:"phases"`   // Phases- required for converting power and current
+	Phases      int64    `mapstructure:"phases"`   // Charger enabled phases
 	ChargerRef  string   `mapstructure:"charger"`  // Charger reference
 	VehicleRef  string   `mapstructure:"vehicle"`  // Vehicle reference
 	VehiclesRef []string `mapstructure:"vehicles"` // Vehicles reference
@@ -103,6 +103,7 @@ type LoadPoint struct {
 	GuardDuration time.Duration // charger enable/disable minimum holding time
 
 	enabled                bool      // Charger enabled state
+	activePhases           int64     // Charger active phases as used by vehicle
 	chargeCurrent          float64   // Charger current limit
 	guardUpdated           time.Time // Charger enabled/disabled timestamp
 	socUpdated             time.Time // SoC updated timestamp (poll: connected)
@@ -127,6 +128,7 @@ type LoadPoint struct {
 	chargeCurrents []float64        // Phase currents
 	connectedTime  time.Time        // Time when vehicle was connected
 	pvTimer        time.Time        // PV enabled/disable timer
+	phaseTimer     time.Time        // 1p3p switch timer
 
 	socCharge      float64       // Vehicle SoC
 	chargedEnergy  float64       // Charged energy while connected in Wh
@@ -231,7 +233,7 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
 		clock:         clock, // mockable time
 		bus:           bus,   // event bus
 		Mode:          api.ModeOff,
-		Phases:        1,
+		Phases:        3,
 		status:        api.StatusNone,
 		MinCurrent:    6,  // A
 		MaxCurrent:    16, // A
@@ -345,7 +347,7 @@ func (lp *LoadPoint) evVehicleConnectHandler() {
 	lp.startVehicleDetection()
 
 	// immediately allow pv mode activity
-	lp.pvDisableTimer()
+	lp.elapsePVTimer()
 
 	lp.pushEvent(evVehicleConnect)
 }
@@ -386,7 +388,7 @@ func (lp *LoadPoint) evChargeCurrentHandler(current float64) {
 // If physical charge meter is present this handler is not used.
 // The actual value is published by the evChargeCurrentHandler
 func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current float64) {
-	power := current * float64(lp.Phases) * Voltage
+	power := current * float64(lp.activePhases) * Voltage
 
 	if !lp.enabled || lp.GetStatus() != api.StatusC {
 		// if disabled we cannot be charging
@@ -418,6 +420,9 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	lp.pushChan = pushChan
 	lp.lpChan = lpChan
 
+	// assume all phases are active
+	lp.activePhases = lp.Phases
+
 	// event handlers
 	_ = lp.bus.Subscribe(evChargeStart, lp.evChargeStartHandler)
 	_ = lp.bus.Subscribe(evChargeStop, lp.evChargeStopHandler)
@@ -430,7 +435,8 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	lp.publish("minCurrent", lp.MinCurrent)
 	lp.publish("maxCurrent", lp.MaxCurrent)
 	lp.publish("phases", lp.Phases)
-	lp.publish("activePhases", lp.Phases)
+	lp.publish("activePhases", lp.activePhases)
+	lp.publish("hasVehicle", len(lp.vehicles) > 0)
 
 	lp.Lock()
 	lp.publish("mode", lp.Mode)
@@ -506,10 +512,12 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) {
 		if err == nil {
 			lp.chargeCurrent = chargeCurrent
 			lp.bus.Publish(evChargeCurrent, chargeCurrent)
+		} else {
+			err = fmt.Errorf("max charge current %.2g: %w", chargeCurrent, err)
 		}
 	}
 
-	// set enabled
+	// set enabled/disabled
 	if enabled := chargeCurrent >= lp.GetMinCurrent(); enabled != lp.enabled && err == nil {
 		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)
@@ -812,27 +820,161 @@ func (lp *LoadPoint) effectiveCurrent() float64 {
 	return lp.chargeCurrent
 }
 
-// pvDisableTimer puts the pv enable/disable timer into elapsed state
-func (lp *LoadPoint) pvDisableTimer() {
+// 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)
+}
+
+// scalePhasesIfAvailable scales if api.ChargePhases is available
+func (lp *LoadPoint) scalePhasesIfAvailable(phases int) error {
+	err := lp.scalePhases(phases)
+	if errors.Is(err, api.ErrNotAvailable) {
+		return nil
+	}
+	return err
+}
+
+// scalePhases adjusts the number of active phases and returns the appropriate charging current.
+// Returns api.ErrNotAvailable if api.ChargePhases is not available.
+func (lp *LoadPoint) scalePhases(phases int) error {
+	if phases != 1 && phases != 3 {
+		return fmt.Errorf("invalid number of phases: %d", phases)
+	}
+
+	cp, ok := lp.charger.(api.ChargePhases)
+	if !ok {
+		return api.ErrNotAvailable
+	}
+
+	lp.Lock()
+	if lp.Phases != int64(phases) {
+		lp.Unlock()
+
+		// disable charger - this will also stop the car charging using the api if available
+		if err := lp.setLimit(0, true); err != nil {
+			return err
+		}
+
+		// switch phases
+		if err := cp.Phases1p3p(phases); err != nil {
+			return fmt.Errorf("switch phases: %w", err)
+		}
+
+		lp.Lock()
+		lp.Phases = int64(phases)
+		lp.publish("phases", lp.Phases)
+
+		// disable phase timer
+		lp.phaseTimer = time.Time{}
+
+		// allow pv mode to re-enable charger right away
+		lp.elapsePVTimer()
+	}
+	lp.Unlock()
+
+	return nil
+}
+
+// pvScalePhases switches phases if necessary and returns if switch occured
+func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float64) bool {
+	var waiting bool
+
+	phases := lp.GetPhases()
+	targetCurrent := availablePower / Voltage / float64(lp.activePhases)
+
+	lp.log.DEBUG.Printf("!!pvScalePhases available power %.0f for target current %.1f @ %dp/%dp", availablePower, targetCurrent, lp.activePhases, phases)
+	if lp.phaseTimer.IsZero() {
+		lp.log.DEBUG.Printf("!!pvScalePhases timer empty")
+	} else {
+		lp.log.DEBUG.Printf("!!pvScalePhases timer remaining: %v", lp.clock.Since(lp.phaseTimer).Truncate(time.Second))
+	}
+
+	// scale down phases
+	if targetCurrent < minCurrent && phases > 1 && 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() {
+			lp.log.DEBUG.Printf("start phase disable timer: %v", lp.Disable.Delay)
+			lp.phaseTimer = lp.clock.Now()
+		}
+
+		elapsed := lp.clock.Since(lp.phaseTimer)
+		if elapsed >= lp.Disable.Delay {
+			lp.log.DEBUG.Println("phase disable timer elapsed")
+			if err := lp.scalePhases(1); err == nil {
+				lp.log.DEBUG.Printf("switched phases: 1p @ %.0fW", availablePower)
+				return true
+			} else {
+				lp.log.ERROR.Printf("switch phases: %v", err)
+			}
+		}
+
+		waiting = true
+		lp.log.DEBUG.Printf("phase disable timer remaining: %v", (lp.Disable.Delay - elapsed).Round(time.Second))
+	}
+
+	// scale up phases
+	if min3pCurrent := powerToCurrent(availablePower, 3); min3pCurrent >= minCurrent && phases == 1 {
+		lp.log.DEBUG.Printf("available power above 3p min threshold of %.0fW", 3*Voltage*minCurrent)
+
+		if lp.phaseTimer.IsZero() {
+			lp.log.DEBUG.Printf("start phase enable timer: %v", lp.Enable.Delay)
+			lp.phaseTimer = lp.clock.Now()
+		}
+
+		elapsed := lp.clock.Since(lp.phaseTimer)
+		if elapsed >= lp.Disable.Delay {
+			lp.log.DEBUG.Println("phase enable timer elapsed")
+			if err := lp.scalePhases(3); err == nil {
+				lp.log.DEBUG.Printf("switched phases: 3p @ %.0fW", availablePower)
+				return true
+			} else {
+				lp.log.ERROR.Printf("switch phases: %v", err)
+			}
+		}
+
+		waiting = true
+		lp.log.DEBUG.Printf("phase enable timer remaining: %v", (lp.Disable.Delay - elapsed).Round(time.Second))
+	}
+
+	// reset timer to disabled state
+	if !waiting && !lp.phaseTimer.IsZero() {
+		lp.log.DEBUG.Printf("phase timer reset")
+		lp.phaseTimer = time.Time{}
+	}
+
+	return false
 }
 
 // pvMaxCurrent calculates the maximum target current for PV mode
 func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float64 {
+	// read only once to simplify testing
+	minCurrent := lp.GetMinCurrent()
+	maxCurrent := lp.GetMaxCurrent()
+
 	// calculate target charge current from delta power and actual current
 	effectiveCurrent := lp.effectiveCurrent()
-	deltaCurrent := powerToCurrent(-sitePower, lp.Phases)
-	targetCurrent := math.Max(math.Min(effectiveCurrent+deltaCurrent, lp.GetMaxCurrent()), 0)
+	deltaCurrent := powerToCurrent(-sitePower, lp.activePhases)
+	targetCurrent := math.Max(effectiveCurrent+deltaCurrent, 0)
+
+	lp.log.DEBUG.Printf("max charge current: %.1fA = %.1fA + %.1fA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, lp.activePhases)
 
-	lp.log.DEBUG.Printf("max charge current: %.1fA = %.1fA + %.1fA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, lp.Phases)
+	// switch phases up/down
+	if _, ok := lp.charger.(api.ChargePhases); ok {
+		availablePower := -sitePower + lp.chargePower
+
+		// in case of scaling, keep charger disabled for this cycle
+		if lp.pvScalePhases(availablePower, minCurrent, maxCurrent) {
+			return 0
+		}
+	}
 
 	// in MinPV mode return at least minCurrent
-	minCurrent := lp.GetMinCurrent()
 	if mode == api.ModeMinPV && targetCurrent < minCurrent {
 		return minCurrent
 	}
 
-	// read only once to simplify testing
 	if mode == api.ModePV && lp.enabled && targetCurrent < minCurrent {
 		// kick off disable sequence
 		if sitePower >= lp.Disable.Threshold {
@@ -889,8 +1031,13 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float6
 	}
 
 	// reset timer to disabled state
-	lp.log.DEBUG.Printf("pv timer reset")
-	lp.pvTimer = time.Time{}
+	if !lp.pvTimer.IsZero() {
+		lp.log.DEBUG.Printf("pv timer reset")
+		lp.pvTimer = time.Time{}
+	}
+
+	// cap at maximum current
+	targetCurrent = math.Min(targetCurrent, maxCurrent)
 
 	return targetCurrent
 }
@@ -920,6 +1067,17 @@ func (lp *LoadPoint) updateChargeCurrents() {
 	lp.chargeCurrents = nil
 	phaseMeter, ok := lp.chargeMeter.(api.MeterCurrent)
 	if !ok {
+		// guess active phases from power consumption
+		// assumes that chargePower has been updated before
+		if lp.charging() && lp.chargeCurrent > 0 {
+			phases := int64(math.Round(lp.chargePower / Voltage / lp.chargeCurrent))
+			if phases >= 1 && phases <= 3 {
+				lp.activePhases = phases
+				lp.log.DEBUG.Printf("detected phases: %dp (%.1fA @ %.0fW)", lp.activePhases, lp.chargeCurrent, lp.chargePower)
+				lp.publish("activePhases", lp.activePhases)
+			}
+		}
+
 		return
 	}
 
@@ -941,11 +1099,10 @@ func (lp *LoadPoint) updateChargeCurrents() {
 			}
 		}
 
-		if phases > 0 {
-			lp.Phases = phases
-			lp.log.DEBUG.Printf("detected phases: %dp %.3gA", lp.Phases, lp.chargeCurrents)
-
-			lp.publish("activePhases", lp.Phases)
+		if phases >= 1 {
+			lp.activePhases = phases
+			lp.log.DEBUG.Printf("detected phases: %dp %.3gA", lp.activePhases, lp.chargeCurrents)
+			lp.publish("activePhases", lp.activePhases)
 		}
 	}
 }
@@ -1121,11 +1278,17 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool) {
 		err = lp.setLimit(0, true)
 
 	case lp.minSocNotReached():
-		err = lp.setLimit(lp.GetMaxCurrent(), true)
-		lp.pvDisableTimer() // let PV mode disable immediately afterwards
+		// 3p if available
+		if err = lp.scalePhasesIfAvailable(3); err == nil {
+			err = lp.setLimit(lp.GetMaxCurrent(), true)
+		}
+		lp.elapsePVTimer() // let PV mode disable immediately afterwards
 
 	case mode == api.ModeNow:
-		err = lp.setLimit(lp.GetMaxCurrent(), true)
+		// 3p if available
+		if err = lp.scalePhasesIfAvailable(3); err == nil {
+			err = lp.setLimit(lp.GetMaxCurrent(), true)
+		}
 
 	// target charging
 	case lp.socTimer.StartRequired():
diff --git a/core/loadpoint_adapter.go b/core/loadpoint_adapter.go
index 13abac45..95276078 100644
--- a/core/loadpoint_adapter.go
+++ b/core/loadpoint_adapter.go
@@ -19,7 +19,7 @@ func (a *adapter) SocEstimator() *soc.Estimator {
 }
 
 func (a *adapter) ActivePhases() int64 {
-	return a.lp.Phases
+	return a.lp.activePhases
 }
 
 func (a *adapter) Voltage() float64 {
diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go
index 792a9fbc..103ae344 100644
--- a/core/loadpoint_api.go
+++ b/core/loadpoint_api.go
@@ -27,6 +27,8 @@ type LoadPointAPI interface {
 	SetTargetSoC(int) error
 	GetMinSoC() int
 	SetMinSoC(int) error
+	GetPhases() int
+	SetPhases(int) error
 	SetTargetCharge(time.Time, int)
 	RemoteControl(string, RemoteDemand)
 
@@ -67,7 +69,7 @@ func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
 		lp.publish("mode", mode)
 
 		// immediately allow pv mode activity
-		lp.pvDisableTimer()
+		lp.elapsePVTimer()
 
 		lp.requestUpdate()
 	}
@@ -129,6 +131,19 @@ func (lp *LoadPoint) SetMinSoC(soc int) error {
 	return nil
 }
 
+// GetPhases returns loadpoint enabled phases
+func (lp *LoadPoint) GetPhases() int {
+	lp.Lock()
+	defer lp.Unlock()
+
+	return int(lp.Phases)
+}
+
+// SetPhases sets loadpoint enabled phases
+func (lp *LoadPoint) SetPhases(phases int) error {
+	return lp.scalePhases(phases)
+}
+
 // SetTargetCharge sets loadpoint charge targetSoC
 func (lp *LoadPoint) SetTargetCharge(finishAt time.Time, targetSoC int) {
 	lp.Lock()
@@ -221,5 +236,5 @@ func (lp *LoadPoint) GetMinPower() float64 {
 
 // GetMaxPower returns the max loadpoint power taking active phases into account
 func (lp *LoadPoint) GetMaxPower() float64 {
-	return Voltage * lp.GetMaxCurrent() * float64(lp.Phases)
+	return Voltage * lp.GetMaxCurrent() * float64(lp.GetPhases())
 }
diff --git a/mock/mock_api.go b/mock/mock_api.go
index 1dde0a78..9b845d6b 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/andig/evcc/api (interfaces: Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery)
+// Source: github.com/andig/evcc/api (interfaces: Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery)
 
 // Package mock is a generated GoMock package.
 package mock
@@ -130,6 +130,43 @@ func (mr *MockChargeStateMockRecorder) Status() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockChargeState)(nil).Status))
 }
 
+// MockChargePhases is a mock of ChargePhases interface.
+type MockChargePhases struct {
+	ctrl     *gomock.Controller
+	recorder *MockChargePhasesMockRecorder
+}
+
+// MockChargePhasesMockRecorder is the mock recorder for MockChargePhases.
+type MockChargePhasesMockRecorder struct {
+	mock *MockChargePhases
+}
+
+// NewMockChargePhases creates a new mock instance.
+func NewMockChargePhases(ctrl *gomock.Controller) *MockChargePhases {
+	mock := &MockChargePhases{ctrl: ctrl}
+	mock.recorder = &MockChargePhasesMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockChargePhases) EXPECT() *MockChargePhasesMockRecorder {
+	return m.recorder
+}
+
+// Phases1p3p mocks base method.
+func (m *MockChargePhases) Phases1p3p(arg0 int) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Phases1p3p", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Phases1p3p indicates an expected call of Phases1p3p.
+func (mr *MockChargePhasesMockRecorder) Phases1p3p(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases1p3p", reflect.TypeOf((*MockChargePhases)(nil).Phases1p3p), arg0)
+}
+
 // MockIdentifier is a mock of Identifier interface.
 type MockIdentifier struct {
 	ctrl     *gomock.Controller
diff --git a/server/http.go b/server/http.go
index 6880f10f..07ad9224 100644
--- a/server/http.go
+++ b/server/http.go
@@ -21,6 +21,10 @@ import (
 // Assets is the embedded assets file system
 var Assets fs.FS
 
+type errorJSON struct {
+	Error string `json:"error"`
+}
+
 type chargeModeJSON struct {
 	Mode api.ChargeMode `json:"mode"`
 }
@@ -33,6 +37,10 @@ type minSoCJSON struct {
 	MinSoC int `json:"minSoC"`
 }
 
+type phasesJSON struct {
+	Phases int `json:"phases"`
+}
+
 type route struct {
 	Methods     []string
 	Pattern     string
@@ -163,6 +171,7 @@ func ChargeModeHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
 		mode := api.ChargeModeString(modeS)
 		if mode == "" || string(mode) != modeS || !ok {
 			w.WriteHeader(http.StatusBadRequest)
+			jsonResponse(w, r, errorJSON{Error: "invalid mode"})
 			return
 		}
 
@@ -195,6 +204,7 @@ func TargetSoCHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
 
 		if !ok || err != nil {
 			w.WriteHeader(http.StatusBadRequest)
+			jsonResponse(w, r, errorJSON{Error: err.Error()})
 			return
 		}
 
@@ -225,6 +235,7 @@ func MinSoCHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
 
 		if !ok || err != nil {
 			w.WriteHeader(http.StatusBadRequest)
+			jsonResponse(w, r, errorJSON{Error: err.Error()})
 			return
 		}
 
@@ -233,6 +244,37 @@ func MinSoCHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
 	}
 }
 
+// CurrentPhasesHandler returns current minimum soc
+func CurrentPhasesHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		res := phasesJSON{Phases: loadpoint.GetPhases()}
+		jsonResponse(w, r, res)
+	}
+}
+
+// PhasesHandler updates minimum soc
+func PhasesHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		vars := mux.Vars(r)
+
+		phasesS, ok := vars["phases"]
+		phases, err := strconv.ParseInt(phasesS, 10, 32)
+
+		if ok && err == nil {
+			err = loadpoint.SetPhases(int(phases))
+		}
+
+		if !ok || err != nil {
+			w.WriteHeader(http.StatusBadRequest)
+			jsonResponse(w, r, errorJSON{Error: err.Error()})
+			return
+		}
+
+		res := phasesJSON{Phases: loadpoint.GetPhases()}
+		jsonResponse(w, r, res)
+	}
+}
+
 // RemoteDemandHandler updates minimum soc
 func RemoteDemandHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
@@ -249,6 +291,7 @@ func RemoteDemandHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
 
 		if !ok || err != nil {
 			w.WriteHeader(http.StatusBadRequest)
+			jsonResponse(w, r, errorJSON{Error: err.Error()})
 			return
 		}
 
@@ -293,8 +336,8 @@ func TargetChargeHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
 		timeV, err := time.ParseInLocation("2006-01-02T15:04:05", timeS, timezone())
 
 		if !ok || err != nil {
-			log.DEBUG.Printf("parse time: %v", err)
 			w.WriteHeader(http.StatusBadRequest)
+			jsonResponse(w, r, errorJSON{Error: err.Error()})
 			return
 		}
 
@@ -372,6 +415,8 @@ func NewHTTPd(url string, site core.SiteAPI, hub *SocketHub, cache *util.Cache)
 			"settargetsoc":    {[]string{"POST", "OPTIONS"}, "/targetsoc/{soc:[0-9]+}", TargetSoCHandler(lp)},
 			"getminsoc":       {[]string{"GET"}, "/minsoc", CurrentMinSoCHandler(lp)},
 			"setminsoc":       {[]string{"POST", "OPTIONS"}, "/minsoc/{soc:[0-9]+}", MinSoCHandler(lp)},
+			"getphases":       {[]string{"GET"}, "/phases", CurrentPhasesHandler(lp)},
+			"setphases":       {[]string{"POST", "OPTIONS"}, "/phases/{phases:[0-9]+}", PhasesHandler(lp)},
 			"settargetcharge": {[]string{"POST", "OPTIONS"}, "/targetcharge/{soc:[0-9]+}/{time:[0-9TZ:-]+}", TargetChargeHandler(lp)},
 			"remotedemand":    {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source}", RemoteDemandHandler(lp)},
 		}
diff --git a/server/mqtt.go b/server/mqtt.go
index 16d3d717..d502e799 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -80,6 +80,12 @@ func (m *MQTT) listenSetters(topic string, apiHandler core.LoadPointAPI) {
 			_ = apiHandler.SetTargetSoC(soc)
 		}
 	})
+	m.Handler.Listen(topic+"/phases/set", func(payload string) {
+		phases, err := strconv.Atoi(payload)
+		if err == nil {
+			_ = apiHandler.SetPhases(phases)
+		}
+	})
 }
 
 // Run starts the MQTT publisher for the MQTT API


Test patch:
diff --git a/charger/openwb_test.go b/charger/openwb_test.go
index 90e69db3..29d1d4b9 100644
--- a/charger/openwb_test.go
+++ b/charger/openwb_test.go
@@ -8,10 +8,14 @@ import (
 
 func TestOpenWBDecorators(t *testing.T) {
 	// host not reachable
-	wb, _ := NewOpenWBFromConfig(map[string]interface{}{
+	wb, err := NewOpenWBFromConfig(map[string]interface{}{
 		"broker": "192.0.2.2",
 	})
 
+	if err != nil {
+		t.Skip(err)
+	}
+
 	if _, ok := wb.(api.Meter); !ok {
 		t.Error("missing Meter api")
 	}
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 431c0ffc..8bf19e6b 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -71,7 +71,7 @@ func attachListeners(t *testing.T, lp *LoadPoint) {
 func TestNew(t *testing.T) {
 	lp := NewLoadPoint(util.NewLogger("foo"))
 
-	if lp.Phases != 1 {
+	if lp.Phases != 3 {
 		t.Errorf("Phases %v", lp.Phases)
 	}
 	if lp.MinCurrent != minA {
@@ -295,12 +295,13 @@ func TestPVHysteresis(t *testing.T) {
 
 			Voltage = 100
 			lp := &LoadPoint{
-				log:        util.NewLogger("foo"),
-				clock:      clck,
-				charger:    charger,
-				MinCurrent: minA,
-				MaxCurrent: maxA,
-				Phases:     10,
+				log:          util.NewLogger("foo"),
+				clock:        clck,
+				charger:      charger,
+				MinCurrent:   minA,
+				MaxCurrent:   maxA,
+				Phases:       10,
+				activePhases: 10,
 				Enable: ThresholdConfig{
 					Threshold: tc.enable,
 					Delay:     dt,
@@ -341,11 +342,12 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
 
 	Voltage = 100
 	lp := &LoadPoint{
-		log:        util.NewLogger("foo"),
-		clock:      clck,
-		MinCurrent: minA,
-		MaxCurrent: maxA,
-		Phases:     10,
+		log:          util.NewLogger("foo"),
+		clock:        clck,
+		MinCurrent:   minA,
+		MaxCurrent:   maxA,
+		Phases:       10,
+		activePhases: 10,
 	}
 
 	// not connected, test PV mode logic  short-circuited
@@ -811,3 +813,114 @@ func TestVehicleDetectByID(t *testing.T) {
 		}
 	}
 }
+
+func TestScalePhases(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	charger := &struct {
+		*mock.MockCharger
+		*mock.MockChargePhases
+	}{
+		mock.NewMockCharger(ctrl),
+		mock.NewMockChargePhases(ctrl),
+	}
+
+	dt := time.Minute
+	Voltage = 230 // V
+
+	tc := []struct {
+		desc                 string
+		phases, activePhases int
+		availablePower       float64
+		toPhases             int
+		res                  bool
+		prepare              func(lp *LoadPoint)
+	}{
+		// switch up from 1p/1p configured/active
+		{"1/1->3, not enough power", 1, 1, 0, 1, false, nil},
+		{"1/1->3, kickoff", 1, 1, 3 * Voltage * minA, 1, false, func(lp *LoadPoint) {
+			lp.phaseTimer = time.Time{}
+		}},
+		{"1/1->3, timer running", 1, 1, 3 * Voltage * minA, 1, false, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now()
+		}},
+		{"1/1->3, timer elapsed", 1, 1, 3 * Voltage * minA, 3, true, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now().Add(-dt)
+		}},
+
+		// omit to switch up (again) from 3p/1p configured/active
+		{"3/1->3, not enough power", 3, 1, 0, 3, false, nil},
+		{"3/1->3, kickoff", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = time.Time{}
+		}},
+		{"3/1->3, timer running", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now()
+		}},
+		{"3/1->3, timer elapsed", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now().Add(-dt)
+		}},
+
+		// omit to switch down from 3p/1p configured/active
+		{"3/1->1, not enough power", 3, 1, 0, 3, false, nil},
+		{"3/1->1, kickoff", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = time.Time{}
+		}},
+		{"3/1->1, timer running", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now()
+		}},
+		{"3/1->1, timer elapsed", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now().Add(-dt)
+		}},
+
+		// switch down from 3p/3p configured/active
+		{"3/3->1, enough power", 3, 3, 1 * Voltage * maxA, 3, false, nil},
+		{"3/3->1, kickoff", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = time.Time{}
+		}},
+		{"3/3->1, timer running", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now()
+		}},
+		{"3/3->1, timer elapsed", 3, 3, 1 * Voltage * maxA, 1, true, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now().Add(-dt)
+		}},
+		{"3/3->1, switch executed", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
+			lp.phaseTimer = lp.clock.Now().Add(-dt)
+		}},
+	}
+
+	for _, tc := range tc {
+		t.Logf("%+v", tc)
+		clock := clock.NewMock()
+
+		lp := &LoadPoint{
+			log:          util.NewLogger("foo"),
+			clock:        clock,
+			charger:      charger,
+			MinCurrent:   minA,
+			MaxCurrent:   maxA,
+			Phases:       int64(tc.phases),
+			activePhases: int64(tc.activePhases),
+			Enable: ThresholdConfig{
+				Delay: dt,
+			},
+			Disable: ThresholdConfig{
+				Delay: dt,
+			},
+		}
+
+		if tc.prepare != nil {
+			tc.prepare(lp)
+		}
+
+		if tc.res {
+			charger.MockChargePhases.EXPECT().Phases1p3p(tc.toPhases).Return(nil)
+		}
+
+		if res := lp.pvScalePhases(tc.availablePower, minA, maxA); tc.res != res {
+			t.Errorf("expected %v, got %v", tc.res, res)
+		} else {
+			if lp.Phases != int64(tc.toPhases) {
+				t.Errorf("expected %dp, got %dp", tc.toPhases, lp.Phases)
+			}
+		}
+	}
+}