Repository: evcc-io/evcc
Pull request #2077 (2021-12-19 13:23:25)

Update appliance


Patch:
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..c70dcfea 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,8 +7,9 @@
 [![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
 
@@ -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/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..7b7027bf 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
@@ -877,8 +885,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)
 }
 
@@ -964,21 +975,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 +1012,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/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/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..f06aeda9 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)
 		}},
 	}