evcc-io/charger/keba-udp.go
2025-10-31 15:25:53 +00:00

319 lines
6.7 KiB
Go

package charger
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/keba"
"github.com/evcc-io/evcc/util"
)
// https://www.keba.com/file/downloads/e-mobility/KeContact_P20_P30_UDP_ProgrGuide_en.pdf
const (
udpTimeout = time.Second
)
// KebaUdp is an api.Charger implementation
type KebaUdp struct {
log *util.Logger
conn string
rfid keba.RFID
timeout time.Duration
recv chan keba.UDPMsg
sender *keba.Sender
}
func init() {
registry.Add("keba-udp", NewKebaUdpFromConfig)
}
//go:generate go tool decorate -f decorateKebaUdp -b *KebaUdp -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)"
// NewKebaUdpFromConfig creates a new Keba UDP charger
func NewKebaUdpFromConfig(other map[string]any) (api.Charger, error) {
cc := struct {
URI string
Serial string
Timeout time.Duration
RFID keba.RFID
}{
Timeout: udpTimeout,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
k, err := NewKebaUdp(cc.URI, cc.Serial, cc.RFID, cc.Timeout)
if err != nil {
return nil, err
}
energy, err := k.totalEnergy()
if err != nil {
return nil, err
}
if energy > 0 {
return decorateKebaUdp(k, k.currentPower, k.totalEnergy, k.currents), nil
}
return k, err
}
// NewKebaUdp creates a new charger
func NewKebaUdp(uri, serial string, rfid keba.RFID, timeout time.Duration) (*KebaUdp, error) {
log := util.NewLogger("keba")
instance, err := keba.Instance(log)
if err != nil {
return nil, err
}
// add default port
conn := util.DefaultPort(uri, keba.Port)
sender, err := keba.NewSender(log, conn)
c := &KebaUdp{
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
}
instance.Subscribe(serial, c.recv)
return c, err
}
func (c *KebaUdp) 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 *KebaUdp) roundtrip(msg string, report int, res any) error {
resC := make(chan keba.UDPMsg)
errC := make(chan error)
closeC := make(chan struct{})
defer close(closeC)
go c.receive(report, resC, errC, closeC)
// add report number to message and send
if report > 0 {
msg = fmt.Sprintf("%s %d", msg, report)
}
if err := c.sender.Send(msg); err != nil {
return err
}
for {
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))
}
res := string(resp.Message)
if res != keba.OK {
continue
}
rv.Elem().SetString(res)
return nil
}
return json.Unmarshal(resp.Message, &res)
case err := <-errC:
return err
}
}
}
// Status implements the api.Charger interface
func (c *KebaUdp) Status() (api.ChargeStatus, error) {
var kr keba.Report2
err := c.roundtrip("report", 2, &kr)
if err != nil {
return api.StatusA, err
}
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.StatusNone, fmt.Errorf("invalid status: %+d", kr.State)
}
// Enabled implements the api.Charger interface
func (c *KebaUdp) Enabled() (bool, error) {
var kr keba.Report2
err := c.roundtrip("report", 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 *KebaUdp) enableRFID() error {
// check if authorization required
var kr keba.Report2
if err := c.roundtrip("report", 2, &kr); err != nil {
return err
}
// no auth required
if kr.AuthReq == 0 {
return nil
}
// auth required but missing tag
if c.rfid.Tag == "" {
return errors.New("missing credentials for RFID authorization")
}
// authorize
var resp string
return c.roundtrip(fmt.Sprintf("start %s", c.rfid.Tag), 0, &resp)
}
// Enable implements the api.Charger interface
func (c *KebaUdp) Enable(enable bool) error {
if enable {
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
}
return nil
}
// MaxCurrent implements the api.Charger interface
func (c *KebaUdp) 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
}
return nil
}
var _ api.ChargerEx = (*KebaUdp)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
func (c *KebaUdp) MaxCurrentMillis(current float64) error {
d := int(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 api.Meter interface
func (c *KebaUdp) currentPower() (float64, error) {
var kr keba.Report3
err := c.roundtrip("report", 3, &kr)
// mW to W
return float64(kr.P) / 1e3, err
}
// totalEnergy implements the api.MeterEnergy interface
func (c *KebaUdp) totalEnergy() (float64, error) {
var kr keba.Report3
err := c.roundtrip("report", 3, &kr)
// mW to W
return float64(kr.ETotal) / 1e4, err
}
// currents implements the api.PhaseCurrents interface
func (c *KebaUdp) currents() (float64, float64, float64, error) {
var kr keba.Report3
err := c.roundtrip("report", 3, &kr)
// 1mA to A
return float64(kr.I1) / 1e3, float64(kr.I2) / 1e3, float64(kr.I3) / 1e3, err
}
var _ api.Identifier = (*KebaUdp)(nil)
// Identify implements the api.Identifier interface
func (c *KebaUdp) Identify() (string, error) {
var kr keba.Report100
err := c.roundtrip("report", 100, &kr)
return kr.RFIDTag, err
}
var _ api.Diagnosis = (*KebaUdp)(nil)
// Diagnose implements the api.Diagnosis interface
func (c *KebaUdp) Diagnose() {
var kr keba.Report100
if err := c.roundtrip("report", 100, &kr); err == nil {
fmt.Printf("%+v\n", kr)
}
}