Repository: andig/evcc
Pull request #154 (2020-05-15 19:16:13)

Allow reading currents
Refs #152 


Patch:
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index 16dbfadf..00000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# Number of days of inactivity before an issue becomes stale
-daysUntilStale: 60
-# Number of days of inactivity before a stale issue is closed
-daysUntilClose: 7
-# Issues with these labels will never be considered stale
-exemptLabels:
-  - pinned
-  - security
-# Label to use when marking an issue as stale
-staleLabel: wontfix
-# Comment to post when marking an issue as stale. Set to `false` to disable
-markComment: >
-  This issue has been automatically marked as stale because it has not had
-  recent activity. It will be closed if no further activity occurs. Thank you
-  for your contributions.
-# Comment to post when closing a stale issue. Set to `false` to disable
-closeComment: false
-
diff --git a/.golangci.yml b/.golangci.yml
index 93e7d7f4..b9eda25a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -5,4 +5,3 @@ issues:
   - .regActualCurrent. is unused
   - .regOverchargeProtect. is unused
   - .regReset. is unused
-  - "`routeLogger` is unused"
diff --git a/.travis.yml b/.travis.yml
index c1e3d58d..a1436d1b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,7 +25,6 @@ before_install:
 script:
 # build
 - make
-- go mod tidy
 # ensure all changes are committed
 - test -z "$(git status --porcelain)" || (git status; git diff; false)
 
diff --git a/README.md b/README.md
index e7ee1963..6a3ba2fd 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
 ## Features
 
 - simple and clean user interface
