evcc-io/charger/keba.go

290 lines
6 KiB
Go

package charger
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/charger/keba"
"github.com/andig/evcc/util"
)
// https://www.keba.com/file/downloads/e-mobility/KeContact_P20_P30_UDP_ProgrGuide_en.pdf
const (
udpTimeout = time.Second
)
// RFID contains access credentials
type RFID struct {
Tag string
}
// Keba is an api.Charger implementation with configurable getters and setters.
type Keba struct {
log *util.Logger
conn string
rfid RFID
timeout time.Duration
recv chan keba.UDPMsg
sender *keba.Sender
}
func init() {
registry.Add("keba", NewKebaFromConfig)
}
// NewKebaFromConfig creates a new configurable charger
func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
URI string
Serial string
Timeout time.Duration
RFID RFID
}{
Timeout: udpTimeout,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewKeba(cc.URI, cc.Serial, cc.RFID, cc.Timeout)
}
// NewKeba creates a new charger
func NewKeba(uri, serial string, rfid RFID, timeout time.Duration) (api.Charger, error) {
log := util.NewLogger("keba")
var err error
if keba.Instance == nil {
keba.Instance, err = keba.New(log)
if err != nil {
return nil, err
}
}
// add default port
conn := util.DefaultPort(uri, keba.Port)
sender, err := keba.NewSender(uri)
c := &Keba{
log: log,
conn: conn,
rfid: rfid,
timeout: timeout,
recv: make(chan keba.UDPMsg),
sender: sender,
}
// use serial to subscribe if defined for docker scenarios
if serial == "" {
serial = conn
}
keba.Instance.Subscribe(serial, c.recv)
return c, err
}
func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, closeC <-chan struct{}) {
t := time.NewTimer(c.timeout)
defer close(resC)
defer close(errC)
for {
select {
case msg := <-c.recv:
// matching result message
if msg.Report == nil && report == 0 {
resC <- msg
return
}
// matching report id
if msg.Report != nil && report == msg.Report.ID {
resC <- msg
return
}
case <-t.C:
errC <- errors.New("recv timeout")
return
case <-closeC:
return
}
}
}
func (c *Keba) roundtrip(msg string, report int, res interface{}) error {
resC := make(chan keba.UDPMsg)
errC := make(chan error)
closeC := make(chan struct{})
defer close(closeC)
go c.receive(report, resC, errC, closeC)
if err := c.sender.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.AuthON == 1 && c.rfid.Tag == "" {
c.log.WARN.Println("missing credentials for RFID authorization")
}
if kr.Plug < 5 {
return api.StatusA, nil
}
if kr.State == 3 {
return api.StatusC, nil
}
if kr.State != 4 {
return api.StatusB, 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
}
// enableRFID sends RFID credentials to enable charge
func (c *Keba) enableRFID() error {
// check if authorization required
var kr keba.Report2
if err := c.roundtrip("report 2", 2, &kr); err != nil {
return err
}
if kr.AuthReq == 0 {
return nil
}
// authorize
var resp string
if err := c.roundtrip(fmt.Sprintf("start %s", c.rfid.Tag), 0, &resp); err != nil {
return err
}
if resp != keba.OK {
return fmt.Errorf("start %s unexpected response: %s", c.rfid.Tag, resp)
}
return nil
}
// Enable implements the Charger.Enable interface
func (c *Keba) Enable(enable bool) error {
if enable && c.rfid.Tag != "" {
if err := c.enableRFID(); err != nil {
return err
}
}
var d int
if enable {
d = 1
}
// ignore result...
var resp string
if err := c.roundtrip(fmt.Sprintf("ena %d", d), 0, &resp); err != nil {
return err
}
if resp != keba.OK {
return fmt.Errorf("ena %d unexpected response: %s", d, resp)
}
return nil
}
// MaxCurrent implements the Charger.MaxCurrent interface
func (c *Keba) MaxCurrent(current int64) error {
d := 1000 * current
var resp string
if err := c.roundtrip(fmt.Sprintf("curr %d", d), 0, &resp); err != nil {
return err
}
if resp != keba.OK {
return fmt.Errorf("curr %d unexpected response: %s", d, resp)
}
return nil
}
// CurrentPower implements the Meter interface
func (c *Keba) CurrentPower() (float64, error) {
var kr keba.Report3
err := c.roundtrip("report 3", 3, &kr)
// mW to W
return float64(kr.P) / 1e3, err
}
// TotalEnergy implements the MeterEnergy interface
func (c *Keba) TotalEnergy() (float64, error) {
var kr keba.Report3
err := c.roundtrip("report 3", 3, &kr)
// mW to W
return float64(kr.ETotal) / 1e4, err
}
// ChargedEnergy implements the ChargeRater interface
func (c *Keba) ChargedEnergy() (float64, error) {
var kr keba.Report3
err := c.roundtrip("report 3", 3, &kr)
// 0,1Wh to kWh
return float64(kr.EPres) / 1e4, err
}
// Currents implements the MeterCurrents interface
func (c *Keba) Currents() (float64, float64, float64, error) {
var kr keba.Report3
err := c.roundtrip("report 3", 3, &kr)
// 1mA to A
return float64(kr.I1) / 1e3, float64(kr.I2) / 1e3, float64(kr.I3) / 1e3, err
}
// Diagnosis implements the Diagnosis interface
func (c *Keba) Diagnosis() {
var kr keba.Report100
if err := c.roundtrip("report 100", 100, &kr); err == nil {
fmt.Printf("%+v\n", kr)
}
}