Add KEBA connect chargers

This commit is contained in:
andig 2020-03-29 16:40:29 +02:00
parent 1729399ef9
commit 2045ce3bd7
6 changed files with 418 additions and 3 deletions

View file

@ -7,8 +7,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
## Features
- simple and clean user interface
- multiple [chargers](#charger): Wallbe (tested with Wallbe Eco S), Phoenix controllers (EM-CP-PP-ETH), go-eCharger, openWB slave, Mobile Charger Connect (currently used by Porsche), any other charger using scripting
- more chargers experimentally supported: NRGKick, SimpleEVSE, EVSEWifi
- multiple [chargers](#charger): Wallbe (tested with Wallbe Eco S), Phoenix controllers (EM-CP-PP-ETH), go-eCharger, NRGKick, SimpleEVSE, EVSEWifi, KEBA KeContact and BMW, openWB slave, Mobile Charger Connect and any other charger using scripting
- 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)
@ -134,6 +133,7 @@ Available charger implementations are:
- `evsewifi`: chargers with SimpleEVSE controllers using [SimpleEVSE-Wifi](https://github.com/CurtRod/SimpleEVSE-WiFi)
- `nrgkick`: NRGKick chargers with Connect module
- `go-e`: go-eCharger chargers
- `keba`: KEBA KeContact P20/P30 and BMW chargers (Note: requires enabled UDP function with DIP switch 1.3 = `ON`, see installation manual)
- `mcc`: Mobile Charger Connect devices (Audi, Bentley, Porsche)
- `default`: default charger implementation using configurable [plugins](#plugins) for integrating any type of charger

View file

@ -30,6 +30,8 @@ func NewFromConfig(log *util.Logger, typ string, other map[string]interface{}) a
c = NewSimpleEVSEFromConfig(log, other)
case "porsche", "audi", "bentley", "mcc":
c = NewMobileConnectFromConfig(log, other)
case "keba", "bmw":
c = NewKebaFromConfig(log, other)
default:
log.FATAL.Fatalf("invalid charger type '%s'", typ)
}

232
charger/keba.go Normal file
View file

@ -0,0 +1,232 @@
package charger
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"reflect"
"strings"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/charger/keba"
"github.com/andig/evcc/util"
)
const (
udpTimeout = time.Second
kebaPort = "7090"
)
// Keba is an api.Charger implementation with configurable getters and setters.
type Keba struct {
log *util.Logger
conn string
timeout time.Duration
recv chan keba.UDPMsg
}
// NewKebaFromConfig creates a new configurable charger
func NewKebaFromConfig(log *util.Logger, other map[string]interface{}) api.Charger {
cc := struct {
URI string
Timeout time.Duration
}{}
util.DecodeOther(log, other, &cc)
return NewKeba(cc.URI, cc.Timeout)
}
// NewKeba creates a new charger
func NewKeba(conn string, timeout time.Duration) api.Charger {
log := util.NewLogger("keba")
if keba.Instance == nil {
keba.Instance = keba.New(log, fmt.Sprintf(":%s", kebaPort))
}
// add default port
if _, _, err := net.SplitHostPort(conn); err != nil {
conn = fmt.Sprintf("%s:%s", conn, kebaPort)
}
if timeout == 0 {
timeout = udpTimeout
}
c := &Keba{
log: log,
conn: conn,
timeout: timeout,
recv: make(chan keba.UDPMsg),
}
keba.Instance.Subscribe(conn, c.recv)
return c
}
func (c *Keba) send(msg string) error {
raddr, err := net.ResolveUDPAddr("udp", c.conn)
if err != nil {
return err
}
conn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
return err
}
defer conn.Close()
_, err = io.Copy(conn, strings.NewReader(msg))
return err
}
func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error) {
t := time.NewTimer(c.timeout)
for {
select {
case msg := <-c.recv:
// matching result message
if msg.Report == nil && report == 0 {
resC <- msg
return
}
// matching report result
if msg.Report != nil && report == msg.Report.ID {
resC <- msg
return
}
case <-t.C:
errC <- errors.New("recv timeout")
return
}
}
}
func (c *Keba) roundtrip(msg string, report int, res interface{}) error {
resC := make(chan keba.UDPMsg)
errC := make(chan error)
defer func() {
close(resC)
close(errC)
}()
go c.receive(report, resC, errC)
if err := c.send(msg); err != nil {
return err
}
select {
case resp := <-resC:
if report == 0 {
// use reflection to write to simple string
rv := reflect.ValueOf(res)
if rv.Kind() != reflect.Ptr || rv.IsNil() || rv.Elem().Kind() != reflect.String {
return fmt.Errorf("invalid type: %s", reflect.TypeOf(res))
}
rv.Elem().SetString(string(resp.Message))
return nil
}
return json.Unmarshal(resp.Message, &res)
case err := <-errC:
return err
}
}
// Status implements the Charger.Status interface
func (c *Keba) Status() (api.ChargeStatus, error) {
var kr keba.Report2
err := c.roundtrip("report 2", 2, &kr)
if err != nil {
return api.StatusA, err
}
if kr.Plug == 0 {
return api.StatusA, nil
}
if kr.State == 2 {
return api.StatusB, nil
}
if kr.State == 3 {
return api.StatusC, nil
}
return api.StatusA, fmt.Errorf("unexpected status: %v", kr)
}
// Enabled implements the Charger.Enabled interface
func (c *Keba) Enabled() (bool, error) {
var kr keba.Report2
err := c.roundtrip("report 2", 2, &kr)
if err != nil {
return false, err
}
return kr.EnableSys == 1 || kr.EnableUser == 1, nil
}
// Enable implements the Charger.Enable interface
func (c *Keba) Enable(enable bool) error {
var d int
if enable {
d = 1
}
var resp string
err := c.roundtrip(fmt.Sprintf("ena %d", d), 0, &resp)
if err != nil {
return err
}
if string(resp) == keba.OK {
return nil
}
return fmt.Errorf("ena unexpected response: %s", resp)
}
// MaxCurrent implements the Charger.MaxCurrent interface
func (c *Keba) MaxCurrent(current int64) error {
var resp string
err := c.roundtrip(fmt.Sprintf("curr %d", 1000*current), 0, &resp)
if err != nil {
return err
}
if resp == keba.OK {
return nil
}
return fmt.Errorf("curr unexpected response: %s", resp)
}
// CurrentPower implements the Meter interface.
func (c *Keba) CurrentPower() (float64, error) {
var kr keba.Report3
err := c.roundtrip("report 3", 3, &kr)
if err != nil {
return 0, err
}
// mW to W
return float64(kr.P) / 1e3, nil
}
// ChargedEnergy implements the ChargeRater interface.
func (c *Keba) ChargedEnergy() (float64, error) {
var kr keba.Report3
err := c.roundtrip("report 3", 3, &kr)
if err != nil {
return 0, err
}
// 0,1Wh to kWh
return float64(kr.EPres) / 1e5, nil
}