-- multiple [chargers](#charger): Wallbe, Phoenix (includes ESL Walli), go-eCharger, NRGKick (Bluetooth and Connect), SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect, any other charger using scripting
-- multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Home Manager and SMA Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC), Tesla PowerWall
+- multiple [chargers](#charger): Wallbe, Phoenix (includes ESL Walli), go-eCharger, NRGKick, SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect, any other charger using scripting
+- multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), SMA Home Manager and SMA Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC), Tesla PowerWall
 - different [vehicles](#vehicle) to show battery status: Audi (eTron), BMW (i3), Tesla, Nissan (Leaf), any other vehicle using scripting
 - [plugins](#plugins) for integrating with hardware devices and home automation: Modbus (meters and grid inverters), MQTT and shell scripts
 - status notifications using [Telegram](https://telegram.org) and [PushOver](https://pushover.net)
@@ -21,40 +21,24 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
 
 ## Index
 
-- [Getting started](#getting-started)
 - [Installation](#installation)
+- [General concepts](#general-concepts)
+  - [Charge modes](#charge-modes)
+  - [Meter setup](#meter-setup)
+  - [Charger setup](#charger-setup)
 - [Configuration](#configuration)
   - [Charger](#charger)
   - [Meter](#meter)
   - [Vehicle](#vehicle)
-  - [Loadpoint](#loadpoint)
-  - [Considerations](#considerations)
 - [Plugins](#plugins)
   - [Modbus](#modbus-read-only)
   - [MQTT](#mqtt-readwrite)
   - [Script](#script-readwrite)
   - [HTTP](#http-readwrite)
   - [Combined status](#combined-status-read-only)
-- [Developer information](#developer-information)
+- [Developer](#developer)
 - [Background](#background)
 
-## Getting started
-
-1. Install EVCC. For details see [installation](#installation).
-2. Copy the default configuration file `evcc.dist.yaml` to `evcc.yaml` and open for editing.
-3. To create a minimal setup you need a [meter](#meter) (either grid meter or pv generation meter) and a supported [charger](#charger). A pv meter can also be replaced by a pv inverter. Both need be combined to a [loadpoint](#loadpoint).
-4. Configure both meter(s) and charger by:
-    - choosing the appropriate `type`
-    - add a `name` attribute than can later be referred to
-    - add configuration details depending on `type`
-  See `evcc.dist.yaml` for examples.
-5. Test your meter, charger and optional vehicle configuration by running
-
-        evcc meter|charger|vehicle
-
-6. Configure a loadpoint and refer to the meter, charger and vehicle using the defined `name` attributes.
-7. Provide optional configuration for MQTT, push messaging, database logging and custom menus.
-
 ## Installation
 
 EVCC is provided as binary executable file and docker image. Download the file for your platform and then execute like this:
@@ -86,6 +70,59 @@ To build EVCC from source, [Go](2) 1.13 is required:
 EVCC requires a supported charger and a combination of grid, PV and charge meter.
 All components **must** be installed by a certified professional.
 
+## General concepts
+
+### Charge modes
+
+Multiple charge modes are supported:
+
+- **Off**: disable the charger, even if car gets connected.
+- **Now** (**Sofortladen**): charge immediately with maximum allowed current.
+- **Min + PV**: charge immediately with minimum configured current. Additionally use PV if available.
+- **PV**: use PV as available. May not charge the car if PV remains dark.
+
+In general, due to the minimum value of 5% for signalling the EV duty cycle, the charger cannot limit the current to below 6A. If the available power calculation demands a limit less than 6A, handling depends on the charge mode. In **PV** mode, the charger will be disabled until available PV power supports charging with at least 6A. In **Min + PV** mode, charging will continue at minimum current of 6A and charge current will be raised as PV power becomes available again.
+
+### Meter setup
+
+For both PV modes, EVCC needs to assess how much residual PV power is available at the grid connection point and how much power the charger actually uses. Various methods are implemented to obtain this information, with different degrees of accuracy.
+
+- **PV meter**: Configuring a *PV meter* is the simplest option. *PV meter* measures the PV generation. The charger is allowed to consume:
+
+      Charge Power = PV Meter Power - Residual Power
+
+  The *pv meter* is expected to deliver negative values for export and should not return positive values.
+
+  *Residual Power* is a configurable assumption how much power remaining facilities beside the charger use.
+
+- **Grid meter**: Configuring a *grid meter* is the preferred option. The *grid meter* is expected to be a two-way meter (import+export) and return the current amount of grid export as negative value measured in Watt (W). The charger is then allowed to consume:
+
+      Charge Power = Current Charge Power - Grid Meter Power - Residual Power
+
+  In this setup, *residual power* is used as margin to account for fluctuations in PV production that may be faster than EVCC's control loop.
+
+- **Battery meter**: *battery meter* is used if a home battery is installed and you want charging the EV take priority over charging the home battery. As the home battery would otherwise "grab" all available PV power, this meter measures the home battery charging power.
+
+  With *grid meter* the charger is then allowed to consume:
+
+      Charge Power = Current Charge Power - Grid Meter Power + Battery Meter Power - Residual Power
+
+  or without *grid meter*
+
+      Charge Power = PV Meter Power + Battery Meter Power - Residual Power
+
+  The *battery meter* is expected to deliver negative values when charging and positive values when discharging.
+
+### Charger setup
+
+When using a *grid meter* for accurate control of PV utilization, EVCC needs to be able to determine the current charge power. There are two configurations for determining the *current charge power*:
+
+- **Charge meter**: A *charge meter* is often integrated into the charger but can also be installed separately. EVCC expects the *charge meter* to supply *charge power* in Watt (W) and preferably *total energy* in kWh.
+If *total energy* is supplied, it can be used to calculate the *charged energy* for the current charging cycle.
+
+- **No charge meter**: If no charge meter is installed, *charge power* is deducted from *charge current* as controlled by the charger. This method is less accurate than using a *charge meter* since the EV may chose to use less power than EVCC has allowed for consumption.
+If the charger supplies *total energy* for the charging cycle this value is preferred over the *charge meter*'s value (if present).
+
 ## Configuration
 
 The EVCC consists of four basic elements: *Charger*, *Meter* and *Vehicle* individually configured and attached to *Loadpoints*.
@@ -99,8 +136,7 @@ Charger is responsible for handling EV state and adjusting charge current. Avail
 - `phoenix-evcc`: chargers with Phoenix EV-CC-AC1-M controllers (ModBus connection)
 - `simpleevse`: chargers with SimpleEVSE controllers connected via ModBus (e.g. OpenWB)
 - `evsewifi`: chargers with SimpleEVSE controllers using [SimpleEVSE-Wifi](https://github.com/CurtRod/SimpleEVSE-WiFi)
-- `nrgkick-bt`: NRGKick chargers with Bluetooth connector (Linux only, not supported on Docker)
-- `nrgkick-connect`: NRGKick chargers with Connect module
+- `nrgkick`: NRGKick chargers with Connect module
 - `go-e`: go-eCharger chargers
 - `keba`: KEBA KeContact P20/P30 and BMW chargers (see [Preparation](#keba-preparation))
 - `mcc`: Mobile Charger Connect devices (Audi, Bentley, Porsche)
@@ -197,74 +233,8 @@ Available vehicle implementations are:
 - `nissan`: Nissan (Leaf)
 - `tesla`: Tesla (any model)
 - `renault`: Renault (Zoe, Kangoo ZE)
-- `porsche`: Porsche (Taycan)
 - `default`: default vehicle implementation using configurable [plugins](#plugins) for integrating any type of vehicle
 
-### Loadpoint
-
-A loadpoint combines meters, charger and vehicle together and adds optional configuration. A minimal loadpoint configuration needs either pv or grid meter and a charger. More meters can be added as needed:
-
-```yaml
-loadpoints:
-- name: main # name for logging
-  charger: wallbe # charger reference
-  vehicle: audi # vehicle reference
-  meters:
-    grid: sdm630 # grid meter reference
-    pv: sma # pv meter reference
-```
-
-More options are documented in the `evcc.dist.yaml` sample configuration.
-
-#### Charge modes
-
-The default *charge mode* upon start of EVCC is configured on the loadpoint. Multiple charge modes are supported:
-
-- **Off**: disable the charger, even if car gets connected.
-- **Now** (**Sofortladen**): charge immediately with maximum allowed current.
-- **Min + PV**: charge immediately with minimum configured current. Additionally use PV if available.
-- **PV**: use PV as available. May not charge the car if PV remains dark.
-
-In general, due to the minimum value of 5% for signalling the EV duty cycle, the charger cannot limit the current to below 6A. If the available power calculation demands a limit less than 6A, handling depends on the charge mode. In **PV** mode, the charger will be disabled until available PV power supports charging with at least 6A. In **Min + PV** mode, charging will continue at minimum current of 6A and charge current will be raised as PV power becomes available again.
-
-### Considerations
-
-For intelligent control of PV power usage, EVCC needs to assess how much residual PV power is available at the grid connection point and how much power the charger actually uses. Various methods are implemented to obtain this information, with different degrees of accuracy.
-
-- **PV meter**: Configuring a *PV meter* is the simplest option. *PV meter* measures the PV generation. The charger is allowed to consume:
-
-      Charge Power = PV Meter Power - Residual Power
-
-  The *pv meter* is expected to deliver negative values for export and should not return positive values.
-
-  *Residual Power* is a configurable assumption how much power remaining facilities beside the charger use.
-
-- **Grid meter**: Configuring a *grid meter* is the preferred option. The *grid meter* is expected to be a two-way meter (import+export) and return the current amount of grid export as negative value measured in Watt (W). The charger is then allowed to consume:
-
-      Charge Power = Current Charge Power - Grid Meter Power - Residual Power
-
-  In this setup, *residual power* is used as margin to account for fluctuations in PV production that may be faster than EVCC's control loop.
-
-- **Battery meter**: *battery meter* is used if a home battery is installed and you want charging the EV take priority over charging the home battery. As the home battery would otherwise "grab" all available PV power, this meter measures the home battery charging power.
-
-  With *grid meter* the charger is then allowed to consume:
-
-      Charge Power = Current Charge Power - Grid Meter Power + Battery Meter Power - Residual Power
-
-  or without *grid meter*
-
-      Charge Power = PV Meter Power + Battery Meter Power - Residual Power
-
-  The *battery meter* is expected to deliver negative values when charging and positive values when discharging.
-
-When using a *grid meter* for accurate control of PV utilization, EVCC needs to be able to determine the current charge power. There are two configurations for determining the *current charge power*:
-
-- **Charge meter**: A *charge meter* is often integrated into the charger but can also be installed separately. EVCC expects the *charge meter* to supply *charge power* in Watt (W) and preferably *total energy* in kWh.
-If *total energy* is supplied, it can be used to calculate the *charged energy* for the current charging cycle.
-
-- **No charge meter**: If no charge meter is installed, *charge power* is deducted from *charge current* as controlled by the charger. This method is less accurate than using a *charge meter* since the EV may chose to use less power than EVCC has allowed for consumption.
-If the charger supplies *total energy* for the charging cycle this value is preferred over the *charge meter*'s value (if present).
-
 ## Plugins
 
 Plugins are used to integrate physical devices and external data sources with EVCC. Plugins support both *read* and *write* access. When using plugins for *write* access, the actual data is provided as variable in form of `${var[:format]}`. If `format` is omitted, data is formatted according to the default Go `%v` [format](https://golang.org/pkg/fmt/). The variable is replaced with the actual data before the plugin is executed.
@@ -419,10 +389,6 @@ uri: https://volkszaehler/api/data/<uuid>.json?from=now
 method: GET # default HTTP method
 headers:
 - content-type: application/json
-auth: # basic authorization
-  type: basic
-  user: foo
-  password: bar
 insecure: false # set to true to trust self-signed certificates
 jq: .data.tuples[0][1] # parse response json
 scale: 0.001 # floating point factor applied to result, e.g. for kW to W conversion
@@ -451,7 +417,7 @@ charging:
   topic: openWB/lp/1/boolChargeStat
 ```
 
-## Developer information
+## Developer
 
 EVCC has the following internal API. The full documentation is available in GoDoc format in https://pkg.go.dev/github.com/andig/evcc/api.
 
diff --git a/api/api.go b/api/api.go
index 8879c4df..a6e4ab88 100644
--- a/api/api.go
+++ b/api/api.go
@@ -37,6 +37,11 @@ type MeterEnergy interface {
 	TotalEnergy() (float64, error)
 }
 
+// MeterCurrent is able to provide per-line current A
+type MeterCurrent interface {
+	Currents() (float64, float64, float64, error)
+}
+
 // Charger is able to provide current charging status and to enable/disabler charging
 type Charger interface {
 	Status() (ChargeStatus, error)
diff --git a/assets/index.html b/assets/index.html
index 1db9da35..7e3a7beb 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -56,10 +56,22 @@
       </div>
     </div>
   </nav>
-  <message-toasts></message-toasts>
   <router-view></router-view>
 </div>
 
+<div id="error">
+  <div class="toast" data-delay="2000" style="position: absolute; top: 4rem; right: 0.5rem;">
+    <div class="toast-header">
+      <strong class="mr-auto"><i class="text-danger fas fa-exclamation-triangle"></i> Error</strong>
+      <small v-if="error.status">HTTP {{error.status}}</small>
+      <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
+        <span aria-hidden="true">&times;</span>
+      </button>
+    </div>
+    <div class="toast-body">{{error.message}}</div>
+  </div>
+</div>
+
 <script type="text/x-template" id="embed-template">
   <div class="container mx-auto text-center">
     <h1 class="display-4 pt-3" v-if="title">{{title}}</h1>
@@ -75,28 +87,6 @@
   </div>
 </script>
 
-<div id="toasts">
-  <div aria-atomic="true" style="position: absolute; top: 4rem; right: 0.5rem; min-height: 200px; min-width: 300px">
-    <!-- Position it -->
-    <message-toast v-for="item in items" v-bind:item="item" :id="'message-id-'+item.id" :key="item.id">
-    </message-toast>
-  </div>
-</div>
-
-<script type="text/x-template" id="message-template">
-  <div class="toast" data-delay="10000" v-bind:data-autohide="item.type == 'warn' ? 'true' : 'false'">
-    <div class="toast-header">
-      <strong class="mr-auto" v-if="item.type != 'warn'"><i class="text-danger fas fa-exclamation-triangle"></i> Error</strong>
-      <strong class="mr-auto" v-if="item.type == 'warn'"><i class="text-warning fas fa-exclamation-triangle"></i> Warning</strong>
-      <small v-if="item.status">HTTP {{item.status}}</small>
-      <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
-        <span aria-hidden="true">&times;</span>
-      </button>
-    </div>
-    <div class="toast-body">{{item.message}}</div>
-  </div>
-</script>
-
 <script type="text/x-template" id="main-template">
   <div class="container">
     <div class="pricing-header px-3 pt-3 mx-auto text-center">
@@ -110,7 +100,7 @@
 
 <script type="text/x-template" id="mode-template">
   <div class="pricing-header px-3 pb-3 mx-auto text-center">
-    <div class="btn-group btn-group-toggle py-4 mb-2">
+    <div class="btn-group btn-group-toggle py-4 mb-2" data-toggle="buttons">
       <label class="btn btn-outline-primary shadow-none" v-bind:class="{active:mode=='off'}">
         <input type="radio" value="off" v-on:click="mode='off'">Stop
       </label>
diff --git a/assets/js/app.js b/assets/js/app.js
index 9a680642..0c2787a9 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -42,13 +42,7 @@ let store = {
       if (force || this[k] !== undefined) {
         this[k] = msg[k];
       } else {
-        if (k == "error") {
-          toasts.error({message: msg[k]});
-        } else if (k == "warn") {
-          toasts.warn({message: msg[k]});
-        } else {
-          console.log("invalid key: " + k);
-        }
+        console.log("invalid key: " + k);
       }
     }, this.state);
   },
@@ -57,7 +51,7 @@ let store = {
       axios.get("config").then(function(response) {
         store.update(response.data);
         store.initialized = true;
-      }).catch(toasts.error);
+      }).catch(error.raise);
     }
   }
 };
@@ -69,7 +63,7 @@ let store = {
 window.setInterval(function() {
   axios.get("health").catch(function(res) {
     res.message = "Server unavailable";
-    toasts.error(res)
+    error.raise(res)
   });
 }, 5000);
 
@@ -77,40 +71,16 @@ window.setInterval(function() {
 // Components
 //
 
-const toasts = new Vue({
-  el: "#toasts",
+const error = new Vue({
+  el: '#error',
   data: {
-    items: {},
-    count: 0,
+    error: {},
   },
   methods: {
-    raise: function (msg) {
-      msg.id = this.count++;
-      Vue.set(this.items, msg.id, msg);
-    },
-    error: function (error) {
-      error.type = "error";
-      this.raise(error)
-    },
-    warn: function (error) {
-      error.type = "warn";
-      this.raise(error);
-    },
-    remove: function (msg) {
-      Vue.delete(this.items, msg.id);
+    raise: function(error) {
+      this.error = error;
+      $('.toast').toast('show');
     },
-  }
-});
-
-Vue.component('message-toast', {
-  props: ['item'],
-  template: '#message-template',
-  mounted: function () {
-    const id = "#message-id-" + this.item.id;
-    $(id).toast('show');
-    $(id).on('hidden.bs.toast', function () {
-      toasts.remove(this.item);
-    })
   },
 });
 
@@ -252,5 +222,4 @@ const router = new VueRouter({
 const app = new Vue({
   router,
 }).$mount("#app");
-
-store.init();
+store.init;
\ No newline at end of file
diff --git a/charger/config.go b/charger/config.go
index cfb49b51..6bffb140 100644
--- a/charger/config.go
+++ b/charger/config.go
@@ -22,10 +22,8 @@ func NewFromConfig(log *util.Logger, typ string, other map[string]interface{}) a
 		c = NewPhoenixEMCPFromConfig(log, other)
 	case "phoenix-evcc":
 		c = NewPhoenixEVCCFromConfig(log, other)
-	case "nrgkick-bluetooth", "nrgkick-bt", "nrgble":
-		c = NewNRGKickBLEFromConfig(log, other)
-	case "nrgkick-connect", "nrgconnect":
-		c = NewNRGKickConnectFromConfig(log, other)
+	case "nrgkick", "nrg", "kick":
+		c = NewNRGKickFromConfig(log, other)
 	case "go-e", "goe":
 		c = NewGoEFromConfig(log, other)
 	case "evsewifi":
diff --git a/charger/go-e.go b/charger/go-e.go
index 6f2c7fe0..c9ee3715 100644
--- a/charger/go-e.go
+++ b/charger/go-e.go
@@ -139,3 +139,13 @@ func (c *GoE) ChargedEnergy() (float64, error) {
 	energy := float64(status.Dws) / 3.6e6
 	return energy, err
 }
+
+// Currents implements the MeterCurrent interface
+func (c *GoE) Currents() (float64, float64, float64, error) {
+	var status goeStatusResponse
+	_, err := c.GetJSON(c.apiURL(goeStatus), &status)
+	if len(status.Nrg) == 16 {
+		return float64(status.Nrg[4]) / 10, float64(status.Nrg[5]) / 10, float64(status.Nrg[6]) / 10, nil
+	}
+	return 0, 0, 0, err
+}
diff --git a/charger/keba.go b/charger/keba.go
index cb4f9d37..bcdd6323 100644
--- a/charger/keba.go
+++ b/charger/keba.go
@@ -87,10 +87,8 @@ func (c *Keba) send(msg string) error {
 	return err
 }
 
-func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, closeC <-chan struct{}) {
+func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error) {
 	t := time.NewTimer(c.timeout)
-	defer close(resC)
-	defer close(errC)
 	for {
 		select {
 		case msg := <-c.recv:
@@ -99,7 +97,7 @@ func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, c
 				resC <- msg
 				return
 			}
-			// matching report id
+			// matching report result
 			if msg.Report != nil && report == msg.Report.ID {
 				resC <- msg
 				return
@@ -107,8 +105,6 @@ func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, c
 		case <-t.C:
 			errC <- errors.New("recv timeout")
 			return
-		case <-closeC:
-			return
 		}
 	}
 }
@@ -116,11 +112,13 @@ func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, c
 func (c *Keba) roundtrip(msg string, report int, res interface{}) error {
 	resC := make(chan keba.UDPMsg)
 	errC := make(chan error)
-	closeC := make(chan struct{})
 
-	defer close(closeC)
+	defer func() {
+		close(resC)
+		close(errC)
+	}()
 
-	go c.receive(report, resC, errC, closeC)
+	go c.receive(report, resC, errC)
 
 	if err := c.send(msg); err != nil {
 		return err
@@ -152,7 +150,7 @@ func (c *Keba) Status() (api.ChargeStatus, error) {
 		return api.StatusA, err
 	}
 
-	if kr.Plug < 5 {
+	if kr.Plug == 0 {
 		return api.StatusA, nil
 	}
 	if kr.State == 2 {
@@ -162,7 +160,7 @@ func (c *Keba) Status() (api.ChargeStatus, error) {
 		return api.StatusC, nil
 	}
 
-	return api.StatusA, fmt.Errorf("unexpected status: %+v", kr)
+	return api.StatusA, fmt.Errorf("unexpected status: %v", kr)
 }
 
 // Enabled implements the Charger.Enabled interface
@@ -220,22 +218,13 @@ func (c *Keba) CurrentPower() (float64, error) {
 	return float64(kr.P) / 1e3, err
 }
 
-// TotalEnergy implements the MeterEnergy interface
-func (c *Keba) TotalEnergy() (float64, error) {
-	var kr keba.Report3
-	err := c.roundtrip("report 3", 3, &kr)
-
-	// mW to W
-	return float64(kr.ETotal) / 1e4, err
-}
-
 // ChargedEnergy implements the ChargeRater interface
 func (c *Keba) ChargedEnergy() (float64, error) {
 	var kr keba.Report3
 	err := c.roundtrip("report 3", 3, &kr)
 
 	// 0,1Wh to kWh
-	return float64(kr.EPres) / 1e4, err
+	return float64(kr.EPres) / 1e5, err
 }
 
 // Currents implements the MeterCurrents interface
diff --git a/charger/mcc.go b/charger/mcc.go
index 3c7eb078..a7852581 100644
--- a/charger/mcc.go
+++ b/charger/mcc.go
@@ -15,13 +15,12 @@ import (
 )
 
 const (
-	mccAPILogin                   apiFunction = "jwt/login"
-	mccAPIRefresh                 apiFunction = "jwt/refresh"
-	mccAPIChargeState             apiFunction = "v1/api/WebServer/properties/chargeState"
-	mccAPICurrentSession          apiFunction = "v1/api/WebServer/properties/swaggerCurrentSession"
-	mccAPIEnergy                  apiFunction = "v1/api/iCAN/properties/propjIcanEnergy"
-	mccAPISetCurrentLimit         apiFunction = "v1/api/SCC/properties/propHMICurrentLimit?value="
-	mccAPICurrentCableInformation apiFunction = "v1/api/SCC/properties/json_CurrentCableInformation"
+	apiLogin                   apiFunction = "jwt/login"
+	apiRefresh                 apiFunction = "jwt/refresh"
+	apiChargeState             apiFunction = "v1/api/WebServer/properties/chargeState"
+	apiCurrentSession          apiFunction = "v1/api/WebServer/properties/swaggerCurrentSession"
+	apiSetCurrentLimit         apiFunction = "v1/api/SCC/properties/propHMICurrentLimit?value="
+	apiCurrentCableInformation apiFunction = "v1/api/SCC/properties/json_CurrentCableInformation"
 )
 
 // MCCErrorResponse is the API response if status not OK
@@ -124,7 +123,7 @@ func (mcc *MobileConnect) fetchToken(request *http.Request) error {
 
 // login as the home user with the given password
 func (mcc *MobileConnect) login(password string) error {
-	uri := fmt.Sprintf("%s/%s", mcc.uri, mccAPILogin)
+	uri := fmt.Sprintf("%s/%s", mcc.uri, apiLogin)
 
 	data := url.Values{
 		"user": []string{"user"},
@@ -143,7 +142,7 @@ func (mcc *MobileConnect) login(password string) error {
 
 // refresh the auth token with a new one
 func (mcc *MobileConnect) refresh() error {
-	uri := fmt.Sprintf("%s/%s", mcc.uri, mccAPIRefresh)
+	uri := fmt.Sprintf("%s/%s", mcc.uri, apiRefresh)
 
 	req, err := http.NewRequest(http.MethodGet, uri, nil)
 	if err != nil {
@@ -200,25 +199,19 @@ func (mcc *MobileConnect) getEscapedJSON(uri string, result interface{}) error {
 	}
 
 	b, err := mcc.Request(req)
-	if err != nil {
-		return err
-	}
-
-	s, err := strconv.Unquote(strings.Trim(string(b), "\n"))
-	if err != nil {
-		return err
-	}
-
-	if s == "" {
-		return nil // empty response
+	if err == nil {
+		var s string
+		if s, err = strconv.Unquote(strings.Trim(string(b), "\n")); err == nil {
+			err = json.Unmarshal([]byte(s), &result)
+		}
 	}
 
-	return json.Unmarshal([]byte(s), &result)
+	return err
 }
 
 // Status implements the Charger.Status interface
 func (mcc *MobileConnect) Status() (api.ChargeStatus, error) {
-	b, err := mcc.getValue(mcc.apiURL(mccAPIChargeState))
+	b, err := mcc.getValue(mcc.apiURL(apiChargeState))
 	if err != nil {
 		return api.StatusNone, err
 	}
@@ -245,7 +238,7 @@ func (mcc *MobileConnect) Status() (api.ChargeStatus, error) {
 // Enabled implements the Charger.Enabled interface
 func (mcc *MobileConnect) Enabled() (bool, error) {
 	// Check if the car is connected and Paused, Active, or Finished
-	b, err := mcc.getValue(mcc.apiURL(mccAPIChargeState))
+	b, err := mcc.getValue(mcc.apiURL(apiChargeState))
 	if err != nil {
 		return false, err
 	}
@@ -278,7 +271,7 @@ func (mcc *MobileConnect) MaxCurrent(current int64) error {
 	// and then return an error if the value is outside of the limits or
 	// otherwise set the new value
 	if mcc.cableInformation.MaxValue == 0 {
-		if err := mcc.getEscapedJSON(mcc.apiURL(mccAPICurrentCableInformation), &mcc.cableInformation); err != nil {
+		if err := mcc.getEscapedJSON(mcc.apiURL(apiCurrentCableInformation), &mcc.cableInformation); err != nil {
 			return err
 		}
 	}
@@ -291,7 +284,7 @@ func (mcc *MobileConnect) MaxCurrent(current int64) error {
 		return fmt.Errorf("value is higher than the allowed maximum value %d", mcc.cableInformation.MaxValue)
 	}
 
-	url := fmt.Sprintf("%s%d", mcc.apiURL(mccAPISetCurrentLimit), current)
+	url := fmt.Sprintf("%s%d", mcc.apiURL(apiSetCurrentLimit), current)
 
 	req, err := mcc.request(http.MethodPut, url)
 	if err != nil {
@@ -311,28 +304,10 @@ func (mcc *MobileConnect) MaxCurrent(current int64) error {
 	return nil
 }
 
-// CurrentPower implements the Meter interface.
-func (mcc *MobileConnect) CurrentPower() (float64, error) {
-	var energy MCCEnergy
-	err := mcc.getEscapedJSON(mcc.apiURL(mccAPIEnergy), &energy)
-
-	return energy.L1.Power + energy.L2.Power + energy.L3.Power, err
-}
-
-// ChargedEnergy implements the ChargeRater interface.
-func (mcc *MobileConnect) ChargedEnergy() (float64, error) {
-	var currentSession MCCCurrentSession
-	if err := mcc.getEscapedJSON(mcc.apiURL(mccAPICurrentSession), &currentSession); err != nil {
-		return 0, err
-	}
-
-	return currentSession.EnergySumKwh, nil
-}
-
 // ChargingTime yields current charge run duration
 func (mcc *MobileConnect) ChargingTime() (time.Duration, error) {
 	var currentSession MCCCurrentSession
-	if err := mcc.getEscapedJSON(mcc.apiURL(mccAPICurrentSession), &currentSession); err != nil {
+	if err := mcc.getEscapedJSON(mcc.apiURL(apiCurrentSession), &currentSession); err != nil {
 		return 0, err
 	}
 
diff --git a/charger/nrgble.go b/charger/nrgble.go
deleted file mode 100644
index ae9fa064..00000000
--- a/charger/nrgble.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// +build !linux
-
-package charger
-
-import (
-	"github.com/andig/evcc/api"
-	"github.com/andig/evcc/util"
-)
-
-// NewNRGKickBLEFromConfig creates a NRGKickBLE charger from generic config
-func NewNRGKickBLEFromConfig(log *util.Logger, other map[string]interface{}) api.Charger {
-	log.FATAL.Fatal("config: NRGKick bluetooth is only supported on linux")
-	return nil
-}
diff --git a/charger/nrgble/nrg_linux.go b/charger/nrgble/nrg_linux.go
deleted file mode 100644
index bf79b671..00000000
--- a/charger/nrgble/nrg_linux.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package nrgble
-
-import (
-	"errors"
-	"fmt"
-	"strings"
-
-	"github.com/muka/go-bluetooth/api"
-	"github.com/muka/go-bluetooth/bluez/profile/adapter"
-	"github.com/muka/go-bluetooth/bluez/profile/agent"
-	"github.com/muka/go-bluetooth/bluez/profile/device"
-)
-
-func FindDevice(a *adapter.Adapter1, hwaddr string) (*device.Device1, error) {
-	dev, err := Discover(a, hwaddr)
-	if err != nil {
-		return nil, err
-	}
-	if dev == nil {
-		return nil, errors.New("Device not found, is it advertising?")
-	}
-
-	return dev, nil
-}
-
-func Discover(a *adapter.Adapter1, hwaddr string) (*device.Device1, error) {
-	err := a.FlushDevices()
-	if err != nil {
-		return nil, err
-	}
-
-	discovery, cancel, err := api.Discover(a, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	defer cancel()
-
-	for ev := range discovery {
-		dev, err1 := device.NewDevice1(ev.Path)
-		if err != nil {
-			return nil, err1
-		}
-
-		if dev == nil || dev.Properties == nil {
-			continue
-		}
-
-		p := dev.Properties
-
-		if p.Address != hwaddr {
-			continue
-		}
-
-		return dev, nil
-	}
-
-	return nil, nil
-}
-
-func Connect(dev *device.Device1, ag *agent.SimpleAgent, adapterID string) error {
-	props, err := dev.GetProperties()
-	if err != nil {
-		return fmt.Errorf("Failed to load props: %s", err)
-	}
-
-	if props.Connected {
-		return nil
-	}
-
-	if !props.Paired || !props.Trusted {
-		if err := dev.Pair(); err != nil {
-			return fmt.Errorf("Pair failed: %s", err)
-		}
-
-		if err := agent.SetTrusted(adapterID, dev.Path()); err != nil {
-			return fmt.Errorf("Set trusted failed: %s", err)
-		}
-	}
-
-	if !props.Connected {
-		err = dev.Connect()
-		if err != nil {
-			if !strings.Contains(err.Error(), "Connection refused") {
-				return fmt.Errorf("Connect failed: %s", err)
-			}
-		}
-	}
-
-	return nil
-}
diff --git a/charger/nrgble/service.go b/charger/nrgble/service.go
deleted file mode 100644
index c076c148..00000000
--- a/charger/nrgble/service.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package nrgble
-
-const (
-	InfoService           = "8f75bba0-c903-11e4-9fe8-0002a5d6b15d"
-	EnergyService         = "0379e580-ad1b-11e4-8bdd-0002a5d6b15d"
-	PowerService          = "fd005380-b065-11e4-9ce2-0002a5d6b15d"
-	VoltageCurrentService = "171bad00-b066-11e4-aeda-0002a5d6b15d"
-	SettingsService       = "14b3afc0-ad1b-11e4-baab-0002a5d6b15d"
-)
-
-type Info struct {
-	Current              int  `struc:"int8"`   // B appInfo[0]
-	KWhPer100            int  `struc:"int16"`  // H appInfo[1] / 10
-	AmountPerKWh         int  `struc:"int8"`   // B appInfo[2] / 100
-	FIEnabled            int  `struc:"int8"`   // B 1 == appInfo[3]
-	ErrorCode            int  `struc:"int8"`   // B appInfo[4]
-	Efficiency           int  `struc:"int8"`   // B appInfo[5]
-	ChargingActive       bool `struc:"int8"`   // B 1 == appInfo[6]
-	PauseCharging        bool `struc:"int8"`   // B 1 == appInfo[7]
-	ChargingCurrentMax   int  `struc:"int8"`   // B appInfo[8]
-	BLETransmissionPower int  `struc:"int8"`   // B appInfo[9]
-	Pad                  int  `struc:"[2]pad"` // xx
-}
-
-type Energy struct {
-	TotalEnergy         int  `struc:"uint32"` // L energie02[0] / 1000
-	EnergyLastCharge    int  `struc:"uint32"` // L energie02[1] / 1000
-	Energy2ndLastCharge int  `struc:"uint32"` // L energie02[2] / 1000
-	Energy3rdLastCharge int  `struc:"uint32"` // L energie02[3] / 1000
-	ChargingEnergyLimit int  `struc:"uint16"` // H energie02[4] / 100
-	Pad                 byte `struc:"pad"`    // x
-}
-
-type Power struct {
-	TotalPower        int `struc:"uint16"` // H leistung[0] / 100
-	L1                int `struc:"uint16"` // H leistung[1] / 100
-	L2                int `struc:"uint16"` // H leistung[2] / 100
-	L3                int `struc:"uint16"` // H leistung[3] / 100
-	PeakPower         int `struc:"uint16"` // H leistung[4] / 100;
-	Frequency         int `struc:"uint16"` // H leistung[5] / 100
-	Temperature       int `struc:"int16"`  // h leistung[6]
-	RemainingDistance int `struc:"uint16"` // H leistung[7] / 10
-	Costs             int `struc:"uint16"` // H leistung[8] / 100
-	CPSignal          int `struc:"int8"`   // b round(((leistung[9] << 8) / 100) + 0.4, 1)
-}
-
-type VoltageCurrent struct {
-	VoltageL1 int `struc:"uint16"` // H voltageAndCurrent[0] / 10
-	VoltageL2 int `struc:"uint16"` // H voltageAndCurrent[1] / 10
-	VoltageL3 int `struc:"uint16"` // H voltageAndCurrent[2] / 10
-	CurrentL1 int `struc:"uint16"` // H voltageAndCurrent[3] / 100
-	CurrentL2 int `struc:"uint16"` // H voltageAndCurrent[4] / 100
-	CurrentL3 int `struc:"uint16"` // H voltageAndCurrent[5] / 100
-	Pad       int `struc:"[2]pad"` // xx
-}
-
-type Settings struct {
-	PIN                  int  `struc:"uint16"` // H SettingsPIN
-	Current              int  `struc:"uint8"`  // B SettingsChargingCurrentValue
-	ChargingEnergyLimit  int  `struc:"uint16"` // H SettingsChargingEnergyOff
-	KWhPer100            int  `struc:"uint16"` // H round(SettingsKWhPer100Value * 10)
-	AmountPerKWh         int  `struc:"uint8"`  // B round(SettingsAmountPerKWhValue * 100)
-	Pad                  int  `struc:"[2]pad"` // xx
-	Efficiency           int  `struc:"uint8"`  // B SettingsEfficacyValue
-	PauseCharging        bool `struc:"uint8"`  // B 1 if SettingsPauseCharging else 0
-	BLETransmissionPower int  `struc:"int8"`   // b SettingsBLETransmissionPowerValue
-	PadTail              int  `struc:"[5]pad"` // xxxxx
-}
diff --git a/charger/nrgble_linux.go b/charger/nrgble_linux.go
deleted file mode 100644
index 5f91ae6c..00000000
--- a/charger/nrgble_linux.go
+++ /dev/null
@@ -1,325 +0,0 @@
-package charger
-
-import (
-	"bytes"
-	"fmt"
-	"os"
-	"strconv"
-	"time"
-
-	"github.com/andig/evcc/api"
-	"github.com/andig/evcc/charger/nrgble"
-	"github.com/andig/evcc/util"
-	"github.com/godbus/dbus"
-	"github.com/lunixbochs/struc"
-	"github.com/muka/go-bluetooth/bluez/profile/adapter"
-	"github.com/muka/go-bluetooth/bluez/profile/agent"
-	"github.com/muka/go-bluetooth/bluez/profile/device"
-	"github.com/muka/go-bluetooth/hw"
-)
-
-// NRGKickBLE charger implementation
-type NRGKickBLE struct {
-	log        *util.Logger
-	timer      *time.Timer
-	adapter    *adapter.Adapter1
-	agent      *agent.SimpleAgent
-	dev        *device.Device1
-	device     string
-	macaddress string
-	pin        int
-}
-
-// NewNRGKickBLEFromConfig creates a NRGKickBLE charger from generic config
-func NewNRGKickBLEFromConfig(log *util.Logger, other map[string]interface{}) api.Charger {
-	cc := struct{ Device, MacAddress, PIN string }{
-		Device: "hci0",
-	}
-	util.DecodeOther(log, other, &cc)
-
-	// decode PIN with leading zero
-	pin, err := strconv.Atoi(cc.PIN)
-	if err != nil {
-		log.FATAL.Fatalf("config: invalid pin '%s'", cc.PIN)
-	}
-
-	return NewNRGKickBLE(cc.Device, cc.MacAddress, pin)
-}
-
-// NewNRGKickBLE creates NRGKickBLE charger
-func NewNRGKickBLE(device, macaddress string, pin int) *NRGKickBLE {
-	logger := util.NewLogger("nrgbt")
-	logger.WARN.Println("-- experimental --")
-
-	// set LE mode
-	btmgmt := hw.NewBtMgmt(device)
-
-	if len(os.Getenv("DOCKER")) > 0 {
-		btmgmt.BinPath = "./bin/docker-btmgmt"
-	}
-
-	err := btmgmt.SetPowered(false)
-	if err == nil {
-		err = btmgmt.SetLe(true)
-		if err == nil {
-			err = btmgmt.SetBredr(false)
-			if err == nil {
-				err = btmgmt.SetPowered(true)
-			}
-		}
-	}
-
-	if err != nil {
-		logger.FATAL.Fatal(err)
-	}
-
-	adapt, err := adapter.NewAdapter1FromAdapterID(device)
-	if err != nil {
-		logger.FATAL.Fatal(err)
-	}
-
-	//Connect DBus System bus
-	conn, err := dbus.SystemBus()
-	if err != nil {
-		logger.FATAL.Fatal(err)
-	}
-
-	// do not reuse agent0 from service
-	agent.NextAgentPath()
-
-	ag := agent.NewSimpleAgent()
-	err = agent.ExposeAgent(conn, ag, agent.CapNoInputNoOutput, true)
-	if err != nil {
-		logger.FATAL.Fatal(err)
-	}
-
-	nrg := &NRGKickBLE{
-		log:        logger,
-		timer:      time.NewTimer(1),
-		device:     device,
-		macaddress: macaddress,
-		pin:        pin,
-		adapter:    adapt,
-		agent:      ag,
-	}
-
-	return nrg
-}
-
-func (nrg *NRGKickBLE) connect() (*device.Device1, error) {
-	dev, err := nrgble.FindDevice(nrg.adapter, nrg.macaddress)
-	if err != nil {
-		return nil, fmt.Errorf("findDevice: %s", err)
-	}
-
-	err = nrgble.Connect(dev, nrg.agent, nrg.device)
-	if err != nil {
-		return nil, err
-	}
-
-	return dev, nil
-}
-
-func (nrg *NRGKickBLE) close() {
-	if nrg.dev != nil {
-		nrg.dev.Close()
-		nrg.dev = nil
-	}
-}
-
-func (nrg *NRGKickBLE) waitTimer() {
-	<-nrg.timer.C
-}
-
-func (nrg *NRGKickBLE) setTimer() {
-	nrg.timer.Stop() // can be stopped without reading as channel is always drained
-	nrg.timer.Reset(2 * time.Second)
-}
-
-func (nrg *NRGKickBLE) read(service string, res interface{}) error {
-	nrg.waitTimer()
-	defer nrg.setTimer()
-
-	if nrg.dev == nil {
-		dev, err := nrg.connect()
-		if err != nil {
-			return err
-		}
-		nrg.dev = dev
-	}
-
-	char, err := nrg.dev.GetCharByUUID(service)
-	if err != nil {
-		nrg.close()
-		return err
-	}
-
-	b, err := char.ReadValue(map[string]interface{}{})
-	if err != nil {
-		nrg.close()
-		return err
-	}
-	nrg.log.TRACE.Printf("read %s %0x", service, b)
-
-	return struc.Unpack(bytes.NewReader(b), res)
-}
-
-func (nrg *NRGKickBLE) write(service string, val interface{}) error {
-	var out bytes.Buffer
-	if err := struc.Pack(&out, val); err != nil {
-		return err
-	}
-	nrg.log.TRACE.Printf("write %s %0x", service, out.Bytes())
-
-	nrg.waitTimer()
-	defer nrg.setTimer()
-
-	if nrg.dev == nil {
-		dev, err := nrg.connect()
-		if err != nil {
-			return err
-		}
-		nrg.dev = dev
-	}
-
-	char, err := nrg.dev.GetCharByUUID(service)
-	if err != nil {
-		nrg.close()
-		return err
-	}
-
-	if err := char.WriteValue(out.Bytes(), map[string]interface{}{}); err != nil {
-		nrg.close()
-		return err
-	}
-
-	return nil
-}
-
-func (nrg *NRGKickBLE) defaultSettings(info nrgble.Info) nrgble.Settings {
-	return nrgble.Settings{
-		PIN:                  nrg.pin,
-		ChargingEnergyLimit:  19997, // magic const for "disable"
-		KWhPer100:            info.KWhPer100,
-		AmountPerKWh:         info.AmountPerKWh,
-		Efficiency:           info.Efficiency,
-		PauseCharging:        info.PauseCharging,
-		BLETransmissionPower: info.BLETransmissionPower,
-	}
-}
-
-// Status implements the Charger.Status interface
-func (nrg *NRGKickBLE) Status() (api.ChargeStatus, error) {
-	res := nrgble.Power{}
-	if err := nrg.read(nrgble.PowerService, &res); err != nil {
-		return api.StatusF, err
-	}
-
-	nrg.log.TRACE.Printf("power: %+v", res)
-
-	switch res.CPSignal {
-	case 3:
-		return api.StatusB, nil
-	case 2:
-		return api.StatusC, nil
-	case 4:
-		return api.StatusA, nil
-	}
-
-	return api.StatusA, fmt.Errorf("unexpected cp signal: %d", res.CPSignal)
-}
-
-// Enabled implements the Charger.Enabled interface
-func (nrg *NRGKickBLE) Enabled() (bool, error) {
-	res := nrgble.Info{}
-	if err := nrg.read(nrgble.InfoService, &res); err != nil {
-		nrg.log.TRACE.Println(err)
-		return false, err
-	}
-
-	nrg.log.TRACE.Printf("info: %+v", res)
-
-	return !res.PauseCharging, nil
-}
-
-// Enable implements the Charger.Enable interface
-func (nrg *NRGKickBLE) Enable(enable bool) error {
-	res := nrgble.Info{}
-	if err := nrg.read(nrgble.InfoService, &res); err != nil {
-		return err
-	}
-
-	nrg.log.TRACE.Printf("info: %+v", res)
-
-	settings := nrg.defaultSettings(res)
-	settings.PauseCharging = !enable
-
-	return nrg.write(nrgble.SettingsService, &settings)
-}
-
-// MaxCurrent implements the Charger.MaxCurrent interface
-func (nrg *NRGKickBLE) MaxCurrent(current int64) error {
-	res := nrgble.Info{}
-	if err := nrg.read(nrgble.InfoService, &res); err != nil {
-		return err
-	}
-
-	nrg.log.TRACE.Printf("info: %+v", res)
-
-	settings := nrg.defaultSettings(res)
-	settings.Current = int(current)
-
-	return nrg.write(nrgble.SettingsService, &settings)
-}
-
-// CurrentPower implements the Meter interface.
-func (nrg *NRGKickBLE) CurrentPower() (float64, error) {
-	res := nrgble.Power{}
-	if err := nrg.read(nrgble.PowerService, &res); err != nil {
-		return 0, err
-	}
-
-	nrg.log.TRACE.Printf("power: %+v", res)
-
-	return float64(res.TotalPower) * 10, nil
-}
-
-// TotalEnergy implements the MeterEnergy interface.
-func (nrg *NRGKickBLE) TotalEnergy() (float64, error) {
-	res := nrgble.Energy{}
-	if err := nrg.read(nrgble.EnergyService, &res); err != nil {
-		return 0, err
-	}
-
-	nrg.log.TRACE.Printf("energy: %+v", res)
-
-	return float64(res.TotalEnergy) / 1000, nil
-}
-
-// Currents implements the MeterCurrent interface.
-func (nrg *NRGKickBLE) Currents() (float64, float64, float64, error) {
-	res := nrgble.VoltageCurrent{}
-	if err := nrg.read(nrgble.VoltageCurrentService, &res); err != nil {
-		return 0, 0, 0, err
-	}
-
-	nrg.log.TRACE.Printf("voltage/current: %+v", res)
-
-	return float64(res.CurrentL1) / 100,
-		float64(res.CurrentL2) / 100,
-		float64(res.CurrentL3) / 100,
-		nil
-}
-
-// ChargedEnergy implements the ChargeRater interface.
-// NOTE: apparently shows energy of a stopped charging session, hence substituted by TotalEnergy
-// func (nrg *NRGKickBLE) ChargedEnergy() (float64, error) {
-// 	res := nrgble.Energy{}
-// 	if err := nrg.read(nrgble.EnergyService, &res); err != nil {
-// 		return 0, err
-// 	}
-
-// 	nrg.log.TRACE.Printf("energy: %+v", res)
-
-// 	return float64(res.EnergyLastCharge) / 1000, nil
-// }
diff --git a/charger/nrgconnect.go b/charger/nrgkick.go
similarity index 74%
rename from charger/nrgconnect.go
rename to charger/nrgkick.go
index f205c2d0..b3385692 100644
--- a/charger/nrgconnect.go
+++ b/charger/nrgkick.go
@@ -57,26 +57,26 @@ type NRGDeviceMetadata struct {
 	Password string
 }
 
-// NRGKickConnect charger implementation
-type NRGKickConnect struct {
+// NRGKick charger implementation
+type NRGKick struct {
 	*util.HTTPHelper
 	IP         string
 	MacAddress string
 	Password   string
 }
 
-// NewNRGKickConnectFromConfig creates a NRGKickConnect charger from generic config
-func NewNRGKickConnectFromConfig(log *util.Logger, other map[string]interface{}) api.Charger {
+// NewNRGKickFromConfig creates a NRGKick charger from generic config
+func NewNRGKickFromConfig(log *util.Logger, other map[string]interface{}) api.Charger {
 	cc := struct{ IP, MacAddress, Password string }{}
 	util.DecodeOther(log, other, &cc)
 
-	return NewNRGKickConnect(cc.IP, cc.MacAddress, cc.Password)
+	return NewNRGKick(cc.IP, cc.MacAddress, cc.Password)
 }
 
-// NewNRGKickConnect creates NRGKickConnect charger
-func NewNRGKickConnect(IP, MacAddress, Password string) *NRGKickConnect {
-	nrg := &NRGKickConnect{
-		HTTPHelper: util.NewHTTPHelper(util.NewLogger("nrgc")),
+// NewNRGKick creates NRGKick charger
+func NewNRGKick(IP, MacAddress, Password string) *NRGKick {
+	nrg := &NRGKick{
+		HTTPHelper: util.NewHTTPHelper(util.NewLogger("kick")),
 		IP:         IP,
 		MacAddress: MacAddress,
 		Password:   Password,
@@ -87,11 +87,11 @@ func NewNRGKickConnect(IP, MacAddress, Password string) *NRGKickConnect {
 	return nrg
 }
 
-func (nrg *NRGKickConnect) apiURL(api apiFunction) string {
+func (nrg *NRGKick) apiURL(api apiFunction) string {
 	return fmt.Sprintf("%s/api/%s/%s", nrg.IP, api, nrg.MacAddress)
 }
 
-func (nrg *NRGKickConnect) getJSON(url string, result interface{}) error {
+func (nrg *NRGKick) getJSON(url string, result interface{}) error {
 	b, err := nrg.GetJSON(url, result)
 	if err != nil && len(b) > 0 {
 		var error NRGResponse
@@ -105,7 +105,7 @@ func (nrg *NRGKickConnect) getJSON(url string, result interface{}) error {
 	return err
 }
 
-func (nrg *NRGKickConnect) putJSON(url string, request interface{}) error {
+func (nrg *NRGKick) putJSON(url string, request interface{}) error {
 	b, err := nrg.PutJSON(url, request)
 	if err != nil && len(b) == 0 {
 		return err
@@ -120,12 +120,12 @@ func (nrg *NRGKickConnect) putJSON(url string, request interface{}) error {
 }
 
 // Status implements the Charger.Status interface
-func (nrg *NRGKickConnect) Status() (api.ChargeStatus, error) {
+func (nrg *NRGKick) Status() (api.ChargeStatus, error) {
 	return api.StatusC, nil
 }
 
 // Enabled implements the Charger.Enabled interface
-func (nrg *NRGKickConnect) Enabled() (bool, error) {
+func (nrg *NRGKick) Enabled() (bool, error) {
 	var settings NRGSettings
 	err := nrg.getJSON(nrg.apiURL(apiSettings), settings)
 
@@ -133,7 +133,7 @@ func (nrg *NRGKickConnect) Enabled() (bool, error) {
 }
 
 // Enable implements the Charger.Enable interface
-func (nrg *NRGKickConnect) Enable(enable bool) error {
+func (nrg *NRGKick) Enable(enable bool) error {
 	settings := NRGSettings{}
 	settings.Values.DeviceMetadata.Password = nrg.Password
 	settings.Values.ChargingStatus.Charging = &enable
@@ -142,7 +142,7 @@ func (nrg *NRGKickConnect) Enable(enable bool) error {
 }
 
 // MaxCurrent implements the Charger.MaxCurrent interface
-func (nrg *NRGKickConnect) MaxCurrent(current int64) error {
+func (nrg *NRGKick) MaxCurrent(current int64) error {
 	settings := NRGSettings{}
 	settings.Values.DeviceMetadata.Password = nrg.Password
 	settings.Values.ChargingCurrent.Value = float64(current)
@@ -151,7 +151,7 @@ func (nrg *NRGKickConnect) MaxCurrent(current int64) error {
 }
 
 // CurrentPower implements the Meter interface.
-func (nrg *NRGKickConnect) CurrentPower() (float64, error) {
+func (nrg *NRGKick) CurrentPower() (float64, error) {
 	var measurements NRGMeasurements
 	err := nrg.getJSON(nrg.apiURL(apiMeasurements), measurements)
 
@@ -159,7 +159,7 @@ func (nrg *NRGKickConnect) CurrentPower() (float64, error) {
 }
 
 // ChargedEnergy implements the ChargeRater interface.
-func (nrg *NRGKickConnect) ChargedEnergy() (float64, error) {
+func (nrg *NRGKick) ChargedEnergy() (float64, error) {
 	var measurements NRGMeasurements
 	err := nrg.getJSON(nrg.apiURL(apiMeasurements), measurements)
 
diff --git a/cmd/dump.go b/cmd/dump.go
index f84ea8bd..24ef1329 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -21,7 +21,15 @@ func dumpAPIs(v interface{}) {
 		if energy, err := v.TotalEnergy(); err != nil {
 			fmt.Printf("Energy: %v\n", err)
 		} else {
-			fmt.Printf("Energy: %.1fkWh\n", energy)
+			fmt.Printf("Energy: %.0fkWh\n", energy)
+		}
+	}
+
+	if v, ok := v.(api.MeterCurrent); ok {
+		if i1, i2, i3, err := v.Currents(); err != nil {
+			fmt.Printf("Current L1..L3: %v\n", err)
+		} else {
+			fmt.Printf("Current L1..L3: %.1fA %.1fA %.1fA\n", i1, i2, i3)
 		}
 	}
 
@@ -29,7 +37,7 @@ func dumpAPIs(v interface{}) {
 		if energy, err := v.ChargedEnergy(); err != nil {
 			fmt.Printf("Charged: %v\n", err)
 		} else {
-			fmt.Printf("Charged: %.1fkWh\n", energy)
+			fmt.Printf("Charged: %.0fkWh\n", energy)
 		}
 	}
 
diff --git a/cmd/root.go b/cmd/root.go
index 5e1e7c68..89ee781d 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -128,13 +128,25 @@ func checkVersion() {
 	}
 }
 
-// handle UI update requests
-func handleUI(triggerChan <-chan struct{}, loadPoints []*core.LoadPoint) {
-	for range triggerChan {
-		for _, lp := range loadPoints {
-			lp.Update()
+var teeIsChained bool // controles piping of first channel in teed chain
+
+// tee splits a tee channel from an input channel and starts a goroutine
+// for duplicateing channel values to replacement input and tee channel
+func tee(in chan core.Param) (chan core.Param, <-chan core.Param) {
+	gen := make(chan core.Param)
+	tee := make(chan core.Param)
+
+	go func(teeIsChained bool) {
+		for i := range gen {
+			if teeIsChained {
+				in <- i
+			}
+			tee <- i
 		}
-	}
+	}(teeIsChained)
+
+	teeIsChained = true
+	return gen, tee
 }
 
 func run(cmd *cobra.Command, args []string) {
@@ -162,7 +174,8 @@ func run(cmd *cobra.Command, args []string) {
 	loadPoints := loadConfig(conf, notificationChan)
 
 	// start broadcasting values
-	tee := &Tee{}
+	valueChan := make(chan core.Param)
+	triggerChan := make(chan struct{})
 
 	// setup influx
 	if viper.Get("influx") != nil {
@@ -174,35 +187,28 @@ func run(cmd *cobra.Command, args []string) {
 			conf.Influx.Password,
 		)
 
+		var teeChan <-chan core.Param
+		valueChan, teeChan = tee(valueChan)
+
 		// eliminate duplicate values
 		dedupe := server.NewDeduplicator(30*time.Minute, "socCharge")
-		pipeChan := dedupe.Pipe(tee.Attach())
+		teeChan = dedupe.Pipe(teeChan)
 
 		// reduce number of values written to influx
 		limiter := server.NewLimiter(5 * time.Second)
-		pipeChan = limiter.Pipe(pipeChan)
+		teeChan = limiter.Pipe(teeChan)
 
-		go influx.Run(pipeChan)
+		go influx.Run(teeChan)
 	}
 
 	// create webserver
 	socketHub := server.NewSocketHub()
 	httpd := server.NewHTTPd(uri, conf.Menu, loadPoints[0], socketHub)
 
-	triggerChan := make(chan struct{})
-
-	// handle UI update requests whenever browser connects
-	go handleUI(triggerChan, loadPoints)
-
-	// publish to UI
-	go socketHub.Run(tee.Attach(), triggerChan)
-
-	// setup values channel
-	valueChan := make(chan util.Param)
-	go tee.Run(valueChan)
+	var teeChan <-chan core.Param
+	valueChan, teeChan = tee(valueChan)
 
-	// capture log messages for UI
-	util.CaptureLogs(valueChan)
+	go socketHub.Run(teeChan, triggerChan)
 
 	// start all loadpoints
 	for _, lp := range loadPoints {
@@ -211,5 +217,14 @@ func run(cmd *cobra.Command, args []string) {
 		go lp.Run(conf.Interval)
 	}
 
+	// handle UI update requests whenever browser connects
+	go func() {
+		for range triggerChan {
+			for _, lp := range loadPoints {
+				lp.Update()
+			}
+		}
+	}()
+
 	log.FATAL.Println(httpd.ListenAndServe())
 }
diff --git a/cmd/tee.go b/cmd/tee.go
deleted file mode 100644
index f1daaf65..00000000
--- a/cmd/tee.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package cmd
-
-import "github.com/andig/evcc/util"
-
-// Tee distributed parameters to subscribers
-type Tee struct {
-	recv []chan<- util.Param
-}
-
-// Attach creates a new receiver channel and attaches it to the tee
-func (t *Tee) Attach() <-chan util.Param {
-	out := make(chan util.Param)
-	t.Add(out)
-	return out
-}
-
-// Add attaches a receiver channel to the tee
-func (t *Tee) Add(out chan<- util.Param) {
-	t.recv = append(t.recv, out)
-}
-
-// Run starts parameter distribution
-func (t *Tee) Run(in <-chan util.Param) {
-	for msg := range in {
-		for _, recv := range t.recv {
-			recv <- msg
-		}
-	}
-}
diff --git a/core/api.go b/core/api.go
index 64361c3b..0c02f5c9 100644
--- a/core/api.go
+++ b/core/api.go
@@ -7,13 +7,20 @@ import (
 	"github.com/andig/evcc/core/wrapper"
 )
 
-// nilVal implements Stringer for nil values
-type nilVal int
+// NilVal implements Stringer for nil values
+type NilVal int
 
-func (n *nilVal) String() string {
+func (n *NilVal) String() string {
 	return "—"
 }
 
+// Param is the broadcast channel data type
+type Param struct {
+	LoadPoint string
+	Key       string
+	Val       interface{}
+}
+
 // Configuration is the loadpoint feature structure
 type Configuration struct {
 	Mode        string `json:"mode"`
@@ -99,12 +106,7 @@ func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
 	defer lp.Unlock()
 
 	log.INFO.Printf("%s set charge mode: %s", lp.Name, string(mode))
-
-	// apply immediately
-	if lp.Mode != mode {
-		lp.resetGuard()
-		lp.Mode = mode
-	}
+	lp.Mode = mode
 
 	lp.Update()
 }
@@ -158,7 +160,7 @@ func (lp *LoadPoint) publishSoC() {
 		log.ERROR.Printf("%s vehicle error: %v", lp.Name, err)
 	}
 
-	var n *nilVal
+	var n *NilVal
 	lp.publish("socCharge", n)
 	lp.publish("chargeEstimate", -1)
 }
diff --git a/core/loadpoint.go b/core/loadpoint.go
index bdbb817e..f51538a3 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -46,14 +46,6 @@ type Config struct {
 	VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
 
 	Meters MetersConfig // Meter references
-
-	Enable, Disable ThresholdConfig
-}
-
-// ThresholdConfig defines enable/disable hysteresis parameters
-type ThresholdConfig struct {
-	Delay     time.Duration
-	Threshold float64
 }
 
 // MetersConfig contains the loadpoint's meter configuration
@@ -72,7 +64,7 @@ type LoadPoint struct {
 	bus              evbus.Bus         // event bus
 	triggerChan      chan struct{}     // API updates
 	notificationChan chan<- push.Event // notifications
-	uiChan           chan<- util.Param // client push messages
+	uiChan           chan<- Param      // client push messages
 
 	Config         `mapstructure:",squash"` // exposed public configuration
 	ChargerHandler `mapstructure:",squash"` // handle charger state and current
@@ -94,8 +86,6 @@ type LoadPoint struct {
 	pvPower      float64          // PV power
 	batteryPower float64          // Battery charge power
 	chargePower  float64          // Charging power
-
-	pvTimer time.Time
 }
 
 // configProvider gives access to configuration repository
@@ -169,7 +159,7 @@ func (lp *LoadPoint) notify(event string, attributes map[string]interface{}) {
 
 // publish sends values to UI and databases
 func (lp *LoadPoint) publish(key string, val interface{}) {
-	lp.uiChan <- util.Param{
+	lp.uiChan <- Param{
 		LoadPoint: lp.Name,
 		Key:       key,
 		Val:       val,
@@ -228,7 +218,7 @@ func (lp *LoadPoint) evChargeCurrentHandler(current int64) {
 }
 
 // Prepare loadpoint configuration by adding missing helper elements
-func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, notificationChan chan<- push.Event) {
+func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.Event) {
 	lp.notificationChan = notificationChan
 	lp.uiChan = uiChan
 
@@ -303,14 +293,14 @@ func (lp *LoadPoint) connected() bool {
 
 // chargingCycle detects charge cycle start and stop events and manages the
 // charge energy counter and charge timer. It guards against duplicate invocation.
-func (lp *LoadPoint) chargingCycle(start bool) {
-	if start == lp.charging {
+func (lp *LoadPoint) chargingCycle(enable bool) {
+	if enable == lp.charging {
 		return
 	}
 
-	lp.charging = start
+	lp.charging = enable
 
-	if start {
+	if enable {
 		log.INFO.Printf("%s start charging ->", lp.Name)
 		lp.bus.Publish(evStartCharge)
 	} else {
@@ -319,13 +309,14 @@ func (lp *LoadPoint) chargingCycle(start bool) {
 	}
 }
 
-// updateChargeStatus updates car status and detects car connected/disconnected events
-func (lp *LoadPoint) updateChargeStatus() error {
+// updateChargeStatus updates car status and stops charging if car disconnected
+func (lp *LoadPoint) updateChargeStatus() api.ChargeStatus {
+	// abort if no vehicle connected
 	status, err := lp.charger.Status()
 	if err != nil {
-		return err
+		log.ERROR.Printf("%s charger error: %v", lp.Name, err)
+		return api.StatusNone
 	}
-
 	log.DEBUG.Printf("%s charger status: %s", lp.Name, status)
 
 	if prevStatus := lp.status; status != prevStatus {
@@ -351,7 +342,7 @@ func (lp *LoadPoint) updateChargeStatus() error {
 		lp.chargingCycle(status == api.StatusC)
 	}
 
-	return nil
+	return status
 }
 
 func (lp *LoadPoint) maxCurrent(mode api.ChargeMode) int64 {
@@ -365,65 +356,15 @@ func (lp *LoadPoint) maxCurrent(mode api.ChargeMode) int64 {
 
 	// get max charge current
 	targetCurrent := clamp(powerToCurrent(targetPower, lp.Voltage, lp.Phases), 0, lp.MaxCurrent)
-
-	if mode == api.ModeMinPV && targetCurrent < lp.MinCurrent {
-		return lp.MinCurrent
-	}
-
-	if mode == api.ModePV && lp.enabled && targetCurrent < lp.MinCurrent {
-		sitePower := lp.gridPower + lp.batteryPower
-
-		// kick off disable sequence
-		if sitePower >= lp.Disable.Threshold {
-			log.DEBUG.Printf("%s site power %.0f >= disable threshold %.0f", lp.Name, sitePower, lp.Disable.Threshold)
-
-			if lp.pvTimer.IsZero() {
-				log.DEBUG.Printf("%s start disable timer", lp.Name)
-				lp.pvTimer = lp.clock.Now()
-			}
-
-			if lp.clock.Since(lp.pvTimer) >= lp.Disable.Delay {
-				log.DEBUG.Printf("%s disable timer elapsed", lp.Name)
-				return 0
-			}
-		} else {
-			// reset timer
-			lp.pvTimer = lp.clock.Now()
+	if targetCurrent < lp.MinCurrent {
+		switch mode {
+		case api.ModeMinPV:
+			targetCurrent = lp.MinCurrent
+		case api.ModePV:
+			targetCurrent = 0
 		}
-
-		return lp.MinCurrent
 	}
 
-	if mode == api.ModePV && !lp.enabled {
-		sitePower := lp.gridPower + lp.batteryPower
-
-		// kick off enable sequence
-		if targetCurrent >= lp.MinCurrent ||
-			(lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) {
-			log.DEBUG.Printf("%s site power %.0f < enable threshold %.0f", lp.Name, sitePower, lp.Enable.Threshold)
-
-			if lp.pvTimer.IsZero() {
-				log.DEBUG.Printf("%s start enable timer", lp.Name)
-				lp.pvTimer = lp.clock.Now()
-			}
-
-			if lp.clock.Since(lp.pvTimer) >= lp.Enable.Delay {
-				log.DEBUG.Printf("%s enable timer elapsed", lp.Name)
-				return lp.MinCurrent
-			}
-		} else {
-			// reset timer
-			lp.pvTimer = lp.clock.Now()
-		}
-
-		return 0
-	}
-
-	log.DEBUG.Printf("%s timer reset", lp.Name)
-
-	// reset pv timer
-	lp.pvTimer = time.Time{}
-
 	return targetCurrent
 }
 
@@ -489,30 +430,21 @@ func (lp *LoadPoint) updateMeters() (err error) {
 	return err
 }
 
-// resetGuard sets guardUpdated to an expired value
-func (lp *LoadPoint) resetGuard() {
-	lp.guardUpdated = lp.clock.Now().Add(-2 * lp.GuardDuration)
-}
-
 // update is the main control function. It reevaluates meters and charger state
 func (lp *LoadPoint) update() {
-	// read and publish meters first
+	lp.updateChargeStatus()
+
+	lp.publish("mode", string(lp.GetMode()))
+	lp.publish("connected", lp.connected())
+	lp.publish("charging", lp.charging)
+
+	// catch any persistent meter update error
 	meterErr := lp.updateMeters()
 
 	// update ChargeRater here to make sure initial meter update is caught
 	lp.bus.Publish(evChargeCurrent, lp.targetCurrent)
 	lp.bus.Publish(evChargePower, lp.chargePower)
 
-	// read and publish status
-	if err := retry.Do(lp.updateChargeStatus, retry.Attempts(3)); err != nil {
-		log.ERROR.Printf("%s charger error: %v", lp.Name, err)
-		return
-	}
-
-	lp.publish("mode", string(lp.GetMode()))
-	lp.publish("connected", lp.connected())
-	lp.publish("charging", lp.charging)
-
 	// check if car connected and ready for charging
 	var err error
 
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 77bc62c1..cf707459 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -69,8 +69,7 @@ meters:
     timeout: 3s # kill script after 3 seconds
 - name: sma
   type: sma # SMA Home Manager 2.0 or SMA Energy Meter 30
-  serial: 1234567890 # Serial number of the device, if this is defined uri is not needed!
-  uri: 192.168.1.4 # IP address of the device, if this is defined serial is not needed!
+  uri: 192.168.1.4 # IP address of the device
 - name: sdm
   type: modbus
   model: sdm
@@ -193,14 +192,6 @@ vehicles:
 #   region: de_DE # gigya region
 #   vin: WREN...
 #   cache: 5m
-# - name: porsche
-#   type: porsche
-#   title: Taycan
-#   capacity: 83 # kWh
-#   user: # user
-#   password: # password
-#   vin: WP...
-#   cache: 5m
 
 loadpoints:
 - name: main # name for logging
@@ -211,12 +202,6 @@ loadpoints:
     pv: pv # pv meter
     battery: battery # battery meter
     charge: charge # charge meter
-  enable: # pv mode enable behavior
-    delay: 1m # threshold must be exceeded for this long
-    threshold: 0 # minimum export power (W). If zero, export must exceeds minimum charge power to enable
-  disable: # pv mode disable behavior
-    delay: 5m # threshold must be exceeded for this long
-    threshold: 200 # maximum import power (W)
   guardduration: 10m # switch charger contactor not more often than this (default 10m)
   maxcurrent: 16 # maximum charge current (default 16A)
   phases: 3 # ev phases (default 3)
diff --git a/go.mod b/go.mod
index 3f0fb259..27b2bcf3 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,6 @@ require (
 	github.com/benbjohnson/clock v1.0.0
 	github.com/eclipse/paho.mqtt.golang v1.2.0
 	github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
-	github.com/godbus/dbus v4.1.0+incompatible
 	github.com/golang/mock v1.4.3
 	github.com/gorilla/handlers v1.4.2
 	github.com/gorilla/mux v1.7.4
@@ -22,13 +21,9 @@ require (
 	github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a
 	github.com/jsgoecke/tesla v0.0.0-20190206234002-112508e1374e
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
-	github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42
 	github.com/mitchellh/mapstructure v1.3.0
 	github.com/mjibson/esc v0.2.0
-	github.com/muka/go-bluetooth v0.0.0-20200505070311-0c1993e4ff21
-	github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da
 	github.com/pkg/errors v0.9.1
-	github.com/smartystreets/assertions v1.0.0 // indirect
 	github.com/spf13/cobra v1.0.0
 	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/viper v1.6.3
@@ -36,7 +31,7 @@ require (
 	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
 	github.com/volkszaehler/mbmd v0.0.0-20200505114042-9732bf1ad846
 	golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 // indirect
-	golang.org/x/net v0.0.0-20200505041828-1ed23360d12c
+	golang.org/x/net v0.0.0-20200505041828-1ed23360d12c // indirect
 	golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 // indirect
 	golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8
 )
diff --git a/go.sum b/go.sum
index 96754413..9a7708aa 100644
--- a/go.sum
+++ b/go.sum
@@ -10,10 +10,8 @@ github.com/alecthomas/participle v0.4.4 h1:ZhPi9STNpixhITQx0RuoA/3UqCMu3cLsJVCfp
 github.com/alecthomas/participle v0.4.4/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY=
 github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY=
 github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
@@ -49,8 +47,6 @@ github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJ
 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
 github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
-github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -65,8 +61,6 @@ github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro
 github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg=
 github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
 github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
-github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
-github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -77,7 +71,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
@@ -134,8 +127,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -146,8 +137,6 @@ github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2t
 github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
 github.com/lestrrat-go/strftime v1.0.1 h1:o7qz5pmLzPDLyGW4lG6JvTKPUfTFXwe+vOamIYWtnVU=
 github.com/lestrrat-go/strftime v1.0.1/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
-github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42 h1:PzBD7QuxXSgSu61TKXxRwVGzWO5d9QZ0HxFFpndZMCg=
-github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -168,11 +157,7 @@ github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/muka/go-bluetooth v0.0.0-20200505070311-0c1993e4ff21 h1:ivI663FFGVCmxfn2r/f+fDpYDE9/+IhO5f19OL0zgxg=
-github.com/muka/go-bluetooth v0.0.0-20200505070311-0c1993e4ff21/go.mod h1:9Y4iuJfFe4N3afRt1qKpHU7vKUqyaWm/wJk2QEk6hgM=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da h1:qiPWuGGr+1GQE6s9NPSK8iggR/6x/V+0snIoOPYsBgc=
-github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da/go.mod h1:DvuJJ/w1Y59rG8UTDxsMk5U+UJXJwuvUgbiJSm9yhX8=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
 github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
@@ -193,7 +178,6 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -204,12 +188,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
-github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
-github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@@ -240,7 +220,6 @@ github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1u
 github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/suapapa/go_eddystone v0.0.0-20190827074641-8d8c1bb79363/go.mod h1:O/oFfbntg0b1z5NM/IGoTMKYPO3lkzPSA53E+J99lDU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k=
@@ -293,7 +272,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -328,7 +306,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
diff --git a/meter/sma.go b/meter/sma.go
index a3fd11a9..25b735b3 100644
--- a/meter/sma.go
+++ b/meter/sma.go
@@ -19,7 +19,6 @@ const (
 type SMA struct {
 	log     *util.Logger
 	uri     string
-	serial  string
 	power   float64
 	energy  float64
 	powerO  sma.Obis
@@ -33,38 +32,30 @@ type SMA struct {
 // NewSMAFromConfig creates a SMA Meter from generic config
 func NewSMAFromConfig(log *util.Logger, other map[string]interface{}) api.Meter {
 	cc := struct {
-		URI, Serial, Power, Energy string
+		URI, Power, Energy string
 	}{}
 	util.DecodeOther(log, other, &cc)
 
-	return NewSMA(cc.URI, cc.Serial, cc.Power, cc.Energy)
+	return NewSMA(cc.URI, cc.Power, cc.Energy)
 }
 
 // NewSMA creates a SMA Meter
-func NewSMA(uri, serial, power, energy string) api.Meter {
+func NewSMA(uri, power, energy string) api.Meter {
 	log := util.NewLogger("sma ")
 
 	sm := &SMA{
 		log:     log,
 		uri:     uri,
-		serial:  serial,
 		powerO:  sma.Obis(power),
 		energyO: sma.Obis(energy),
 		recv:    make(chan sma.Telegram),
 	}
 
 	if sma.Instance == nil {
-		sma.Instance = sma.New(log)
+		sma.Instance = sma.New(log, sm.uri)
 	}
 
-	// we only need to subscribe to one of the two possible identifiers
-	if uri != "" {
-		sma.Instance.Subscribe(uri, sm.recv)
-	} else if serial != "" {
-		sma.Instance.Subscribe(serial, sm.recv)
-	} else {
-		log.FATAL.Fatalf("config: missing uri or serial")
-	}
+	sma.Instance.Subscribe(uri, sm.recv)
 
 	go sm.receive()
 
@@ -93,41 +84,41 @@ func (sm *SMA) waitForInitialValue() {
 	}
 }
 
-// update the actual meter data
-func (sm *SMA) updatePower(msg sma.Telegram) {
-	sm.mux.Lock()
-
-	if sm.powerO != "" {
-		// use user-defined obis
-		if power, ok := msg.Values[sm.powerO]; ok {
-			sm.power = power
-			sm.updated = time.Now()
+// receive processes the channel message containing the multicast data
+func (sm *SMA) receive() {
+	for msg := range sm.recv {
+		if msg.Values == nil {
+			continue
 		}
-	} else {
-		sm.power = msg.Values[sma.ImportPower] - msg.Values[sma.ExportPower]
-		sm.updated = time.Now()
-	}
 
-	if sm.energyO != "" {
-		if energy, ok := msg.Values[sm.energyO]; ok {
-			sm.energy = energy
+		sm.mux.Lock()
+
+		if sm.powerO != "" {
+			// use user-defined obis
+			if power, ok := msg.Values[sm.powerO]; ok {
+				sm.power = power
+				sm.updated = time.Now()
+			}
+		} else if power, ok := msg.Values[sma.ExportPower]; ok {
+			sm.power = -power
+			sm.updated = time.Now()
+		} else if power, ok := msg.Values[sma.ImportPower]; ok {
+			sm.power = power
 			sm.updated = time.Now()
 		} else {
-			sm.log.WARN.Println("missing obis for energy")
+			sm.log.WARN.Println("missing obis for import/export power")
 		}
-	}
-
-	sm.mux.Unlock()
-}
 
-// receive processes the channel message containing the multicast data
-func (sm *SMA) receive() {
-	for msg := range sm.recv {
-		if msg.Values == nil {
-			continue
+		if sm.energyO != "" {
+			if energy, ok := msg.Values[sm.energyO]; ok {
+				sm.energy = energy
+				sm.updated = time.Now()
+			} else {
+				sm.log.WARN.Println("missing obis for energy")
+			}
 		}
 
-		sm.updatePower(msg)
+		sm.mux.Unlock()
 	}
 }
 
diff --git a/meter/sma/listener.go b/meter/sma/listener.go
index 9297ca23..f0c68a83 100644
--- a/meter/sma/listener.go
+++ b/meter/sma/listener.go
@@ -93,7 +93,7 @@ type Listener struct {
 }
 
 // New creates a Listener
-func New(log *util.Logger) *Listener {
+func New(log *util.Logger, addr string) *Listener {
 	// Parse the string address
 	gaddr, err := net.ResolveUDPAddr("udp4", multicastAddr)
 	if err != nil {
@@ -157,8 +157,6 @@ func (l *Listener) processMessage(src *net.UDPAddr, b []byte) (Telegram, error)
 		Values: obisValues,
 	}
 
-	//	l.log.TRACE.Printf("recv %v", msg.Values)
-
 	return msg, nil
 }
 
@@ -180,7 +178,7 @@ func (l *Listener) listen() {
 }
 
 // Subscribe adds a client address and message channel
-func (l *Listener) Subscribe(identifier string, c chan<- Telegram) {
+func (l *Listener) Subscribe(addr string, c chan<- Telegram) {
 	l.mux.Lock()
 	defer l.mux.Unlock()
 
@@ -188,15 +186,15 @@ func (l *Listener) Subscribe(identifier string, c chan<- Telegram) {
 		l.clients = make(map[string]chan<- Telegram)
 	}
 
-	l.clients[identifier] = c
+	l.clients[addr] = c
 }
 
 func (l *Listener) send(msg Telegram) {
 	l.mux.Lock()
 	defer l.mux.Unlock()
 
-	for identifier, client := range l.clients {
-		if identifier == msg.Addr || identifier == msg.Serial {
+	for addr, client := range l.clients {
+		if addr == msg.Addr {
 			select {
 			case client <- msg:
 			default:
diff --git a/provider/http.go b/provider/http.go
index 049e90ac..d00c6e5d 100644
--- a/provider/http.go
+++ b/provider/http.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"crypto/tls"
-	"encoding/base64"
 	"fmt"
 	"io"
 	"math"
@@ -26,21 +25,6 @@ type HTTP struct {
 	jq          *gojq.Query
 }
 
-// Auth is the authorization config
-type Auth struct {
-	Type, User, Password string
-}
-
-// NewAuth creates authorization headers from config
-func NewAuth(log *util.Logger, auth Auth, headers map[string]string) {
-	if strings.ToLower(auth.Type) != "basic" {
-		log.FATAL.Fatalf("config: unsupported auth type: %s", auth.Type)
-	}
-
-	basicAuth := auth.User + ":" + auth.Password
-	headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(basicAuth))
-}
-
 // NewHTTPProviderFromConfig creates a HTTP provider
 func NewHTTPProviderFromConfig(log *util.Logger, other map[string]interface{}) *HTTP {
 	cc := struct {
@@ -50,7 +34,6 @@ func NewHTTPProviderFromConfig(log *util.Logger, other map[string]interface{}) *
 		Jq          string
 		Scale       float64
 		Insecure    bool
-		Auth        Auth
 	}{}
 	util.DecodeOther(log, other, &cc)
 
@@ -66,14 +49,6 @@ func NewHTTPProviderFromConfig(log *util.Logger, other map[string]interface{}) *
 		scale:      cc.Scale,
 	}
 
-	// handle basic auth
-	if cc.Auth.Type != "" {
-		if p.headers == nil {
-			p.headers = make(map[string]string)
-		}
-		NewAuth(log, cc.Auth, p.headers)
-	}
-
 	// ignore the self signed certificate
 	if cc.Insecure {
 		customTransport := http.DefaultTransport.(*http.Transport).Clone()
diff --git a/server/assets.go b/server/assets.go
index 081b3575..902d4e98 100644
--- a/server/assets.go
+++ b/server/assets.go
@@ -3768,101 +3768,95 @@ oJE6K0E084u3ea7ne0X73qsc2P2KOB+ngKecxITsMAgTVJfqMwAA//+21HeD5gEAAA==
 	"/index.html": {
 		name:    "index.html",
 		local:   "../assets/index.html",
-		size:    13184,
+		size:    12664,
 		modtime: 1566640112,
 		compressed: `
-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
+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
+AP//MWVC4HgxAAA=
 `,
 	},
 
 	"/js/app.js": {
 		name:    "app.js",
 		local:   "../assets/js/app.js",
-		size:    5745,
+		size:    5012,
 		modtime: 1566640112,
 		compressed: `
-H4sIAAAAAAAC/6xY747cthH/vk8xodOsFidLa7tNAJ0Vo726SIukLmwn/WAcEK40u2JWIgWS2r3rZYE8
-RJ4wT1JwqP+7lzpFPt2KnD+/mR85nLk4Bn4nlAGDtqnhKGwBFo2FLS/LDc/2i0xJY6FUGaRwFDJXx6hU
-GbdCyajQuI2EzPHuzTZghbV1EsdutyyUsTEesizmxqA1MVtBmqawhlfwsACotbIqU2UCXo2FCwCnJHmF
-CbDeCG3UStsE2BfrL9YsXJwgmSO5XiwojCjHLW9Ka6INN/jt268hddCjzh1cAYtjBle02vmDKwhISmkL
-r4AlnQAtJMDYihR5Ldj13FGBPEdtoloZ+2F5o6RFaZ++v69xeQspLHldl8KjjH8wSi6vF4s4XsQxvLPc
-ovu9KNGCsUojpJQdIYUVvBT/wTxxTBh0WTBOPiEBgDiGTMmt2DWabNNipXJMQDZlGdK3Udns84bXPBP2
-frb8XthyornTIv8GLeoErG7QL9aHs6Ws4HqH55IFN2jGBishbxqtUdrJKr+7sBrHoBtpRYXeh5ISM+ty
-MciQYyF3c9D/UkcHZVisD2dLHvQjy39tE3q+89pYUREFT5+N1/PXEvVuntIb2kuA/fLTz2wBcHI7TZ2T
-gW0jM+ckqMwuhK3SGa5aYt9sfsDMRnu8N253FW2Vfs2zIuh19p0ogNhCQNrw449gC2E+7G/hkzSFRua4
-FRLzQRZ6gRQqs/uwv71ud06ApcGRoDO7hzQFhlorzcZGAKzixpqItoKHCo3hLlBv87S67kVbu4O1I9fy
-sjG38zG2xqquMqkSo1LtAibkgZcihz3eJ+Du736svBj/PYWUiYjuE0kRN+7SjZjpYDr0n9DljEbXcgjC
-l4Md2oD5C8lWkS1QDnxpNLWSBseBe4P+OPQCUc4tH8E+8wop3bGet1WUcZsVwZiRVt+Felqc+mLzFXJt
-N8gtFZy2fBq0f5cW9YGXwSzwUVgF8tIWrHM2jqsLSaOJWvIgBfYO9QE1NJIfuCj5pkTmUU2OjtN3KFfX
-i1MIf1qv16se7o2qaiVRWkN4/SPktSEFiUf4rsHAOUf3hjzxW/RYuCR2VVJYrEwCD6f2vqrGVZp12HFe
-oS1UbjpxzYUZ3U6gC9izVpldJIgDd3rI1tVVx8V3Dbp0BrRHbsNWgf52rHgcFP/Yj2eu90Sfkb2vKZv+
-DnaOyAEBbbXGht01+mi7dBsfNTsBrLFSh8cz44LPsUSLF+IfWzotHNsLJ591DAfL9uQ8JRKXYdcg1CaB
-D0tna3nrlC1WdUnlc/mkV2nXlsSmo4SezB5mh9EfIGKP9coif+oqRQ85ErnH+mkg8lVEcIKlKdRxuRpv
-KBksC5HnKKONiTrU50778+7TN+SmS8nKn8OLOVE5mqOwWdEmZBK9ymeh+0N/Vrw02kbLHk7bQfi6Qh/u
-rd2VasNLv+lx9TXRAWoopQ+jBqMzt8NLBXPkdyizkdPs61bY4ZkYcCJjI74Eub7KpyNewhUh+JgCC3Pn
-kMKkzk4QAZyijZA5UbTqKp2/LnQp+qp8GqrrReKYs11ziSWb88aeuL2eN/bxvFmR7VF/xWU+7dJ+B0bb
-CnmBRffwjXJoVAaffTbOatsOXuD9j7PO4oKptl/5n6ZePNqktALPLzLjSqHNii5K5t1MOzx2Mez2acxK
-5Lp/HAnqmIX+PMxC69rScSRxDEqW9y2LToNntuFled93sZMWbeJomLp+5bG+cOSnocJVCs8uH/YQnvmX
-d9oiPfJCbqvxjT3wcgBw4CWk8A23RcQ3hvY6oy1VTuLL1PmDV+AEIIZn+MJV2r+JO8yDZytInFS/sJ4+
-Q9vKfjvt0iYIWjcTCINDtmc0y80tDg1/bzWf3oIcXsL6wtlc/vLTz8tp4lwaNBjMlMxdp8LW7oUJcvgD
-fD4k2clUQjYWRzIEe1sqpYMcYid+rlSoRpOK05gpvPh8RKODTcJRiXJnC3gJz8cR9IbIN33NA2mD9JJX
-7VTcoo5MszFWB0+fr/qtNujR1iTT7RR38c7517kf0mczu5scCmtrkzBH49G4H4n7kfTti7fQaAEp/D7D
-/tHMjB+7zvPfuHmnsj3aoNFiFYLBctt2hZ3G0URK0vMB6RAwHuyYg6OJslIZDIbLN9GnzV/T7+vCe1Gh
-amzgkERtouf3emJ66NUfM271/aS00JE1O0jhH+/e/DOquXad4sHOZ5bZaDO0vuOjBUAPLASzJ7ub6Px8
-gCH0Ds5Huetpuc808knv1x+vtlunpATDuJejsVrdX1T5DW9A29H6qeWtaqyr5sPIUnEhIYVZj+BWL7QH
-bnncHpBlb8ePlueW2pHz3Jbf+H+ajd/YUoxAYrWhGXWGkZYvQKT1KcKu62dW2BJZCMw0m/63qHb0Z6t5
-RQulkHt2O8GgVVtVPywAHqDmtkiAxSyEHlHiWTmFi1t3LjJuA9LSXwu5N8Eqqng9vLAEKQSR3/l8ddnq
-TTsQhsqMyO8mbijAsAuK/tB5WXVupxh7LkcmWt4d1lmMepiD39I3TcM+/NAxZgpX0LZKw/d+NWl3v18A
-ONCv73hm/5xZccCbkhuTAOP0xZz6X5Syxmpew0vJD1+Oc8zrejaEe0SOiOhTmsIC9oTXNXM6w38x3O37
-bwAAAP//8qQcsHEWAAA=
+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
 `,
 	},
 
diff --git a/server/http.go b/server/http.go
index 8c627b95..a62911c7 100644
--- a/server/http.go
+++ b/server/http.go
@@ -204,7 +204,7 @@ func NewHTTPd(url string, links []MenuConfig, lp loadPoint, hub *SocketHub) *htt
 		api.
 			Methods(r.Methods...).
 			Path(r.Pattern).
-			Handler(r.HandlerFunc) // routeLogger
+			Handler(routeLogger(r.HandlerFunc))
 	}
 
 	srv := &http.Server{
diff --git a/server/influxdb.go b/server/influxdb.go
index 9602769d..36473016 100644
--- a/server/influxdb.go
+++ b/server/influxdb.go
@@ -4,6 +4,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/andig/evcc/core"
 	"github.com/andig/evcc/util"
 	influxdb "github.com/influxdata/influxdb1-client/v2"
 )
@@ -130,7 +131,7 @@ func (m *Influx) asyncWriter(exit <-chan struct{}) <-chan struct{} {
 }
 
 // Run Influx publisher
-func (m *Influx) Run(in <-chan util.Param) {
+func (m *Influx) Run(in <-chan core.Param) {
 	exit := make(chan struct{}) // exit signals to stop writer
 	done := m.asyncWriter(exit) // done signals writer stopped
 
diff --git a/server/limiter.go b/server/limiter.go
index bc00f298..cd011636 100644
--- a/server/limiter.go
+++ b/server/limiter.go
@@ -3,13 +3,13 @@ package server
 import (
 	"time"
 
-	"github.com/andig/evcc/util"
+	"github.com/andig/evcc/core"
 	"github.com/benbjohnson/clock"
 )
 
 // Piper is the interface that data flow plugins must implement
 type Piper interface {
-	Pipe(in <-chan util.Param) <-chan util.Param
+	Pipe(in <-chan core.Param) <-chan core.Param
 }
 
 type cacheItem struct {
@@ -41,7 +41,7 @@ func NewDeduplicator(interval time.Duration, filter ...string) Piper {
 	return l
 }
 
-func (l *Deduplicator) pipe(in <-chan util.Param, out chan<- util.Param) {
+func (l *Deduplicator) pipe(in <-chan core.Param, out chan<- core.Param) {
 	for p := range in {
 		// use loadpoint + param.Key as lookup key to value cache
 		key := p.LoadPoint + "." + p.Key
@@ -58,8 +58,8 @@ func (l *Deduplicator) pipe(in <-chan util.Param, out chan<- util.Param) {
 }
 
 // Pipe creates a new filtered output channel for given input channel
-func (l *Deduplicator) Pipe(in <-chan util.Param) <-chan util.Param {
-	out := make(chan util.Param)
+func (l *Deduplicator) Pipe(in <-chan core.Param) <-chan core.Param {
+	out := make(chan core.Param)
 	go l.pipe(in, out)
 	return out
 }
@@ -82,7 +82,7 @@ func NewLimiter(interval time.Duration) Piper {
 	return l
 }
 
-func (l *Limiter) pipe(in <-chan util.Param, out chan<- util.Param) {
+func (l *Limiter) pipe(in <-chan core.Param, out chan<- core.Param) {
 	for p := range in {
 		// use loadpoint + param.Key as lookup key to value cache
 		key := p.LoadPoint + "." + p.Key
@@ -97,8 +97,8 @@ func (l *Limiter) pipe(in <-chan util.Param, out chan<- util.Param) {
 }
 
 // Pipe creates a new filtered output channel for given input channel
-func (l *Limiter) Pipe(in <-chan util.Param) <-chan util.Param {
-	out := make(chan util.Param)
+func (l *Limiter) Pipe(in <-chan core.Param) <-chan core.Param {
+	out := make(chan core.Param)
 	go l.pipe(in, out)
 	return out
 }
diff --git a/server/socket.go b/server/socket.go
index d9d2100a..508684c7 100644
--- a/server/socket.go
+++ b/server/socket.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/andig/evcc/util"
+	"github.com/andig/evcc/core"
 	"github.com/gorilla/websocket"
 )
 
@@ -82,33 +82,32 @@ func NewSocketHub() *SocketHub {
 	}
 }
 
-func encode(v interface{}) (string, error) {
+func (h *SocketHub) encode(v core.Param) ([]byte, error) {
 	var s string
-	switch val := v.(type) {
+	switch val := v.Val.(type) {
 	case time.Duration:
 		// must be before stringer to convert to seconds instead of string
-		s = fmt.Sprintf("%d", int64(val.Seconds()))
+		s = fmt.Sprintf("{\"%s\": %d}", v.Key, int64(val.Seconds()))
 	case fmt.Stringer, string:
-		s = fmt.Sprintf("\"%s\"", val)
+		s = fmt.Sprintf("{\"%s\": \"%s\"}", v.Key, val)
 	case float64:
-		s = fmt.Sprintf("%.3f", val)
+		s = fmt.Sprintf("{\"%s\": %.3f}", v.Key, val)
 	default:
-		s = fmt.Sprintf("%v", val)
+		s = fmt.Sprintf("{\"%s\": %v}", v.Key, val)
 	}
-	return s, nil
+	return []byte(s), nil
 }
 
-func (h *SocketHub) broadcast(i util.Param) {
+func (h *SocketHub) broadcast(i core.Param) {
 	if len(h.clients) > 0 {
-		val, err := encode(i.Val)
+		message, err := h.encode(i)
 		if err != nil {
 			log.FATAL.Fatal(err)
 		}
-		message := fmt.Sprintf("{\"%s\": %s}", i.Key, val)
 
 		for client := range h.clients {
 			select {
-			case client.send <- []byte(message):
+			case client.send <- message:
 			default:
 				close(client.send)
 				delete(h.clients, client)
@@ -118,7 +117,7 @@ func (h *SocketHub) broadcast(i util.Param) {
 }
 
 // Run starts data and status distribution
-func (h *SocketHub) Run(in <-chan util.Param, triggerChan chan<- struct{}) {
+func (h *SocketHub) Run(in <-chan core.Param, triggerChan chan<- struct{}) {
 	for {
 		select {
 		case client := <-h.register:
diff --git a/util/http.go b/util/http.go
index 7e4c1f52..f1d30af7 100644
--- a/util/http.go
+++ b/util/http.go
@@ -6,10 +6,8 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
-	"time"
 )
 
-// HTTPHelper provides utility primitives
 type HTTPHelper struct {
 	Log    *Logger
 	Client *http.Client
@@ -19,7 +17,7 @@ type HTTPHelper struct {
 func NewHTTPHelper(log *Logger) *HTTPHelper {
 	r := &HTTPHelper{
 		Log:    log,
-		Client: &http.Client{Timeout: 10 * time.Second},
+		Client: &http.Client{},
 	}
 	return r
 }
diff --git a/util/log.go b/util/log.go
index 94bc7a01..3b66129b 100644
--- a/util/log.go
+++ b/util/log.go
@@ -1,11 +1,9 @@
 package util
 
 import (
-	"io"
 	"io/ioutil"
 	"log"
 	"os"
-	"strconv"
 	"strings"
 
 	jww "github.com/spf13/jwalterweatherman"
@@ -70,39 +68,3 @@ func LogLevelToThreshold(level string) jww.Threshold {
 		panic("invalid log level " + level)
 	}
 }
-
-var uiChan chan<- Param
-
-type uiWriter struct {
-	level string
-}
-
-func (w *uiWriter) Write(p []byte) (n int, err error) {
-	uiChan <- Param{
-		LoadPoint: "",
-		Key:       w.level,
-		Val:       strings.Trim(strconv.Quote(string(p)), "\""),
-	}
-
-	return 0, nil
-}
-
-// CaptureLogs appends uiWriter to relevant log levels
-func CaptureLogs(c chan<- Param) {
-	uiChan = c
-
-	for _, l := range loggers {
-		captureLogger("warn", l.Notepad.WARN)
-		captureLogger("error", l.Notepad.ERROR)
-		captureLogger("error", l.Notepad.FATAL)
-	}
-}
-
-func captureLogger(level string, l *log.Logger) {
-	ui := uiWriter{
-		level: level,
-	}
-
-	mw := io.MultiWriter(l.Writer(), &ui)
-	l.SetOutput(mw)
-}
diff --git a/util/param.go b/util/param.go
deleted file mode 100644
index b5ff544f..00000000
--- a/util/param.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package util
-
-// Param is the broadcast channel data type
-type Param struct {
-	LoadPoint string
-	Key       string
-	Val       interface{}
-}
diff --git a/vehicle/bmw.go b/vehicle/bmw.go
index 52dccb39..858e1c92 100644
--- a/vehicle/bmw.go
+++ b/vehicle/bmw.go
@@ -77,7 +77,6 @@ func (v *BMW) login(user, password string) error {
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 
 	client := &http.Client{
-		Timeout:       v.HTTPHelper.Client.Timeout,
 		CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, // don't follow redirects
 	}
 
diff --git a/vehicle/config.go b/vehicle/config.go
index 050e4d33..767e8847 100644
--- a/vehicle/config.go
+++ b/vehicle/config.go
@@ -24,8 +24,6 @@ func NewFromConfig(log *util.Logger, typ string, other map[string]interface{}) a
 		c = NewNissanFromConfig(log, other)
 	case "renault", "zoe":
 		c = NewRenaultFromConfig(log, other)
-	case "porsche", "taycan":
-		c = NewPorscheFromConfig(log, other)
 	default:
 		log.FATAL.Fatalf("invalid vehicle type '%s'", typ)
 	}
diff --git a/vehicle/porsche.go b/vehicle/porsche.go
deleted file mode 100644
index 83411b3c..00000000
--- a/vehicle/porsche.go
+++ /dev/null
@@ -1,248 +0,0 @@
-package vehicle
-
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-	"net/http/cookiejar"
-	"net/url"
-	"strings"
-	"time"
-
-	"github.com/andig/evcc/api"
-	"github.com/andig/evcc/provider"
-	"github.com/andig/evcc/util"
-	cv "github.com/nirasan/go-oauth-pkce-code-verifier"
-	"golang.org/x/net/publicsuffix"
-)
-
-const (
-	porscheLogin          = "https://login.porsche.com/auth/de/de_DE"
-	porscheLoginAuth      = "https://login.porsche.com/auth/api/v1/de/de_DE/public/login"
-	porscheAPIClientID    = "TZ4Vf5wnKeipJxvatJ60lPHYEzqZ4WNp"
-	porscheAPIRedirectUri = "https://my-static02.porsche.com/static/cms/auth.html"
-	porscheAPIAuth        = "https://login.porsche.com/as/authorization.oauth2"
-	porscheAPIToken       = "https://login.porsche.com/as/token.oauth2"
-	porscheAPI            = "https://connect-portal.porsche.com/core/api/v3/de/de_DE"
-)
-
-type porscheTokenResponse struct {
-	AccessToken string `json:"access_token"`
-	IdToken     string `json:"id_token"`
-	TokenType   string `json:"token_type"`
-	ExpiresIn   int    `json:"expires_in"`
-}
-
-type porscheVehicleResponse struct {
-	CarControlData struct {
-		BatteryLevel struct {
-			Unit  string
-			Value float64
-		}
-		Mileage struct {
-			Unit  string
-			Value float64
-		}
-	}
-}
-
-// Porsche is an api.Vehicle implementation for Porsche cars
-type Porsche struct {
-	*embed
-	*util.HTTPHelper
-	user, password, vin string
-	token               string
-	tokenValid          time.Time
-	chargeStateG        provider.FloatGetter
-}
-
-// NewPorscheFromConfig creates a new vehicle
-func NewPorscheFromConfig(log *util.Logger, other map[string]interface{}) api.Vehicle {
-	cc := struct {
-		Title               string
-		Capacity            int64
-		User, Password, VIN string
-		Cache               time.Duration
-	}{}
-	util.DecodeOther(log, other, &cc)
-
-	v := &Porsche{
-		embed:      &embed{cc.Title, cc.Capacity},
-		HTTPHelper: util.NewHTTPHelper(util.NewLogger("porsche")),
-		user:       cc.User,
-		password:   cc.Password,
-		vin:        cc.VIN,
-	}
-
-	v.chargeStateG = provider.NewCached(log, v.chargeState, cc.Cache).FloatGetter()
-
-	return v
-}
-
-// login with a my Porsche account
-// looks like the backend is using a PingFederate Server with OAuth2
-func (v *Porsche) login(user, password string) error {
-	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
-	if err != nil {
-		return err
-	}
-
-	// the flow is using Oauth2 and >10 redirects
-	client := &http.Client{
-		Jar:     jar,
-		Timeout: v.HTTPHelper.Client.Timeout,
-		CheckRedirect: func(req *http.Request, via []*http.Request) error {
-			return nil // allow >10 redirects
-		},
-	}
-
-	// get the login page to get the cookies for the subsequent requests
-	reqLogin, err := http.NewRequest(http.MethodGet, porscheLogin, nil)
-	if err != nil {
-		return err
-	}
-
-	respLogin, err := client.Do(reqLogin)
-	if err != nil {
-		return err
-	}
-
-	queryLogin, err := url.ParseQuery(respLogin.Request.URL.RawQuery)
-	if err != nil {
-		return err
-	}
-
-	sec := queryLogin.Get("sec")
-	resume := queryLogin.Get("resume")
-	state := queryLogin.Get("state")
-	thirdPartyId := queryLogin.Get("thirdPartyId")
-
-	dataLoginAuth := url.Values{
-		"sec":          []string{sec},
-		"resume":       []string{resume},
-		"thirdPartyId": []string{thirdPartyId},
-		"state":        []string{state},
-		"username":     []string{user},
-		"password":     []string{password},
-		"keeploggedin": []string{"false"},
-	}
-
-	reqLoginAuth, err := http.NewRequest(http.MethodPost, porscheLoginAuth, strings.NewReader(dataLoginAuth.Encode()))
-	if err != nil {
-		return err
-	}
-	reqLoginAuth.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
-	// process the auth so the session is authenticated
-	_, err = client.Do(reqLoginAuth)
-	if err != nil {
-		return err
-	}
-
-	var CodeVerifier, _ = cv.CreateCodeVerifier()
-	codeChallenge := CodeVerifier.CodeChallengeS256()
-
-	dataAuth := url.Values{
-		"scope":                 []string{"openid"},
-		"response_type":         []string{"code"},
-		"access_type":           []string{"offline"},
-		"prompt":                []string{"none"},
-		"client_id":             []string{porscheAPIClientID},
-		"redirect_uri":          []string{porscheAPIRedirectUri},
-		"code_challenge":        []string{codeChallenge},
-		"code_challenge_method": []string{"S256"},
-	}
-
-	reqAPIAuth, err := http.NewRequest(http.MethodGet, porscheAPIAuth, nil)
-	if err != nil {
-		return err
-	}
-	reqAPIAuth.URL.RawQuery = dataAuth.Encode()
-
-	respAPIAuth, err := client.Do(reqAPIAuth)
-	if err != nil {
-		return err
-	}
-
-	queryAPIAuth, err := url.ParseQuery(respAPIAuth.Request.URL.RawQuery)
-	if err != nil {
-		return err
-	}
-
-	authCode := queryAPIAuth.Get("code")
-
-	codeVerifier := CodeVerifier.CodeChallengePlain()
-
-	dataAPIToken := url.Values{
-		"grant_type":    []string{"authorization_code"},
-		"client_id":     []string{porscheAPIClientID},
-		"redirect_uri":  []string{porscheAPIRedirectUri},
-		"code":          []string{authCode},
-		"prompt":        []string{"none"},
-		"code_verifier": []string{codeVerifier},
-	}
-
-	reqAPIToken, err := http.NewRequest(http.MethodPost, porscheAPIToken, strings.NewReader(dataAPIToken.Encode()))
-	if err != nil {
-		return err
-	}
-	reqAPIToken.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
-	respAPIToken, err := client.Do(reqAPIToken)
-	if err != nil {
-		return err
-	}
-
-	b, _ := ioutil.ReadAll(respAPIToken.Body)
-
-	var pr porscheTokenResponse
-	err = json.Unmarshal(b, &pr)
-	if err != nil {
-		return err
-	}
-
-	if pr.AccessToken == "" || pr.ExpiresIn == 0 {
-		return errors.New("could not obtain token")
-	}
-
-	v.token = pr.AccessToken
-	v.tokenValid = time.Now().Add(time.Duration(pr.ExpiresIn) * time.Second)
-
-	return nil
-}
-
-func (v *Porsche) request(uri string) (*http.Request, error) {
-	if v.token == "" || time.Since(v.tokenValid) > 0 {
-		if err := v.login(v.user, v.password); err != nil {
-			return nil, err
-		}
-	}
-
-	req, err := http.NewRequest(http.MethodGet, uri, nil)
-	if err == nil {
-		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", v.token))
-	}
-
-	return req, nil
-}
-
-// chargeState implements the Vehicle.ChargeState interface
-func (v *Porsche) chargeState() (float64, error) {
-	uri := fmt.Sprintf("%s/vehicles/%s", porscheAPI, v.vin)
-	req, err := v.request(uri)
-	if err != nil {
-		return 0, err
-	}
-
-	var pr porscheVehicleResponse
-	_, err = v.RequestJSON(req, &pr)
-
-	return pr.CarControlData.BatteryLevel.Value, err
-}
-
-// ChargeState implements the Vehicle.ChargeState interface
-func (v *Porsche) ChargeState() (float64, error) {
-	return v.chargeStateG()
-}
diff --git a/vehicle/tesla.go b/vehicle/tesla.go
index 3f9eec79..e2ca03f9 100644
--- a/vehicle/tesla.go
+++ b/vehicle/tesla.go
@@ -48,13 +48,9 @@ func NewTeslaFromConfig(log *util.Logger, other map[string]interface{}) api.Vehi
 		embed: &embed{cc.Title, cc.Capacity},
 	}
 
-	if cc.VIN == "" && len(vehicles) == 1 {
-		v.vehicle = vehicles[0].Vehicle
-	} else {
-		for _, vehicle := range vehicles {
-			if vehicle.Vin == cc.VIN {
-				v.vehicle = vehicle.Vehicle
-			}
+	for _, vehicle := range vehicles {
+		if vehicle.Vin == cc.VIN {
+			v.vehicle = vehicle.Vehicle
 		}
 	}
 
@@ -71,10 +67,7 @@ func NewTeslaFromConfig(log *util.Logger, other map[string]interface{}) api.Vehi
 // chargeState implements the Vehicle.ChargeState interface
 func (v *Tesla) chargeState() (float64, error) {
 	state, err := v.vehicle.ChargeState()
-	if err != nil {
-		return 0, err
-	}
-	return float64(state.BatteryLevel), nil
+	return float64(state.BatteryLevel), err
 }
 
 // ChargeState implements the Vehicle.ChargeState interface
@@ -85,10 +78,7 @@ func (v *Tesla) ChargeState() (float64, error) {
 // chargedEnergy implements the ChargeRater.ChargedEnergy interface
 func (v *Tesla) chargedEnergy() (float64, error) {
 	state, err := v.vehicle.ChargeState()
-	if err != nil {
-		return 0, err
-	}
-	return state.ChargeEnergyAdded, nil
+	return state.ChargeEnergyAdded, err
 }
 
 // ChargedEnergy implements the ChargeRater.ChargedEnergy interface
@@ -101,8 +91,5 @@ func (v *Tesla) ChargedEnergy() (float64, error) {
 // CurrentPower implements the ChargeRater.CurrentPower interface
 // func (v *Tesla) CurrentPower() (float64, error) {
 // 	state, err := v.vehicle.ChargeState()
-// 	if err != nil {
-// 		return 0, err
-// 	}
 // 	return state.ChargerPower, err
 // }


Test patch:
diff --git a/charger/mcc_test.go b/charger/mcc_test.go
index d6c90228..90a6d875 100644
--- a/charger/mcc_test.go
+++ b/charger/mcc_test.go
@@ -75,9 +75,9 @@ func TestMobileConnect_login(t *testing.T) {
 		wantErr   bool
 	}{
 		// test cases for software version 2914
-		{"login - success", []apiResponse{{mccAPILogin, "{\n    \"token\": \"1234567890._abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n}"}}, "password", false},
-		{"login - wrong password", []apiResponse{{mccAPILogin, "{\n    \"error\": \"wrong password\"\n}"}}, "wrong", true},
-		{"login - bad return", []apiResponse{{mccAPILogin, "{{\n    \"error\": \"wrong password\"\n}"}}, "wrong", true},
+		{"login - success", []apiResponse{{apiLogin, "{\n    \"token\": \"1234567890._abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n}"}}, "password", false},
+		{"login - wrong password", []apiResponse{{apiLogin, "{\n    \"error\": \"wrong password\"\n}"}}, "wrong", true},
+		{"login - bad return", []apiResponse{{apiLogin, "{{\n    \"error\": \"wrong password\"\n}"}}, "wrong", true},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -97,9 +97,9 @@ func TestMobileConnect_refresh(t *testing.T) {
 		wantErr   bool
 	}{
 		// test cases for software version 2914
-		{"refresh - success", []apiResponse{{mccAPIRefresh, "{\n    \"token\": \"1234567890._abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n}"}}, false},
-		{"refresh - wrong password", []apiResponse{{mccAPIRefresh, "{\n    \"error\": \"signature mismatch: OP-gWPOgQ9fdKujMgRNHkeH4WHqYrHe3Z2RqVXeUEuw1\"\n}"}}, true},
-		{"refresh - bad return", []apiResponse{{mccAPIRefresh, "{{\n    \"error\": \"\"\n}"}}, true},
+		{"refresh - success", []apiResponse{{apiRefresh, "{\n    \"token\": \"1234567890._abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n}"}}, false},
+		{"refresh - wrong password", []apiResponse{{apiRefresh, "{\n    \"error\": \"signature mismatch: OP-gWPOgQ9fdKujMgRNHkeH4WHqYrHe3Z2RqVXeUEuw1\"\n}"}}, true},
+		{"refresh - bad return", []apiResponse{{apiRefresh, "{{\n    \"error\": \"\"\n}"}}, true},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -120,15 +120,15 @@ func TestMobileConnect_Status(t *testing.T) {
 		wantErr   bool
 	}{
 		// test cases for software version 2914
-		{"home plug - Unexpected API response", []apiResponse{{mccAPIChargeState, "abc"}}, api.StatusNone, true},
-		{"home plug - Unplugged", []apiResponse{{mccAPIChargeState, "0\n"}}, api.StatusA, false},
-		{"home plug - Connecting", []apiResponse{{mccAPIChargeState, "1\n"}}, api.StatusB, false},
-		{"home plug - Error", []apiResponse{{mccAPIChargeState, "2\n"}}, api.StatusF, false},
-		{"home plug - Established", []apiResponse{{mccAPIChargeState, "3\n"}}, api.StatusB, false},
-		{"home plug - Paused", []apiResponse{{mccAPIChargeState, "4\n"}}, api.StatusB, false},
-		{"home plug - Active", []apiResponse{{mccAPIChargeState, "5\n"}}, api.StatusC, false},
-		{"home plug - Finished", []apiResponse{{mccAPIChargeState, "6\n"}}, api.StatusB, false},
-		{"home plug - Unexpected status value", []apiResponse{{mccAPIChargeState, "10\n"}}, api.StatusNone, true},
+		{"home plug - Unexpected API response", []apiResponse{{apiChargeState, "abc"}}, api.StatusNone, true},
+		{"home plug - Unplugged", []apiResponse{{apiChargeState, "0\n"}}, api.StatusA, false},
+		{"home plug - Connecting", []apiResponse{{apiChargeState, "1\n"}}, api.StatusB, false},
+		{"home plug - Error", []apiResponse{{apiChargeState, "2\n"}}, api.StatusF, false},
+		{"home plug - Established", []apiResponse{{apiChargeState, "3\n"}}, api.StatusB, false},
+		{"home plug - Paused", []apiResponse{{apiChargeState, "4\n"}}, api.StatusB, false},
+		{"home plug - Active", []apiResponse{{apiChargeState, "5\n"}}, api.StatusC, false},
+		{"home plug - Finished", []apiResponse{{apiChargeState, "6\n"}}, api.StatusB, false},
+		{"home plug - Unexpected status value", []apiResponse{{apiChargeState, "10\n"}}, api.StatusNone, true},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -154,14 +154,14 @@ func TestMobileConnect_Enabled(t *testing.T) {
 		wantErr   bool
 	}{
 		// test cases for software version 2914
-		{"home plug - Unexpected API response", []apiResponse{{mccAPIChargeState, "abc"}}, false, true},
-		{"home plug - Unplugged", []apiResponse{{mccAPIChargeState, "0\n"}}, false, false},
-		{"home plug - Connecting", []apiResponse{{mccAPIChargeState, "1\n"}}, false, false},
-		{"home plug - Error", []apiResponse{{mccAPIChargeState, "2\n"}}, false, false},
-		{"home plug - Established", []apiResponse{{mccAPIChargeState, "3\n"}}, false, false},
-		{"home plug - Paused", []apiResponse{{mccAPIChargeState, "4\n"}}, true, false},
-		{"home plug - Active", []apiResponse{{mccAPIChargeState, "5\n"}}, true, false},
-		{"home plug - Finished", []apiResponse{{mccAPIChargeState, "6\n"}}, true, false},
+		{"home plug - Unexpected API response", []apiResponse{{apiChargeState, "abc"}}, false, true},
+		{"home plug - Unplugged", []apiResponse{{apiChargeState, "0\n"}}, false, false},
+		{"home plug - Connecting", []apiResponse{{apiChargeState, "1\n"}}, false, false},
+		{"home plug - Error", []apiResponse{{apiChargeState, "2\n"}}, false, false},
+		{"home plug - Established", []apiResponse{{apiChargeState, "3\n"}}, false, false},
+		{"home plug - Paused", []apiResponse{{apiChargeState, "4\n"}}, true, false},
+		{"home plug - Active", []apiResponse{{apiChargeState, "5\n"}}, true, false},
+		{"home plug - Finished", []apiResponse{{apiChargeState, "6\n"}}, true, false},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
@@ -190,38 +190,38 @@ func TestMobileConnect_MaxCurrent(t *testing.T) {
 		{
 			"home plug - success min value",
 			[]apiResponse{
-				{mccAPICurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
-				{mccAPISetCurrentLimit, "\"OK\"\n"},
+				{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
+				{apiSetCurrentLimit, "\"OK\"\n"},
 			},
 			6, false,
 		},
 		{
 			"home plug - success max value",
 			[]apiResponse{
-				{mccAPICurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
-				{mccAPISetCurrentLimit, "\"OK\"\n"},
+				{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
+				{apiSetCurrentLimit, "\"OK\"\n"},
 			},
 			10, false,
 		},
 		{
 			"home plug - error value too small",
 			[]apiResponse{
-				{mccAPICurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
+				{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
 			},
 			0, true,
 		},
 		{
 			"home plug - error value too big",
 			[]apiResponse{
-				{mccAPICurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
+				{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
 			},
 			16, true,
 		},
 		{
 			"home plug - 1st API success but 2nd API error",
 			[]apiResponse{
-				{mccAPICurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
-				{mccAPISetCurrentLimit, "Unexpected response"},
+				{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
+				{apiSetCurrentLimit, "Unexpected response"},
 			},
 			10, true,
 		},
@@ -237,93 +237,6 @@ func TestMobileConnect_MaxCurrent(t *testing.T) {
 	}
 }
 
-func TestMobileConnect_CurrentPower(t *testing.T) {
-	tests := []struct {
-		name      string
-		responses []apiResponse
-		want      float64
-		wantErr   bool
-	}{
-		// test cases for software version 2914
-		{
-			"home plug - charging",
-			[]apiResponse{
-				{mccAPIEnergy, "\"{\\n    \\\"L1\\\": {\\n        \\\"Ampere\\\": 9.9000000000000004,\\n        \\\"Power\\\": 2308,\\n        \\\"Volts\\\": 230.5\\n    },\\n    \\\"L2\\\": {\\n        \\\"Ampere\\\": 0,\\n        \\\"Power\\\": 0,\\n        \\\"Volts\\\": 13.700000000000001\\n    },\\n    \\\"L3\\\": {\\n        \\\"Ampere\\\": 0,\\n        \\\"Power\\\": 0,\\n        \\\"Volts\\\": 13.9\\n    }\\n}\\n\""},
-			},
-			2308, false,
-		},
-		{
-			"no data response",
-			[]apiResponse{
-				{mccAPIEnergy, "\"\"\n"},
-			}, 0, false,
-		},
-		{
-			"home plug - error response",
-			[]apiResponse{
-				{mccAPIEnergy, "\"{\\n    \\\"L1\\\": {\\n        \\\"Ampere\\\": 0,\\n        \\\"Power\\\": 0,\\n        \\\"Volts\\\": 246.60000000000002\\n    },\\n    \\\"L2\\\": {\\n        \\\"Ampere\\\": 0,\\n        \\\"Power\\\": 0,\\n        \\\"Volts\\\": 16.800000000000001\\n    },\\n    \\\"L3\\\": {\\n        \\\"Ampere\\\": 0,\\n        \\\"Power\\\": 0,\\n        \\\"Volts\\\": 16.300000000000001\\n    }\\n}\\n\""},
-			}, 0, false,
-		},
-	}
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			mcc := NewTestMobileConnect(t, tc.responses)
-
-			got, err := mcc.CurrentPower()
-			if (err != nil) != tc.wantErr {
-				t.Errorf("MobileConnect.CurrentPower() error = %v, wantErr %v", err, tc.wantErr)
-				return
-			}
-			if got != tc.want {
-				t.Errorf("MobileConnect.CurrentPower() = %v, want %v", got, tc.want)
-			}
-		})
-	}
-}
-
-func TestMobileConnect_ChargedEnergy(t *testing.T) {
-	tests := []struct {
-		name      string
-		responses []apiResponse
-		want      float64
-		wantErr   bool
-	}{
-		// test cases for software version 2914
-		{
-			"valid response",
-			[]apiResponse{
-				{mccAPICurrentSession, "\"{\\n    \\\"account\\\": \\\"PRIVATE\\\",\\n    \\\"chargingRate\\\": 0,\\n    \\\"chargingType\\\": \\\"AC\\\",\\n    \\\"clockSrc\\\": \\\"NTP\\\",\\n    \\\"costs\\\": 0,\\n    \\\"currency\\\": \\\"\\\",\\n    \\\"departTime\\\": \\\"\\\",\\n    \\\"duration\\\": 30789,\\n    \\\"endOfChargeTime\\\": \\\"\\\",\\n    \\\"endSoc\\\": 0,\\n    \\\"endTime\\\": \\\"\\\",\\n    \\\"energySumKwh\\\": 18.832000000000001,\\n    \\\"evChargingRatekW\\\": 0,\\n    \\\"evTargetSoc\\\": -1,\\n    \\\"evVasAvailability\\\": false,\\n    \\\"pcid\\\": \\\"\\\",\\n    \\\"powerRange\\\": 0,\\n    \\\"selfEnergy\\\": 0,\\n    \\\"sessionId\\\": 13,\\n    \\\"soc\\\": -1,\\n    \\\"solarEnergyShare\\\": 0,\\n    \\\"startSoc\\\": 0,\\n    \\\"startTime\\\": \\\"2020-04-15T10:07:22+02:00\\\",\\n    \\\"totalRange\\\": 0,\\n    \\\"vehicleBrand\\\": \\\"\\\",\\n    \\\"vehicleModel\\\": \\\"\\\",\\n    \\\"whitelist\\\": false\\n}\\n\""},
-			}, 18.832000000000001, false,
-		},
-		{
-			"no data response",
-			[]apiResponse{
-				{mccAPICurrentSession, "\"\"\n"},
-			}, 0, false,
-		},
-		{
-			"error response",
-			[]apiResponse{
-				{mccAPICurrentSession, "invalidjson"},
-			}, 0, true,
-		},
-	}
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			mcc := NewTestMobileConnect(t, tc.responses)
-
-			got, err := mcc.ChargedEnergy()
-			if (err != nil) != tc.wantErr {
-				t.Errorf("MobileConnect.ChargedEnergy() error = %v, wantErr %v", err, tc.wantErr)
-				return
-			}
-			if got != tc.want {
-				t.Errorf("MobileConnect.ChargedEnergy() = %v, want %v", got, tc.want)
-			}
-		})
-	}
-}
-
 func TestMobileConnect_ChargingTime(t *testing.T) {
 	tests := []struct {
 		name      string
@@ -334,19 +247,13 @@ func TestMobileConnect_ChargingTime(t *testing.T) {
 		{
 			"valid response",
 			[]apiResponse{
-				{mccAPICurrentSession, "\"{\\n    \\\"account\\\": \\\"PRIVATE\\\",\\n    \\\"chargingRate\\\": 0,\\n    \\\"chargingType\\\": \\\"AC\\\",\\n    \\\"clockSrc\\\": \\\"NTP\\\",\\n    \\\"costs\\\": 0,\\n    \\\"currency\\\": \\\"\\\",\\n    \\\"departTime\\\": \\\"\\\",\\n    \\\"duration\\\": 30789,\\n    \\\"endOfChargeTime\\\": \\\"\\\",\\n    \\\"endSoc\\\": 0,\\n    \\\"endTime\\\": \\\"\\\",\\n    \\\"energySumKwh\\\": 18.832000000000001,\\n    \\\"evChargingRatekW\\\": 0,\\n    \\\"evTargetSoc\\\": -1,\\n    \\\"evVasAvailability\\\": false,\\n    \\\"pcid\\\": \\\"\\\",\\n    \\\"powerRange\\\": 0,\\n    \\\"selfEnergy\\\": 0,\\n    \\\"sessionId\\\": 13,\\n    \\\"soc\\\": -1,\\n    \\\"solarEnergyShare\\\": 0,\\n    \\\"startSoc\\\": 0,\\n    \\\"startTime\\\": \\\"2020-04-15T10:07:22+02:00\\\",\\n    \\\"totalRange\\\": 0,\\n    \\\"vehicleBrand\\\": \\\"\\\",\\n    \\\"vehicleModel\\\": \\\"\\\",\\n    \\\"whitelist\\\": false\\n}\\n\""},
+				{apiCurrentSession, "\"{\\n    \\\"account\\\": \\\"PRIVATE\\\",\\n    \\\"chargingRate\\\": 0,\\n    \\\"chargingType\\\": \\\"AC\\\",\\n    \\\"clockSrc\\\": \\\"NTP\\\",\\n    \\\"costs\\\": 0,\\n    \\\"currency\\\": \\\"\\\",\\n    \\\"departTime\\\": \\\"\\\",\\n    \\\"duration\\\": 30789,\\n    \\\"endOfChargeTime\\\": \\\"\\\",\\n    \\\"endSoc\\\": 0,\\n    \\\"endTime\\\": \\\"\\\",\\n    \\\"energySumKwh\\\": 18.832000000000001,\\n    \\\"evChargingRatekW\\\": 0,\\n    \\\"evTargetSoc\\\": -1,\\n    \\\"evVasAvailability\\\": false,\\n    \\\"pcid\\\": \\\"\\\",\\n    \\\"powerRange\\\": 0,\\n    \\\"selfEnergy\\\": 0,\\n    \\\"sessionId\\\": 13,\\n    \\\"soc\\\": -1,\\n    \\\"solarEnergyShare\\\": 0,\\n    \\\"startSoc\\\": 0,\\n    \\\"startTime\\\": \\\"2020-04-15T10:07:22+02:00\\\",\\n    \\\"totalRange\\\": 0,\\n    \\\"vehicleBrand\\\": \\\"\\\",\\n    \\\"vehicleModel\\\": \\\"\\\",\\n    \\\"whitelist\\\": false\\n}\\n\""},
 			}, 30789 * time.Second, false,
 		},
-		{
-			"no data response",
-			[]apiResponse{
-				{mccAPICurrentSession, "\"\"\n"},
-			}, 0, false,
-		},
 		{
 			"error response",
 			[]apiResponse{
-				{mccAPICurrentSession, "invalidjson"},
+				{apiCurrentSession, "\"\""},
 			}, 0, true,
 		},
 	}
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 3caec860..69684de8 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -10,7 +10,6 @@ import (
 	"github.com/andig/evcc/mock"
 	"github.com/andig/evcc/provider"
 	"github.com/andig/evcc/push"
-	"github.com/andig/evcc/util"
 	"github.com/benbjohnson/clock"
 	"github.com/golang/mock/gomock"
 )
@@ -67,7 +66,7 @@ func newLoadPoint(charger api.Charger, pv, gm, cm api.Meter) *LoadPoint {
 		lp.chargeMeter = cm
 	}
 
-	uiChan := make(chan util.Param)
+	uiChan := make(chan Param)
 	notificationChan := make(chan push.Event)
 
 	lp.Prepare(uiChan, notificationChan)
@@ -342,146 +341,3 @@ func TestConsumedPower(t *testing.T) {
 		}
 	}
 }
-
-func TestPVHysteresis(t *testing.T) {
-	dt := time.Minute
-	type se struct {
-		site    float64
-		delay   time.Duration // test case delay since start
-		current int64
-	}
-	tc := []struct {
-		enabled         bool
-		enable, disable float64
-		series          []se
-	}{
-		// keep disabled
-		{false, 0, 0, []se{
-			{0, 0, 0},
-			{0, 1, 0},
-			{0, dt - 1, 0},
-			{0, dt + 1, 0},
-		}},
-		// enable when threshold not configured but min power met
-		{false, 0, 0, []se{
-			{-6 * 100 * 10, 0, 0},
-			{-6 * 100 * 10, 1, 0},
-			{-6 * 100 * 10, dt - 1, 0},
-			{-6 * 100 * 10, dt + 1, lpMinCurrent},
-		}},
-		// keep disabled when threshold not configured
-		{false, 0, 0, []se{
-			{-400, 0, 0},
-			{-400, 1, 0},
-			{-400, dt - 1, 0},
-			{-400, dt + 1, 0},
-		}},
-		// keep disabled when threshold not met
-		{false, -500, 0, []se{
-			{-400, 0, 0},
-			{-400, 1, 0},
-			{-400, dt - 1, 0},
-			{-400, dt + 1, 0},
-		}},
-		// enable when threshold met
-		{false, -500, 0, []se{
-			{-500, 0, 0},
-			{-500, 1, 0},
-			{-500, dt - 1, 0},
-			{-500, dt + 1, lpMinCurrent},
-		}},
-		// keep enabled at max
-		{true, 500, 0, []se{
-			{-16 * 100 * 10, 0, lpMaxCurrent},
-			{-16 * 100 * 10, 1, lpMaxCurrent},
-			{-16 * 100 * 10, dt - 1, lpMaxCurrent},
-			{-16 * 100 * 10, dt + 1, lpMaxCurrent},
-		}},
-		// keep enabled at min
-		{true, 500, 0, []se{
-			{-6 * 100 * 10, 0, lpMinCurrent},
-			{-6 * 100 * 10, 1, lpMinCurrent},
-			{-6 * 100 * 10, dt - 1, lpMinCurrent},
-			{-6 * 100 * 10, dt + 1, lpMinCurrent},
-		}},
-		// keep enabled at min (negative threshold)
-		{true, 0, 500, []se{
-			{-500, 0, lpMinCurrent},
-			{-500, 1, lpMinCurrent},
-			{-500, dt - 1, lpMinCurrent},
-			{-500, dt + 1, lpMinCurrent},
-		}},
-		// disable when threshold met
-		{true, 0, 500, []se{
-			{500, 0, lpMinCurrent},
-			{500, 1, lpMinCurrent},
-			{500, dt - 1, lpMinCurrent},
-			{500, dt + 1, 0},
-		}},
-		// reset enable timer when threshold not met while timer active
-		{false, -500, 0, []se{
-			{-500, 0, 0},
-			{-500, 1, 0},
-			{-499, dt - 1, 0}, // should reset timer
-			{-500, dt + 1, 0}, // new begin of timer
-			{-500, 2*dt - 2, 0},
-			{-500, 2*dt - 1, lpMinCurrent},
-		}},
-		// reset enable timer when threshold not met while timer active and threshold not configured
-		{false, 0, 0, []se{
-			{-6*100*10 - 1, dt + 1, 0},
-			{-6 * 100 * 10, dt + 1, 0},
-			{-6 * 100 * 10, dt + 2, 0},
-			{-6 * 100 * 10, 2 * dt, 0},
-			{-6 * 100 * 10, 2*dt + 2, lpMinCurrent},
-		}},
-		// reset disable timer when threshold not met while timer active
-		{true, 0, 500, []se{
-			{500, 0, lpMinCurrent},
-			{500, 1, lpMinCurrent},
-			{499, dt - 1, lpMinCurrent},   // reset timer
-			{500, dt + 1, lpMinCurrent},   // within reset timer duration
-			{500, 2*dt - 2, lpMinCurrent}, // still within reset timer duration
-			{500, 2*dt - 1, 0},            // reset timer elapsed
-		}},
-	}
-
-	for _, tc := range tc {
-		t.Log(tc)
-
-		clck := clock.NewMock()
-		lp := LoadPoint{
-			clock: clck,
-			ChargerHandler: ChargerHandler{
-				MinCurrent: lpMinCurrent,
-				MaxCurrent: lpMaxCurrent,
-				enabled:    tc.enabled,
-			},
-			Config: Config{
-				Voltage: 100,
-				Phases:  10,
-				Enable: ThresholdConfig{
-					Threshold: tc.enable,
-					Delay:     dt,
-				},
-				Disable: ThresholdConfig{
-					Threshold: tc.disable,
-					Delay:     dt,
-				},
-			},
-			gridPower: 0,
-		}
-
-		start := clck.Now()
-
-		for step, se := range tc.series {
-			clck.Set(start.Add(se.delay))
-			lp.gridPower = se.site
-			current := lp.maxCurrent(api.ModePV)
-
-			if current != se.current {
-				t.Errorf("step %d: wanted %d, got %d", step, se.current, current)
-			}
-		}
-	}
-}
diff --git a/meter/sma/listener_test.go b/meter/sma/listener_test.go
index 56b0552a..1d92af47 100644
--- a/meter/sma/listener_test.go
+++ b/meter/sma/listener_test.go
@@ -4,8 +4,6 @@ import (
 	"net"
 	"reflect"
 	"testing"
-
-	"github.com/andig/evcc/util"
 )
 
 func TestListenerProcessMessage(t *testing.T) {
@@ -188,9 +186,7 @@ func TestListenerProcessMessage(t *testing.T) {
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			l := &Listener{
-				log: util.NewLogger("sma "),
-			}
+			l := &Listener{}
 
 			buffer := tc.response
 			read := len(buffer)
diff --git a/meter/sma_test.go b/meter/sma_test.go
deleted file mode 100644
index 363b591a..00000000
--- a/meter/sma_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package meter
-
-import (
-	"testing"
-
-	"github.com/andig/evcc/meter/sma"
-	"github.com/andig/evcc/util"
-)
-
-func TestSMAUpdatePower(t *testing.T) {
-	tests := []struct {
-		name      string
-		messsage  sma.Telegram
-		wantPower float64
-	}{
-		{
-			"success export",
-			sma.Telegram{
-				Values: map[string]float64{
-					"1:1.4.0": 0,
-					"1:2.4.0": 37.9,
-				},
-			},
-			-37.9,
-		},
-		{
-			"success import",
-			sma.Telegram{
-				Values: map[string]float64{
-					"1:1.4.0": 20,
-					"1:2.4.0": 0,
-				},
-			},
-			20,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			sm := &SMA{
-				log: util.NewLogger("sma "),
-			}
-
-			sm.updatePower(tt.messsage)
-			if sm.power != tt.wantPower {
-				t.Errorf("Listener.processMessage() got %v, want %v", sm.power, tt.wantPower)
-			}
-		})
-	}
-}
diff --git a/server/limiter_test.go b/server/limiter_test.go
index 78576018..98737074 100644
--- a/server/limiter_test.go
+++ b/server/limiter_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/andig/evcc/util"
+	"github.com/andig/evcc/core"
 	"github.com/benbjohnson/clock"
 )
 
@@ -14,10 +14,10 @@ func TestLimiter(t *testing.T) {
 	clck := clock.NewMock()
 	l.clock = clck
 
-	in := make(chan util.Param)
+	in := make(chan core.Param)
 	out := l.Pipe(in)
 
-	p := util.Param{Key: "k", Val: 1}
+	p := core.Param{Key: "k", Val: 1}
 	in <- p
 
 	if o := <-out; o.Key != p.Key || o.Val != p.Val {
@@ -57,17 +57,17 @@ func TestDeduplicator(t *testing.T) {
 	clck := clock.NewMock()
 	l.clock = clck
 
-	in := make(chan util.Param)
+	in := make(chan core.Param)
 	out := l.Pipe(in)
 
-	p := util.Param{Key: "k", Val: 1}
+	p := core.Param{Key: "k", Val: 1}
 	in <- p
 
 	if o := <-out; o.Key != p.Key || o.Val != p.Val {
 		t.Errorf("unexpected param %v", o)
 	}
 
-	p = util.Param{Key: "k", Val: 2}
+	p = core.Param{Key: "k", Val: 2}
 	in <- p
 
 	if o := <-out; o.Key != p.Key || o.Val != p.Val {
@@ -75,21 +75,21 @@ func TestDeduplicator(t *testing.T) {
 	}
 
 	// allow nils
-	p = util.Param{Key: "k", Val: nil}
+	p = core.Param{Key: "k", Val: nil}
 	in <- p
 
 	if o := <-out; o.Key != p.Key || o.Val != p.Val {
 		t.Errorf("unexpected param %v", o)
 	}
 
-	p = util.Param{Key: "filtered", Val: 3}
+	p = core.Param{Key: "filtered", Val: 3}
 	in <- p
 
 	if o := <-out; o.Key != p.Key || o.Val != p.Val {
 		t.Errorf("unexpected param %v", o)
 	}
 
-	p = util.Param{Key: "filtered", Val: 4}
+	p = core.Param{Key: "filtered", Val: 4}
 	in <- p
 
 	if o := <-out; o.Key != p.Key || o.Val != p.Val {
@@ -115,7 +115,7 @@ func TestDeduplicator(t *testing.T) {
 	}
 
 	// allow nils
-	p = util.Param{Key: "filtered", Val: nil}
+	p = core.Param{Key: "filtered", Val: nil}
 	in <- p
 
 	if o := <-out; o.Key != p.Key || o.Val != p.Val {
diff --git a/server/socket_test.go b/server/socket_test.go
deleted file mode 100644
index 3d37bee7..00000000
--- a/server/socket_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package server
-
-import (
-	"reflect"
-	"testing"
-	"time"
-)
-
-type nilVal int
-
-func (n *nilVal) String() string {
-	return "—"
-}
-
-func TestEncode(t *testing.T) {
-	var nv nilVal
-	tc := []struct {
-		in, out interface{}
-	}{
-		{int64(1), "1"},
-		{float64(1.2345), "1.234"},
-		{"1.2345", "\"1.2345\""},
-		{&nv, "\"—\""},
-		{time.Duration(time.Hour), "3600"},
-		{"minpv", "\"minpv\""},
-	}
-
-	for _, tc := range tc {
-		t.Logf("%+v", tc)
-		out, err := encode(tc.in)
-		if err != nil {
-			t.Error(err)
-		}
-
-		if out != tc.out {
-			t.Errorf("expected %v (string), got %v (%s)",
-				tc.out, out, reflect.TypeOf(out).Kind(),
-			)
-		}
-	}
-}