diff --git a/README.md b/README.md index c78b18758..8f9797996 100644 --- a/README.md +++ b/README.md @@ -7,7 +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, NRGKick, SimpleEVSE, EVSEWifi, KEBA KeContact and BMW, openWB slave, Mobile Charger Connect and any other charger using scripting +- multiple [chargers](#charger): Wallbe, Phoenix, go-eCharger, NRGKick, SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect, 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) @@ -118,7 +118,8 @@ EVCC consists of four basic elements: *Charger*, *Meter*, *SoC* and *Loadpoint*. Charger is responsible for handling EV state and adjusting charge current. Available charger implementations are: - `wallbe`: Wallbe Eco chargers (see [Hardware Preparation](#Wallbe-hardware-preparation) for preparing the Wallbe). For older Wallbe boxes (pre 2019) with Phoenix EV-CC-AC1-M3-CBC-RCM-ETH controllers make sure to set `legacy: true` to enable correct current configuration. -- `phoenix`: chargers with Phoenix EM-CP-PP-ETH controllers +- `phoenix-emcp`: chargers with Phoenix EM-CP-PP-ETH controllers (Ethernet connection) +- `phoenix-evcc`: chargers with Phoenix EV-CC-AC1-M controllers (ModBus connection) - `simpleevse`: chargers with SimpleEVSE controllers connected via ModBus (e.g. OpenWB) - `evsewifi`: chargers with SimpleEVSE controllers using [SimpleEVSE-Wifi](https://github.com/CurtRod/SimpleEVSE-WiFi) - `nrgkick`: NRGKick chargers with Connect module diff --git a/charger/config.go b/charger/config.go index 4b3863601..6bffb1406 100644 --- a/charger/config.go +++ b/charger/config.go @@ -18,8 +18,10 @@ func NewFromConfig(log *util.Logger, typ string, other map[string]interface{}) a c = NewConfigurableFromConfig(log, other) case "wallbe": c = NewWallbeFromConfig(log, other) - case "phoenix": - c = NewPhoenixFromConfig(log, other) + case "phoenix-emcp": + c = NewPhoenixEMCPFromConfig(log, other) + case "phoenix-evcc": + c = NewPhoenixEVCCFromConfig(log, other) case "nrgkick", "nrg", "kick": c = NewNRGKickFromConfig(log, other) case "go-e", "goe": diff --git a/charger/phoenix-emcp.go b/charger/phoenix-emcp.go new file mode 100644 index 000000000..3306cb5c4 --- /dev/null +++ b/charger/phoenix-emcp.go @@ -0,0 +1,129 @@ +package charger + +import ( + "fmt" + "time" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/util" + "github.com/grid-x/modbus" +) + +const ( + phEMCPRegStatus = 100 // Input + phEMCPRegChargeTime = 102 // Input + phEMCPRegMaxCurrent = 300 // Holding + phEMCPRegEnable = 400 // Coil +) + +// PhoenixEMCP is an api.ChargeController implementation for Phoenix EM-CP-PP-ETH wallboxes. +// It uses Modbus TCP to communicate with the wallbox at modbus client id 255. +type PhoenixEMCP struct { + log *util.Logger + client modbus.Client + handler *modbus.TCPClientHandler +} + +// NewPhoenixEMCPFromConfig creates a Phoenix charger from generic config +func NewPhoenixEMCPFromConfig(log *util.Logger, other map[string]interface{}) api.Charger { + cc := struct { + URI string + ID uint8 + }{} + util.DecodeOther(log, other, &cc) + + if cc.ID == 0 { + log.FATAL.Fatal("config: missing slave id") + } + + return NewPhoenixEMCP(cc.URI, cc.ID) +} + +// NewPhoenixEMCP creates a Phoenix charger +func NewPhoenixEMCP(uri string, id uint8) api.Charger { + log := util.NewLogger("emcp") + + handler := modbus.NewTCPClientHandler(uri) + client := modbus.NewClient(handler) + + handler.SlaveID = id + handler.Timeout = timeout + handler.ProtocolRecoveryTimeout = protocolTimeout + + wb := &PhoenixEMCP{ + log: log, + client: client, + handler: handler, + } + + return wb +} + +// Status implements the Charger.Status interface +func (wb *PhoenixEMCP) Status() (api.ChargeStatus, error) { + b, err := wb.client.ReadInputRegisters(phEMCPRegStatus, 1) + wb.log.TRACE.Printf("read status (%d): %0 X", phEMCPRegStatus, b) + if err != nil { + wb.handler.Close() + return api.StatusNone, err + } + + return api.ChargeStatus(string(b[1])), nil +} + +// Enabled implements the Charger.Enabled interface +func (wb *PhoenixEMCP) Enabled() (bool, error) { + b, err := wb.client.ReadCoils(phEMCPRegEnable, 1) + wb.log.TRACE.Printf("read charge enable (%d): %0 X", phEMCPRegEnable, b) + if err != nil { + wb.handler.Close() + return false, err + } + + return b[0] == 1, nil +} + +// Enable implements the Charger.Enable interface +func (wb *PhoenixEMCP) Enable(enable bool) error { + var u uint16 + if enable { + u = 0xFF00 + } + + b, err := wb.client.WriteSingleCoil(phEMCPRegEnable, u) + wb.log.TRACE.Printf("write charge enable %d %0X: %0 X", phEMCPRegEnable, u, b) + if err != nil { + wb.handler.Close() + } + + return err +} + +// MaxCurrent implements the Charger.MaxCurrent interface +func (wb *PhoenixEMCP) MaxCurrent(current int64) error { + if current < 6 { + return fmt.Errorf("invalid current %d", current) + } + + b, err := wb.client.WriteSingleRegister(phEMCPRegMaxCurrent, uint16(current)) + wb.log.TRACE.Printf("write max current %d %0X: %0 X", phEMCPRegMaxCurrent, current, b) + if err != nil { + wb.handler.Close() + } + + return err +} + +// ChargingTime yields current charge run duration +func (wb *PhoenixEMCP) ChargingTime() (time.Duration, error) { + b, err := wb.client.ReadInputRegisters(phEMCPRegChargeTime, 2) + wb.log.TRACE.Printf("read charge time (%d): %0 X", phEMCPRegChargeTime, b) + if err != nil { + wb.handler.Close() + return 0, err + } + + // 2 words, least significant word first + secs := uint64(b[3])<<16 | uint64(b[2])<<24 | uint64(b[1]) | uint64(b[0])<<8 + return time.Duration(time.Duration(secs) * time.Second), nil +} diff --git a/charger/phoenix-evcc.go b/charger/phoenix-evcc.go new file mode 100644 index 000000000..7a5dbac3a --- /dev/null +++ b/charger/phoenix-evcc.go @@ -0,0 +1,107 @@ +package charger + +import ( + "fmt" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/provider/modbus" + "github.com/andig/evcc/util" + gridx "github.com/grid-x/modbus" + "github.com/volkszaehler/mbmd/meters" +) + +const ( + phEVCCRegStatus = 24000 // Input + phEVCCRegMaxCurrent = 22000 // Holding + phEVCCRegEnable = 20000 // Coil +) + +// PhoenixEVCC is an api.ChargeController implementation for Phoenix EV-CC-AC1-M wallboxes. +// It uses Modbus TCP to communicate with the wallbox at modbus client id 255. +type PhoenixEVCC struct { + log *util.Logger + handler meters.Connection // alias for close method + client gridx.Client +} + +// NewPhoenixEVCCFromConfig creates a Phoenix charger from generic config +func NewPhoenixEVCCFromConfig(log *util.Logger, other map[string]interface{}) api.Charger { + var cc modbus.Connection + util.DecodeOther(log, other, &cc) + + if cc.ID == 0 { + log.FATAL.Fatal("config: missing slave id") + } + + return NewPhoenixEVCC(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID) +} + +// NewPhoenixEVCC creates a Phoenix charger +func NewPhoenixEVCC(uri, device, comset string, baudrate int, id uint8) api.Charger { + log := util.NewLogger("evcc") + conn := modbus.NewConnection(log, uri, device, comset, baudrate, true) + conn.Slave(id) + + wb := &PhoenixEVCC{ + log: log, + client: conn.ModbusClient(), + handler: conn, + } + + return wb +} + +// Status implements the Charger.Status interface +func (wb *PhoenixEVCC) Status() (api.ChargeStatus, error) { + b, err := wb.client.ReadInputRegisters(phEVCCRegStatus, 1) + wb.log.TRACE.Printf("read status (%d): %0 X", phEVCCRegStatus, b) + if err != nil { + wb.handler.Close() + return api.StatusNone, err + } + + return api.ChargeStatus(string(b[1])), nil +} + +// Enabled implements the Charger.Enabled interface +func (wb *PhoenixEVCC) Enabled() (bool, error) { + b, err := wb.client.ReadCoils(phEVCCRegEnable, 1) + wb.log.TRACE.Printf("read charge enable (%d): %0 X", phEVCCRegEnable, b) + if err != nil { + wb.handler.Close() + return false, err + } + + return b[0] == 1, nil +} + +// Enable implements the Charger.Enable interface +func (wb *PhoenixEVCC) Enable(enable bool) error { + var u uint16 + if enable { + u = 0xFF00 + } + + b, err := wb.client.WriteSingleCoil(phEVCCRegEnable, u) + wb.log.TRACE.Printf("write charge enable %d %0X: %0 X", phEVCCRegEnable, u, b) + if err != nil { + wb.handler.Close() + } + + return err +} + +// MaxCurrent implements the Charger.MaxCurrent interface +func (wb *PhoenixEVCC) MaxCurrent(current int64) error { + if current < 6 { + return fmt.Errorf("invalid current %d", current) + } + + b, err := wb.client.WriteSingleRegister(phEVCCRegMaxCurrent, uint16(current)) + wb.log.TRACE.Printf("write max current %d %0X: %0 X", phEVCCRegMaxCurrent, current, b) + if err != nil { + wb.handler.Close() + } + + return err +} diff --git a/charger/phoenix.go b/charger/phoenix.go deleted file mode 100644 index 0b24467f3..000000000 --- a/charger/phoenix.go +++ /dev/null @@ -1,125 +0,0 @@ -package charger - -import ( - "fmt" - "time" - - "github.com/andig/evcc/api" - "github.com/andig/evcc/util" - "github.com/grid-x/modbus" -) - -const ( - phRegStatus = 100 // Input - phRegChargeTime = 102 // Input - phRegMaxCurrent = 300 // Holding - phRegEnable = 400 // Coil -) - -// Phoenix is an api.ChargeController implementation for Phoenix EM-CP-PP-ETH wallboxes. -// It uses Modbus TCP to communicate with the wallbox at modbus client id 255. -type Phoenix struct { - log *util.Logger - client modbus.Client - handler *modbus.TCPClientHandler -} - -// NewPhoenixFromConfig creates a Phoenix charger from generic config -func NewPhoenixFromConfig(log *util.Logger, other map[string]interface{}) api.Charger { - cc := struct{ URI string }{} - util.DecodeOther(log, other, &cc) - - return NewPhoenix(cc.URI) -} - -// NewPhoenix creates a Phoenix charger -func NewPhoenix(conn string) api.Charger { - log := util.NewLogger("phoe") - if conn == "" { - log.FATAL.Fatal("missing connection") - } - - handler := modbus.NewTCPClientHandler(conn) - client := modbus.NewClient(handler) - - handler.SlaveID = slaveID - handler.Timeout = timeout - handler.ProtocolRecoveryTimeout = protocolTimeout - - wb := &Phoenix{ - log: log, - client: client, - handler: handler, - } - - return wb -} - -// Status implements the Charger.Status interface -func (wb *Phoenix) Status() (api.ChargeStatus, error) { - b, err := wb.client.ReadInputRegisters(phRegStatus, 1) - wb.log.TRACE.Printf("read status (%d): %0 X", phRegStatus, b) - if err != nil { - wb.handler.Close() - return api.StatusNone, err - } - - return api.ChargeStatus(string(b[1])), nil -} - -// Enabled implements the Charger.Enabled interface -func (wb *Phoenix) Enabled() (bool, error) { - b, err := wb.client.ReadCoils(phRegEnable, 1) - wb.log.TRACE.Printf("read charge enable (%d): %0 X", phRegEnable, b) - if err != nil { - wb.handler.Close() - return false, err - } - - return b[0] == 1, nil -} - -// Enable implements the Charger.Enable interface -func (wb *Phoenix) Enable(enable bool) error { - var u uint16 - if enable { - u = 0xFF00 - } - - b, err := wb.client.WriteSingleCoil(phRegEnable, u) - wb.log.TRACE.Printf("write charge enable %d %0X: %0 X", phRegEnable, u, b) - if err != nil { - wb.handler.Close() - } - - return err -} - -// MaxCurrent implements the Charger.MaxCurrent interface -func (wb *Phoenix) MaxCurrent(current int64) error { - if current < 6 { - return fmt.Errorf("invalid current %d", current) - } - - b, err := wb.client.WriteSingleRegister(phRegMaxCurrent, uint16(current)) - wb.log.TRACE.Printf("write max current %d %0X: %0 X", phRegMaxCurrent, current, b) - if err != nil { - wb.handler.Close() - } - - return err -} - -// ChargingTime yields current charge run duration -func (wb *Phoenix) ChargingTime() (time.Duration, error) { - b, err := wb.client.ReadInputRegisters(phRegChargeTime, 2) - wb.log.TRACE.Printf("read charge time (%d): %0 X", phRegChargeTime, b) - if err != nil { - wb.handler.Close() - return 0, err - } - - // 2 words, least significant word first - secs := uint64(b[3])<<16 | uint64(b[2])<<24 | uint64(b[1]) | uint64(b[0])<<8 - return time.Duration(time.Duration(secs) * time.Second), nil -} diff --git a/charger/wallbe.go b/charger/wallbe.go index 7a66edc26..12849bcd8 100644 --- a/charger/wallbe.go +++ b/charger/wallbe.go @@ -11,7 +11,7 @@ import ( ) const ( - slaveID = 255 + wbSlaveID = 255 wbRegStatus = 100 // Input wbRegChargeTime = 102 // Input @@ -60,7 +60,7 @@ func NewWallbe(conn string) *Wallbe { handler := modbus.NewTCPClientHandler(conn) client := modbus.NewClient(handler) - handler.SlaveID = slaveID + handler.SlaveID = wbSlaveID handler.Timeout = timeout handler.ProtocolRecoveryTimeout = protocolTimeout