Add Phoenix EV-CC-AC1-M charge controller (#76)

This commit is contained in:
andig 2020-04-29 21:45:25 +02:00 • committed by GitHub
parent 9fb634b070
commit 7c62c219af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 245 additions and 131 deletions

View file

@ -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

View file

@ -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
View 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
View 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
}

View file

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

View file

@ -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