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) } }