From 28ba92e50d1f8e2f8a71715cf07f30207bbe01e5 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 28 Aug 2024 07:58:30 +0200 Subject: [PATCH] Revert "Move setup from charger to chargepoint (part 1)" This reverts commit fc3f563df22426af073c7dd48c74c7f5b243d327. --- charger/ocpp.go | 327 ++++++++++++++++++++++++++++++--------- charger/ocpp/cp.go | 13 +- charger/ocpp/cp_setup.go | 196 ----------------------- charger/ocpp/helper.go | 8 - 4 files changed, 258 insertions(+), 286 deletions(-) delete mode 100644 charger/ocpp/cp_setup.go diff --git a/charger/ocpp.go b/charger/ocpp.go index cfd9d9b07..21c9ca35c 100644 --- a/charger/ocpp.go +++ b/charger/ocpp.go @@ -6,6 +6,8 @@ import ( "fmt" "math" "slices" + "strconv" + "strings" "time" "github.com/evcc-io/evcc/api" @@ -13,32 +15,35 @@ import ( "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/util" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) // OCPP charger implementation type OCPP struct { - log *util.Logger - cp *ocpp.CP - conn *ocpp.Connector - idtag string - phases int - enabled bool - current float64 - meterValuesSample string - timeout time.Duration - // phaseSwitching bool - remoteStart bool - // hasRemoteTriggerFeature bool - // chargingRateUnit types.ChargingRateUnitType - // chargingProfileId int - // stackLevel int - lp loadpoint.API - bootNotification *core.BootNotificationRequest + log *util.Logger + conn *ocpp.Connector + idtag string + phases int + enabled bool + current float64 + meterValuesSample string + timeout time.Duration + phaseSwitching bool + remoteStart bool + hasRemoteTriggerFeature bool + chargingRateUnit types.ChargingRateUnitType + chargingProfileId int + stackLevel int + lp loadpoint.API + bootNotification *core.BootNotificationRequest } -const defaultIdTag = "evcc" // RemoteStartTransaction only +const ( + defaultIdTag = "evcc" // RemoteStartTransaction only + desiredMeasurands = "Power.Active.Import,Energy.Active.Import.Register,Current.Import,Voltage,Current.Offered,Power.Offered,SoC" +) func init() { registry.Add("ocpp", NewOCPPFromConfig) @@ -47,21 +52,19 @@ func init() { // NewOCPPFromConfig creates a OCPP charger from generic config func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) { cc := struct { - StationId string - IdTag string - Connector int - MeterInterval time.Duration - MeterValues string - ConnectTimeout time.Duration // Initial Timeout - Timeout time.Duration // Message Timeout - - BootNotification *bool // TODO deprecated - GetConfiguration *bool // TODO deprecated - ChargingRateUnit types.ChargingRateUnitType // TODO deprecated - AutoStart bool // TODO deprecated - NoStop bool // TODO deprecated - - RemoteStart bool + StationId string + IdTag string + Connector int + MeterInterval time.Duration + MeterValues string + ConnectTimeout time.Duration // Initial Timeout + Timeout time.Duration // Message Timeout + BootNotification *bool // TODO deprecated + GetConfiguration *bool // TODO deprecated + ChargingRateUnit string // TODO deprecated + AutoStart bool // TODO deprecated + NoStop bool // TODO deprecated + RemoteStart bool }{ Connector: 1, IdTag: defaultIdTag, @@ -91,33 +94,33 @@ func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) { currentsG, voltagesG func() (float64, float64, float64, error) ) - if c.cp.HasMeasurement(types.MeasurandPowerActiveImport) { + if c.hasMeasurement(types.MeasurandPowerActiveImport) { powerG = c.conn.CurrentPower } - if c.cp.HasMeasurement(types.MeasurandEnergyActiveImportRegister) { + if c.hasMeasurement(types.MeasurandEnergyActiveImportRegister) { totalEnergyG = c.conn.TotalEnergy } - if c.cp.HasMeasurement(types.MeasurandCurrentImport) { + if c.hasMeasurement(types.MeasurandCurrentImport) { currentsG = c.conn.Currents } - if c.cp.HasMeasurement(types.MeasurandVoltage) { + if c.hasMeasurement(types.MeasurandVoltage) { voltagesG = c.conn.Voltages } - if c.cp.HasMeasurement(types.MeasurandSoC) { + if c.hasMeasurement(types.MeasurandSoC) { socG = c.conn.Soc } var phasesS func(int) error - if c.cp.PhaseSwitching { + if c.phaseSwitching { phasesS = c.phases1p3p } // var currentG func() (float64, error) - // if c.cp.HasMeasurement(types.MeasurandCurrentOffered) { + // if c.hasMeasurement(types.MeasurandCurrentOffered) { // currentG = c.conn.GetMaxCurrent // } @@ -131,7 +134,7 @@ func NewOCPP(id string, connector int, idtag string, meterValues string, meterInterval time.Duration, boot, noConfig, remoteStart bool, connectTimeout, timeout time.Duration, - chargingRateUnit types.ChargingRateUnitType, + chargingRateUnit string, ) (*OCPP, error) { unit := "ocpp" if id != "" { @@ -143,27 +146,12 @@ func NewOCPP(id string, connector int, idtag string, cp, err := ocpp.Instance().ChargepointByID(id) if err != nil { - cp = ocpp.NewChargePoint(log, id, chargingRateUnit) + cp = ocpp.NewChargePoint(log, id) // should not error if err := ocpp.Instance().Register(id, cp); err != nil { return nil, err } - - // fix timing issue in EVBox when switching OCPP protocol version - time.Sleep(time.Second) - - log.DEBUG.Printf("waiting for chargepoint: %v", connectTimeout) - - select { - case <-time.After(connectTimeout): - return nil, api.ErrTimeout - case <-cp.HasConnected(): - } - - if err := cp.Setup(); err != nil { - return nil, err - } } conn, err := ocpp.NewConnector(log, connector, cp, timeout) @@ -173,23 +161,185 @@ func NewOCPP(id string, connector int, idtag string, c := &OCPP{ log: log, - cp: cp, conn: conn, idtag: idtag, remoteStart: remoteStart, - // chargingRateUnit: types.ChargingRateUnitType(chargingRateUnit), - // hasRemoteTriggerFeature: true, // assume remote trigger feature is available - timeout: timeout, + chargingRateUnit: types.ChargingRateUnitType(chargingRateUnit), + hasRemoteTriggerFeature: true, // assume remote trigger feature is available + timeout: timeout, } + c.log.DEBUG.Printf("waiting for chargepoint: %v", connectTimeout) + + select { + case <-time.After(connectTimeout): + return nil, api.ErrTimeout + case <-cp.HasConnected(): + } + + // fix timing issue in EVBox when switching OCPP protocol version + time.Sleep(time.Second) + + if err := ocpp.Instance().ChangeAvailabilityRequest(cp.ID(), 0, core.AvailabilityTypeOperative); err != nil { + c.log.DEBUG.Printf("failed configuring availability: %v", err) + } + + var meterValuesSampledData string + meterValuesSampledDataMaxLength := len(strings.Split(desiredMeasurands, ",")) + + rc := make(chan error, 1) + + // CP + err = ocpp.Instance().GetConfiguration(cp.ID(), func(resp *core.GetConfigurationConfirmation, err error) { + if err == nil { + for _, opt := range resp.ConfigurationKey { + if opt.Value == nil { + continue + } + + switch opt.Key { + case ocpp.KeyChargeProfileMaxStackLevel: + if val, err := strconv.Atoi(*opt.Value); err == nil { + c.stackLevel = val + } + + case ocpp.KeyChargingScheduleAllowedChargingRateUnit: + if *opt.Value == "Power" || *opt.Value == "W" { // "W" is not allowed by spec but used by some CPs + c.chargingRateUnit = types.ChargingRateUnitWatts + } + + case ocpp.KeyConnectorSwitch3to1PhaseSupported: + var val bool + if val, err = strconv.ParseBool(*opt.Value); err == nil { + c.phaseSwitching = val + } + + case ocpp.KeyMaxChargingProfilesInstalled: + if val, err := strconv.Atoi(*opt.Value); err == nil { + c.chargingProfileId = val + } + + case ocpp.KeyMeterValuesSampledData: + if opt.Readonly { + meterValuesSampledDataMaxLength = 0 + } + meterValuesSampledData = *opt.Value + + case ocpp.KeyMeterValuesSampledDataMaxLength: + if val, err := strconv.Atoi(*opt.Value); err == nil { + meterValuesSampledDataMaxLength = val + } + + case ocpp.KeyNumberOfConnectors: + var val int + if val, err = strconv.Atoi(*opt.Value); err == nil && connector > val { + err = fmt.Errorf("connector %d exceeds max available connectors: %d", connector, val) + } + + case ocpp.KeySupportedFeatureProfiles: + if !c.hasProperty(*opt.Value, smartcharging.ProfileName) { + c.log.WARN.Printf("the required SmartCharging feature profile is not indicated as supported") + } + // correct the availability assumption of RemoteTrigger only in case of a valid looking FeatureProfile list + if c.hasProperty(*opt.Value, core.ProfileName) { + c.hasRemoteTriggerFeature = c.hasProperty(*opt.Value, remotetrigger.ProfileName) + } + + // vendor-specific keys + case ocpp.KeyAlfenPlugAndChargeIdentifier: + if c.idtag == defaultIdTag { + c.idtag = *opt.Value + c.log.DEBUG.Printf("overriding default `idTag` with Alfen-specific value: %s", c.idtag) + } + + case ocpp.KeyEvBoxSupportedMeasurands: + if meterValues == "" { + meterValues = *opt.Value + } + } + + if err != nil { + break + } + } + } + + rc <- err + }, nil) + + if err := c.wait(err, rc); err != nil { + return nil, err + } + + // see who's there + if c.hasRemoteTriggerFeature { + // CP + if err := ocpp.Instance().TriggerMessageRequest(cp.ID(), core.BootNotificationFeatureName); err != nil { + c.log.DEBUG.Printf("failed triggering BootNotification: %v", err) + } + + select { + case <-time.After(timeout): + c.log.DEBUG.Printf("BootNotification timeout") + case res := <-cp.BootNotificationRequest(): + if res != nil { + c.bootNotification = res + } + } + } + + // autodetect measurands + if meterValues == "" && meterValuesSampledDataMaxLength > 0 { + sampledMeasurands := c.tryMeasurands(desiredMeasurands, ocpp.KeyMeterValuesSampledData) + meterValues = strings.Join(sampledMeasurands[:min(len(sampledMeasurands), meterValuesSampledDataMaxLength)], ",") + } + + // configure measurands + if meterValues != "" { + // CP + if err := c.configure(ocpp.KeyMeterValuesSampledData, meterValues); err == nil { + meterValuesSampledData = meterValues + } + } + + c.meterValuesSample = meterValuesSampledData + + // trigger initial meter values + if c.hasRemoteTriggerFeature { + // CP + if err := conn.TriggerMessageRequest(core.MeterValuesFeatureName); err == nil { + // wait for meter values + select { + case <-time.After(timeout): + c.log.WARN.Println("meter timeout") + case <-c.conn.MeterSampled(): + } + } + } + + // configure sample rate + if meterInterval > 0 { + // CP + if err := c.configure(ocpp.KeyMeterValueSampleInterval, strconv.Itoa(int(meterInterval.Seconds()))); err != nil { + c.log.WARN.Printf("failed configuring MeterValueSampleInterval: %v", err) + } + } + + if c.hasRemoteTriggerFeature { + // CP + go conn.WatchDog(10 * time.Second) + } + + // configure ping interval + // CP + c.configure(ocpp.KeyWebSocketPingInterval, "30") + // CONN - if cp.HasRemoteTriggerFeature { + if c.hasRemoteTriggerFeature { if err := conn.TriggerMessageRequest(core.StatusNotificationFeatureName); err != nil { c.log.DEBUG.Printf("failed triggering StatusNotification: %v", err) } - - go conn.WatchDog(10 * time.Second) } return c, conn.Initialized() @@ -200,6 +350,18 @@ func (c *OCPP) Connector() *ocpp.Connector { return c.conn } +// hasMeasurement checks if meterValuesSample contains given measurement +func (c *OCPP) hasMeasurement(val types.Measurand) bool { + return c.hasProperty(c.meterValuesSample, string(val)) +} + +// hasProperty checks if comma-separated string contains given string ignoring whitespaces +func (c *OCPP) hasProperty(props string, prop string) bool { + return slices.ContainsFunc(strings.Split(props, ","), func(s string) bool { + return strings.HasPrefix(strings.ReplaceAll(s, " ", ""), prop) + }) +} + func (c *OCPP) effectiveIdTag() string { if idtag := c.conn.IdTag(); idtag != "" { return idtag @@ -207,6 +369,31 @@ func (c *OCPP) effectiveIdTag() string { return c.idtag } +func (c *OCPP) tryMeasurands(measurands string, key string) []string { + var accepted []string + for _, m := range strings.Split(measurands, ",") { + if err := c.configure(key, m); err == nil { + accepted = append(accepted, m) + } + } + return accepted +} + +// configure updates CP configuration +func (c *OCPP) configure(key, val string) error { + rc := make(chan error, 1) + + err := ocpp.Instance().ChangeConfiguration(c.conn.ChargePoint().ID(), func(resp *core.ChangeConfigurationConfirmation, err error) { + if err == nil && resp != nil && resp.Status != core.ConfigurationStatusAccepted { + rc <- fmt.Errorf("ChangeConfiguration failed: %s", resp.Status) + } + + rc <- err + }, key, val) + + return c.wait(err, rc) +} + // wait waits for a CP roundtrip with timeout func (c *OCPP) wait(err error, rc chan error) error { return ocpp.Wait(err, rc, c.timeout) @@ -290,12 +477,12 @@ func (c *OCPP) Enabled() (bool, error) { } // fallback to the "offered" measurands - if c.cp.HasMeasurement(types.MeasurandCurrentOffered) { + if c.hasMeasurement(types.MeasurandCurrentOffered) { if v, err := c.conn.GetMaxCurrent(); err == nil { return v > 0, nil } } - if c.cp.HasMeasurement(types.MeasurandPowerOffered) { + if c.hasMeasurement(types.MeasurandPowerOffered) { if v, err := c.conn.GetMaxPower(); err == nil { return v > 0, nil } @@ -398,7 +585,7 @@ func (c *OCPP) getScheduleLimit() (float64, error) { func (c *OCPP) createTxDefaultChargingProfile(current float64) *types.ChargingProfile { phases := c.phases period := types.NewChargingSchedulePeriod(0, current) - if c.cp.ChargingRateUnit == types.ChargingRateUnitWatts { + if c.chargingRateUnit == types.ChargingRateUnitWatts { // get (expectedly) active phases from loadpoint if c.lp != nil { phases = c.lp.GetPhases() @@ -415,13 +602,13 @@ func (c *OCPP) createTxDefaultChargingProfile(current float64) *types.ChargingPr } return &types.ChargingProfile{ - ChargingProfileId: c.cp.ChargingProfileId, - StackLevel: c.cp.StackLevel, + ChargingProfileId: c.chargingProfileId, + StackLevel: c.stackLevel, ChargingProfilePurpose: types.ChargingProfilePurposeTxDefaultProfile, ChargingProfileKind: types.ChargingProfileKindAbsolute, ChargingSchedule: &types.ChargingSchedule{ StartSchedule: types.Now(), - ChargingRateUnit: c.cp.ChargingRateUnit, + ChargingRateUnit: c.chargingRateUnit, ChargingSchedulePeriod: []types.ChargingSchedulePeriod{period}, }, } diff --git a/charger/ocpp/cp.go b/charger/ocpp/cp.go index f9ad29663..05621d58b 100644 --- a/charger/ocpp/cp.go +++ b/charger/ocpp/cp.go @@ -6,7 +6,6 @@ import ( "github.com/evcc-io/evcc/util" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" - "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) // TODO support multiple connectors @@ -25,17 +24,10 @@ type CP struct { connected bool connectC chan struct{} - // configuration properties - PhaseSwitching bool - HasRemoteTriggerFeature bool - ChargingRateUnit types.ChargingRateUnitType - ChargingProfileId int - StackLevel int - connectors map[int]*Connector } -func NewChargePoint(log *util.Logger, id string, chargingRateUnit types.ChargingRateUnitType) *CP { +func NewChargePoint(log *util.Logger, id string) *CP { return &CP{ log: log, id: id, @@ -44,9 +36,6 @@ func NewChargePoint(log *util.Logger, id string, chargingRateUnit types.Charging connectors: make(map[int]*Connector), bootNotificationRequestC: make(chan *core.BootNotificationRequest, 1), - - ChargingRateUnit: chargingRateUnit, - HasRemoteTriggerFeature: true, // assume remote trigger feature is available } } diff --git a/charger/ocpp/cp_setup.go b/charger/ocpp/cp_setup.go deleted file mode 100644 index 753a0cb8d..000000000 --- a/charger/ocpp/cp_setup.go +++ /dev/null @@ -1,196 +0,0 @@ -package ocpp - -import ( - "fmt" - "strconv" - "strings" - "time" - - "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" - "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" - "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" - "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" -) - -const desiredMeasurands = "Power.Active.Import,Energy.Active.Import.Register,Current.Import,Voltage,Current.Offered,Power.Offered,SoC" - -func (cp *CP) Setup() error { - if err := Instance().ChangeAvailabilityRequest(cp.ID(), 0, core.AvailabilityTypeOperative); err != nil { - cp.log.DEBUG.Printf("failed configuring availability: %v", err) - } - - var meterValuesSampledData string - meterValuesSampledDataMaxLength := len(strings.Split(desiredMeasurands, ",")) - - rc := make(chan error, 1) - - // CP - err := Instance().GetConfiguration(cp.ID(), func(resp *core.GetConfigurationConfirmation, err error) { - if err == nil { - for _, opt := range resp.ConfigurationKey { - if opt.Value == nil { - continue - } - - switch opt.Key { - case KeyChargeProfileMaxStackLevel: - if val, err := strconv.Atoi(*opt.Value); err == nil { - cp.StackLevel = val - } - - case KeyChargingScheduleAllowedChargingRateUnit: - if *opt.Value == "Power" || *opt.Value == "W" { // "W" is not allowed by spec but used by some CPs - cp.ChargingRateUnit = types.ChargingRateUnitWatts - } - - case KeyConnectorSwitch3to1PhaseSupported: - var val bool - if val, err = strconv.ParseBool(*opt.Value); err == nil { - cp.PhaseSwitching = val - } - - case KeyMaxChargingProfilesInstalled: - if val, err := strconv.Atoi(*opt.Value); err == nil { - cp.ChargingProfileId = val - } - - case KeyMeterValuesSampledData: - if opt.Readonly { - meterValuesSampledDataMaxLength = 0 - } - meterValuesSampledData = *opt.Value - - case KeyMeterValuesSampledDataMaxLength: - if val, err := strconv.Atoi(*opt.Value); err == nil { - meterValuesSampledDataMaxLength = val - } - - case KeyNumberOfConnectors: - var val int - if val, err = strconv.Atoi(*opt.Value); err == nil && connector > val { - err = fmt.Errorf("connector %d exceeds max available connectors: %d", connector, val) - } - - case KeySupportedFeatureProfiles: - if !hasProperty(*opt.Value, smartcharging.ProfileName) { - cp.log.WARN.Printf("the required SmartCharging feature profile is not indicated as supported") - } - // correct the availability assumption of RemoteTrigger only in case of a valid looking FeatureProfile list - if hasProperty(*opt.Value, core.ProfileName) { - cp.HasRemoteTriggerFeature = hasProperty(*opt.Value, remotetrigger.ProfileName) - } - - // vendor-specific keys - case KeyAlfenPlugAndChargeIdentifier: - if cp.idtag == defaultIdTag { - cp.idtag = *opt.Value - cp.log.DEBUG.Printf("overriding default `idTag` with Alfen-specific value: %s", cp.idtag) - } - - case KeyEvBoxSupportedMeasurands: - if meterValues == "" { - meterValues = *opt.Value - } - } - - if err != nil { - break - } - } - } - - rc <- err - }, nil) - - if err := cp.wait(err, rc); err != nil { - return nil, err - } - - // see who's there - if cp.HasRemoteTriggerFeature { - // CP - if err := Instance().TriggerMessageRequest(cp.ID(), core.BootNotificationFeatureName); err != nil { - cp.log.DEBUG.Printf("failed triggering BootNotification: %v", err) - } - - select { - case <-time.After(timeout): - cp.log.DEBUG.Printf("BootNotification timeout") - case res := <-cp.BootNotificationRequest(): - if res != nil { - cp.bootNotification = res - } - } - } - - // autodetect measurands - if meterValues == "" && meterValuesSampledDataMaxLength > 0 { - sampledMeasurands := cp.tryMeasurands(desiredMeasurands, KeyMeterValuesSampledData) - meterValues = strings.Join(sampledMeasurands[:min(len(sampledMeasurands), meterValuesSampledDataMaxLength)], ",") - } - - // configure measurands - if meterValues != "" { - // CP - if err := cp.configure(KeyMeterValuesSampledData, meterValues); err == nil { - meterValuesSampledData = meterValues - } - } - - cp.meterValuesSample = meterValuesSampledData - - // trigger initial meter values - if cp.HasRemoteTriggerFeature { - // CP - if err := conn.TriggerMessageRequest(core.MeterValuesFeatureName); err == nil { - // wait for meter values - select { - case <-time.After(timeout): - cp.log.WARN.Println("meter timeout") - case <-cp.conn.MeterSampled(): - } - } - } - - // configure sample rate - if meterInterval > 0 { - // CP - if err := cp.configure(KeyMeterValueSampleInterval, strconv.Itoa(int(meterInterval.Seconds()))); err != nil { - cp.log.WARN.Printf("failed configuring MeterValueSampleInterval: %v", err) - } - } - - // configure ping interval - // CP - cp.configure(KeyWebSocketPingInterval, "30") -} - -// HasMeasurement checks if meterValuesSample contains given measurement -func (cp *CP) HasMeasurement(val types.Measurand) bool { - return hasProperty(cp.meterValuesSample, string(val)) -} - -func (cp *CP) tryMeasurands(measurands string, key string) []string { - var accepted []string - for _, m := range strings.Split(measurands, ",") { - if err := cp.configure(key, m); err == nil { - accepted = append(accepted, m) - } - } - return accepted -} - -// configure updates CP configuration -func (cp *CP) configure(key, val string) error { - rc := make(chan error, 1) - - err := Instance().ChangeConfiguration(cp.id, func(resp *core.ChangeConfigurationConfirmation, err error) { - if err == nil && resp != nil && resp.Status != core.ConfigurationStatusAccepted { - rc <- fmt.Errorf("ChangeConfiguration failed: %s", resp.Status) - } - - rc <- err - }, key, val) - - return c.wait(err, rc) -} diff --git a/charger/ocpp/helper.go b/charger/ocpp/helper.go index 285bf90fe..12dce8d5e 100644 --- a/charger/ocpp/helper.go +++ b/charger/ocpp/helper.go @@ -2,7 +2,6 @@ package ocpp import ( "slices" - "strings" "time" "github.com/evcc-io/evcc/api" @@ -34,10 +33,3 @@ func sortByAge(values []types.MeterValue) []types.MeterValue { return at.Compare(bt) }) } - -// hasProperty checks if comma-separated string contains given string ignoring whitespaces -func hasProperty(props string, prop string) bool { - return slices.ContainsFunc(strings.Split(props, ","), func(s string) bool { - return strings.HasPrefix(strings.ReplaceAll(s, " ", ""), prop) - }) -}