118
charger/keba/listener.go Normal file
View file

@ -0,0 +1,118 @@
package keba
import (
"encoding/json"
"net"
"strings"
"sync"
"github.com/andig/evcc/util"
)
const (
udpBufferSize = 1024
// OK is the KEBA confirmation message
OK = "TCH-OK :done"
)
// Instance is the KEBA listener instance
// This is needed since KEBAs ignore the sender port and always UDP back to port 7090
var Instance *Listener
// UDPMsg transports the KEBA response. Report is any of Report1,2,3
type UDPMsg struct {
Addr string
Message []byte
Report *Report
}
// Listener singleton listens for KEBA UDP messages
type Listener struct {
mux sync.Mutex
log *util.Logger
conn *net.UDPConn
clients map[string]chan<- UDPMsg
}
// New creates a UDP listener that clients can subscribe to
func New(log *util.Logger, addr string) *Listener {
laddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
log.FATAL.Fatal(err)
}
conn, err := net.ListenUDP("udp", laddr)
if err != nil {
log.FATAL.Fatal(err)
}
l := &Listener{
log: log,
conn: conn,
}
go l.listen()
return l
}
// Subscribe adds a client address and message channel
func (l *Listener) Subscribe(addr string, c chan<- UDPMsg) {
l.mux.Lock()
defer l.mux.Unlock()
if l.clients == nil {
l.clients = make(map[string]chan<- UDPMsg)
}
l.clients[addr] = c
}
func (l *Listener) listen() {
b := make([]byte, udpBufferSize)
for {
read, addr, err := l.conn.ReadFrom(b)
if err != nil {
l.log.WARN.Printf("listener: %v", err)
continue
}
body := strings.TrimSpace(string(b[:read]))
l.log.TRACE.Printf("recv from %s %v", addr.String(), body)
msg := UDPMsg{
Addr: addr.String(),
Message: []byte(body),
}
if body != OK {
var report Report
if err := json.Unmarshal([]byte(body), &report); err != nil {
l.log.WARN.Printf("listener: %v", err)
continue
}
msg.Report = &report
}
l.send(msg)
}
}
func (l *Listener) send(msg UDPMsg) {
l.mux.Lock()
defer l.mux.Unlock()
for addr, client := range l.clients {
if addr == msg.Addr {
select {
case client <- msg:
default:
l.log.TRACE.Println("listener: recv blocked")
}
break
}
}
}

