Add Phoenix EV-CC-AC1-M charge controller (#76)
This commit is contained in:
parent
9fb634b070
commit
7c62c219af
6 changed files with 245 additions and 131 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
129
charger/phoenix-emcp.go
Normal file
129
charger/phoenix-emcp.go
Normal file
|
|
@ -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
|
||||
}
|
||||
107
charger/phoenix-evcc.go
Normal file
107
charger/phoenix-evcc.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue