diff --git a/charger/daheimladen-mb.go b/charger/daheimladen-mb.go index cf5773ce9..be25a4782 100644 --- a/charger/daheimladen-mb.go +++ b/charger/daheimladen-mb.go @@ -103,7 +103,9 @@ func NewDaheimLadenMB(uri string, id uint8) (api.Charger, error) { if err != nil { return nil, fmt.Errorf("failsafe timeout: %w", err) } - go wb.heartbeat(time.Duration(binary.BigEndian.Uint16(b)/2) * time.Second) + if u := binary.BigEndian.Uint16(b); u > 0 { + go wb.heartbeat(time.Duration(u/2) * time.Second) + } return wb, err } diff --git a/charger/obo.go b/charger/obo.go new file mode 100644 index 000000000..f96da84d0 --- /dev/null +++ b/charger/obo.go @@ -0,0 +1,136 @@ +package charger + +import ( + "encoding/binary" + "fmt" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" +) + +// Obo charger implementation +type Obo struct { + log *util.Logger + conn *modbus.Connection +} + +const ( + oboRegEnable = 5 + oboRegAmpsConfig = 6 + oboRegStatus = 11 + oboRegTimeout = 28 +) + +func init() { + registry.Add("obo", NewOboFromConfig) +} + +// NewOboFromConfig creates a OBO Bettermann charger from generic config +func NewOboFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := modbus.Settings{ + Baudrate: 19200, + Comset: "8E1", + ID: 101, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewObo(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID) +} + +// NewObo creates OBO Bettermann charger +func NewObo(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) { + conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID) + if err != nil { + return nil, err + } + + log := util.NewLogger("obo") + conn.Logger(log.TRACE) + + wb := &Obo{ + log: log, + conn: conn, + } + + // get failsafe timeout from charger + b, err := conn.ReadHoldingRegisters(oboRegTimeout, 1) + if err != nil { + return nil, fmt.Errorf("failsafe timeout: %w", err) + } + if u := binary.BigEndian.Uint16(b); u > 0 { + go wb.heartbeat(time.Duration(u/2) * time.Millisecond) + } + + // lightshow + // go func() { + // conn.WriteSingleRegister(3, 1) + // for { + // for i := 0; i < 3; i++ { + // u := rand.Int31n(256) + // conn.WriteSingleRegister(uint16(i), uint16(u)) + // } + // time.Sleep(10 * time.Millisecond) + // } + // }() + // time.Sleep(10 * time.Second) + + return wb, nil +} + +func (wb *Obo) heartbeat(timeout time.Duration) { + for range time.Tick(timeout) { + if _, err := wb.conn.ReadHoldingRegisters(dlRegSafeCurrent, 1); err != nil { + wb.log.ERROR.Println("heartbeat:", err) + } + } +} + +// Status implements the api.Charger interface +func (wb *Obo) Status() (api.ChargeStatus, error) { + b, err := wb.conn.ReadHoldingRegisters(oboRegStatus, 1) + if err != nil { + return api.StatusNone, err + } + + switch u := binary.BigEndian.Uint16(b); u { + case 0, 1, 2: + // A..C + return api.ChargeStatus(string('A' + rune(u))), nil + default: + // D, F + return api.StatusNone, fmt.Errorf("invalid status: %d", u) + } +} + +// Enabled implements the api.Charger interface +func (wb *Obo) Enabled() (bool, error) { + b, err := wb.conn.ReadHoldingRegisters(oboRegEnable, 1) + if err != nil { + return false, err + } + + return binary.BigEndian.Uint16(b) > 0, nil +} + +// Enable implements the api.Charger interface +func (wb *Obo) Enable(enable bool) error { + var u uint16 + if enable { + u = 1 + } + + _, err := wb.conn.WriteSingleRegister(oboRegEnable, u) + + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *Obo) MaxCurrent(current int64) error { + _, err := wb.conn.WriteSingleRegister(oboRegAmpsConfig, uint16(current)) + return err +} diff --git a/charger/webasto-next.go b/charger/webasto-next.go index 03c5f149b..ef1076bd2 100644 --- a/charger/webasto-next.go +++ b/charger/webasto-next.go @@ -97,8 +97,9 @@ func NewWebastoNext(uri string, id uint8) (api.Charger, error) { if err != nil { return nil, fmt.Errorf("failsafe timeout: %w", err) } - - go wb.heartbeat(time.Duration(binary.BigEndian.Uint16(b)/2) * time.Second) + if u := binary.BigEndian.Uint16(b); u > 0 { + go wb.heartbeat(time.Duration(u/2) * time.Second) + } return wb, err } diff --git a/templates/definition/charger/obo.yaml b/templates/definition/charger/obo.yaml new file mode 100644 index 000000000..ebbda7912 --- /dev/null +++ b/templates/definition/charger/obo.yaml @@ -0,0 +1,14 @@ +template: obo +products: + - brand: OBO Bettermann + description: + generic: Ion +params: + - name: modbus + choice: ["rs485"] + baudrate: 19200 + comset: 8E1 + id: 101 +render: | + type: obo + {{- include "modbus" . }} diff --git a/templates/docs/charger/obo_0.yaml b/templates/docs/charger/obo_0.yaml new file mode 100644 index 000000000..f7ac33186 --- /dev/null +++ b/templates/docs/charger/obo_0.yaml @@ -0,0 +1,20 @@ +product: + brand: OBO Bettermann + description: Ion +render: + - default: | + type: template + template: obo + + # RS485 via adapter (Modbus RTU) + modbus: rs485serial + id: 101 + device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse + baudrate: 19200 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200 + comset: "8E1" # Kommunikationsparameter für den Adapter + + # RS485 via TCP/IP (Modbus RTU) + modbus: rs485tcpip + id: 101 + host: 192.0.2.2 # Hostname + port: 502 # Port diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json index 3b36acea2..3a2a5c705 100644 --- a/templates/evcc.io/brands.json +++ b/templates/evcc.io/brands.json @@ -35,6 +35,7 @@ "LadeFoxx", "Mennekes", "NRGKick", + "OBO Bettermann", "OpenEVSE", "openWB", "Optec",