Skip to content

Commit

Permalink
Add openWB support via MQTT (evcc-io#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Nov 11, 2020
1 parent e8f3b59 commit b11381b
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 31 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,20 +155,25 @@ In general, due to the minimum value of 5% for signalling the EV duty cycle, the

Charger is responsible for handling EV state and adjusting charge current. Available charger implementations are:

- `wallbe`: Wallbe Eco chargers (see [Preparation](#wallbe-preparation)). For older Wallbe boxes (pre 2019) with Phoenix EV-CC-AC1-M3-CBC-RCM-ETH controllers make sure to set `legacy: true` to enable correct current configuration.
- `phoenix-emcp`: chargers with Phoenix EM-CP-PP-ETH controllers like the ESL Walli (Ethernet connection).
- `phoenix-evcc`: chargers with Phoenix EV-CC-AC1-M controllers (ModBus connection)
- `simpleevse`: chargers with SimpleEVSE controllers connected via ModBus (e.g. OpenWB Wallbox, Easy Wallbox B163, ...)
- `evsewifi`: chargers with SimpleEVSE controllers using [EVSE-WiFi](https://www.evse-wifi.de/)
- `nrgkick-bluetooth`: NRGkick chargers with Bluetooth connector (Linux only, not supported on Docker)
- `nrgkick-connect`: NRGkick chargers with additional NRGkick Connect module
- `go-e`: go-eCharger chargers (both local and cloud API are supported)
- `keba`: KEBA KeContact P20/P30 and BMW chargers (see [Preparation](#keba-preparation))
- `mcc`: Mobile Charger Connect devices (Audi, Bentley, Porsche)
- `openWB`: openWB chargers using openWB's MQTT interface
- `phoenix-emcp`: chargers with Phoenix EM-CP-PP-ETH controllers like the ESL Walli (Ethernet connection).
- `phoenix-evcc`: chargers with Phoenix EV-CC-AC1-M controllers (ModBus connection)
- `nrgkick-bluetooth`: NRGkick chargers with Bluetooth connector (Linux only, not supported on Docker)
- `nrgkick-connect`: NRGkick chargers with additional NRGkick Connect module
- `simpleevse`: chargers with SimpleEVSE controllers connected via ModBus (e.g. OpenWB Wallbox, Easy Wallbox B163, ...)
- `wallbe`: Wallbe Eco chargers (see [Preparation](#wallbe-preparation)). For older Wallbe boxes (pre 2019) with Phoenix EV-CC-AC1-M3-CBC-RCM-ETH controllers make sure to set `legacy: true` to enable correct current configuration.
- `default`: default charger implementation using configurable [plugins](#plugins) for integrating any type of charger

Configuration examples are documented at [andig/evcc-config#chargers](https://github.com/andig/evcc-config#chargers)

#### KEBA preparation

KEBA chargers require UDP function to be enabled with DIP switch 1.3 = `ON`, see KEBA installation manual.

#### Wallbe preparation

Wallbe chargers are supported out of the box. The Wallbe must be connected using Ethernet. If not configured, the default address `192.168.0.8:502` is used.
Expand All @@ -189,9 +194,6 @@ Compare the value to what you see as *Actual Charge Current Setting* in the Wall

**NOTE:** Opening the wall box **must** only be done by certified professionals. The box **must** be disconnected from mains before opening.

#### KEBA preparation

KEBA chargers require UDP function to be enabled with DIP switch 1.3 = `ON`, see KEBA installation manual.

### Meter

Expand Down
89 changes: 89 additions & 0 deletions charger/openwb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package charger

import (
"fmt"
"time"

"github.com/andig/evcc/api"
"github.com/andig/evcc/charger/openwb"
"github.com/andig/evcc/meter"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/util"
)

func init() {
registry.Add("openwb", NewOpenWBFromConfig)
}

// OpenWB configures generic charger and charge meter for an openWB loadpoint
type OpenWB struct {
api.Charger
api.Meter
}

// NewOpenWBFromConfig creates a new configurable charger
func NewOpenWBFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
Broker string
User, Password string
Topic string
ID int
Timeout time.Duration
}{
Topic: "openWB",
ID: 1,
Timeout: 5 * time.Second,
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

clientID := provider.MqttClientID()
client := provider.NewMqttClient(cc.Broker, cc.User, cc.Password, clientID, 1)

// check if loadpoint configured
configured := client.BoolGetter(fmt.Sprintf("%s/lp/%d/%s", cc.Topic, cc.ID, openwb.ConfiguredTopic), cc.Timeout)
if isConfigured, err := configured(); err != nil || !isConfigured {
return nil, fmt.Errorf("openWB loadpoint %d is not configured", cc.ID)
}

// adapt plugged/charging to status
plugged := client.BoolGetter(fmt.Sprintf("%s/lp/%d/%s", cc.Topic, cc.ID, openwb.PluggedTopic), cc.Timeout)
charging := client.BoolGetter(fmt.Sprintf("%s/lp/%d/%s", cc.Topic, cc.ID, openwb.ChargingTopic), cc.Timeout)
status := provider.NewOpenWBStatusProvider(plugged, charging).StringGetter

// remaining getters
enabled := client.BoolGetter(fmt.Sprintf("%s/lp/%d/%s", cc.Topic, cc.ID, openwb.EnabledTopic), cc.Timeout)

// setters
enable := client.BoolSetter("enable", fmt.Sprintf("%s/set/lp%d/%s", cc.Topic, cc.ID, openwb.EnabledTopic), "")
maxcurrent := client.IntSetter("maxcurrent", fmt.Sprintf("%s/set/lp%d/%s", cc.Topic, cc.ID, openwb.MaxCurrentTopic), "")

// meter getters
power := client.FloatGetter(fmt.Sprintf("%s/lp/%d/%s", cc.Topic, cc.ID, openwb.ChargePowerTopic), 1, cc.Timeout)
totalEnergy := client.FloatGetter(fmt.Sprintf("%s/lp/%d/%s", cc.Topic, cc.ID, openwb.ChargeTotalEnergyTopic), 1, cc.Timeout)

var currents []func() (float64, error)
for i := 1; i <= 3; i++ {
current := client.FloatGetter(fmt.Sprintf("%s/lp/%d/%s%d", cc.Topic, cc.ID, openwb.CurrentTopic, i), 1, cc.Timeout)
currents = append(currents, current)
}

c, err := NewConfigurable(status, enabled, enable, maxcurrent)
if err != nil {
return nil, err
}

m, err := meter.NewConfigurable(power)
if err != nil {
return nil, err
}

res := &OpenWB{
Charger: c,
Meter: m.Decorate(totalEnergy, currents, nil),
}

return res, nil
}
21 changes: 21 additions & 0 deletions charger/openwb/topics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package openwb

// predefined openWB topic names
const (
// status
PluggedTopic = "boolPlugStat"
ChargingTopic = "boolChargeStat"
ConfiguredTopic = "boolChargePointConfigured"

// getter/setter
EnabledTopic = "ChargePointEnabled"
MaxCurrentTopic = "DirectChargeAmps"

// charge power
ChargePowerTopic = "W"
ChargeTotalEnergyTopic = "kWhCounter"

// general measurements
PowerTopic = "W"
CurrentTopic = "APhase" // 1..3
)
9 changes: 2 additions & 7 deletions cmd/setup.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"fmt"
"math/rand"
"strconv"
"time"
Expand Down Expand Up @@ -42,14 +41,10 @@ func configureDatabase(conf server.InfluxConfig, loadPoints []core.LoadPointAPI,
go influx.Run(loadPoints, in)
}

func mqttClientID() string {
pid := rand.Int31()
return fmt.Sprintf("evcc-%d", pid)
}

// setup mqtt
func configureMQTT(conf provider.MqttConfig) {
provider.MQTT = provider.NewMqttClient(conf.Broker, conf.User, conf.Password, mqttClientID(), 1)
clientID := provider.MqttClientID()
provider.MQTT = provider.NewMqttClient(conf.Broker, conf.User, conf.Password, clientID, 1)
}

// setup HEMS
Expand Down
36 changes: 28 additions & 8 deletions meter/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,14 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error)
m, _ := NewConfigurable(power)

// decorate Meter with MeterEnergy
var totalEnergy func() (float64, error)
if cc.Energy != nil {
m.totalEnergyG, err = provider.NewFloatGetterFromConfig(*cc.Energy)
if err != nil {
return nil, err
}
totalEnergy = m.totalEnergy
}

// decorate Meter with MeterCurrent
var currents func() (float64, float64, float64, error)
if len(cc.Currents) > 0 {
if len(cc.Currents) != 3 {
return nil, errors.New("need 3 currents")
Expand All @@ -66,21 +63,17 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error)

m.currentsG = append(m.currentsG, c)
}

currents = m.currents
}

// decorate Meter with BatterySoC
var batterySoC func() (float64, error)
if cc.SoC != nil {
m.batterySoCG, err = provider.NewFloatGetterFromConfig(*cc.SoC)
if err != nil {
return nil, err
}
batterySoC = m.batterySoC
}

res := decorateMeter(m, totalEnergy, currents, batterySoC)
res := m.Decorate(m.totalEnergyG, m.currentsG, m.batterySoCG)

return res, nil
}
Expand All @@ -101,6 +94,33 @@ type Meter struct {
batterySoCG func() (float64, error)
}

// Decorate attaches additional capabilities to the base meter
func (m *Meter) Decorate(
totalEnergyG func() (float64, error),
currentsG []func() (float64, error),
batterySoCG func() (float64, error),
) api.Meter {
var totalEnergy func() (float64, error)
if totalEnergyG != nil {
m.totalEnergyG = totalEnergyG
totalEnergy = m.totalEnergy
}

var currents func() (float64, float64, float64, error)
if currentsG != nil {
m.currentsG = currentsG
currents = m.currents
}

var batterySoC func() (float64, error)
if batterySoCG != nil {
m.batterySoCG = batterySoCG
batterySoC = m.batterySoC
}

return decorateMeter(m, totalEnergy, currents, batterySoC)
}

// CurrentPower implements the Meter.CurrentPower interface
func (m *Meter) CurrentPower() (float64, error) {
return m.currentPowerG()
Expand Down
2 changes: 1 addition & 1 deletion provider/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func NewStringGetterFromConfig(config Config) (res func() (string, error), err e
res = NewCached(res, pc.Cache).StringGetter()
}
case "combined", "openwb":
res, err = openWBStatusFromConfig(config.Other)
res, err = NewOpenWBStatusProviderFromConfig(config.Other)
default:
err = fmt.Errorf("invalid plugin type: %s", config.Type)
}
Expand Down
7 changes: 7 additions & 0 deletions provider/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provider
import (
"fmt"
"math"
"math/rand"
"strconv"
"sync"
"time"
Expand All @@ -13,6 +14,12 @@ import (

const publishTimeout = 2 * time.Second

// MqttClientID created unique mqtt client id
func MqttClientID() string {
pid := rand.Int31()
return fmt.Sprintf("evcc-%d", pid)
}

// MqttConfig is the public configuration
type MqttConfig struct {
Broker string
Expand Down
20 changes: 14 additions & 6 deletions provider/openwb.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"github.com/andig/evcc/util"
)

type openWBStatusProvider struct {
// OpenWBStatus implements status conversion from openWB to api.Status
type OpenWBStatus struct {
plugged, charging func() (bool, error)
}

func openWBStatusFromConfig(other map[string]interface{}) (func() (string, error), error) {
// NewOpenWBStatusProviderFromConfig creates OpenWBStatus from given configuration
func NewOpenWBStatusProviderFromConfig(other map[string]interface{}) (func() (string, error), error) {
cc := struct {
Plugged, Charging Config
}{}
Expand All @@ -28,15 +30,21 @@ func openWBStatusFromConfig(other map[string]interface{}) (func() (string, error
return nil, err
}

o := &openWBStatusProvider{
o := NewOpenWBStatusProvider(plugged, charging)

return o.StringGetter, nil
}

// NewOpenWBStatusProvider creates provider for OpenWB status converted from MQTT topics
func NewOpenWBStatusProvider(plugged, charging func() (bool, error)) *OpenWBStatus {
return &OpenWBStatus{
plugged: plugged,
charging: charging,
}

return o.stringGetter, nil
}

func (o *openWBStatusProvider) stringGetter() (string, error) {
// StringGetter returns string from OpenWB charging/ plugged status
func (o *OpenWBStatus) StringGetter() (string, error) {
charging, err := o.charging()
if err != nil {
return "", err
Expand Down

0 comments on commit b11381b

Please sign in to comment.