From 59e5ad767caafda9b21e84c3406df656f8637e65 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 27 Oct 2022 23:07:59 +0200 Subject: [PATCH] OCPP: use TxProfile and enable phase switching (#4946) (#4969) --- charger/ocpp.go | 77 ++++++++++-------- charger/ocpp_decorators.go | 162 ++++++++++++++++++++++++++++++++++--- 2 files changed, 198 insertions(+), 41 deletions(-) diff --git a/charger/ocpp.go b/charger/ocpp.go index 21b7a266c..206f35d90 100644 --- a/charger/ocpp.go +++ b/charger/ocpp.go @@ -29,6 +29,7 @@ type OCPP struct { current float64 meterValuesSample string timeout time.Duration + phaseSwitching bool } func init() { @@ -86,10 +87,15 @@ func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) { currentsG = c.currents } - return decorateOCPP(c, powerG, totalEnergyG, currentsG), nil + var phasesS func(int) error + if c.phaseSwitching { + phasesS = c.phases1p3p + } + + return decorateOCPP(c, powerG, totalEnergyG, currentsG, phasesS), nil } -//go:generate go run ../cmd/tools/decorate.go -f decorateOCPP -b *OCPP -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" +// go:generate go run ../cmd/tools/decorate.go -f decorateOCPP -b *OCPP -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" -t "api.PhaseSwitcher,Phases1p3p,func(int) (error)" // NewOCPP creates OCPP charger func NewOCPP(id string, connector int, idtag string, meterValues string, meterInterval time.Duration, quirks bool, timeout time.Duration) (*OCPP, error) { @@ -180,6 +186,12 @@ func NewOCPP(id string, connector int, idtag string, meterValues string, meterIn if val, err = strconv.Atoi(*opt.Value); err == nil { meterSampleInterval = time.Duration(val) * time.Second } + + case ocpp.KeyConnectorSwitch3to1PhaseSupported: + var val bool + if val, err = strconv.ParseBool(*opt.Value); err == nil { + c.phaseSwitching = val + } } if err != nil { @@ -301,6 +313,7 @@ func (c *OCPP) Enable(enable bool) error { rc <- err }, c.idtag, func(request *core.RemoteStartTransactionRequest) { request.ConnectorId = &c.connector + request.ChargingProfile = getTxChargingProfile(c.current, c.phases) }) } else { err = ocpp.Instance().RemoteStopTransaction(c.cp.ID(), func(resp *core.RemoteStopTransactionConfirmation, err error) { @@ -317,7 +330,7 @@ func (c *OCPP) Enable(enable bool) error { return c.wait(err, rc) } -func (c *OCPP) setChargingProfile(connectorid int, profile *types.ChargingProfile) error { +func (c *OCPP) setChargingProfile(connectorId int, profile *types.ChargingProfile) error { c.log.TRACE.Printf("SetChargingProfileRequest: %+v (%+v)", profile, *profile.ChargingSchedule) rc := make(chan error, 1) @@ -329,37 +342,40 @@ func (c *OCPP) setChargingProfile(connectorid int, profile *types.ChargingProfil } rc <- err - }, connectorid, profile) + }, connectorId, profile) return c.wait(err, rc) } -// setPeriod sets a single charging schedule period with given current and phases -func (c *OCPP) setPeriod(current float64, phases int) error { - period := types.NewChargingSchedulePeriod(0, current) - - c.log.TRACE.Printf("current phases: %d, current current: %f", phases, current) - if phases > 0 { - period.NumberPhases = &phases +// updatePeriod sets a single charging schedule period with given current and phases +func (c *OCPP) updatePeriod(current float64, phases int) error { + // current period can only be updated if transaction is active + if enabled, err := c.Enabled(); err != nil || !enabled { + return err } - // connectorID: 0 - profile will be applied to all connectors - err := c.setChargingProfile(0, getMaxChargingProfile(period)) + c.log.TRACE.Printf("update period with phases: %d, current: %f", phases, current) + + err := c.setChargingProfile(c.connector, getTxChargingProfile(current, phases)) if err != nil { - err = fmt.Errorf("failed to set charging profile: %w", err) + err = fmt.Errorf("set charging profile: %w", err) } return err } -func getMaxChargingProfile(period types.ChargingSchedulePeriod) *types.ChargingProfile { +func getTxChargingProfile(current float64, phases int) *types.ChargingProfile { + period := types.NewChargingSchedulePeriod(0, current) + if phases != 0 { + period.NumberPhases = &phases + } + return &types.ChargingProfile{ ChargingProfileId: 1, - StackLevel: 1, - ChargingProfilePurpose: types.ChargingProfilePurposeChargePointMaxProfile, - ChargingProfileKind: types.ChargingProfileKindAbsolute, + StackLevel: 0, + ChargingProfilePurpose: types.ChargingProfilePurposeTxProfile, + ChargingProfileKind: types.ChargingProfileKindRelative, ChargingSchedule: &types.ChargingSchedule{ - StartSchedule: types.NewDateTime(time.Now().Add(-1 * time.Hour)), ChargingRateUnit: types.ChargingRateUnitAmperes, ChargingSchedulePeriod: []types.ChargingSchedulePeriod{period}, }, @@ -375,7 +391,7 @@ var _ api.ChargerEx = (*OCPP)(nil) // MaxCurrentMillis implements the api.ChargerEx interface func (c *OCPP) MaxCurrentMillis(current float64) error { - err := c.setPeriod(current, c.phases) + err := c.updatePeriod(current, c.phases) if err == nil { c.current = current } @@ -397,21 +413,18 @@ func (c *OCPP) currents() (float64, float64, float64, error) { return c.cp.Currents() } -// // TODO: Phases1p3p implements the api.PhaseSwitcher interface -// func (c *OCPP) Phases1p3p(phases int) error { -// if !c.phaseSwitchingSupported { -// return fmt.Errorf("phase switching is not supported by the charger") -// } +// Phases1p3p implements the api.PhaseSwitcher interface +func (c *OCPP) phases1p3p(phases int) error { + c.phases = phases -// err := c.setPeriod(c.current, phases) -// if err == nil { -// c.phases = phases -// } - -// return err -// } + // NOTE: this will currently _never_ do anything since + // loadpoint disabled the charger before switching so + // updatePeriod will short-circuit + return c.updatePeriod(c.current, c.phases) +} // // Identify implements the api.Identifier interface +// Unless charger uses vehicle ID as idTag in authorize.req it is not possible to implement this in ocpp1.6 // func (c *OCPP) Identify() (string, error) { // return "", errors.New("not implemented") // } diff --git a/charger/ocpp_decorators.go b/charger/ocpp_decorators.go index eca403f40..923630035 100644 --- a/charger/ocpp_decorators.go +++ b/charger/ocpp_decorators.go @@ -6,12 +6,12 @@ import ( "github.com/evcc-io/evcc/api" ) -func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error)) api.Charger { +func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error), phaseSwitcher func(int) error) api.Charger { switch { - case meter == nil && meterCurrent == nil && meterEnergy == nil: + case meter == nil && meterCurrent == nil && meterEnergy == nil && phaseSwitcher == nil: return base - case meter != nil && meterCurrent == nil && meterEnergy == nil: + case meter != nil && meterCurrent == nil && meterEnergy == nil && phaseSwitcher == nil: return &struct { *OCPP api.Meter @@ -22,7 +22,7 @@ func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() }, } - case meter == nil && meterCurrent == nil && meterEnergy != nil: + case meter == nil && meterCurrent == nil && meterEnergy != nil && phaseSwitcher == nil: return &struct { *OCPP api.MeterEnergy @@ -33,7 +33,7 @@ func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() }, } - case meter != nil && meterCurrent == nil && meterEnergy != nil: + case meter != nil && meterCurrent == nil && meterEnergy != nil && phaseSwitcher == nil: return &struct { *OCPP api.Meter @@ -48,7 +48,7 @@ func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() }, } - case meter == nil && meterCurrent != nil && meterEnergy == nil: + case meter == nil && meterCurrent != nil && meterEnergy == nil && phaseSwitcher == nil: return &struct { *OCPP api.MeterCurrent @@ -59,7 +59,7 @@ func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() }, } - case meter != nil && meterCurrent != nil && meterEnergy == nil: + case meter != nil && meterCurrent != nil && meterEnergy == nil && phaseSwitcher == nil: return &struct { *OCPP api.Meter @@ -74,7 +74,7 @@ func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() }, } - case meter == nil && meterCurrent != nil && meterEnergy != nil: + case meter == nil && meterCurrent != nil && meterEnergy != nil && phaseSwitcher == nil: return &struct { *OCPP api.MeterCurrent @@ -89,7 +89,7 @@ func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() }, } - case meter != nil && meterCurrent != nil && meterEnergy != nil: + case meter != nil && meterCurrent != nil && meterEnergy != nil && phaseSwitcher == nil: return &struct { *OCPP api.Meter @@ -107,6 +107,142 @@ func decorateOCPP(base *OCPP, meter func() (float64, error), meterEnergy func() meterEnergy: meterEnergy, }, } + + case meter == nil && meterCurrent == nil && meterEnergy == nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.PhaseSwitcher + }{ + OCPP: base, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case meter != nil && meterCurrent == nil && meterEnergy == nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.Meter + api.PhaseSwitcher + }{ + OCPP: base, + Meter: &decorateOCPPMeterImpl{ + meter: meter, + }, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case meter == nil && meterCurrent == nil && meterEnergy != nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.MeterEnergy + api.PhaseSwitcher + }{ + OCPP: base, + MeterEnergy: &decorateOCPPMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case meter != nil && meterCurrent == nil && meterEnergy != nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.Meter + api.MeterEnergy + api.PhaseSwitcher + }{ + OCPP: base, + Meter: &decorateOCPPMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateOCPPMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case meter == nil && meterCurrent != nil && meterEnergy == nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.MeterCurrent + api.PhaseSwitcher + }{ + OCPP: base, + MeterCurrent: &decorateOCPPMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case meter != nil && meterCurrent != nil && meterEnergy == nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.Meter + api.MeterCurrent + api.PhaseSwitcher + }{ + OCPP: base, + Meter: &decorateOCPPMeterImpl{ + meter: meter, + }, + MeterCurrent: &decorateOCPPMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case meter == nil && meterCurrent != nil && meterEnergy != nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.MeterCurrent + api.MeterEnergy + api.PhaseSwitcher + }{ + OCPP: base, + MeterCurrent: &decorateOCPPMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + MeterEnergy: &decorateOCPPMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case meter != nil && meterCurrent != nil && meterEnergy != nil && phaseSwitcher != nil: + return &struct { + *OCPP + api.Meter + api.MeterCurrent + api.MeterEnergy + api.PhaseSwitcher + }{ + OCPP: base, + Meter: &decorateOCPPMeterImpl{ + meter: meter, + }, + MeterCurrent: &decorateOCPPMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + MeterEnergy: &decorateOCPPMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateOCPPPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } } return nil @@ -135,3 +271,11 @@ type decorateOCPPMeterEnergyImpl struct { func (impl *decorateOCPPMeterEnergyImpl) TotalEnergy() (float64, error) { return impl.meterEnergy() } + +type decorateOCPPPhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateOCPPPhaseSwitcherImpl) Phases1p3p(phases int) error { + return impl.phaseSwitcher(phases) +}