Merge pull request #322 from andig/feature/wallbe-meters
Refactor Wallbe to support optional readings
This commit is contained in:
commit
1e53598af8
4 changed files with 269 additions and 54 deletions
|
|
@ -17,6 +17,11 @@ const (
|
|||
phEMCPRegEnable = 400 // Coil
|
||||
)
|
||||
|
||||
const (
|
||||
timeout = 1 * time.Second
|
||||
protocolTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
// 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 180.
|
||||
type PhoenixEMCP struct {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/grid-x/modbus"
|
||||
"github.com/andig/evcc/util/modbus"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -18,72 +21,100 @@ const (
|
|||
wbRegEnable = 400 // Coil
|
||||
wbRegMaxCurrent = 528 // Holding
|
||||
|
||||
timeout = 1 * time.Second
|
||||
protocolTimeout = 2 * time.Second
|
||||
wbRegPower = 120 // power reading
|
||||
wbRegEnergy = 128 // energy reading
|
||||
|
||||
encodingSDM = "sdm"
|
||||
)
|
||||
|
||||
var wbRegCurrents = []uint16{114, 116, 118} // current readings
|
||||
|
||||
// Wallbe is an api.ChargeController implementation for Wallbe wallboxes.
|
||||
// It supports both wallbe controllers (post 2019 models) and older ones using the
|
||||
// Phoenix EV-CC-AC1-M3-CBC-RCM-ETH controller.
|
||||
// It uses Modbus TCP to communicate with the wallbox at modbus client id 255.
|
||||
type Wallbe struct {
|
||||
log *util.Logger
|
||||
client modbus.Client
|
||||
handler *modbus.TCPClientHandler
|
||||
factor int64
|
||||
log *util.Logger
|
||||
conn *modbus.Connection
|
||||
factor int64
|
||||
encoding string
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("wallbe", NewWallbeFromConfig)
|
||||
}
|
||||
|
||||
//go:generate go run ../cmd/tools/decorate.go -p charger -f decorateWallbe -b api.Charger -o wallbe_decorators -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
|
||||
|
||||
// NewWallbeFromConfig creates a Wallbe charger from generic config
|
||||
func NewWallbeFromConfig(other map[string]interface{}) (api.Charger, error) {
|
||||
cc := struct {
|
||||
URI string
|
||||
Legacy bool
|
||||
Meter struct {
|
||||
Power, Energy, Currents bool
|
||||
Encoding string
|
||||
}
|
||||
}{
|
||||
URI: "192.168.0.8:502",
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wb := NewWallbe(cc.URI)
|
||||
wb, err := NewWallbe(cc.URI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.Legacy {
|
||||
wb.factor = 1
|
||||
}
|
||||
|
||||
return wb, nil
|
||||
var currentPower func() (float64, error)
|
||||
if cc.Meter.Power {
|
||||
currentPower = wb.currentPower
|
||||
}
|
||||
|
||||
var totalEnergy func() (float64, error)
|
||||
if cc.Meter.Energy {
|
||||
totalEnergy = wb.totalEnergy
|
||||
}
|
||||
|
||||
var currents func() (float64, float64, float64, error)
|
||||
if cc.Meter.Currents {
|
||||
currents = wb.currents
|
||||
}
|
||||
|
||||
// special case for SDM meters
|
||||
if encoding := strings.ToLower(cc.Meter.Encoding); strings.HasPrefix(encoding, encodingSDM) {
|
||||
wb.encoding = encodingSDM
|
||||
}
|
||||
|
||||
return decorateWallbe(wb, currentPower, totalEnergy, currents), nil
|
||||
}
|
||||
|
||||
// NewWallbe creates a Wallbe charger
|
||||
func NewWallbe(conn string) *Wallbe {
|
||||
handler := modbus.NewTCPClientHandler(conn)
|
||||
client := modbus.NewClient(handler)
|
||||
|
||||
handler.SlaveID = wbSlaveID
|
||||
handler.Timeout = timeout
|
||||
handler.ProtocolRecoveryTimeout = protocolTimeout
|
||||
|
||||
wb := &Wallbe{
|
||||
log: util.NewLogger("wallbe"),
|
||||
client: client,
|
||||
handler: handler,
|
||||
factor: 10,
|
||||
func NewWallbe(uri string) (*Wallbe, error) {
|
||||
conn, err := modbus.NewConnection(uri, "", "", 0, false, wbSlaveID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wb
|
||||
wb := &Wallbe{
|
||||
log: util.NewLogger("wallbe"),
|
||||
conn: conn,
|
||||
factor: 10,
|
||||
}
|
||||
|
||||
return wb, nil
|
||||
}
|
||||
|
||||
// Status implements the Charger.Status interface
|
||||
func (wb *Wallbe) Status() (api.ChargeStatus, error) {
|
||||
b, err := wb.client.ReadInputRegisters(wbRegStatus, 1)
|
||||
b, err := wb.conn.ReadInputRegisters(wbRegStatus, 1)
|
||||
wb.log.TRACE.Printf("read status (%d): %0 X", wbRegStatus, b)
|
||||
if err != nil {
|
||||
wb.handler.Close()
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
|
|
@ -92,10 +123,9 @@ func (wb *Wallbe) Status() (api.ChargeStatus, error) {
|
|||
|
||||
// Enabled implements the Charger.Enabled interface
|
||||
func (wb *Wallbe) Enabled() (bool, error) {
|
||||
b, err := wb.client.ReadCoils(wbRegEnable, 1)
|
||||
b, err := wb.conn.ReadCoils(wbRegEnable, 1)
|
||||
wb.log.TRACE.Printf("read charge enable (%d): %0 X", wbRegEnable, b)
|
||||
if err != nil {
|
||||
wb.handler.Close()
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
|
@ -109,11 +139,8 @@ func (wb *Wallbe) Enable(enable bool) error {
|
|||
u = 0xFF00
|
||||
}
|
||||
|
||||
b, err := wb.client.WriteSingleCoil(wbRegEnable, u)
|
||||
wb.log.TRACE.Printf("write charge enable %d %0X: %0 X", wbRegEnable, u, b)
|
||||
if err != nil {
|
||||
wb.handler.Close()
|
||||
}
|
||||
b, err := wb.conn.WriteSingleCoil(wbRegEnable, u)
|
||||
wb.log.TRACE.Printf("write charge enable (%d) %0X: %0 X", wbRegEnable, u, b)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -126,21 +153,17 @@ func (wb *Wallbe) MaxCurrent(current int64) error {
|
|||
|
||||
u := uint16(current * wb.factor)
|
||||
|
||||
b, err := wb.client.WriteSingleRegister(wbRegMaxCurrent, u)
|
||||
wb.log.TRACE.Printf("write max current %d %0X: %0 X", wbRegMaxCurrent, u, b)
|
||||
if err != nil {
|
||||
wb.handler.Close()
|
||||
}
|
||||
b, err := wb.conn.WriteSingleRegister(wbRegMaxCurrent, u)
|
||||
wb.log.TRACE.Printf("write max current (%d) %0X: %0 X", wbRegMaxCurrent, u, b)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ChargingTime yields current charge run duration
|
||||
func (wb *Wallbe) ChargingTime() (time.Duration, error) {
|
||||
b, err := wb.client.ReadInputRegisters(wbRegChargeTime, 2)
|
||||
b, err := wb.conn.ReadInputRegisters(wbRegChargeTime, 2)
|
||||
wb.log.TRACE.Printf("read charge time (%d): %0 X", wbRegChargeTime, b)
|
||||
if err != nil {
|
||||
wb.handler.Close()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
|
|
@ -148,3 +171,50 @@ func (wb *Wallbe) ChargingTime() (time.Duration, error) {
|
|||
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
|
||||
}
|
||||
|
||||
func (wb *Wallbe) decodeReading(b []byte) float64 {
|
||||
v := binary.BigEndian.Uint32(b)
|
||||
|
||||
if wb.encoding == encodingSDM {
|
||||
bits := (v&0xffff)<<16 | (v>>16)&0xffff
|
||||
f := math.Float32frombits(bits)
|
||||
return float64(f)
|
||||
}
|
||||
|
||||
return float64(v)
|
||||
}
|
||||
|
||||
// CurrentPower implements the Meter.CurrentPower interface
|
||||
func (wb *Wallbe) currentPower() (float64, error) {
|
||||
b, err := wb.conn.ReadInputRegisters(wbRegPower, 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return wb.decodeReading(b), err
|
||||
}
|
||||
|
||||
// totalEnergy implements the Meter.TotalEnergy interface
|
||||
func (wb *Wallbe) totalEnergy() (float64, error) {
|
||||
b, err := wb.conn.ReadInputRegisters(wbRegEnergy, 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return wb.decodeReading(b), err
|
||||
}
|
||||
|
||||
// currents implements the Meter.Currents interface
|
||||
func (wb *Wallbe) currents() (float64, float64, float64, error) {
|
||||
var currents []float64
|
||||
for _, regCurrent := range wbRegCurrents {
|
||||
b, err := wb.conn.ReadInputRegisters(regCurrent, 2)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
currents = append(currents, wb.decodeReading(b))
|
||||
}
|
||||
|
||||
return currents[0], currents[1], currents[2], nil
|
||||
}
|
||||
|
|
|
|||
137
charger/wallbe_decorators.go
Normal file
137
charger/wallbe_decorators.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package charger
|
||||
|
||||
// This file has been generated - do not modify
|
||||
|
||||
import (
|
||||
"github.com/andig/evcc/api"
|
||||
)
|
||||
|
||||
func decorateWallbe(base api.Charger, meter func() (float64, error), meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error)) api.Charger {
|
||||
switch {
|
||||
case meter == nil && meterCurrent == nil && meterEnergy == nil:
|
||||
return base
|
||||
|
||||
case meter != nil && meterCurrent == nil && meterEnergy == nil:
|
||||
return &struct{
|
||||
api.Charger
|
||||
api.Meter
|
||||
}{
|
||||
Charger: base,
|
||||
Meter: &decorateWallbeMeterImpl{
|
||||
meter: meter,
|
||||
},
|
||||
}
|
||||
|
||||
case meter == nil && meterCurrent == nil && meterEnergy != nil:
|
||||
return &struct{
|
||||
api.Charger
|
||||
api.MeterEnergy
|
||||
}{
|
||||
Charger: base,
|
||||
MeterEnergy: &decorateWallbeMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
|
||||
case meter != nil && meterCurrent == nil && meterEnergy != nil:
|
||||
return &struct{
|
||||
api.Charger
|
||||
api.Meter
|
||||
api.MeterEnergy
|
||||
}{
|
||||
Charger: base,
|
||||
Meter: &decorateWallbeMeterImpl{
|
||||
meter: meter,
|
||||
},
|
||||
MeterEnergy: &decorateWallbeMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
|
||||
case meter == nil && meterCurrent != nil && meterEnergy == nil:
|
||||
return &struct{
|
||||
api.Charger
|
||||
api.MeterCurrent
|
||||
}{
|
||||
Charger: base,
|
||||
MeterCurrent: &decorateWallbeMeterCurrentImpl{
|
||||
meterCurrent: meterCurrent,
|
||||
},
|
||||
}
|
||||
|
||||
case meter != nil && meterCurrent != nil && meterEnergy == nil:
|
||||
return &struct{
|
||||
api.Charger
|
||||
api.Meter
|
||||
api.MeterCurrent
|
||||
}{
|
||||
Charger: base,
|
||||
Meter: &decorateWallbeMeterImpl{
|
||||
meter: meter,
|
||||
},
|
||||
MeterCurrent: &decorateWallbeMeterCurrentImpl{
|
||||
meterCurrent: meterCurrent,
|
||||
},
|
||||
}
|
||||
|
||||
case meter == nil && meterCurrent != nil && meterEnergy != nil:
|
||||
return &struct{
|
||||
api.Charger
|
||||
api.MeterCurrent
|
||||
api.MeterEnergy
|
||||
}{
|
||||
Charger: base,
|
||||
MeterCurrent: &decorateWallbeMeterCurrentImpl{
|
||||
meterCurrent: meterCurrent,
|
||||
},
|
||||
MeterEnergy: &decorateWallbeMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
|
||||
case meter != nil && meterCurrent != nil && meterEnergy != nil:
|
||||
return &struct{
|
||||
api.Charger
|
||||
api.Meter
|
||||
api.MeterCurrent
|
||||
api.MeterEnergy
|
||||
}{
|
||||
Charger: base,
|
||||
Meter: &decorateWallbeMeterImpl{
|
||||
meter: meter,
|
||||
},
|
||||
MeterCurrent: &decorateWallbeMeterCurrentImpl{
|
||||
meterCurrent: meterCurrent,
|
||||
},
|
||||
MeterEnergy: &decorateWallbeMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type decorateWallbeMeterImpl struct {
|
||||
meter func() (float64, error)
|
||||
}
|
||||
|
||||
func (impl *decorateWallbeMeterImpl) CurrentPower() (float64, error) {
|
||||
return impl.meter()
|
||||
}
|
||||
|
||||
type decorateWallbeMeterCurrentImpl struct {
|
||||
meterCurrent func() (float64, float64, float64, error)
|
||||
}
|
||||
|
||||
func (impl *decorateWallbeMeterCurrentImpl) Currents() (float64, float64, float64, error) {
|
||||
return impl.meterCurrent()
|
||||
}
|
||||
|
||||
type decorateWallbeMeterEnergyImpl struct {
|
||||
meterEnergy func() (float64, error)
|
||||
}
|
||||
|
||||
func (impl *decorateWallbeMeterEnergyImpl) TotalEnergy() (float64, error) {
|
||||
return impl.meterEnergy()
|
||||
}
|
||||
31
core/site.go
31
core/site.go
|
|
@ -189,6 +189,16 @@ func (site *Site) Configuration() SiteConfiguration {
|
|||
return c
|
||||
}
|
||||
|
||||
func logMeter(log *util.Logger, meter interface{}) {
|
||||
_, power := meter.(api.Meter)
|
||||
_, energy := meter.(api.MeterEnergy)
|
||||
_, currents := meter.(api.MeterCurrent)
|
||||
|
||||
log.INFO.Printf(" power %s", presence[power])
|
||||
log.INFO.Printf(" energy %s", presence[energy])
|
||||
log.INFO.Printf(" currents %s", presence[currents])
|
||||
}
|
||||
|
||||
// DumpConfig site configuration
|
||||
func (site *Site) DumpConfig() {
|
||||
site.log.INFO.Println("site config:")
|
||||
|
|
@ -197,14 +207,8 @@ func (site *Site) DumpConfig() {
|
|||
site.log.INFO.Printf(" battery %s", presence[site.batteryMeter != nil])
|
||||
|
||||
if site.gridMeter != nil {
|
||||
_, power := site.gridMeter.(api.Meter)
|
||||
_, energy := site.gridMeter.(api.MeterEnergy)
|
||||
_, currents := site.gridMeter.(api.MeterCurrent)
|
||||
|
||||
site.log.INFO.Println(" grid config:")
|
||||
site.log.INFO.Printf(" power %s", presence[power])
|
||||
site.log.INFO.Printf(" energy %s", presence[energy])
|
||||
site.log.INFO.Printf(" currents %s", presence[currents])
|
||||
site.log.INFO.Println(" grid meter config:")
|
||||
logMeter(site.log, site.gridMeter)
|
||||
}
|
||||
|
||||
for i, lp := range site.loadpoints {
|
||||
|
|
@ -212,17 +216,16 @@ func (site *Site) DumpConfig() {
|
|||
|
||||
lp.log.INFO.Printf(" vehicle %s", presence[lp.vehicle != nil])
|
||||
lp.log.INFO.Printf(" charge %s", presence[lp.hasChargeMeter()])
|
||||
if lp.hasChargeMeter() {
|
||||
lp.log.INFO.Println(" charge meter config:")
|
||||
logMeter(site.log, lp.chargeMeter)
|
||||
}
|
||||
|
||||
charger := lp.handler.(*ChargerHandler).charger
|
||||
_, power := charger.(api.Meter)
|
||||
_, currents := charger.(api.MeterCurrent)
|
||||
_, energy := charger.(api.ChargeRater)
|
||||
_, timer := charger.(api.ChargeTimer)
|
||||
|
||||
lp.log.INFO.Println(" charger config:")
|
||||
lp.log.INFO.Printf(" power %s", presence[power])
|
||||
lp.log.INFO.Printf(" energy %s", presence[energy])
|
||||
lp.log.INFO.Printf(" currents %s", presence[currents])
|
||||
logMeter(lp.log, charger)
|
||||
lp.log.INFO.Printf(" timer %s", presence[timer])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue