Repository: evcc-io/evcc
Pull request #2425 (2022-01-28 15:58:17)

Implement oauth login proxy


Patch:
diff --git a/README.md b/README.md
index 4996ecba..ef492b27 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Evcc is an extensible EV Charge Controller with PV integration implemented in [G
 - simple and clean user interface
 - multiple [chargers](https://docs.evcc.io/docs/devices/chargers):
   - Open source: [openWB](https://openwb.de/), [EVSEWifi](https://www.evse-wifi.de) (includes smartWB)
-  - Other commercial: ABL eMH1, Alfen Eve, cFos PowerBrain, Daheimladen, go-eCharger, Heidelberg Energy Control, Innogy eBox, KEBA/BMW, NRGkick, PC Electric (includes Garo), Wallbe, Mobile Charger Connect, EEBUS (experimental)
+  - Other commercial: ABL eMH1, Alfen Eve, cFos PowerBrain, Daheimladen, go-eCharger, Heidelberg Energy Control, Innogy eBox, KEBA/BMW, Menneckes  Amtron (Xtra/Premium), NRGkick, PC Electric (includes Garo), Wallbe, Mobile Charger Connect, EEBUS (experimental)
   - Build-your-own: Phoenix (includes ESL Walli), [EVSE DIN](https://www.evse-wifi.de/produkt-schlagwort/simple-evse-wb/)
   - Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link
 - multiple [meters](https://docs.evcc.io/docs/devices/meters): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall, LG ESS HOME
diff --git a/api/api.go b/api/api.go
index 7e5e5e10..a4d89be8 100644
--- a/api/api.go
+++ b/api/api.go
@@ -187,6 +187,11 @@ type VehicleStopCharge interface {
 	StopCharge() error
 }
 
+// AlarmClock provides wakeup calls to the vehicle with an API call or a CP interrupt from the charger
+type AlarmClock interface {
+	WakeUp() error
+}
+
 type Tariff interface {
 	IsCheap() (bool, error)
 	CurrentPrice() (float64, error) // EUR/kWh, CHF/kWh, ...
@@ -198,8 +203,7 @@ type WebController interface {
 
 // ProviderLogin is the ability to provide OAuth authentication through the ui
 type ProviderLogin interface {
-	SetCallbackParams(uri string, authenticated chan<- bool)
+	SetCallbackParams(baseURL, redirectURL string, authenticated chan<- bool)
 	LoginHandler() http.HandlerFunc
 	LogoutHandler() http.HandlerFunc
-	CallbackHandler(baseURI string) http.HandlerFunc
 }
diff --git a/charger/abl.go b/charger/abl.go
index 92db0729..a4901345 100644
--- a/charger/abl.go
+++ b/charger/abl.go
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
diff --git a/charger/alfen.go b/charger/alfen.go
index 3c7ec9b4..be4716e9 100644
--- a/charger/alfen.go
+++ b/charger/alfen.go
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
@@ -66,12 +66,12 @@ func NewAlfenFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewAlfen(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID)
+	return NewAlfen(cc.URI, cc.ID)
 }
 
 // NewAlfen creates Alfen charger
-func NewAlfen(uri, device, comset string, baudrate int, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.Tcp, slaveID)
+func NewAlfen(uri string, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/amtron.go b/charger/amtron.go
new file mode 100644
index 00000000..36aecedf
--- /dev/null
+++ b/charger/amtron.go
@@ -0,0 +1,190 @@
+package charger
+
+// LICENSE
+
+// Copyright (c) 2019-2022 andig
+
+// This module is NOT covered by the MIT license. All rights reserved.
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import (
+	"encoding/binary"
+	"fmt"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/evcc-io/evcc/util/sponsor"
+)
+
+// https://update.mennekes.de/hcc3/1.13/Description%20Modbus_AMTRON%20HCC3_v01_2021-06-25_en.pdf
+
+// Amtron Xtra/Premium charger implementation
+type Amtron struct {
+	conn *modbus.Connection
+}
+
+const (
+	amtronRegEnergy     = 0x030D
+	amtronRegPower      = 0x030F
+	amtronRegStatus     = 0x0312
+	amtronRegEnabled    = 0x0401
+	amtronRegAmpsConfig = 0x0400
+	amtronRegSerial     = 0x030B
+	amtronRegName       = 0x0311
+)
+
+func init() {
+	registry.Add("amtron", NewAmtronFromConfig)
+}
+
+// NewAmtronFromConfig creates a Mennekes Amtron charger from generic config
+func NewAmtronFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := modbus.Settings{
+		ID: 0xff,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewAmtron(cc.URI, cc.ID)
+}
+
+// NewAmtron creates Amtron charger
+func NewAmtron(uri string, slaveID uint8) (api.Charger, error) {
+	uri = util.DefaultPort(uri, 502)
+
+	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	log := util.NewLogger("amtron")
+	conn.Logger(log.TRACE)
+
+	wb := &Amtron{
+		conn: conn,
+	}
+
+	return wb, err
+}
+
+// Status implements the api.Charger interface
+func (wb *Amtron) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadInputRegisters(amtronRegStatus, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	switch b[0] {
+	case 1, 2:
+		return api.StatusA, nil
+	case 3, 4:
+		return api.StatusB, nil
+	case 5, 6:
+		// TODO check C1 -> B?
+		return api.StatusC, nil
+	case 7, 8:
+		return api.StatusD, nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %0x", b[:1])
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Amtron) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(amtronRegEnabled, 1)
+	if err != nil {
+		return false, err
+	}
+
+	var res bool
+	switch b[0] {
+	case 0, 4:
+		res = true
+	}
+
+	return res, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *Amtron) Enable(enable bool) error {
+	b := make([]byte, 2)
+	if enable {
+		b[0] = 0x04
+	}
+
+	_, err := wb.conn.WriteMultipleRegisters(amtronRegEnabled, 1, b)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Amtron) MaxCurrent(current int64) error {
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, uint16(current))
+
+	_, err := wb.conn.WriteMultipleRegisters(amtronRegAmpsConfig, 1, b)
+
+	return err
+}
+
+var _ api.Meter = (*Amtron)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *Amtron) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(amtronRegPower, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.LittleEndian.Uint32(b)), err
+}
+
+var _ api.MeterEnergy = (*Amtron)(nil)
+
+// TotalEnergy implements the api.MeterEnergy interface
+func (wb *Amtron) TotalEnergy() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(amtronRegEnergy, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.LittleEndian.Uint32(b)) / 1e3, err
+}
+
+// var _ api.ChargePhases = (*Amtron)(nil)
+
+// // Phases1p3p implements the api.ChargePhases interface
+// func (c *Amtron) Phases1p3p(phases int) error {
+// 	_, err := c.conn.WriteSingleRegister(amtronRegPhases, uint16(phases))
+// 	return err
+// }
+
+var _ api.Diagnosis = (*Amtron)(nil)
+
+// Diagnose implements the api.Diagnosis interface
+func (wb *Amtron) Diagnose() {
+	if b, err := wb.conn.ReadInputRegisters(amtronRegName, 11); err == nil {
+		fmt.Printf("Name: %s\n", string(b))
+	}
+
+	if b, err := wb.conn.ReadInputRegisters(amtronRegSerial, 2); err == nil {
+		fmt.Printf("Serial: %d\n", binary.LittleEndian.Uint32(b))
+	}
+}
diff --git a/charger/cfos.go b/charger/cfos.go
index 9a20bb82..08b37cba 100644
--- a/charger/cfos.go
+++ b/charger/cfos.go
@@ -51,6 +51,10 @@ func NewCfosPowerBrain(uri string, id uint8) (*CfosPowerBrain, error) {
 		return nil, err
 	}
 
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
 	log := util.NewLogger("cfos")
 	conn.Logger(log.TRACE)
 
diff --git a/charger/easee.go b/charger/easee.go
index f8323721..e68c964d 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
diff --git a/charger/go-e.go b/charger/go-e.go
index 93b05107..60cfa18c 100644
--- a/charger/go-e.go
+++ b/charger/go-e.go
@@ -1,19 +1,8 @@
 package charger
 
-import (
-	"errors"
-	"fmt"
-	"time"
-
-	"github.com/evcc-io/evcc/api"
-	goe "github.com/evcc-io/evcc/charger/go-e"
-	"github.com/evcc-io/evcc/util"
-	"github.com/evcc-io/evcc/util/sponsor"
-)
-
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
@@ -28,6 +17,17 @@ import (
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 // SOFTWARE.
 
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	goe "github.com/evcc-io/evcc/charger/go-e"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/sponsor"
+)
+
 // https://go-e.co/app/api.pdf
 // https://github.com/goecharger/go-eCharger-API-v1/
 // https://github.com/goecharger/go-eCharger-API-v2/
diff --git a/charger/heidelberg-ec.go b/charger/heidelberg-ec.go
index ce494eee..60c240e6 100644
--- a/charger/heidelberg-ec.go
+++ b/charger/heidelberg-ec.go
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
diff --git a/charger/innogy.go b/charger/innogy.go
index 81e1a265..5c0ea0f7 100644
--- a/charger/innogy.go
+++ b/charger/innogy.go
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
@@ -43,6 +43,8 @@ var (
 	igyRegCurrents    = []uint16{1006, 1008, 1010} // current readings per phase
 )
 
+// https://www.innogy-emobility.com/content/dam/revu-global/emobility-solutions/neue-website-feb-2021/downloadcenter/digital-services/eld_instman_modbustcpde.pdf
+
 // Innogy is an api.Charger implementation for Innogy eBox wallboxes.
 type Innogy struct {
 	conn *modbus.Connection
diff --git a/charger/vestel.go b/charger/vestel.go
index 428a0a07..15ac62c1 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
diff --git a/cmd/config.go b/cmd/config.go
index 12079c5a..fee86303 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -3,8 +3,6 @@ package cmd
 import (
 	"fmt"
 	"net/http"
-	"net/url"
-	"strings"
 	"time"
 
 	"github.com/dustin/go-humanize"
@@ -14,6 +12,7 @@ import (
 	"github.com/evcc-io/evcc/provider/mqtt"
 	"github.com/evcc-io/evcc/push"
 	"github.com/evcc-io/evcc/server"
+	autoauth "github.com/evcc-io/evcc/server/auth"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/vehicle"
 	"github.com/evcc-io/evcc/vehicle/wrapper"
@@ -203,51 +202,39 @@ func (cp *ConfigProvider) configureVehicles(conf config) error {
 	return nil
 }
 
-func canonicalName(s string) string {
-	return strings.ToLower(strings.ReplaceAll(s, " ", "_"))
-}
-
 // webControl handles routing for devices. For now only api.ProviderLogin related routes
 func (cp *ConfigProvider) webControl(httpd *server.HTTPd, paramC chan<- util.Param) {
 	router := httpd.Router()
 
-	auth := router.PathPrefix("/auth").Subrouter()
+	auth := router.PathPrefix("/oauth").Subrouter()
 	auth.Use(handlers.CompressHandler)
 	auth.Use(handlers.CORS(
 		handlers.AllowedHeaders([]string{"Content-Type"}),
 	))
 
+	// wire the handler
+	autoauth.Setup(auth)
+
 	// initialize
 	cp.auth = util.NewAuthCollection(paramC)
 
+	// TODO make evccURI configurable, add warnings for any network/ localhost
+	evccURI := fmt.Sprintf("http://%s", httpd.Addr)
+	authURI := fmt.Sprintf("%s/oauth", evccURI)
+
+	var id int
 	for _, v := range cp.vehicles {
 		if provider, ok := v.(api.ProviderLogin); ok {
-			title := url.QueryEscape(canonicalName(v.Title()))
-			basePath := fmt.Sprintf("vehicles/%s", title)
+			id += 1
 
-			// TODO make evccURI configurable, add warnings for any network/ localhost
-			evccURI := fmt.Sprintf("http://%s", httpd.Addr)
+			basePath := fmt.Sprintf("vehicles/%d", id)
 			baseURI := fmt.Sprintf("%s/auth/%s", evccURI, basePath)
 
 			// register vehicle
-			ap := cp.auth.Register(v.Title(), baseURI)
+			ap := cp.auth.Register(baseURI, v.Title())
 
-			redirectURI := fmt.Sprintf("%s/callback", baseURI)
-			provider.SetCallbackParams(redirectURI, ap.Handler())
-			log.INFO.Printf("ensure the oauth client redirect/callback is configured for %s: %s", v.Title(), redirectURI)
+			provider.SetCallbackParams(evccURI, authURI, ap.Handler())
 
-			// TODO how to handle multiple vehicles of the same type
-			//
-			// problems, thoughts and ideas:
-			// conflicting callbacks!
-			// - some unique part has to be added.
-			// - or a general callback handler and the specific vehicle is transported in the state?
-			//   - callback handler needs an option to set the token at the right vehicle and use the right code exchange
-
-			auth.
-				Methods(http.MethodGet).
-				Path(fmt.Sprintf("/%s/callback", basePath)).
-				HandlerFunc(provider.CallbackHandler(evccURI))
 			auth.
 				Methods(http.MethodPost).
 				Path(fmt.Sprintf("/%s/login", basePath)).
@@ -259,5 +246,9 @@ func (cp *ConfigProvider) webControl(httpd *server.HTTPd, paramC chan<- util.Par
 		}
 	}
 
+	if id > 0 {
+		log.INFO.Printf("ensure the oauth client redirect/callback is configured for: %s", authURI)
+	}
+
 	cp.auth.Publish()
 }
diff --git a/cmd/configure/flow.go b/cmd/configure/flow.go
index 3b88f32e..feaad3a7 100644
--- a/cmd/configure/flow.go
+++ b/cmd/configure/flow.go
@@ -53,7 +53,7 @@ func (c *CmdConfigure) configureDeviceGuidedSetup() {
 		// we only ask for the configuration for the first usage
 		deviceCategory = supportedDeviceCategories[0]
 
-		values = c.processConfig(templateItem, deviceCategory)
+		values = c.processConfig(&templateItem, deviceCategory)
 
 		deviceItem, err = c.processDeviceValues(values, templateItem, deviceItem, deviceCategory)
 		if err != nil {
@@ -158,7 +158,7 @@ func (c *CmdConfigure) configureLinkedTemplate(templateItem templates.Template,
 	for ok := true; ok; {
 		deviceItem := device{}
 
-		values := c.processConfig(templateItem, category)
+		values := c.processConfig(&templateItem, category)
 		deviceItem, err := c.processDeviceValues(values, templateItem, deviceItem, category)
 		if err != nil {
 			if !errors.Is(err, c.errDeviceNotValid) {
@@ -206,8 +206,7 @@ func (c *CmdConfigure) configureDeviceCategory(deviceCategory DeviceCategory) (d
 
 		deviceDescription = templateItem.Title()
 		capabilities = templateItem.Capabilities
-		values := c.processConfig(templateItem, deviceCategory)
-
+		values := c.processConfig(&templateItem, deviceCategory)
 		device, err = c.processDeviceValues(values, templateItem, device, deviceCategory)
 		if err != nil {
 			if err != c.errDeviceNotValid {
diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go
index 85293dca..a4b52c57 100644
--- a/cmd/configure/helper.go
+++ b/cmd/configure/helper.go
@@ -340,23 +340,23 @@ func (c *CmdConfigure) paramChoiceValues(params []templates.Param, name string)
 // processConfig processes an EVCC configuration item
 // Returns:
 //   a map with param name and values
-func (c *CmdConfigure) processConfig(templateItem templates.Template, deviceCategory DeviceCategory) map[string]interface{} {
+func (c *CmdConfigure) processConfig(templateItem *templates.Template, deviceCategory DeviceCategory) map[string]interface{} {
 	fmt.Println()
 	fmt.Println(c.localizedString("Config_Title", nil))
 	fmt.Println()
 
-	c.processModbusConfig(&templateItem, deviceCategory)
+	c.processModbusConfig(templateItem, deviceCategory)
 
-	return c.processParams(templateItem, templateItem.Params, deviceCategory)
+	return c.processParams(templateItem, deviceCategory)
 }
 
 // process a list of params
-func (c *CmdConfigure) processParams(templateItem templates.Template, params []templates.Param, deviceCategory DeviceCategory) map[string]interface{} {
+func (c *CmdConfigure) processParams(templateItem *templates.Template, deviceCategory DeviceCategory) map[string]interface{} {
 	usageFilter := DeviceCategories[deviceCategory].categoryFilter
 
 	additionalConfig := make(map[string]interface{})
 
-	for _, param := range params {
+	for _, param := range templateItem.Params {
 		if param.Dependencies != nil {
 			valid := true
 			for _, dep := range param.Dependencies {
@@ -507,5 +507,5 @@ func (c *CmdConfigure) processModbusConfig(templateItem *templates.Template, dev
 	// add the interface type specific modbus params
 	templateItem.ModbusParams(choiceTypes[index], values)
 	// Update the modbus default values
-	templateItem.ModbusValues(templates.TemplateRenderModeInstance, values)
+	_ = templateItem.ModbusValues(templates.TemplateRenderModeInstance, true, values)
 }
diff --git a/cmd/configure/survey.go b/cmd/configure/survey.go
index 5aa8b392..5ef05071 100644
--- a/cmd/configure/survey.go
+++ b/cmd/configure/survey.go
@@ -112,7 +112,7 @@ func (c *CmdConfigure) askYesNo(label string) bool {
 
 type question struct {
 	label, help                    string
-	defaultValue, exampleValue     interface{}
+	defaultValue, exampleValue     string
 	invalidValues                  []string
 	valueType                      string
 	minNumberValue, maxNumberValue int64
@@ -206,7 +206,7 @@ func (c *CmdConfigure) askValue(q question) string {
 	} else {
 		help += " (" + c.localizedString("Value_Optional", nil) + ")"
 	}
-	if q.exampleValue != nil && q.exampleValue != "" {
+	if q.exampleValue != "" {
 		help += fmt.Sprintf(" ("+c.localizedString("Value_Sample", nil)+": %s)", q.exampleValue)
 	}
 
@@ -220,18 +220,9 @@ func (c *CmdConfigure) askValue(q question) string {
 	} else {
 		prompt := &survey.Input{
 			Message: q.label,
+			Default: q.defaultValue,
 			Help:    help,
 		}
-		if q.defaultValue != nil {
-			switch q.defaultValue.(type) {
-			case string:
-				prompt.Default = q.defaultValue.(string)
-			case int:
-				prompt.Default = strconv.Itoa(q.defaultValue.(int))
-			case bool:
-				prompt.Default = strconv.FormatBool(q.defaultValue.(bool))
-			}
-		}
 		err = c.surveyAskOne(prompt, &input, survey.WithValidator(validate))
 	}
 
diff --git a/core/loadpoint.go b/core/loadpoint.go
index e5de57f0..d8017782 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -142,6 +142,7 @@ type LoadPoint struct {
 	connectedTime  time.Time              // Time when vehicle was connected
 	pvTimer        time.Time              // PV enabled/disable timer
 	phaseTimer     time.Time              // 1p3p switch timer
+	wakeUpTimer    *Timer                 // Vehicle wake-up timeout
 
 	// charge progress
 	vehicleSoc              float64       // Vehicle SoC
@@ -335,6 +336,9 @@ func (lp *LoadPoint) configureChargerType(charger api.Charger) {
 		_ = lp.bus.Subscribe(evChargeStop, ct.StopCharge)
 		lp.chargeTimer = ct
 	}
+
+	// add wakeup timer
+	lp.wakeUpTimer = NewTimer()
 }
 
 // pushEvent sends push messages to clients
@@ -354,6 +358,8 @@ func (lp *LoadPoint) evChargeStartHandler() {
 	lp.log.INFO.Println("start charging ->")
 	lp.pushEvent(evChargeStart)
 
+	lp.wakeUpTimer.Stop()
+
 	// soc update reset
 	lp.socUpdated = time.Time{}
 }
@@ -427,9 +433,7 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() {
 	if len(lp.vehicles) == 1 {
 		// but reset values if poll mode is not always (i.e. connected or charging)
 		if lp.SoC.Poll.Mode != pollAlways {
-			lp.publish("vehicleSoC", -1)
-			lp.publish("vehicleRange", -1)
-			lp.setRemainingDuration(-1)
+			lp.unpublishVehicle()
 		}
 	}
 
@@ -606,7 +610,7 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) error {
 			return nil
 		}
 
-		// sleep vehicle
+		// remote stop
 		// TODO https://github.com/evcc-io/evcc/discussions/1929
 		// if car, ok := lp.vehicle.(api.VehicleStopCharge); !enabled && ok {
 		// 	// log but don't propagate
@@ -625,7 +629,16 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) error {
 
 		lp.bus.Publish(evChargeCurrent, chargeCurrent)
 
-		// wake up vehicle
+		// start/stop vehicle wake-up timer
+		if enabled {
+			lp.log.DEBUG.Printf("wake-up timer: start")
+			lp.wakeUpTimer.Start()
+		} else {
+			lp.log.DEBUG.Printf("wake-up timer: stop")
+			lp.wakeUpTimer.Stop()
+		}
+
+		// remote start
 		// TODO https://github.com/evcc-io/evcc/discussions/1929
 		// if car, ok := lp.vehicle.(api.VehicleStartCharge); enabled && ok {
 		// 	// log but don't propagate
@@ -807,8 +820,37 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
 		lp.publish("vehicleCapacity", int64(0))
 	}
 
+	lp.unpublishVehicle()
+}
+
+func (lp *LoadPoint) wakeUpVehicle() {
+	// charger
+	if c, ok := lp.charger.(api.AlarmClock); ok {
+		if err := c.WakeUp(); err != nil {
+			lp.log.ERROR.Printf("wake-up charger: %v", err)
+		}
+		return
+	}
+
+	// vehicle
+	if lp.vehicle != nil {
+		if vs, ok := lp.vehicle.(api.AlarmClock); ok {
+			if err := vs.WakeUp(); err != nil {
+				lp.log.ERROR.Printf("wake-up vehicle: %v", err)
+			}
+		}
+	}
+}
+
+// unpublishVehicle resets published vehicle data
+func (lp *LoadPoint) unpublishVehicle() {
+	lp.vehicleSoc = 0
+
+	lp.publish("vehicleSoC", 0.0)
 	lp.publish("vehicleRange", int64(0))
 	lp.publish("vehicleOdometer", 0.0)
+
+	lp.setRemainingDuration(-1)
 }
 
 // startVehicleDetection resets connection timer and starts api refresh timer
@@ -1027,7 +1069,7 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
 	phases := lp.GetPhases()
 
 	// observed phase state inconsistency (https://github.com/evcc-io/evcc/issues/1572, https://github.com/evcc-io/evcc/issues/2230)
-	if phases < lp.activePhases {
+	if phases > 0 && phases < lp.activePhases {
 		lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, lp.activePhases)
 	}
 
@@ -1357,7 +1399,7 @@ func (lp *LoadPoint) publishSoCAndRange() {
 			// range
 			if vs, ok := lp.vehicle.(api.VehicleRange); ok {
 				if rng, err := vs.Range(); err == nil {
-					lp.log.DEBUG.Printf("vehicle range: %vkm", rng)
+					lp.log.DEBUG.Printf("vehicle range: %dkm", rng)
 					lp.publish("vehicleRange", rng)
 				}
 			}
@@ -1511,6 +1553,12 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool)
 		err = lp.setLimit(targetCurrent, required)
 	}
 
+	// Wake-up checks
+	if lp.enabled && lp.status == api.StatusB &&
+		int(lp.vehicleSoc) < lp.SoC.Target && lp.wakeUpTimer.Expired() {
+		lp.wakeUpVehicle()
+	}
+
 	// stop an active target charging session if not currently evaluated
 	if !lp.socTimer.DemandValidated() {
 		lp.socTimer.Stop()
diff --git a/core/timer.go b/core/timer.go
new file mode 100644
index 00000000..d34889e8
--- /dev/null
+++ b/core/timer.go
@@ -0,0 +1,57 @@
+package core
+
+import (
+	"sync"
+	"time"
+
+	"github.com/benbjohnson/clock"
+)
+
+const wakeupTimeout = 30 * time.Second
+
+// Timer measures active time between start and stop events
+type Timer struct {
+	sync.Mutex
+	clck    clock.Clock
+	started time.Time
+}
+
+// NewTimer creates timer that can expire
+func NewTimer() *Timer {
+	return &Timer{
+		clck: clock.New(),
+	}
+}
+
+// Start starts the timer if not started already
+func (m *Timer) Start() {
+	m.Lock()
+	defer m.Unlock()
+
+	if !m.started.IsZero() {
+		return
+	}
+
+	m.started = m.clck.Now()
+}
+
+// Reset resets the timer
+func (m *Timer) Stop() {
+	m.Lock()
+	defer m.Unlock()
+
+	m.started = time.Time{}
+}
+
+// Expired checks if the timer has elapsed and if resets its status
+func (m *Timer) Expired() bool {
+	m.Lock()
+	defer m.Unlock()
+
+	res := !m.started.IsZero() && (m.clck.Since(m.started) >= wakeupTimeout)
+	if res {
+		m.started = time.Time{}
+	}
+
+	return res
+}
diff --git a/provider/calc.go b/provider/calc.go
index dcf1e504..cd0651d3 100644
--- a/provider/calc.go
+++ b/provider/calc.go
@@ -1,13 +1,14 @@
 package provider
 
 import (
+	"errors"
 	"fmt"
 
 	"github.com/evcc-io/evcc/util"
 )
 
 type calcProvider struct {
-	add []func() (float64, error)
+	add, mul []func() (float64, error)
 }
 
 func init() {
@@ -18,6 +19,7 @@ func init() {
 func NewCalcFromConfig(other map[string]interface{}) (IntProvider, error) {
 	cc := struct {
 		Add []Config
+		Mul []Config
 	}{}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
@@ -26,6 +28,10 @@ func NewCalcFromConfig(other map[string]interface{}) (IntProvider, error) {
 
 	o := &calcProvider{}
 
+	if len(cc.Add) > 0 && len(cc.Mul) > 0 {
+		return nil, errors.New("can only have either add or mul")
+	}
+
 	for idx, cc := range cc.Add {
 		f, err := NewFloatGetterFromConfig(cc)
 		if err != nil {
@@ -34,6 +40,14 @@ func NewCalcFromConfig(other map[string]interface{}) (IntProvider, error) {
 		o.add = append(o.add, f)
 	}
 
+	for idx, cc := range cc.Mul {
+		f, err := NewFloatGetterFromConfig(cc)
+		if err != nil {
+			return nil, fmt.Errorf("mul[%d]: %w", idx, err)
+		}
+		o.mul = append(o.mul, f)
+	}
+
 	return o, nil
 }
 
@@ -56,14 +70,26 @@ func (o *calcProvider) FloatGetter() func() (float64, error) {
 }
 
 func (o *calcProvider) floatGetter() (float64, error) {
-	var sum float64
-	for idx, p := range o.add {
-		v, err := p()
-		if err != nil {
-			return 0, fmt.Errorf("add[%d]: %w", idx, err)
+	var res float64
+
+	if len(o.mul) > 0 {
+		res = 1
+		for idx, p := range o.mul {
+			v, err := p()
+			if err != nil {
+				return 0, fmt.Errorf("mul[%d]: %w", idx, err)
+			}
+			res *= v
+		}
+	} else {
+		for idx, p := range o.add {
+			v, err := p()
+			if err != nil {
+				return 0, fmt.Errorf("add[%d]: %w", idx, err)
+			}
+			res += v
 		}
-		sum += v
 	}
 
-	return sum, nil
+	return res, nil
 }
diff --git a/server/auth/auth.go b/server/auth/auth.go
new file mode 100644
index 00000000..dcfcd7f0
--- /dev/null
+++ b/server/auth/auth.go
@@ -0,0 +1,85 @@
+package auth
+
+import (
+	"crypto/rand"
+	"fmt"
+	"io"
+	"net/http"
+	"sync"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/gorilla/mux"
+)
+
+var instance *Auth
+
+type Auth struct {
+	mu     sync.Mutex
+	secret []byte
+	routes map[string]http.HandlerFunc
+}
+
+func generateSecret() ([]byte, error) {
+	var b [16]byte
+	_, err := io.ReadFull(rand.Reader, b[:])
+	return b[:], err
+}
+
+func init() {
+	secret, err := generateSecret()
+	if err != nil {
+		panic(err)
+	}
+
+	instance = &Auth{
+		secret: secret,
+		routes: make(map[string]http.HandlerFunc),
+	}
+}
+
+func Setup(router *mux.Router) {
+	router.Methods(http.MethodGet).HandlerFunc(instance.handle)
+}
+
+func Register(handler http.HandlerFunc) string {
+	return instance.register(handler)
+}
+
+func (a *Auth) register(handler http.HandlerFunc) string {
+	a.mu.Lock()
+	defer a.mu.Unlock()
+
+	state := util.NewState()
+	key := state.Encrypt(a.secret)
+
+	a.routes[key] = handler
+
+	return key
+}
+
+func (a *Auth) handle(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+
+	if error, ok := vars["error"]; ok {
+		w.WriteHeader(http.StatusBadRequest)
+		fmt.Fprintf(w, "error: %s: %s\n", error, vars["error_description"])
+		return
+	}
+
+	state, err := util.DecryptState(vars["state"], a.secret)
+	if err == nil {
+		err = state.Validate()
+	}
+
+	a.mu.Lock()
+	handler := a.routes[vars["state"]]
+	a.mu.Unlock()
+
+	if err != nil || handler == nil {
+		w.WriteHeader(http.StatusBadRequest)
+		fmt.Fprintf(w, "invalid state")
+		return
+	}
+
+	handler(w, r)
+}
diff --git a/templates/definition/charger/abl.yaml b/templates/definition/charger/abl.yaml
index 4195cc7e..0455fbd2 100644
--- a/templates/definition/charger/abl.yaml
+++ b/templates/definition/charger/abl.yaml
@@ -12,7 +12,7 @@ params:
 - name: modbus
   choice: ["rs485"]
   baudrate: 38400
-  comset: "8E1"
+  comset: 8E1
 render: |
   type: abl
   {{include "modbus" .}}
diff --git a/templates/definition/charger/alphatec.yaml b/templates/definition/charger/alphatec.yaml
index eec793c2..1f2659b0 100644
--- a/templates/definition/charger/alphatec.yaml
+++ b/templates/definition/charger/alphatec.yaml
@@ -9,7 +9,7 @@ params:
 - name: modbus
   choice: ["rs485"]
   baudrate: 9600
-  comset: "8N1"
+  comset: 8N1
 render: |
   type: alphatec
   {{include "modbus" .}}
diff --git a/templates/definition/charger/amtron.yaml b/templates/definition/charger/amtron.yaml
new file mode 100644
index 00000000..9978ef6f
--- /dev/null
+++ b/templates/definition/charger/amtron.yaml
@@ -0,0 +1,16 @@
+template: amtron
+products: 
+- brand: Mennekes
+  description:
+    generic: AMTRON XTRA
+- brand: Mennekes
+  description:
+    generic: AMTRON PREMIUM
+requirements: 
+  evcc: ["sponsorship"]
+params:
+- name: modbus
+  choice: ["tcpip"]
+render: |
+  type: amtron
+  {{include "modbus" .}}
diff --git a/templates/definition/charger/heidelberg.yaml b/templates/definition/charger/heidelberg.yaml
index 263d36fd..93b11e4e 100644
--- a/templates/definition/charger/heidelberg.yaml
+++ b/templates/definition/charger/heidelberg.yaml
@@ -15,7 +15,7 @@ params:
 - name: modbus
   choice: ["rs485"]
   baudrate: 19200
-  comset: "8E1"
+  comset: 8E1
 render: |
   type: heidelberg
   {{include "modbus" .}}
diff --git a/templates/definition/defaults.yaml b/templates/definition/defaults.yaml
index 058cd424..4c0d0f09 100644
--- a/templates/definition/defaults.yaml
+++ b/templates/definition/defaults.yaml
@@ -35,7 +35,7 @@ params:
   help:
     de: Kommunikationsparameter für den Adapter
     en: Communication parameter for the adapter
-  default: "8N1"
+  default: 8N1
 - name: host
   description:
     de: IP-Adresse oder Hostname
diff --git a/templates/definition/meter/solax-hybrid-cloud.yaml b/templates/definition/meter/solax-hybrid-cloud.yaml
index 065e6561..0cd11d28 100644
--- a/templates/definition/meter/solax-hybrid-cloud.yaml
+++ b/templates/definition/meter/solax-hybrid-cloud.yaml
@@ -41,6 +41,7 @@ render: |
     uri: https://www.eu.solaxcloud.com:9443/proxy/api/getRealtimeInfo.do?tokenId=={{ .tokenid }}&sn={{ .serial}}
     jq: .result.feedinpower
     cache: 2m30s
+    scale: -1
   {{- end}}
   {{- if eq .usage "pv" }}
     source: calc
diff --git a/templates/docs/charger/amtron_0.yaml b/templates/docs/charger/amtron_0.yaml
new file mode 100644
index 00000000..92246795
--- /dev/null
+++ b/templates/docs/charger/amtron_0.yaml
@@ -0,0 +1,14 @@
+product:
+  brand: Mennekes
+  description: AMTRON XTRA
+requirements: ["sponsorship"]
+
+render:
+- default: |
+    type: template
+    template: amtron
+    # Modbus Start
+    id: 1
+    # TCPIP
+    uri: 192.0.2.2:502 # IP-Adresse oder Hostname: Port
+    # Modbus End
\ No newline at end of file
diff --git a/templates/docs/charger/amtron_1.yaml b/templates/docs/charger/amtron_1.yaml
new file mode 100644
index 00000000..624addff
--- /dev/null
+++ b/templates/docs/charger/amtron_1.yaml
@@ -0,0 +1,14 @@
+product:
+  brand: Mennekes
+  description: AMTRON PREMIUM
+requirements: ["sponsorship"]
+
+render:
+- default: |
+    type: template
+    template: amtron
+    # Modbus Start
+    id: 1
+    # TCPIP
+    uri: 192.0.2.2:502 # IP-Adresse oder Hostname: Port
+    # Modbus End
\ No newline at end of file
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index dc0d031d..deb38757 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -161,7 +161,7 @@ func ProtocolFromRTU(rtu *bool) Protocol {
 }
 
 // NewConnection creates physical modbus device from config
-func NewConnection(uri, device, comset string, baudrate int, wire Protocol, slaveID uint8) (*Connection, error) {
+func NewConnection(uri, device, comset string, baudrate int, proto Protocol, slaveID uint8) (*Connection, error) {
 	var conn meters.Connection
 
 	if device != "" && uri != "" {
@@ -181,17 +181,17 @@ func NewConnection(uri, device, comset string, baudrate int, wire Protocol, slav
 			return nil, errors.New("invalid modbus configuration: need baudrate and comset")
 		}
 
-		if wire == Rtu {
-			conn = registeredConnection(device, meters.NewRTU(device, baudrate, comset))
+		if proto == Ascii {
+			conn = registeredConnection(device, meters.NewASCII(device, baudrate, comset))
 		} else {
-			conn = registeredConnection(uri, meters.NewASCII(device, baudrate, comset))
+			conn = registeredConnection(device, meters.NewRTU(device, baudrate, comset))
 		}
 	}
 
 	if uri != "" {
 		uri = util.DefaultPort(uri, 502)
 
-		switch wire {
+		switch proto {
 		case Rtu:
 			conn = registeredConnection(uri, meters.NewRTUOverTCP(uri))
 		case Ascii:
diff --git a/util/providerauth.go b/util/providerauth.go
index 72204f1e..248c0d7c 100644
--- a/util/providerauth.go
+++ b/util/providerauth.go
@@ -15,7 +15,7 @@ func NewAuthCollection(paramC chan<- Param) *AuthCollection {
 	}
 }
 
-func (ac *AuthCollection) Register(title, baseURI string) *AuthProvider {
+func (ac *AuthCollection) Register(baseURI, title string) *AuthProvider {
 	ap := &AuthProvider{
 		ac:  ac,
 		Uri: baseURI,
diff --git a/vehicle/mercedes/state.go b/util/state.go
similarity index 62%
rename from vehicle/mercedes/state.go
rename to util/state.go
index fdeeb9ae..1cef42f2 100644
--- a/vehicle/mercedes/state.go
+++ b/util/state.go
@@ -1,4 +1,4 @@
-package mercedes
+package util
 
 import (
 	"crypto/aes"
@@ -6,59 +6,61 @@ import (
 	"crypto/rand"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"time"
 )
 
-var ErrExpiredState = fmt.Errorf("state expired")
+var ErrStateExpired = fmt.Errorf("state expired")
 
 const stateValidity = 2 * time.Minute
 
 type State struct {
-	key  []byte
 	Time time.Time
 }
 
-// TODO Move to another more general place in the repo
-func NewState(key []byte) State {
+func NewState() State {
 	return State{
-		key:  key,
 		Time: time.Now(),
 	}
 }
 
-func (c *State) Encrypt() string {
-	plain, err := json.Marshal(c)
+func DecryptState(enc string, key []byte) (*State, error) {
+	ciphertext, err := base64.URLEncoding.DecodeString(enc)
 	if err != nil {
-		panic(err)
+		return nil, fmt.Errorf("failed to base64 decode encrypted state: %w", err)
 	}
 
-	block, err := aes.NewCipher(c.key)
+	block, err := aes.NewCipher(key)
 	if err != nil {
-		panic(err)
+		return nil, err
 	}
 
 	// The IV needs to be unique, but not secure. Therefore it's common to
 	// include it at the beginning of the ciphertext.
-	ciphertext := make([]byte, aes.BlockSize+len(plain))
+	if len(ciphertext) < aes.BlockSize {
+		return nil, errors.New("ciphertext too short")
+	}
 
 	iv := ciphertext[:aes.BlockSize]
-	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
-		panic(err)
-	}
+	ciphertext = ciphertext[aes.BlockSize:]
 
-	stream := cipher.NewCFBEncrypter(block, iv)
-	stream.XORKeyStream(ciphertext[aes.BlockSize:], plain)
+	stream := cipher.NewCFBDecrypter(block, iv)
 
-	// convert to base64
-	return base64.URLEncoding.EncodeToString(ciphertext)
+	// XORKeyStream can work in-place if the two arguments are the same.
+	stream.XORKeyStream(ciphertext, ciphertext)
+
+	var state State
+	err = json.Unmarshal(ciphertext, &state)
+
+	return &state, err
 }
 
-func Decrypt(enc string, key []byte) (State, error) {
-	ciphertext, err := base64.URLEncoding.DecodeString(enc)
+func (c *State) Encrypt(key []byte) string {
+	plain, err := json.Marshal(c)
 	if err != nil {
-		return State{}, fmt.Errorf("failed to base64 decode encrypted state: %w", err)
+		panic(err)
 	}
 
 	block, err := aes.NewCipher(key)
@@ -68,35 +70,24 @@ func Decrypt(enc string, key []byte) (State, error) {
 
 	// The IV needs to be unique, but not secure. Therefore it's common to
 	// include it at the beginning of the ciphertext.
-	if len(ciphertext) < aes.BlockSize {
-		panic("ciphertext too short")
-	}
+	ciphertext := make([]byte, aes.BlockSize+len(plain))
 
 	iv := ciphertext[:aes.BlockSize]
-	ciphertext = ciphertext[aes.BlockSize:]
-
-	stream := cipher.NewCFBDecrypter(block, iv)
-
-	// XORKeyStream can work in-place if the two arguments are the same.
-	stream.XORKeyStream(ciphertext, ciphertext)
-
-	var state State
-	if err := json.Unmarshal(ciphertext, &state); err != nil {
-		return State{}, fmt.Errorf("failed to unmarshal encrypted state: %w", err)
+	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+		panic(err)
 	}
 
-	return state, nil
-}
+	stream := cipher.NewCFBEncrypter(block, iv)
+	stream.XORKeyStream(ciphertext[aes.BlockSize:], plain)
 
-func Validate(rawState string, encryptionKey []byte) error {
-	state, err := Decrypt(rawState, encryptionKey)
-	if err != nil {
-		return fmt.Errorf("failed to validate state: %w", err)
-	}
+	// convert to base64
+	return base64.URLEncoding.EncodeToString(ciphertext)
+}
 
-	if state.Time.Add(stateValidity).After(time.Now()) {
+func (c *State) Validate() error {
+	if c.Time.Add(stateValidity).After(time.Now()) {
 		return nil
 	}
 
-	return ErrExpiredState
+	return ErrStateExpired
 }
diff --git a/util/templates/template.go b/util/templates/template.go
index d724c214..266d7689 100644
--- a/util/templates/template.go
+++ b/util/templates/template.go
@@ -317,7 +317,7 @@ func (t *Template) RenderResult(renderMode string, other map[string]interface{})
 		return nil, values, err
 	}
 
-	t.ModbusValues(renderMode, values)
+	values = t.ModbusValues(renderMode, false, values)
 
 	// add the common templates
 	for _, v := range t.ConfigDefaults.Config.Presets {
diff --git a/util/templates/template_documentation.go b/util/templates/template_documentation.go
index b5a0924f..75f02284 100644
--- a/util/templates/template_documentation.go
+++ b/util/templates/template_documentation.go
@@ -50,7 +50,7 @@ func (t *Template) RenderDocumentation(product Product, values map[string]interf
 			}
 
 			modbusData := map[string]interface{}{}
-			t.ModbusValues(TemplateRenderModeDocs, modbusData)
+			modbusData = t.ModbusValues(TemplateRenderModeDocs, true, modbusData)
 
 			modbusOut := new(bytes.Buffer)
 
diff --git a/util/templates/template_modbus.go b/util/templates/template_modbus.go
index 7afa0bc0..b35ce816 100644
--- a/util/templates/template_modbus.go
+++ b/util/templates/template_modbus.go
@@ -29,10 +29,10 @@ func (t *Template) ModbusParams(modbusType string, values map[string]interface{}
 }
 
 // set the modbus values required from modbus.tpl and and the template to the render
-func (t *Template) ModbusValues(renderMode string, values map[string]interface{}) {
+func (t *Template) ModbusValues(renderMode string, setDefaults bool, values map[string]interface{}) map[string]interface{} {
 	choices := t.ModbusChoices()
 	if len(choices) == 0 {
-		return
+		return values
 	}
 
 	// only add the template once, when testing multiple usages, it might already be present
@@ -40,11 +40,8 @@ func (t *Template) ModbusValues(renderMode string, values map[string]interface{}
 		t.Render = fmt.Sprintf("%s\n%s", t.Render, modbusTmpl)
 	}
 
-	// either modbus param is defined, which means it ran through configuration
-	// or defaults for all modbus choices need to be set for rendering all cases for documentation
-	if modbusValue := values[ParamModbus]; renderMode != TemplateRenderModeInstance && modbusValue != nil && modbusValue != "" {
-		values[fmt.Sprintf("%s", modbusValue)] = true
-		return
+	if !setDefaults {
+		return values
 	}
 
 	modbusConfig := t.ConfigDefaults.Config.Modbus
@@ -60,20 +57,20 @@ func (t *Template) ModbusValues(renderMode string, values map[string]interface{}
 		for _, p := range typeParams {
 			values[p.Name] = p.DefaultValue(renderMode)
 
-			var defaultValue interface{}
+			var defaultValue string
 
 			switch p.Name {
 			case ModbusParamNameId:
 				if modbusParam.ID != 0 {
-					defaultValue = modbusParam.ID
+					defaultValue = fmt.Sprintf("%d", modbusParam.ID)
 				}
 			case ModbusParamNamePort:
 				if modbusParam.Port != 0 {
-					defaultValue = modbusParam.Port
+					defaultValue = fmt.Sprintf("%d", modbusParam.Port)
 				}
 			case ModbusParamNameBaudrate:
 				if modbusParam.Baudrate != 0 {
-					defaultValue = modbusParam.Baudrate
+					defaultValue = fmt.Sprintf("%d", modbusParam.Baudrate)
 				}
 			case ModbusParamNameComset:
 				if modbusParam.Comset != "" {
@@ -81,12 +78,12 @@ func (t *Template) ModbusValues(renderMode string, values map[string]interface{}
 				}
 			}
 
-			if defaultValue == nil {
+			if defaultValue == "" {
 				continue
 			}
 
 			if renderMode == TemplateRenderModeInstance {
-				t.SetParamDefault(p.Name, fmt.Sprintf("%d", defaultValue))
+				t.SetParamDefault(p.Name, defaultValue)
 			} else {
 				values[p.Name] = defaultValue
 			}
@@ -96,4 +93,6 @@ func (t *Template) ModbusValues(renderMode string, values map[string]interface{}
 			values[iface] = true
 		}
 	}
+
+	return values
 }
diff --git a/vehicle/fiat/provider.go b/vehicle/fiat/provider.go
index 2322862f..734c29c3 100644
--- a/vehicle/fiat/provider.go
+++ b/vehicle/fiat/provider.go
@@ -156,7 +156,7 @@ var _ api.VehiclePosition = (*Provider)(nil)
 func (v *Provider) Position() (float64, float64, error) {
 	res, err := v.locationG()
 	if res, ok := res.(LocationResponse); err == nil && ok {
-		return res.Latitude, res.Latitude, nil
+		return res.Latitude, res.Longitude, nil
 	}
 
 	return 0, 0, err
diff --git a/vehicle/ford.go b/vehicle/ford.go
index f028dab4..fdef337a 100644
--- a/vehicle/ford.go
+++ b/vehicle/ford.go
@@ -14,13 +14,6 @@ import (
 // Ford is an api.Vehicle implementation for Ford cars
 type Ford struct {
 	*embed
-	// *request.Helper
-	// log         *util.Logger
-	// vin         string
-	// tokenSource oauth2.TokenSource
-	// statusG     func() (interface{}, error)
-	// refreshId   string
-	// refreshTime time.Time
 	*ford.Provider
 }
 
diff --git a/vehicle/ford/api.go b/vehicle/ford/api.go
index c43d5a07..19300985 100644
--- a/vehicle/ford/api.go
+++ b/vehicle/ford/api.go
@@ -99,3 +99,12 @@ func (v *API) RefreshRequest(vin string) (string, error) {
 
 	return resp.CommandId, err
 }
+
+// WakeUp performs a wakeup request
+func (v *API) WakeUp(vin string) error {
+	uri := fmt.Sprintf("%s/api/dashboard/v1/users/vehicles?wakeupVin=%s", TokenURI, vin)
+
+	_, err := v.GetBody(uri)
+
+	return err
+}
diff --git a/vehicle/ford/provider.go b/vehicle/ford/provider.go
index dccc1fe9..1e965c36 100644
--- a/vehicle/ford/provider.go
+++ b/vehicle/ford/provider.go
@@ -14,6 +14,7 @@ type Provider struct {
 	expiry      time.Duration
 	refreshTime time.Time
 	refreshId   string
+	wakeup      func() error
 }
 
 func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider {
@@ -29,6 +30,8 @@ func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider {
 		)
 	}, cache).InterfaceGetter()
 
+	impl.wakeup = func() error { return api.WakeUp(vin) }
+
 	return impl
 }
 
@@ -136,3 +139,10 @@ func (v *Provider) Position() (float64, float64, error) {
 
 	return 0, 0, err
 }
+
+var _ api.AlarmClock = (*Provider)(nil)
+
+// WakeUp implements the api.AlarmClock interface
+func (v *Provider) WakeUp() error {
+	return v.wakeup()
+}
diff --git a/vehicle/mercedes/identity.go b/vehicle/mercedes/identity.go
index 4079166b..2609e78b 100644
--- a/vehicle/mercedes/identity.go
+++ b/vehicle/mercedes/identity.go
@@ -2,16 +2,15 @@ package mercedes
 
 import (
 	"context"
-	"crypto/rand"
 	"encoding/json"
 	"fmt"
-	"io"
 	"net/http"
 	"net/url"
 
 	"github.com/coreos/go-oidc"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/provider"
+	"github.com/evcc-io/evcc/server/auth"
 	"github.com/evcc-io/evcc/util"
 	"golang.org/x/oauth2"
 )
@@ -29,15 +28,9 @@ func WithToken(t *oauth2.Token) IdentityOptions {
 type Identity struct {
 	log *util.Logger
 	*ReuseTokenSource
-	sessionSecret []byte
-	oc            *oauth2.Config
-	authC         chan<- bool
-}
-
-func generateSecret() ([]byte, error) {
-	var b [16]byte
-	_, err := io.ReadFull(rand.Reader, b[:])
-	return b[:], err
+	oc      *oauth2.Config
+	baseURL string
+	authC   chan<- bool
 }
 
 // TODO SessionSecret from config/persistence
@@ -66,7 +59,6 @@ func NewIdentity(log *util.Logger, id, secret string, options ...IdentityOptions
 	ts.Apply(nil)
 
 	v.ReuseTokenSource = ts
-	v.sessionSecret, err = generateSecret()
 
 	for _, o := range options {
 		if err == nil {
@@ -86,19 +78,20 @@ func (v *Identity) invalidToken() {
 
 var _ api.ProviderLogin = (*Identity)(nil)
 
-func (v *Identity) SetCallbackParams(uri string, authC chan<- bool) {
-	v.oc.RedirectURL = uri
+func (v *Identity) SetCallbackParams(baseURL, redirectURL string, authC chan<- bool) {
+	v.baseURL = baseURL
+	v.oc.RedirectURL = redirectURL
 	v.authC = authC
 }
 
 func (v *Identity) LoginHandler() http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		state := NewState(v.sessionSecret)
+		state := auth.Register(v.callbackHandler)
 
 		b, _ := json.Marshal(struct {
 			LoginUri string `json:"loginUri"`
 		}{
-			LoginUri: v.oc.AuthCodeURL(state.Encrypt(), oauth2.AccessTypeOffline,
+			LoginUri: v.oc.AuthCodeURL(state, oauth2.AccessTypeOffline,
 				oauth2.SetAuthURLParam("prompt", "login consent"),
 			),
 		})
@@ -118,50 +111,34 @@ func (v *Identity) LogoutHandler() http.HandlerFunc {
 	}
 }
 
-func (v *Identity) CallbackHandler(baseURI string) http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		v.log.TRACE.Println("callback request retrieved")
-
-		data, err := url.ParseQuery(r.URL.RawQuery)
-		if err != nil {
-			fmt.Fprintln(w, "invalid response:", data)
-			return
-		}
-
-		if error, ok := data["error"]; ok {
-			fmt.Fprintf(w, "error: %s: %s\n", error, data["error_description"])
-			return
-		}
-
-		states, ok := data["state"]
-		if !ok || len(states) != 1 {
-			fmt.Fprintln(w, "invalid state response:", data)
-			return
-		} else if err := Validate(states[0], v.sessionSecret); err != nil {
-			fmt.Fprintf(w, "failed state validation: %s", err)
-			return
-		}
+func (v *Identity) callbackHandler(w http.ResponseWriter, r *http.Request) {
+	v.log.TRACE.Println("callback request retrieved")
 
-		codes, ok := data["code"]
-		if !ok || len(codes) != 1 {
-			fmt.Fprintln(w, "invalid response:", data)
-			return
-		}
+	data, err := url.ParseQuery(r.URL.RawQuery)
+	if err != nil {
+		fmt.Fprintln(w, "invalid response:", data)
+		return
+	}
 
-		token, err := v.oc.Exchange(context.Background(), codes[0])
-		if err != nil {
-			fmt.Fprintln(w, "token error:", err)
-			return
-		}
+	codes, ok := data["code"]
+	if !ok || len(codes) != 1 {
+		fmt.Fprintln(w, "invalid response:", data)
+		return
+	}
 
-		if token.Valid() {
-			v.log.TRACE.Println("sending login update...")
-			v.ReuseTokenSource.Apply(token)
-			v.authC <- true
+	token, err := v.oc.Exchange(context.Background(), codes[0])
+	if err != nil {
+		fmt.Fprintln(w, "token error:", err)
+		return
+	}
 
-			provider.ResetCached()
-		}
+	if token.Valid() {
+		v.log.TRACE.Println("sending login update...")
+		v.ReuseTokenSource.Apply(token)
+		v.authC <- true
 
-		http.Redirect(w, r, baseURI, http.StatusFound)
+		provider.ResetCached()
 	}
+
+	http.Redirect(w, r, v.baseURL, http.StatusFound)
 }
diff --git a/vehicle/porsche.go b/vehicle/porsche.go
index e8baecff..d1f8e528 100644
--- a/vehicle/porsche.go
+++ b/vehicle/porsche.go
@@ -48,8 +48,16 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	}
 
 	api := porsche.NewAPI(log, identity.DefaultSource)
+	mobile := porsche.NewMobileAPI(log, identity.MobileSource)
 
 	cc.VIN, err = ensureVehicle(cc.VIN, func() ([]string, error) {
+		mobileVehicles, err := mobile.Vehicles()
+		if err == nil {
+			return funk.Map(mobileVehicles, func(v porsche.StatusResponseMobile) string {
+				return v.VIN
+			}).([]string), err
+		}
+
 		vehicles, err := api.Vehicles()
 		return funk.Map(vehicles, func(v porsche.Vehicle) string {
 			return v.VIN
@@ -61,13 +69,22 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	}
 
 	// check if vehicle is paired
-	if res, err := api.PairingStatus(cc.VIN); err == nil && res.Status != porsche.PairingComplete {
-		return nil, errors.New("vehicle is not paired with the My Porsche account")
-	}
+	// if res, err := api.PairingStatus(cc.VIN); err == nil && res.Status != porsche.PairingComplete {
+	// 	return nil, errors.New("vehicle is not paired with the My Porsche account")
+	// }
 
 	// check if vehicle provides status:
 	// some PHEVs do not provide any data
-	if _, err := api.Status(cc.VIN); err != nil {
+	statusAvailable := false
+	if _, err := mobile.Status(cc.VIN); err == nil {
+		statusAvailable = true
+	}
+
+	if _, err := api.Status(cc.VIN); err == nil {
+		statusAvailable = true
+	}
+
+	if !statusAvailable {
 		return nil, errors.New("vehicle is not capable of providing data")
 	}
 
@@ -77,17 +94,14 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	//   call to status() as it otherwise returns an HTTP 502 error.
 	//   The reason is unknown, even when tested with 100% identical Headers.
 	//   It seems to be a new backend related issue.
-	if _, err := api.Status(cc.VIN); err != nil {
-		return nil, err
-	}
-
-	emobility := porsche.NewEmobilityAPI(log, identity.EmobilitySource)
-	capabilities, err := emobility.Capabilities(cc.VIN)
-	if err != nil {
-		return nil, err
+	var emobility *porsche.EmobilityAPI
+	var capabilities porsche.CapabilitiesResponse
+	if _, err := api.Status(cc.VIN); err == nil {
+		emobility = porsche.NewEmobilityAPI(log, identity.EmobilitySource)
+		capabilities, _ = emobility.Capabilities(cc.VIN)
 	}
 
-	provider := porsche.NewProvider(log, api, emobility, cc.VIN, capabilities.CarModel, cc.Cache)
+	provider := porsche.NewProvider(log, api, emobility, mobile, cc.VIN, capabilities.CarModel, cc.Cache)
 
 	v := &Porsche{
 		embed:    &cc.embed,
diff --git a/vehicle/porsche/api.go b/vehicle/porsche/api.go
index 551eeec5..772770b4 100644
--- a/vehicle/porsche/api.go
+++ b/vehicle/porsche/api.go
@@ -32,7 +32,8 @@ func NewAPI(log *util.Logger, identity oauth2.TokenSource) *API {
 			Base:   v.Client.Transport,
 		},
 		Decorator: transport.DecorateHeaders(map[string]string{
-			"apikey": OAuth2Config.ClientID,
+			"apikey":      OAuth2Config.ClientID,
+			"x-client-id": "52064df8-6daa-46f7-bc9e-e3232622ab26",
 		}),
 	}
 
diff --git a/vehicle/porsche/api_mobile.go b/vehicle/porsche/api_mobile.go
new file mode 100644
index 00000000..ba03b93f
--- /dev/null
+++ b/vehicle/porsche/api_mobile.go
@@ -0,0 +1,55 @@
+package porsche
+
+import (
+	"fmt"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/evcc-io/evcc/util/transport"
+	"golang.org/x/oauth2"
+)
+
+const (
+	MobileApiURI = "https://api.ppa.porsche.com"
+)
+
+// API is an api.Vehicle implementation for Porsche PHEV cars
+type MobileAPI struct {
+	*request.Helper
+}
+
+// NewAPI creates a new vehicle
+func NewMobileAPI(log *util.Logger, identity oauth2.TokenSource) *MobileAPI {
+	v := &MobileAPI{
+		Helper: request.NewHelper(log),
+	}
+
+	v.Client.Transport = &transport.Decorator{
+		Base: &oauth2.Transport{
+			Source: identity,
+			Base:   v.Client.Transport,
+		},
+		Decorator: transport.DecorateHeaders(map[string]string{
+			"apikey":      OAuth2Config.ClientID,
+			"x-client-id": "52064df8-6daa-46f7-bc9e-e3232622ab26",
+		}),
+	}
+
+	return v
+}
+
+// Vehicles implements the vehicle list response
+func (v *MobileAPI) Vehicles() ([]StatusResponseMobile, error) {
+	var res []StatusResponseMobile
+	uri := fmt.Sprintf("%s/app/connect/v1/vehicles", MobileApiURI)
+	err := v.GetJSON(uri, &res)
+	return res, err
+}
+
+// Status implements the vehicle status response
+func (v *MobileAPI) Status(vin string) (StatusResponseMobile, error) {
+	var res StatusResponseMobile
+	uri := fmt.Sprintf("%s/app/connect/v1/vehicles/%s?mf=*", MobileApiURI, vin)
+	err := v.GetJSON(uri, &res)
+	return res, err
+}
diff --git a/vehicle/porsche/identity.go b/vehicle/porsche/identity.go
index 3a27b54d..1c8522e4 100644
--- a/vehicle/porsche/identity.go
+++ b/vehicle/porsche/identity.go
@@ -37,16 +37,23 @@ var (
 		ClientID:    "NJOxLv4QQNrpZnYQbb7mCvdiMxQWkHDq",
 		RedirectURL: "https://my.porsche.com/myservices/auth/auth.html",
 		Endpoint:    OAuth2Config.Endpoint,
-		Scopes:      OAuth2Config.Scopes,
+		Scopes:      []string{"openid"},
+	}
+
+	MobileOAuth2Config = &oauth2.Config{
+		ClientID:    "L20OiZ0kBgWt958NWbuCB8gb970y6V6U",
+		RedirectURL: "One-Product-App://porsche-id/oauth2redirect",
+		Endpoint:    OAuth2Config.Endpoint,
+		Scopes:      []string{"openid", "magiclink", "mbb"},
 	}
 )
 
 // Identity is the Porsche Identity client
 type Identity struct {
 	*request.Helper
-	user, password                 string
-	defaultToken, emobilityToken   *oauth2.Token
-	DefaultSource, EmobilitySource oauth2.TokenSource
+	user, password                               string
+	defaultToken, emobilityToken, mobileToken    *oauth2.Token
+	DefaultSource, EmobilitySource, MobileSource oauth2.TokenSource
 }
 
 // NewIdentity creates Porsche identity
@@ -66,6 +73,7 @@ func (v *Identity) Login() error {
 	if err == nil {
 		v.DefaultSource = oauth.RefreshTokenSource(v.defaultToken, v)
 		v.EmobilitySource = oauth.RefreshTokenSource(v.emobilityToken, &emobilityAdapter{v})
+		v.MobileSource = oauth.RefreshTokenSource(v.mobileToken, &mobileAdapter{v})
 	}
 
 	return err
@@ -137,6 +145,10 @@ func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
 		if token, err = v.fetchToken(EmobilityOAuth2Config); err == nil {
 			v.emobilityToken = token
 		}
+
+		if token, err = v.fetchToken(MobileOAuth2Config); err == nil {
+			v.mobileToken = token
+		}
 	}
 
 	return v.defaultToken, err
@@ -155,13 +167,24 @@ func (v *Identity) fetchToken(oc *oauth2.Config) (*oauth2.Token, error) {
 		oauth2.SetAuthURLParam("locale", "de_DE"),
 	)
 
+	v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+		if req.URL.Scheme != "https" {
+			return http.ErrUseLastResponse
+		}
+		return nil
+	}
+
 	resp, err := v.Client.Get(uri)
 	if err != nil {
 		return nil, err
 	}
 	resp.Body.Close()
 
-	query, err := url.ParseQuery(resp.Request.URL.RawQuery)
+	rawQuery := resp.Request.URL.RawQuery
+	if strings.HasPrefix(resp.Header.Get("Location"), "One-Product-App") {
+		rawQuery = strings.Replace(resp.Header.Get("Location"), "One-Product-App://porsche-id/oauth2redirect?", "", 1)
+	}
+	query, err := url.ParseQuery(rawQuery)
 	if err != nil {
 		return nil, err
 	}
@@ -195,3 +218,15 @@ func (v *emobilityAdapter) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error)
 	}
 	return token, err
 }
+
+type mobileAdapter struct {
+	tr *Identity
+}
+
+func (v *mobileAdapter) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
+	token, err := v.tr.RefreshToken(nil)
+	if err == nil {
+		token = v.tr.mobileToken
+	}
+	return token, err
+}
diff --git a/vehicle/porsche/provider.go b/vehicle/porsche/provider.go
index 58802768..6c6dcdf4 100644
--- a/vehicle/porsche/provider.go
+++ b/vehicle/porsche/provider.go
@@ -1,6 +1,7 @@
 package porsche
 
 import (
+	"errors"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -12,17 +13,25 @@ import (
 type Provider struct {
 	statusG    func() (interface{}, error)
 	emobilityG func() (interface{}, error)
+	mobileG    func() (interface{}, error)
 }
 
 // NewProvider creates a new vehicle
-func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, vin, model string, cache time.Duration) *Provider {
+func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, mobile *MobileAPI, vin string, carModel string, cache time.Duration) *Provider {
 	impl := &Provider{
 		statusG: provider.NewCached(func() (interface{}, error) {
 			return api.Status(vin)
 		}, cache).InterfaceGetter(),
 
 		emobilityG: provider.NewCached(func() (interface{}, error) {
-			return emobility.Status(vin, model)
+			if carModel != "" {
+				return emobility.Status(vin, carModel)
+			}
+			return EmobilityResponse{}, errors.New("no car model")
+		}, cache).InterfaceGetter(),
+
+		mobileG: provider.NewCached(func() (interface{}, error) {
+			return mobile.Status(vin)
 		}, cache).InterfaceGetter(),
 	}
 
@@ -33,7 +42,18 @@ var _ api.Battery = (*Provider)(nil)
 
 // SoC implements the api.Vehicle interface
 func (v *Provider) SoC() (float64, error) {
-	res, err := v.emobilityG()
+	res, err := v.mobileG()
+	if res, ok := res.(StatusResponseMobile); err == nil && ok {
+		m, err := res.MeasurementByKey("BATTERY_LEVEL")
+		if err != nil && err != api.ErrNotAvailable {
+			return 0, err
+		}
+		if err != api.ErrNotAvailable {
+			return float64(m.Value.Percent), nil
+		}
+	}
+
+	res, err = v.emobilityG()
 	if res, ok := res.(EmobilityResponse); err == nil && ok {
 		return float64(res.BatteryChargeStatus.StateOfChargeInPercentage), nil
 	}
@@ -50,7 +70,18 @@ var _ api.VehicleRange = (*Provider)(nil)
 
 // Range implements the api.VehicleRange interface
 func (v *Provider) Range() (int64, error) {
-	res, err := v.emobilityG()
+	res, err := v.mobileG()
+	if res, ok := res.(StatusResponseMobile); err == nil && ok {
+		m, err := res.MeasurementByKey("E_RANGE")
+		if err != nil && err != api.ErrNotAvailable {
+			return 0, err
+		}
+		if err != api.ErrNotAvailable {
+			return int64(m.Value.Kilometers), nil
+		}
+	}
+
+	res, err = v.emobilityG()
 	if res, ok := res.(EmobilityResponse); err == nil && ok {
 		return res.BatteryChargeStatus.RemainingERange.ValueInKilometers, nil
 	}
@@ -67,7 +98,24 @@ var _ api.VehicleFinishTimer = (*Provider)(nil)
 
 // FinishTime implements the api.VehicleFinishTimer interface
 func (v *Provider) FinishTime() (time.Time, error) {
-	res, err := v.emobilityG()
+	res, err := v.mobileG()
+	if res, ok := res.(StatusResponseMobile); err == nil && ok {
+		m, err := res.MeasurementByKey("BATTERY_CHARGING_STATE")
+		if err != nil && err != api.ErrNotAvailable {
+			return time.Time{}, err
+		}
+		if err != api.ErrNotAvailable {
+			if m.Value.EndsAt == "" {
+				if m.Value.LastModified != "" {
+					return time.Parse(time.RFC3339, m.Value.LastModified)
+				}
+				return time.Time{}, nil
+			}
+			return time.Parse(time.RFC3339, m.Value.EndsAt)
+		}
+	}
+
+	res, err = v.emobilityG()
 	if res, ok := res.(EmobilityResponse); err == nil && ok {
 		return time.Now().Add(time.Duration(res.BatteryChargeStatus.RemainingChargeTimeUntil100PercentInMinutes) * time.Minute), err
 	}
@@ -79,7 +127,30 @@ var _ api.ChargeState = (*Provider)(nil)
 
 // Status implements the api.ChargeState interface
 func (v *Provider) Status() (api.ChargeStatus, error) {
-	res, err := v.emobilityG()
+	res, err := v.mobileG()
+	if res, ok := res.(StatusResponseMobile); err == nil && ok {
+		m, err := res.MeasurementByKey("BATTERY_CHARGING_STATE")
+		if err != nil && err != api.ErrNotAvailable {
+			return api.StatusNone, err
+		}
+		if err != api.ErrNotAvailable {
+			switch m.Value.Status {
+			case "FAST_CHARGING", "NOT_PLUGGED", "UNKNOWN":
+				return api.StatusA, nil
+			case "CHARGING_COMPLETED", "CHARGING_PAUSED", "READY_TO_CHARGE", "SOC_REACHED",
+				"INITIALISING", "STANDBY", "SUSPENDED", "PLUGGED_LOCKED", "PLUGGED_NOT_LOCKED":
+				return api.StatusB, nil
+			case "CHARGING":
+				return api.StatusC, nil
+			case "CHARGING_ERROR":
+				return api.StatusF, nil
+			default:
+				return api.StatusNone, errors.New("mobile - unknown charging status: " + m.Value.Status)
+			}
+		}
+	}
+
+	res, err = v.emobilityG()
 	if res, ok := res.(EmobilityResponse); err == nil && ok {
 		switch res.BatteryChargeStatus.PlugState {
 		case "DISCONNECTED":
@@ -94,8 +165,10 @@ func (v *Provider) Status() (api.ChargeStatus, error) {
 				return api.StatusF, nil
 			case "OFF", "COMPLETED":
 				return api.StatusB, nil
-			case "ON":
+			case "ON", "CHARGING":
 				return api.StatusC, nil
+			default:
+				return api.StatusNone, errors.New("emobility - unknown charging state: " + res.BatteryChargeStatus.ChargingState)
 			}
 		}
 	}
@@ -107,13 +180,26 @@ var _ api.VehicleClimater = (*Provider)(nil)
 
 // Climater implements the api.VehicleClimater interface
 func (v *Provider) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) {
-	res, err := v.emobilityG()
+	res, err := v.mobileG()
+	if res, ok := res.(StatusResponseMobile); err == nil && ok {
+		m, err := res.MeasurementByKey("CLIMATIZER_STATE")
+		if err != nil && err != api.ErrNotAvailable {
+			return active, 20, 20, err
+		}
+		if err != api.ErrNotAvailable {
+			return m.Value.IsOn, 20, 20, err
+		}
+	}
+
+	res, err = v.emobilityG()
 	if res, ok := res.(EmobilityResponse); err == nil && ok {
 		switch res.DirectClimatisation.ClimatisationState {
 		case "OFF":
 			return false, 20, 20, nil
 		case "ON":
 			return true, 20, 20, nil
+		default:
+			return active, outsideTemp, targetTemp, errors.New("emobility - unknown climate state: " + res.DirectClimatisation.ClimatisationState)
 		}
 	}
 
@@ -124,7 +210,18 @@ var _ api.VehicleOdometer = (*Provider)(nil)
 
 // Odometer implements the api.VehicleOdometer interface
 func (v *Provider) Odometer() (float64, error) {
-	res, err := v.statusG()
+	res, err := v.mobileG()
+	if res, ok := res.(StatusResponseMobile); err == nil && ok {
+		m, err := res.MeasurementByKey("MILEAGE")
+		if err != nil && err != api.ErrNotAvailable {
+			return 0, err
+		}
+		if err != api.ErrNotAvailable {
+			return float64(m.Value.Kilometers), nil
+		}
+	}
+
+	res, err = v.statusG()
 	if res, ok := res.(StatusResponse); err == nil && ok {
 		return res.Mileage.Value, nil
 	}
diff --git a/vehicle/porsche/types.go b/vehicle/porsche/types.go
index 81ae15a1..10b3a446 100644
--- a/vehicle/porsche/types.go
+++ b/vehicle/porsche/types.go
@@ -1,5 +1,11 @@
 package porsche
 
+import (
+	"errors"
+
+	"github.com/evcc-io/evcc/api"
+)
+
 type Vehicle struct {
 	VIN              string
 	ModelDescription string
@@ -20,6 +26,134 @@ type VehiclePairingResponse struct {
 	CanSendPairingCode bool
 }
 
+type MeasurementMobile struct {
+	Key string
+	/*
+		RANGE,
+		E_RANGE,
+		MILEAGE,
+		FUEL_LEVEL,
+		FUEL_RESERVE,
+		BATTERY_LEVEL // status, value
+		BATTERY_CHARGING_STATE // status, value
+		OIL_SERVICE_RANGE,
+		OIL_SERVICE_TIME,
+		MAIN_SERVICE_RANGE,
+		MAIN_SERVICE_TIME,
+		INTERMEDIATE_SERVICE_RANGE,
+		INTERMEDIATE_SERVICE_TIME,
+		OIL_LEVEL_MAX,
+		OIL_LEVEL_CURRENT,
+		OIL_LEVEL_MIN_WARNING,
+		OPEN_STATE_DOOR_FRONT_LEFT,
+		OPEN_STATE_DOOR_REAR_LEFT,
+		OPEN_STATE_DOOR_FRONT_RIGHT,
+		OPEN_STATE_DOOR_REAR_RIGHT,
+		OPEN_STATE_LID_FRONT,
+		OPEN_STATE_LID_REAR,
+		OPEN_STATE_WINDOW_FRONT_LEFT,
+		OPEN_STATE_WINDOW_REAR_LEFT,
+		OPEN_STATE_WINDOW_FRONT_RIGHT,
+		OPEN_STATE_WINDOW_REAR_RIGHT,
+		OPEN_STATE_SUNROOF,
+		OPEN_STATE_SUNROOF_REAR,
+		OPEN_STATE_TOP,
+		OPEN_STATE_SERVICE_FLAP,
+		OPEN_STATE_SPOILER,
+		OPEN_STATE_CHARGE_FLAP_LEFT,
+		OPEN_STATE_CHARGE_FLAP_RIGHT,
+		LOCK_STATE_VEHICLE,
+		GPS_LOCATION,
+		GLOBAL_PRIVACY_MODE,
+		REMOTE_ACCESS_AUTHORIZATION,
+		THEFT_MODE,
+		TRIP_STATISTICS_CYCLIC,
+		TRIP_STATISTICS_LONG_TERM,
+		TRIP_STATISTICS_SHORT_TERM,
+		PARKING_LIGHT,
+		HEATING_STATE,
+		ACV_STATE,
+		CLIMATIZER_STATE // isOn, minutesLeft, targetTemperature
+		TIMERS,
+		BLEID_DDADATA,
+		VTS_MODES,
+		SPEED_ALARMS,
+		LOCATION_ALARMS,
+		CHARGING_PROFILES,
+		BATTERY_TYPE // lastModified, plugTypes, capacityAh capacityKWh
+		VALET_ALARM;
+	*/
+	Status struct {
+		IsEnabled bool
+		Cause     string // AVAILABLE, PRIVACY_ACTIVATED, LICENSE_DEACTIVATED, NOT_SUPPORTED, UNKNOWN
+	}
+	Value struct {
+		LastModified string
+
+		IsEnabled              bool
+		SoCPhoneNumber         string
+		Percent                int64
+		DistanceKM             float64
+		ZeroEmissionDistanceKm float64
+		AvgLiterPerHundredKm   float64
+		AvgKwhPerHundredKm     float64
+		AvgSpeedKmh            float64
+		DrivingTimeMinutes     float64
+		TripEndTime            string
+
+		IsOn              bool
+		TargetTemperature float64
+		// MinutesLeft
+
+		Kilometers int64
+		Days       int64
+		IsLocked   bool
+		IsOpen     bool
+		Direction  int64
+
+		PlugTypes string // COMBINED_CS, TYP2, UNKNOWN
+		// CapacityAh
+		// CapacityKWh
+
+		Status              string // CHARGING, FAST_CHARGING, CHARGING_COMPLETED, CHARGING_PAUSED, READY_TO_CHARGE, SOC_REACHED, INITIALISING, STANDBY, SUSPENDED, PLUGGED_LOCKED, PLUGGED_NOT_LOCKED, CHARGING_ERROR, NOT_PLUGGED, UNKNOWN
+		Mode                string // DIRECT, TIMER_PROFILE, LONGTERM, TCP, UNKNOWN
+		DirectChargingState string // ENABLED_ON, ENABLED_OFF, DISABLED_ON, DISABLED_OFF, HIDDEN, UNKNOWN
+		EndsAt              string
+		ChargingPower       float64
+		ChargingRate        float64
+	}
+}
+
+type StatusResponseMobile struct {
+	VIN        string
+	ModelName  string
+	CustomName string
+	ModelType  struct {
+		Code       string
+		Year       string
+		Body       string
+		Generation string
+		Model      string
+		Engine     string
+	}
+	Connect             bool
+	GreyConnectStoreURL string
+	PairingCodeV2       string
+	Measurements        []MeasurementMobile
+}
+
+func (s *StatusResponseMobile) MeasurementByKey(key string) (*MeasurementMobile, error) {
+	for _, m := range s.Measurements {
+		if m.Key == key {
+			if !m.Status.IsEnabled {
+				return nil, errors.New(m.Status.Cause)
+			}
+			return &m, nil
+		}
+	}
+	return nil, api.ErrNotAvailable
+}
+
 type StatusResponse struct {
 	BatteryLevel struct {
 		Unit  string
diff --git a/vehicle/tronity.go b/vehicle/tronity.go
index a1df4fa3..8a285f9c 100644
--- a/vehicle/tronity.go
+++ b/vehicle/tronity.go
@@ -2,7 +2,7 @@ package vehicle
 
 // LICENSE
 
-// Copyright (c) 2019-2021 andig
+// Copyright (c) 2019-2022 andig
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
diff --git a/vehicle/vw/forms.go b/vehicle/vw/forms.go
index 41613e30..df0e831e 100644
--- a/vehicle/vw/forms.go
+++ b/vehicle/vw/forms.go
@@ -1,8 +1,12 @@
 package vw
 
 import (
+	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
+	"regexp"
+	"strings"
 
 	"github.com/PuerkitoBio/goquery"
 )
@@ -53,3 +57,37 @@ func FormValues(reader io.Reader, id string) (FormVars, error) {
 
 	return vars, err
 }
+
+type CredentialParams struct {
+	TemplateModel struct {
+		Hmac          string `json:"hmac"`
+		RelayState    string `json:"relayState"`
+		PostAction    string `json:"postAction"`
+		IdentifierUrl string `json:"identifierUrl"`
+		Error         string `json:"error"`
+	} `json:"templateModel"`
+	CurrentLocale     string `json:"currentLocale"`
+	CsrfParameterName string `json:"csrf_parameterName"`
+	CsrfToken         string `json:"csrf_token"`
+}
+
+func ParseCredentialsPage(r io.ReadCloser) (CredentialParams, error) {
+	var res CredentialParams
+
+	buf := new(strings.Builder)
+	if _, err := io.Copy(buf, r); err != nil {
+		return res, err
+	}
+
+	re := regexp.MustCompile(`(?s)window._IDK\s*=\s*(.*?);`)
+	match := re.FindAllStringSubmatch(buf.String(), -1)
+
+	tmpl := strings.ReplaceAll(match[0][1], `'`, `"`)
+	for _, v := range []string{"templateModel", "currentLocale", "csrf_parameterName", "csrf_token"} {
+		tmpl = strings.Replace(tmpl, v, fmt.Sprintf(`"%s"`, v), 1)
+	}
+
+	err := json.Unmarshal([]byte(tmpl), &res)
+
+	return res, err
+}
diff --git a/vehicle/vw/identity.go b/vehicle/vw/identity.go
index 78abb468..d2208f42 100644
--- a/vehicle/vw/identity.go
+++ b/vehicle/vw/identity.go
@@ -13,6 +13,8 @@ import (
 	"golang.org/x/oauth2"
 )
 
+// https://identity.vwgroup.io/.well-known/openid-configuration
+
 const (
 	// IdentityURI is the VW OIDC identity provider uri
 	IdentityURI = "https://identity.vwgroup.io"
diff --git a/vehicle/vw/tokenprovider.go b/vehicle/vw/tokenprovider.go
index 97d0bf0c..32e45aa2 100644
--- a/vehicle/vw/tokenprovider.go
+++ b/vehicle/vw/tokenprovider.go
@@ -1,6 +1,7 @@
 package vw
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"net/http/cookiejar"
@@ -70,6 +71,8 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
 		resp.Body.Close()
 	}
 
+	var params CredentialParams
+
 	// POST identity.vwgroup.io/signin-service/v1/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com/login/identifier
 	if err == nil {
 		data := url.Values(map[string][]string{
@@ -81,7 +84,9 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
 
 		uri = IdentityURI + vars.Action
 		if resp, err = v.PostForm(uri, data); err == nil {
-			vars, err = FormValues(resp.Body, "form#credentialsForm")
+			if params, err = ParseCredentialsPage(resp.Body); err == nil && params.TemplateModel.Error != "" {
+				err = errors.New(params.TemplateModel.Error)
+			}
 			resp.Body.Close()
 		}
 	}
@@ -89,17 +94,25 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
 	// POST identity.vwgroup.io/signin-service/v1/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com/login/authenticate
 	if err == nil {
 		data := url.Values(map[string][]string{
-			"_csrf":      {vars.Inputs["_csrf"]},
-			"relayState": {vars.Inputs["relayState"]},
-			"hmac":       {vars.Inputs["hmac"]},
+			"_csrf":      {params.CsrfToken},
+			"relayState": {params.TemplateModel.RelayState},
+			"hmac":       {params.TemplateModel.Hmac},
 			"email":      {v.user},
 			"password":   {v.password},
 		})
 
-		uri = IdentityURI + vars.Action
+		// reuse url from identifier step before
+		uri = strings.ReplaceAll(uri, params.TemplateModel.IdentifierUrl, params.TemplateModel.PostAction)
+
 		if resp, err = v.PostForm(uri, data); err == nil {
 			resp.Body.Close()
 
+			if resp.StatusCode >= http.StatusBadRequest {
+				err = errors.New(resp.Status)
+			}
+		}
+
+		if err == nil {
 			if e := resp.Request.URL.Query().Get("error"); e != "" {
 				err = fmt.Errorf(e)
 			}


Test patch:
diff --git a/charger/template_test.go b/charger/template_test.go
index 1648d61b..aba9f5bb 100644
--- a/charger/template_test.go
+++ b/charger/template_test.go
@@ -50,6 +50,7 @@ func TestChargerTemplates(t *testing.T) {
 			} else {
 				values[templates.ModbusKeyRS485TCPIP] = true
 			}
+			values = tmpl.ModbusValues(templates.TemplateRenderModeInstance, true, values)
 		}
 
 		t.Run(tmpl.Template, func(t *testing.T) {
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 387e0797..6f5e1be4 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -151,6 +151,7 @@ func TestUpdatePowerZero(t *testing.T) {
 			chargeMeter: &Null{}, // silence nil panics
 			chargeRater: &Null{}, // silence nil panics
 			chargeTimer: &Null{}, // silence nil panics
+			wakeUpTimer: NewTimer(),
 			MinCurrent:  minA,
 			MaxCurrent:  maxA,
 			Phases:      1,
@@ -383,6 +384,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
 		chargeRater:  &Null{},            // silence nil panics
 		chargeTimer:  &Null{},            // silence nil panics
 		progress:     NewProgress(0, 10), // silence nil panics
+		wakeUpTimer:  NewTimer(),         // silence nil panics
 		MinCurrent:   minA,
 		MaxCurrent:   maxA,
 		vehicle:      vehicle,      // needed for targetSoC check
@@ -454,6 +456,7 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
 		chargeMeter: &Null{}, // silence nil panics
 		chargeRater: &Null{}, // silence nil panics
 		chargeTimer: &Null{}, // silence nil panics
+		wakeUpTimer: NewTimer(),
 		MinCurrent:  minA,
 		MaxCurrent:  maxA,
 		status:      api.StatusC,
@@ -525,6 +528,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 		chargeMeter: &Null{}, // silence nil panics
 		chargeRater: rater,
 		chargeTimer: &Null{}, // silence nil panics
+		wakeUpTimer: NewTimer(),
 		MinCurrent:  minA,
 		MaxCurrent:  maxA,
 		status:      api.StatusC,
diff --git a/core/timer_test.go b/core/timer_test.go
new file mode 100644
index 00000000..3d953f86
--- /dev/null
+++ b/core/timer_test.go
@@ -0,0 +1,30 @@
+package core
+
+import (
+	"testing"
+	"time"
+
+	"github.com/benbjohnson/clock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestTimer(t *testing.T) {
+	at := NewTimer()
+
+	clck := clock.NewMock()
+	at.clck = clck
+
+	// start
+	at.Start()
+	clck.Add(20 * time.Second)
+	require.Equal(t, at.Expired(), false)
+
+	// wait another 20 sec to expire the timer - this will reset the timer as well
+	clck.Add(20 * time.Second)
+	require.Equal(t, at.Expired(), true)
+
+	// start
+	at.Start()
+	clck.Add(time.Minute)
+	require.Equal(t, at.Expired(), true)
+}
diff --git a/meter/template_test.go b/meter/template_test.go
index 3a84edc5..d64c3c44 100644
--- a/meter/template_test.go
+++ b/meter/template_test.go
@@ -48,6 +48,7 @@ func TestMeterTemplates(t *testing.T) {
 			} else {
 				values[templates.ModbusKeyRS485TCPIP] = true
 			}
+			values = tmpl.ModbusValues(templates.TemplateRenderModeInstance, true, values)
 		}
 
 		usages := tmpl.Usages()