From e0270ab013763b40a53650b2632a704e281ffa3c Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:48:22 +0200 Subject: [PATCH 01/16] Add peblar charger --- charger/peblar.go | 306 +++++++++++++++++++++++ charger/peblar_decorators.go | 35 +++ templates/definition/charger/peblar.yaml | 16 ++ 3 files changed, 357 insertions(+) create mode 100644 charger/peblar.go create mode 100644 charger/peblar_decorators.go create mode 100644 templates/definition/charger/peblar.yaml diff --git a/charger/peblar.go b/charger/peblar.go new file mode 100644 index 0000000000..d2a5f7548f --- /dev/null +++ b/charger/peblar.go @@ -0,0 +1,306 @@ +package charger + +// LICENSE + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Details on the Peblar modbus server obtained from: https://developer.peblar.com/modbus-api + +import ( + "encoding/binary" + "fmt" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" + "github.com/volkszaehler/mbmd/encoding" +) + +// Peblar charger implementation +type Peblar struct { + log *util.Logger + conn *modbus.Connection + curr uint32 + enabled bool + phases uint16 +} + +const ( + // Meter addresses + peblarEnergyTotalAddress = 30000 + peblarSessionEnergyAddress = 30004 + peblarPowerPhase1Address = 30008 + peblarPowerPhase2Address = 30010 + peblarPowerPhase3Address = 30012 + peblarPowerTotalAddress = 30014 + peblarVoltagePhase1Address = 30016 + peblarVoltagePhase2Address = 30018 + peblarVoltagePhase3Address = 30020 + peblarCurrentPhase1Address = 30022 + peblarCurrentPhase2Address = 30024 + peblarCurrentPhase3Address = 30026 + + // Config addresses + peblarSerialNumberAddress = 30050 + peblarProductNumberAddress = 30062 + peblarFwIdentifierAddress = 30074 + peblarPhaseCountAddress = 30092 + peblarIndepRelayAddress = 30093 + + // Control addresses + peblarCurrentLimitSourceAddress = 30112 + peblarCurrentLimitActualAddress = 30113 + peblarModbusCurrentLimitAddress = 40000 + peblarForce1PhaseAddress = 40002 + + // Diagnostic addresses + peblarCpStateAddress = 30110 +) + +func init() { + registry.Add("peblar", NewPeblarFromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decoratePeblar -b *Peblar -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" + +// NewPeblarFromConfig creates a Peblar charger from generic config +func NewPeblarFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := modbus.TcpSettings{ + ID: 255, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewPeblar(cc.URI, cc.ID) +} + +// NewPeblar creates Peblar charger +func NewPeblar(uri string, id uint8) (api.Charger, error) { + conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id) + if err != nil { + return nil, err + } + + log := util.NewLogger("peblar") + conn.Logger(log.TRACE) + + // Register contains the physically connected phases + b, err := conn.ReadInputRegisters(peblarPhaseCountAddress, 1) + if err != nil { + return nil, err + } + log.DEBUG.Println("detected connected phases:", binary.BigEndian.Uint16(b)) + + wb := &Peblar{ + log: log, + conn: conn, + curr: 6000, // assume min current + phases: binary.BigEndian.Uint16(b), + } + + c, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) + if err != nil { + log.DEBUG.Println("failed to read independent relays register") + return nil, err + } + var indepRelays uint16 = binary.BigEndian.Uint16(c) + + var phasesS func(int) error + + if indepRelays == 0 { + log.DEBUG.Println("detected 1x4 or 1x2-pole relay") + } else { + log.DEBUG.Println("detected 2x2-pole relay") + phasesS = wb.phases1p3p + } + + return decoratePeblar(wb, phasesS), err +} + +// Status implements the api.Charger interface +func (wb *Peblar) Status() (api.ChargeStatus, error) { + b, err := wb.conn.ReadInputRegisters(peblarCpStateAddress, 1) + if err != nil { + return api.StatusNone, err + } + + switch s := encoding.Uint16(b); s { + case 'A': + return api.StatusA, nil + case 'B': + return api.StatusB, nil + case 'C': + return api.StatusC, nil + case 'D': + return api.StatusD, nil + case 'E': + return api.StatusE, nil + case 'F': + return api.StatusF, nil + default: + return api.StatusNone, fmt.Errorf("invalid status: %d", s) + } +} + +// Enabled implements the api.Charger interface +func (wb *Peblar) Enabled() (bool, error) { + return verifyEnabled(wb, wb.enabled) +} + +// Enable implements the api.Charger interface +func (wb *Peblar) Enable(enable bool) error { + var current uint32 + if enable { + current = wb.curr + } + + return wb.setCurrent(current) +} + +// setCurrent writes the current limit in mA +func (wb *Peblar) setCurrent(current uint32) error { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, current) + + _, err := wb.conn.WriteMultipleRegisters(peblarModbusCurrentLimitAddress, 2, b) + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *Peblar) MaxCurrent(current int64) error { + return wb.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*Peblar)(nil) + +// MaxCurrent implements the api.ChargerEx interface +func (wb *Peblar) MaxCurrentMillis(current float64) error { + if current < 6 { + return fmt.Errorf("invalid current %.1f", current) + } + + wb.curr = uint32(current * 1e3) + + return wb.setCurrent(wb.curr) +} + +var _ api.Meter = (*Peblar)(nil) + +// CurrentPower implements the api.Meter interface +func (wb *Peblar) CurrentPower() (float64, error) { + b, err := wb.conn.ReadInputRegisters(peblarPowerTotalAddress, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)), err +} + +var _ api.ChargeRater = (*Peblar)(nil) + +// ChargedEnergy implements the api.MeterEnergy interface +func (wb *Peblar) ChargedEnergy() (float64, error) { + b, err := wb.conn.ReadInputRegisters(peblarSessionEnergyAddress, 4) + if err != nil { + return 0, err + } + + return float64(encoding.Int64(b)) / 1e3, err +} + +// TotalEnergy implements the api.MeterEnergy interface +func (wb *Peblar) TotalEnergy() (float64, error) { + b, err := wb.conn.ReadInputRegisters(peblarEnergyTotalAddress, 4) + if err != nil { + return 0, err + } + + return float64(encoding.Int64(b)) / 1e3, nil +} + +// getPhaseValues returns 1..3 sequential register values +func (wb *Peblar) getPhaseValues(reg uint16, divider float64) (float64, float64, float64, error) { + b, err := wb.conn.ReadInputRegisters(reg, wb.phases * 2) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := 0; i < int(wb.phases); i++ { + res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / divider + } + + return res[0], res[1], res[2], nil +} + +var _ api.PhaseCurrents = (*Peblar)(nil) + +// Currents implements the api.PhaseCurrents interface +func (wb *Peblar) Currents() (float64, float64, float64, error) { + return wb.getPhaseValues(peblarCurrentPhase1Address, 1e3) +} + +var _ api.PhaseVoltages = (*Peblar)(nil) + +// Voltages implements the api.PhaseVoltages interface +func (wb *Peblar) Voltages() (float64, float64, float64, error) { + return wb.getPhaseValues(peblarVoltagePhase1Address, 1) +} + +// phases1p3p implements the api.PhaseSwitcher interface via the decorator +func (wb *Peblar) phases1p3p(phases int) error { + var b uint16 = 0 + + wb.log.DEBUG.Println("attempt to change phases to:", phases) + + if phases == 1 { + b = 1 + } + + _, err := wb.conn.WriteSingleRegister(peblarForce1PhaseAddress, b) + return err +} + +var _ api.PhaseGetter = (*Peblar)(nil) + +// GetPhases implements the api.PhaseGetter interface +func (wb *Peblar) GetPhases() (int, error) { + c, err := wb.conn.ReadHoldingRegisters(peblarForce1PhaseAddress, 1) + if err != nil { + return 0, err + } + + var force1p uint16 = binary.BigEndian.Uint16(c) + wb.log.DEBUG.Println("forced to 1 phase:", force1p) + var phases uint16 = wb.phases + wb.log.DEBUG.Println("connected phases:", phases) + + if wb.phases > 1 && force1p == 1 { + phases = 1 + } + + return int(phases), nil +} + +var _ api.Diagnosis = (*Peblar)(nil) + +// Diagnose implements the api.Diagnosis interface +func (wb *Peblar) Diagnose() { + if b, err := wb.conn.ReadInputRegisters(peblarSerialNumberAddress, 12); err == nil { + fmt.Printf("\tSN:\t%s\n", b) + } + if b, err := wb.conn.ReadInputRegisters(peblarProductNumberAddress, 12); err == nil { + fmt.Printf("\tPN:\t%s\n", b) + } + if b, err := wb.conn.ReadInputRegisters(peblarFwIdentifierAddress, 12); err == nil { + fmt.Printf("\tFirmware:\t%s\n", b) + } +} diff --git a/charger/peblar_decorators.go b/charger/peblar_decorators.go new file mode 100644 index 0000000000..c5fb746afa --- /dev/null +++ b/charger/peblar_decorators.go @@ -0,0 +1,35 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decoratePeblar(base *Peblar, phaseSwitcher func(int) error) api.Charger { + switch { + case phaseSwitcher == nil: + return base + + case phaseSwitcher != nil: + return &struct { + *Peblar + api.PhaseSwitcher + }{ + Peblar: base, + PhaseSwitcher: &decoratePeblarPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + } + + return nil +} + +type decoratePeblarPhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decoratePeblarPhaseSwitcherImpl) Phases1p3p(p0 int) error { + return impl.phaseSwitcher(p0) +} diff --git a/templates/definition/charger/peblar.yaml b/templates/definition/charger/peblar.yaml new file mode 100644 index 0000000000..5f0d6af8fd --- /dev/null +++ b/templates/definition/charger/peblar.yaml @@ -0,0 +1,16 @@ +template: peblar +products: + - brand: Peblar + description: + generic: Home, Home Plus, Business +capabilities: ["1p3p", "mA"] +requirements: + description: + de: Peblar-Ladegeräte mit Firmware-Version 1.6 und höher unterstützen Modbus TCP über Port 502. Der Modbus-Server muss über die Weboberfläche des Ladegeräts aktiviert werden. Stellen Sie sicher, dass Smart-Charging-Strategien deaktiviert und auf Standard gesetzt sind. + en: Peblar chargers with firmware version 1.6 and onwards support Modbus TCP via port 502. The Modbus server needs to be enabled via the charger web interface. Ensure that smart charging strategies are disabled and set to Default. +params: + - name: modbus + choice: ["tcpip"] +render: | + type: peblar + {{- include "modbus" . }} From 78a4f376369ea84c486b3e4abbf83981c4ea797a Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:14:14 +0200 Subject: [PATCH 02/16] Run lint on peblar.go --- charger/peblar.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index d2a5f7548f..6db11812de 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -26,19 +26,19 @@ import ( type Peblar struct { log *util.Logger conn *modbus.Connection - curr uint32 + curr uint32 enabled bool phases uint16 } const ( // Meter addresses - peblarEnergyTotalAddress = 30000 + peblarEnergyTotalAddress = 30000 peblarSessionEnergyAddress = 30004 - peblarPowerPhase1Address = 30008 - peblarPowerPhase2Address = 30010 - peblarPowerPhase3Address = 30012 - peblarPowerTotalAddress = 30014 + peblarPowerPhase1Address = 30008 + peblarPowerPhase2Address = 30010 + peblarPowerPhase3Address = 30012 + peblarPowerTotalAddress = 30014 peblarVoltagePhase1Address = 30016 peblarVoltagePhase2Address = 30018 peblarVoltagePhase3Address = 30020 @@ -47,17 +47,17 @@ const ( peblarCurrentPhase3Address = 30026 // Config addresses - peblarSerialNumberAddress = 30050 + peblarSerialNumberAddress = 30050 peblarProductNumberAddress = 30062 - peblarFwIdentifierAddress = 30074 - peblarPhaseCountAddress = 30092 - peblarIndepRelayAddress = 30093 + peblarFwIdentifierAddress = 30074 + peblarPhaseCountAddress = 30092 + peblarIndepRelayAddress = 30093 // Control addresses peblarCurrentLimitSourceAddress = 30112 peblarCurrentLimitActualAddress = 30113 peblarModbusCurrentLimitAddress = 40000 - peblarForce1PhaseAddress = 40002 + peblarForce1PhaseAddress = 40002 // Diagnostic addresses peblarCpStateAddress = 30110 @@ -72,11 +72,11 @@ func init() { // NewPeblarFromConfig creates a Peblar charger from generic config func NewPeblarFromConfig(other map[string]interface{}) (api.Charger, error) { cc := modbus.TcpSettings{ - ID: 255, + ID: 255, } if err := util.DecodeOther(other, &cc); err != nil { - return nil, err + return nil, err } return NewPeblar(cc.URI, cc.ID) @@ -86,7 +86,7 @@ func NewPeblarFromConfig(other map[string]interface{}) (api.Charger, error) { func NewPeblar(uri string, id uint8) (api.Charger, error) { conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id) if err != nil { - return nil, err + return nil, err } log := util.NewLogger("peblar") @@ -100,10 +100,10 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { log.DEBUG.Println("detected connected phases:", binary.BigEndian.Uint16(b)) wb := &Peblar{ - log: log, - conn: conn, - curr: 6000, // assume min current - phases: binary.BigEndian.Uint16(b), + log: log, + conn: conn, + curr: 6000, // assume min current + phases: binary.BigEndian.Uint16(b), } c, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) @@ -228,7 +228,7 @@ func (wb *Peblar) TotalEnergy() (float64, error) { // getPhaseValues returns 1..3 sequential register values func (wb *Peblar) getPhaseValues(reg uint16, divider float64) (float64, float64, float64, error) { - b, err := wb.conn.ReadInputRegisters(reg, wb.phases * 2) + b, err := wb.conn.ReadInputRegisters(reg, wb.phases*2) if err != nil { return 0, 0, 0, err } From e0f62f65fee3bd55802204b8ad9b6722bcc7d9e1 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 2 Oct 2024 12:48:11 +0200 Subject: [PATCH 03/16] Simplify --- charger/peblar.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index 6db11812de..fe07f331b1 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -97,21 +97,19 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { if err != nil { return nil, err } - log.DEBUG.Println("detected connected phases:", binary.BigEndian.Uint16(b)) wb := &Peblar{ log: log, conn: conn, - curr: 6000, // assume min current - phases: binary.BigEndian.Uint16(b), + curr: 6000, // assume min current + phases: binary.BigEndian.Uint16(b), // TODO why do we need this? } c, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) if err != nil { - log.DEBUG.Println("failed to read independent relays register") return nil, err } - var indepRelays uint16 = binary.BigEndian.Uint16(c) + indepRelays := binary.BigEndian.Uint16(c) var phasesS func(int) error @@ -187,9 +185,12 @@ func (wb *Peblar) MaxCurrentMillis(current float64) error { return fmt.Errorf("invalid current %.1f", current) } - wb.curr = uint32(current * 1e3) + err := wb.setCurrent(wb.curr) + if err == nil { + wb.curr = uint32(current * 1e3) + } - return wb.setCurrent(wb.curr) + return err } var _ api.Meter = (*Peblar)(nil) @@ -257,10 +258,7 @@ func (wb *Peblar) Voltages() (float64, float64, float64, error) { // phases1p3p implements the api.PhaseSwitcher interface via the decorator func (wb *Peblar) phases1p3p(phases int) error { - var b uint16 = 0 - - wb.log.DEBUG.Println("attempt to change phases to:", phases) - + var b uint16 if phases == 1 { b = 1 } From 026c33cc5f090f40f8fe11f27624fd26cad18095 Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:24:06 +0200 Subject: [PATCH 04/16] Run lint-ui too --- templates/definition/charger/peblar.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/charger/peblar.yaml b/templates/definition/charger/peblar.yaml index 5f0d6af8fd..e901e685df 100644 --- a/templates/definition/charger/peblar.yaml +++ b/templates/definition/charger/peblar.yaml @@ -6,7 +6,7 @@ products: capabilities: ["1p3p", "mA"] requirements: description: - de: Peblar-Ladegeräte mit Firmware-Version 1.6 und höher unterstützen Modbus TCP über Port 502. Der Modbus-Server muss über die Weboberfläche des Ladegeräts aktiviert werden. Stellen Sie sicher, dass Smart-Charging-Strategien deaktiviert und auf Standard gesetzt sind. + de: Peblar-Ladegeräte mit Firmware-Version 1.6 und höher unterstützen Modbus TCP über Port 502. Der Modbus-Server muss über die Weboberfläche des Ladegeräts aktiviert werden. Stellen Sie sicher, dass Smart-Charging-Strategien deaktiviert und auf Standard gesetzt sind. en: Peblar chargers with firmware version 1.6 and onwards support Modbus TCP via port 502. The Modbus server needs to be enabled via the charger web interface. Ensure that smart charging strategies are disabled and set to Default. params: - name: modbus From 5f96354938f0396a97758de0611361e15d39a7ad Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:50:43 +0200 Subject: [PATCH 05/16] decorate peblar getPhases --- charger/peblar.go | 46 +++++++++++++----------------------- charger/peblar_decorators.go | 40 ++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index fe07f331b1..89d5237c21 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -67,7 +67,7 @@ func init() { registry.Add("peblar", NewPeblarFromConfig) } -//go:generate go run ../cmd/tools/decorate.go -f decoratePeblar -b *Peblar -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" +//go:generate go run ../cmd/tools/decorate.go -f decoratePeblar -b *Peblar -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)" // NewPeblarFromConfig creates a Peblar charger from generic config func NewPeblarFromConfig(other map[string]interface{}) (api.Charger, error) { @@ -92,35 +92,27 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { log := util.NewLogger("peblar") conn.Logger(log.TRACE) - // Register contains the physically connected phases - b, err := conn.ReadInputRegisters(peblarPhaseCountAddress, 1) - if err != nil { - return nil, err - } - wb := &Peblar{ - log: log, - conn: conn, - curr: 6000, // assume min current - phases: binary.BigEndian.Uint16(b), // TODO why do we need this? + log: log, + conn: conn, + curr: 6000, // assume min current } - c, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) + b, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) if err != nil { return nil, err } - indepRelays := binary.BigEndian.Uint16(c) + indepRelays := binary.BigEndian.Uint16(b) var phasesS func(int) error + var phasesG func() (int, error) - if indepRelays == 0 { - log.DEBUG.Println("detected 1x4 or 1x2-pole relay") - } else { - log.DEBUG.Println("detected 2x2-pole relay") + if indepRelays == 1 { phasesS = wb.phases1p3p + phasesG = wb.getPhases } - return decoratePeblar(wb, phasesS), err + return decoratePeblar(wb, phasesS, phasesG), err } // Status implements the api.Charger interface @@ -267,25 +259,19 @@ func (wb *Peblar) phases1p3p(phases int) error { return err } -var _ api.PhaseGetter = (*Peblar)(nil) - -// GetPhases implements the api.PhaseGetter interface -func (wb *Peblar) GetPhases() (int, error) { - c, err := wb.conn.ReadHoldingRegisters(peblarForce1PhaseAddress, 1) +// getPhases implements the api.PhaseGetter interface +func (wb *Peblar) getPhases() (int, error) { + b, err := wb.conn.ReadHoldingRegisters(peblarForce1PhaseAddress, 1) if err != nil { return 0, err } - var force1p uint16 = binary.BigEndian.Uint16(c) - wb.log.DEBUG.Println("forced to 1 phase:", force1p) - var phases uint16 = wb.phases - wb.log.DEBUG.Println("connected phases:", phases) - - if wb.phases > 1 && force1p == 1 { + phases := 3 + if binary.BigEndian.Uint16(b) == 1 { phases = 1 } - return int(phases), nil + return phases, nil } var _ api.Diagnosis = (*Peblar)(nil) diff --git a/charger/peblar_decorators.go b/charger/peblar_decorators.go index c5fb746afa..f8ad28484e 100644 --- a/charger/peblar_decorators.go +++ b/charger/peblar_decorators.go @@ -6,12 +6,12 @@ import ( "github.com/evcc-io/evcc/api" ) -func decoratePeblar(base *Peblar, phaseSwitcher func(int) error) api.Charger { +func decoratePeblar(base *Peblar, phaseSwitcher func(int) error, phaseGetter func() (int, error)) api.Charger { switch { - case phaseSwitcher == nil: + case phaseGetter == nil && phaseSwitcher == nil: return base - case phaseSwitcher != nil: + case phaseGetter == nil && phaseSwitcher != nil: return &struct { *Peblar api.PhaseSwitcher @@ -21,11 +21,45 @@ func decoratePeblar(base *Peblar, phaseSwitcher func(int) error) api.Charger { phaseSwitcher: phaseSwitcher, }, } + + case phaseGetter != nil && phaseSwitcher == nil: + return &struct { + *Peblar + api.PhaseGetter + }{ + Peblar: base, + PhaseGetter: &decoratePeblarPhaseGetterImpl{ + phaseGetter: phaseGetter, + }, + } + + case phaseGetter != nil && phaseSwitcher != nil: + return &struct { + *Peblar + api.PhaseGetter + api.PhaseSwitcher + }{ + Peblar: base, + PhaseGetter: &decoratePeblarPhaseGetterImpl{ + phaseGetter: phaseGetter, + }, + PhaseSwitcher: &decoratePeblarPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } } return nil } +type decoratePeblarPhaseGetterImpl struct { + phaseGetter func() (int, error) +} + +func (impl *decoratePeblarPhaseGetterImpl) GetPhases() (int, error) { + return impl.phaseGetter() +} + type decoratePeblarPhaseSwitcherImpl struct { phaseSwitcher func(int) error } From 7605784aefdd2f765f6e55ae7f0064dcc60de155 Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:38:51 +0200 Subject: [PATCH 06/16] restore reading connected phases for current/voltage functions --- charger/peblar.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index 89d5237c21..c9d10e4ac7 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -92,17 +92,24 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { log := util.NewLogger("peblar") conn.Logger(log.TRACE) + // Register contains the physically connected phases + b, err := conn.ReadInputRegisters(peblarPhaseCountAddress, 1) + if err != nil { + return nil, err + } + wb := &Peblar{ - log: log, - conn: conn, - curr: 6000, // assume min current + log: log, + conn: conn, + curr: 6000, // assume min current + phases: binary.BigEndian.Uint16(b), // required for retrieving the right amount of voltage/current registers } - b, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) + c, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) if err != nil { return nil, err } - indepRelays := binary.BigEndian.Uint16(b) + indepRelays := binary.BigEndian.Uint16(c) var phasesS func(int) error var phasesG func() (int, error) From b51dd7d2da6cf19cc1d6967815e1b1492cb5d190 Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:52:47 +0200 Subject: [PATCH 07/16] drop logging --- charger/peblar.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index c9d10e4ac7..bbea0aa087 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -24,7 +24,6 @@ import ( // Peblar charger implementation type Peblar struct { - log *util.Logger conn *modbus.Connection curr uint32 enabled bool @@ -89,9 +88,6 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { return nil, err } - log := util.NewLogger("peblar") - conn.Logger(log.TRACE) - // Register contains the physically connected phases b, err := conn.ReadInputRegisters(peblarPhaseCountAddress, 1) if err != nil { @@ -99,7 +95,6 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { } wb := &Peblar{ - log: log, conn: conn, curr: 6000, // assume min current phases: binary.BigEndian.Uint16(b), // required for retrieving the right amount of voltage/current registers From 3472c80b8c6b5c9342649f4463d08b6c25543e0b Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 3 Oct 2024 12:17:11 +0200 Subject: [PATCH 08/16] Align --- charger/peblar.go | 71 +++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index bbea0aa087..f09f2f8f02 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -32,34 +32,30 @@ type Peblar struct { const ( // Meter addresses - peblarEnergyTotalAddress = 30000 - peblarSessionEnergyAddress = 30004 - peblarPowerPhase1Address = 30008 - peblarPowerPhase2Address = 30010 - peblarPowerPhase3Address = 30012 - peblarPowerTotalAddress = 30014 - peblarVoltagePhase1Address = 30016 - peblarVoltagePhase2Address = 30018 - peblarVoltagePhase3Address = 30020 - peblarCurrentPhase1Address = 30022 - peblarCurrentPhase2Address = 30024 - peblarCurrentPhase3Address = 30026 + peblarRegEnergyTotal = 30000 + peblarRegSessionEnergy = 30004 + peblarRegPowerPhase1 = 30008 + peblarRegPowerPhase2 = 30010 + peblarRegPowerPhase3 = 30012 + peblarRegPowerTotal = 30014 + peblarRegVoltages = 30016 + peblarRegCurrents = 30022 // Config addresses - peblarSerialNumberAddress = 30050 - peblarProductNumberAddress = 30062 - peblarFwIdentifierAddress = 30074 - peblarPhaseCountAddress = 30092 - peblarIndepRelayAddress = 30093 + peblarRegSerialNumber = 30050 + peblarRegProductNumber = 30062 + peblarRegFwIdentifier = 30074 + peblarRegPhaseCount = 30092 + peblarRegIndepRelay = 30093 // Control addresses - peblarCurrentLimitSourceAddress = 30112 - peblarCurrentLimitActualAddress = 30113 - peblarModbusCurrentLimitAddress = 40000 - peblarForce1PhaseAddress = 40002 + peblarRegCurrentLimitSource = 30112 + peblarRegCurrentLimitActual = 30113 + peblarRegModbusCurrentLimit = 40000 + peblarRegForce1Phase = 40002 // Diagnostic addresses - peblarCpStateAddress = 30110 + peblarRegCpState = 30110 ) func init() { @@ -88,8 +84,11 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { return nil, err } + log := util.NewLogger("peblar") + conn.Logger(log.TRACE) + // Register contains the physically connected phases - b, err := conn.ReadInputRegisters(peblarPhaseCountAddress, 1) + b, err := conn.ReadInputRegisters(peblarRegPhaseCount, 1) if err != nil { return nil, err } @@ -100,7 +99,7 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { phases: binary.BigEndian.Uint16(b), // required for retrieving the right amount of voltage/current registers } - c, err := conn.ReadInputRegisters(peblarIndepRelayAddress, 1) + c, err := conn.ReadInputRegisters(peblarRegIndepRelay, 1) if err != nil { return nil, err } @@ -119,7 +118,7 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { // Status implements the api.Charger interface func (wb *Peblar) Status() (api.ChargeStatus, error) { - b, err := wb.conn.ReadInputRegisters(peblarCpStateAddress, 1) + b, err := wb.conn.ReadInputRegisters(peblarRegCpState, 1) if err != nil { return api.StatusNone, err } @@ -162,7 +161,7 @@ func (wb *Peblar) setCurrent(current uint32) error { b := make([]byte, 4) binary.BigEndian.PutUint32(b, current) - _, err := wb.conn.WriteMultipleRegisters(peblarModbusCurrentLimitAddress, 2, b) + _, err := wb.conn.WriteMultipleRegisters(peblarRegModbusCurrentLimit, 2, b) return err } @@ -191,7 +190,7 @@ var _ api.Meter = (*Peblar)(nil) // CurrentPower implements the api.Meter interface func (wb *Peblar) CurrentPower() (float64, error) { - b, err := wb.conn.ReadInputRegisters(peblarPowerTotalAddress, 2) + b, err := wb.conn.ReadInputRegisters(peblarRegPowerTotal, 2) if err != nil { return 0, err } @@ -203,7 +202,7 @@ var _ api.ChargeRater = (*Peblar)(nil) // ChargedEnergy implements the api.MeterEnergy interface func (wb *Peblar) ChargedEnergy() (float64, error) { - b, err := wb.conn.ReadInputRegisters(peblarSessionEnergyAddress, 4) + b, err := wb.conn.ReadInputRegisters(peblarRegSessionEnergy, 4) if err != nil { return 0, err } @@ -213,7 +212,7 @@ func (wb *Peblar) ChargedEnergy() (float64, error) { // TotalEnergy implements the api.MeterEnergy interface func (wb *Peblar) TotalEnergy() (float64, error) { - b, err := wb.conn.ReadInputRegisters(peblarEnergyTotalAddress, 4) + b, err := wb.conn.ReadInputRegisters(peblarRegEnergyTotal, 4) if err != nil { return 0, err } @@ -240,14 +239,14 @@ var _ api.PhaseCurrents = (*Peblar)(nil) // Currents implements the api.PhaseCurrents interface func (wb *Peblar) Currents() (float64, float64, float64, error) { - return wb.getPhaseValues(peblarCurrentPhase1Address, 1e3) + return wb.getPhaseValues(peblarRegCurrents, 1e3) } var _ api.PhaseVoltages = (*Peblar)(nil) // Voltages implements the api.PhaseVoltages interface func (wb *Peblar) Voltages() (float64, float64, float64, error) { - return wb.getPhaseValues(peblarVoltagePhase1Address, 1) + return wb.getPhaseValues(peblarRegVoltages, 1) } // phases1p3p implements the api.PhaseSwitcher interface via the decorator @@ -257,13 +256,13 @@ func (wb *Peblar) phases1p3p(phases int) error { b = 1 } - _, err := wb.conn.WriteSingleRegister(peblarForce1PhaseAddress, b) + _, err := wb.conn.WriteSingleRegister(peblarRegForce1Phase, b) return err } // getPhases implements the api.PhaseGetter interface func (wb *Peblar) getPhases() (int, error) { - b, err := wb.conn.ReadHoldingRegisters(peblarForce1PhaseAddress, 1) + b, err := wb.conn.ReadHoldingRegisters(peblarRegForce1Phase, 1) if err != nil { return 0, err } @@ -280,13 +279,13 @@ var _ api.Diagnosis = (*Peblar)(nil) // Diagnose implements the api.Diagnosis interface func (wb *Peblar) Diagnose() { - if b, err := wb.conn.ReadInputRegisters(peblarSerialNumberAddress, 12); err == nil { + if b, err := wb.conn.ReadInputRegisters(peblarRegSerialNumber, 12); err == nil { fmt.Printf("\tSN:\t%s\n", b) } - if b, err := wb.conn.ReadInputRegisters(peblarProductNumberAddress, 12); err == nil { + if b, err := wb.conn.ReadInputRegisters(peblarRegProductNumber, 12); err == nil { fmt.Printf("\tPN:\t%s\n", b) } - if b, err := wb.conn.ReadInputRegisters(peblarFwIdentifierAddress, 12); err == nil { + if b, err := wb.conn.ReadInputRegisters(peblarRegFwIdentifier, 12); err == nil { fmt.Printf("\tFirmware:\t%s\n", b) } } From bcb3ba8b36703a94a61e49c22b38f05f260d3ec2 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 3 Oct 2024 12:20:26 +0200 Subject: [PATCH 09/16] minor --- charger/peblar.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index f09f2f8f02..48461228bb 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -99,16 +99,15 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { phases: binary.BigEndian.Uint16(b), // required for retrieving the right amount of voltage/current registers } - c, err := conn.ReadInputRegisters(peblarRegIndepRelay, 1) + b, err = conn.ReadInputRegisters(peblarRegIndepRelay, 1) if err != nil { return nil, err } - indepRelays := binary.BigEndian.Uint16(c) var phasesS func(int) error var phasesG func() (int, error) - if indepRelays == 1 { + if binary.BigEndian.Uint16(b) == 1 { phasesS = wb.phases1p3p phasesG = wb.getPhases } From 217fc01519d1fc97dc422f75778bbdbf3b5cd4cc Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 3 Oct 2024 12:22:15 +0200 Subject: [PATCH 10/16] supported statuses --- charger/peblar.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index 48461228bb..6c3e057dd8 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -123,18 +123,8 @@ func (wb *Peblar) Status() (api.ChargeStatus, error) { } switch s := encoding.Uint16(b); s { - case 'A': - return api.StatusA, nil - case 'B': - return api.StatusB, nil - case 'C': - return api.StatusC, nil - case 'D': - return api.StatusD, nil - case 'E': - return api.StatusE, nil - case 'F': - return api.StatusF, nil + case 'A', 'B', 'C': + return api.ChargeStatus(s), nil default: return api.StatusNone, fmt.Errorf("invalid status: %d", s) } From e14a2778426fb556b3547c6b46c1eab695504f60 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 3 Oct 2024 12:27:28 +0200 Subject: [PATCH 11/16] wip --- charger/peblar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charger/peblar.go b/charger/peblar.go index 6c3e057dd8..dfcdcaf2ed 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -122,7 +122,7 @@ func (wb *Peblar) Status() (api.ChargeStatus, error) { return api.StatusNone, err } - switch s := encoding.Uint16(b); s { + switch s := rune(encoding.Uint16(b)); s { case 'A', 'B', 'C': return api.ChargeStatus(s), nil default: From e8260711da379d8ff58a41d83b46c6e4970c6c97 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 3 Oct 2024 12:31:46 +0200 Subject: [PATCH 12/16] wip --- charger/peblar_decorators.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/charger/peblar_decorators.go b/charger/peblar_decorators.go index f8ad28484e..7483f57f77 100644 --- a/charger/peblar_decorators.go +++ b/charger/peblar_decorators.go @@ -8,7 +8,7 @@ import ( func decoratePeblar(base *Peblar, phaseSwitcher func(int) error, phaseGetter func() (int, error)) api.Charger { switch { - case phaseGetter == nil && phaseSwitcher == nil: + case phaseSwitcher == nil: return base case phaseGetter == nil && phaseSwitcher != nil: @@ -22,17 +22,6 @@ func decoratePeblar(base *Peblar, phaseSwitcher func(int) error, phaseGetter fun }, } - case phaseGetter != nil && phaseSwitcher == nil: - return &struct { - *Peblar - api.PhaseGetter - }{ - Peblar: base, - PhaseGetter: &decoratePeblarPhaseGetterImpl{ - phaseGetter: phaseGetter, - }, - } - case phaseGetter != nil && phaseSwitcher != nil: return &struct { *Peblar From fdb0e2cb599ba56e6493c46ac2ee3574b5557ced Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:57:38 +0200 Subject: [PATCH 13/16] sync wb.enabled --- charger/peblar.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/charger/peblar.go b/charger/peblar.go index dfcdcaf2ed..dc19a6206e 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -142,7 +142,12 @@ func (wb *Peblar) Enable(enable bool) error { current = wb.curr } - return wb.setCurrent(current) + err := wb.setCurrent(current) + if err == nil { + wb.enabled = enable + } + + return err } // setCurrent writes the current limit in mA From e9086afadd81643325c1f823cda20db97bf89614 Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:18:02 +0200 Subject: [PATCH 14/16] Drop last logging remnant --- charger/peblar.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index dc19a6206e..0dad9cce35 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -84,9 +84,6 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { return nil, err } - log := util.NewLogger("peblar") - conn.Logger(log.TRACE) - // Register contains the physically connected phases b, err := conn.ReadInputRegisters(peblarRegPhaseCount, 1) if err != nil { From c9bb2292216ab8ee48bb3c46248b65b8657e03dd Mon Sep 17 00:00:00 2001 From: PieVo <5901627+PieVo@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:50:53 +0200 Subject: [PATCH 15/16] Fix MaxCurrentMillis Co-authored-by: andig --- charger/peblar.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/charger/peblar.go b/charger/peblar.go index 0dad9cce35..3a1d288d46 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -169,9 +169,11 @@ func (wb *Peblar) MaxCurrentMillis(current float64) error { return fmt.Errorf("invalid current %.1f", current) } - err := wb.setCurrent(wb.curr) + curr := uint32(current * 1e3) + + err := wb.setCurrent(curr) if err == nil { - wb.curr = uint32(current * 1e3) + wb.curr = curr } return err From 08ed595d21e5fcf0cac6f08b8752b05433e44847 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 6 Nov 2024 15:36:10 +0100 Subject: [PATCH 16/16] Add sponsorship --- charger/peblar.go | 12 ++++++++++++ templates/definition/charger/peblar.yaml | 1 + 2 files changed, 13 insertions(+) diff --git a/charger/peblar.go b/charger/peblar.go index 3a1d288d46..8e696192ee 100644 --- a/charger/peblar.go +++ b/charger/peblar.go @@ -2,6 +2,13 @@ package charger // LICENSE +// Copyright (c) 2024 evcc + +// This module is NOT covered by the MIT license. All rights reserved. + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -19,6 +26,7 @@ import ( "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" "github.com/volkszaehler/mbmd/encoding" ) @@ -84,6 +92,10 @@ func NewPeblar(uri string, id uint8) (api.Charger, error) { return nil, err } + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + // Register contains the physically connected phases b, err := conn.ReadInputRegisters(peblarRegPhaseCount, 1) if err != nil { diff --git a/templates/definition/charger/peblar.yaml b/templates/definition/charger/peblar.yaml index e901e685df..65f88ec8d1 100644 --- a/templates/definition/charger/peblar.yaml +++ b/templates/definition/charger/peblar.yaml @@ -5,6 +5,7 @@ products: generic: Home, Home Plus, Business capabilities: ["1p3p", "mA"] requirements: + evcc: ["sponsorship"] description: de: Peblar-Ladegeräte mit Firmware-Version 1.6 und höher unterstützen Modbus TCP über Port 502. Der Modbus-Server muss über die Weboberfläche des Ladegeräts aktiviert werden. Stellen Sie sicher, dass Smart-Charging-Strategien deaktiviert und auf Standard gesetzt sind. en: Peblar chargers with firmware version 1.6 and onwards support Modbus TCP via port 502. The Modbus server needs to be enabled via the charger web interface. Ensure that smart charging strategies are disabled and set to Default.