Merge pull request #322 from andig/feature/wallbe-meters

Refactor Wallbe to support optional readings
This commit is contained in:
andig 2020-08-31 18:40:20 +02:00 • committed by GitHub
commit 1e53598af8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 269 additions and 54 deletions

View file

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

View file

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

View 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()
}

View file

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