Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CFOS PowerBrain #1809

Merged
merged 10 commits into from
Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- simple and clean user interface
- multiple [chargers](#charger):
- Open source: [openWB](https://openwb.de/), [EVSEWifi](https://www.evse-wifi.de) (includes smartWB)
- Other commercial: ABL eMH1, go-eCharger, Heidelberg Energy Control, KEBA/BMW, NRGkick, Wallbe, Mobile Charger Connect, EEBUS (experimental)
- Closed source: ABL eMH1, cFos PowerBrain, go-eCharger, Heidelberg Energy Control, KEBA/BMW, NRGkick, Wallbe, Mobile Charger Connect, EEBUS (experimental)
- Build-your-own: Phoenix (includes ESL Walli), [SimpleEVSE](https://www.evse-wifi.de/produkt-schlagwort/simple-evse-wb/)
- Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link
- multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall, LG ESS HOME
Expand Down Expand Up @@ -187,6 +187,7 @@ Charger is responsible for handling EV state and adjusting charge current.
Available charger implementations are:

- `abl`: ABL eMH1 (requires Modbus adapter; [sponsors only](#sponsorship))
- `cfos`: cFos PowerBrain charger (meters must configured separately, [sponsors only](#sponsorship))
- `easee`: Easee Home charger ([sponsors only](#sponsorship))
- `eebus`: EEBUS compatible chargers (experimental)
- `evsewifi`: chargers with SimpleEVSE controllers using [EVSE-WiFi](https://www.evse-wifi.de/) (includes smartWB)
Expand Down
126 changes: 126 additions & 0 deletions charger/cfos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package charger

import (
"errors"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/modbus"
"github.com/evcc-io/evcc/util/sponsor"
)

const (
cfosRegStatus = 8092 // Input
cfosRegMaxCurrent = 8093 // Holding
cfosRegEnable = 8094 // Coil
)

// CfosPowerBrain is an charger implementation for cFos PowerBrain wallboxes.
// It uses Modbus TCP to communicate at modbus client id 1 and power meters at id 2 and 3.
// https://www.cfos-emobility.de/en-gb/cfos-power-brain/modbus-registers.htm
type CfosPowerBrain struct {
conn *modbus.Connection
}

func init() {
registry.Add("cfos", NewCfosPowerBrainFromConfig)
}

// NewCfosPowerBrainFromConfig creates a cFos charger from generic config
func NewCfosPowerBrainFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
URI string
ID uint8
}{
ID: 1,
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

return NewCfosPowerBrain(cc.URI, cc.ID)
}

// NewCfosPowerBrain creates a cFos charger
func NewCfosPowerBrain(uri string, id uint8) (*CfosPowerBrain, error) {
uri = util.DefaultPort(uri, 4701)

conn, err := modbus.NewConnection(uri, "", "", 0, modbus.TcpFormat, id)
if err != nil {
return nil, err
}

log := util.NewLogger("cfos")
conn.Logger(log.TRACE)

if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}

wb := &CfosPowerBrain{
conn: conn,
}

return wb, nil
}

// Status implements the Charger.Status interface
func (wb *CfosPowerBrain) Status() (api.ChargeStatus, error) {
b, err := wb.conn.ReadHoldingRegisters(cfosRegStatus, 1)
if err != nil {
return api.StatusNone, err
}

switch b[0] {
case 0: // warten
return api.StatusA, nil
case 1: // Fahrzeug erkannt
return api.StatusB, nil
case 2: // laden
return api.StatusC, nil
case 3: // laden mit Kühlung
return api.StatusD, nil
case 4: // kein Strom
return api.StatusE, nil
case 5: // Fehler
return api.StatusF, nil
default:
return api.StatusNone, errors.New("invalid response")
}
}

// Enabled implements the Charger.Enabled interface
func (wb *CfosPowerBrain) Enabled() (bool, error) {
b, err := wb.conn.ReadHoldingRegisters(cfosRegEnable, 1)
if err != nil {
return false, err
}

return b[0] == 1, nil
}

// Enable implements the Charger.Enable interface
func (wb *CfosPowerBrain) Enable(enable bool) error {
var u uint16
if enable {
u = 1
}

_, err := wb.conn.WriteSingleCoil(cfosRegEnable, u)

return err
}

// MaxCurrent implements the Charger.MaxCurrent interface
func (wb *CfosPowerBrain) MaxCurrent(current int64) error {
return wb.MaxCurrentMillis(float64(current))
}

var _ api.ChargerEx = (*CfosPowerBrain)(nil)

// MaxCurrentMillis implements the api.ChargerEx interface
func (wb *CfosPowerBrain) MaxCurrentMillis(current float64) error {
_, err := wb.conn.WriteSingleRegister(cfosRegMaxCurrent, uint16(current*10))
return err
}
97 changes: 97 additions & 0 deletions meter/cfos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package meter

import (
"encoding/binary"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/modbus"
)

const (
cfosRegEnergy = 8058 // energy reading
cfosRegPower = 8062 // power reading
)

var cfosRegCurrents = []uint16{8064, 8066, 8068} // current readings

// CfosPowerBrain is a meter implementation for cFos PowerBrain wallboxes.
// It uses Modbus TCP to communicate at modbus client id 1 and power meters at id 2 and 3.
// https://www.cfos-emobility.de/en-gb/cfos-power-brain/modbus-registers.htm
type CfosPowerBrain struct {
conn *modbus.Connection
}

func init() {
registry.Add("cfos", NewCfosPowerBrainFromConfig)
}

// NewCfosPowerBrainFromConfig creates a cFos meter from generic config
func NewCfosPowerBrainFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
URI string
ID uint8
}{}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

return NewCfosPowerBrain(cc.URI, cc.ID)
}

// NewCfosPowerBrain creates a cFos meter
func NewCfosPowerBrain(uri string, id uint8) (*CfosPowerBrain, error) {
conn, err := modbus.NewConnection(uri, "", "", 0, modbus.TcpFormat, id)
if err != nil {
return nil, err
}

log := util.NewLogger("cfos")
conn.Logger(log.TRACE)

wb := &CfosPowerBrain{
conn: conn,
}

return wb, nil
}

// CurrentPower implements the api.Meter interface
func (wb *CfosPowerBrain) CurrentPower() (float64, error) {
b, err := wb.conn.ReadHoldingRegisters(cfosRegPower, 2)
if err != nil {
return 0, err
}

return float64(binary.BigEndian.Uint32(b)) / 10, err
}

var _ api.MeterEnergy = (*CfosPowerBrain)(nil)

// TotalEnergy implements the api.MeterEnergy interface
func (wb *CfosPowerBrain) TotalEnergy() (float64, error) {
b, err := wb.conn.ReadHoldingRegisters(cfosRegEnergy, 4)
if err != nil {
return 0, err
}

return float64(binary.BigEndian.Uint64(b)) / 1e3, err
}

var _ api.MeterCurrent = (*CfosPowerBrain)(nil)

// Currents implements the api.MeterCurrent interface
func (wb *CfosPowerBrain) Currents() (float64, float64, float64, error) {
var currents []float64
for _, regCurrent := range cfosRegCurrents {
b, err := wb.conn.ReadHoldingRegisters(regCurrent, 2)
if err != nil {
return 0, 0, 0, err
}

currents = append(currents, float64(binary.BigEndian.Uint32(b))/10)
}

return currents[0], currents[1], currents[2], nil
}