60
charger/keba/reports.go Normal file
View file

@ -0,0 +1,60 @@
package keba
// Report contains report id and device serial
type Report struct {
ID int `json:"ID,string"`
Serial string `json:"Serial"`
}
// Report1 is the report 1 command answer
type Report1 struct {
ID int `json:"ID,string"`
Serial string `json:"Serial"`
Product string `json:"Product"`
Firmware string `json:"Firmware"`
COMModule int `json:"COM-module"`
Sec int64 `json:"Sec"`
}
// Report2 is the report 2 command answer
type Report2 struct {
ID int `json:"ID,string"`
Serial string `json:"Serial"`
State int `json:"State"`
Error1 int `json:"Error1"`
Error2 int `json:"Error2"`
Plug int `json:"Plug"`
AuthON int `json:"AuthON"`
AuthReq int `json:"Authreq"`
EnableSys int `json:"Enable sys"`
EnableUser int `json:"Enable user"`
MaxCurr int `json:"Max curr"`
MaxCurrPercent int `json:"Max curr %"`
CurrHW int `json:"Curr HW"`
Curruser int `json:"Curr user"`
CurrFS int `json:"Curr FS"`
TmoFS int `json:"Tmo FS"`
CurrTimer int `json:"Curr timer"`
TmoCT int `json:"Tmo CT"`
SetEnergy int `json:"Setenergy"`
Output int `json:"Output"`
Input int `json:"Input"`
Sec int64 `json:"Sec"`
}
// Report3 is the report 3 command answer
type Report3 struct {
ID int `json:"ID,string"`
Serial string `json:"Serial"`
U1 int64 `json:"U1"`
U2 int64 `json:"U2"`
U3 int64 `json:"U3"`
I1 int64 `json:"I1"`
I2 int64 `json:"I2"`
I3 int64 `json:"I3"`
P int64 `json:"P"`
PF int64 `json:"PF"`
EPres int64 `json:"E pres"`
ETotal int64 `json:"E total"`
Sec int64 `json:"Sec"`
}

View file

@ -91,8 +91,11 @@ chargers:
macaddress: 00:99:22 # MAC address
password: # password
- name: go-e
type: go-e # go-e charger
type: go-e # go-eCharger
uri: http://192.168.1.4 # go-e address
- name: keba
type: keba # KEBA charger
uri: 192.168.1.4:7090 # KEBA address
- name: mcc
type: mcc # Mobile Charger Connect (Audi, Bentley, Porsche)
uri: https://192.168.1.4 # Mobile Charger Connect address