195 lines
6.1 KiB
Go
195 lines
6.1 KiB
Go
package ocpp
|
|
|
|
import (
|
|
"context"
|
|
"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"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
func (cp *CP) Setup(ctx context.Context, meterValues string, meterInterval time.Duration, forcePowerCtrl, noChangeAvailability bool) error {
|
|
if !noChangeAvailability {
|
|
if err := cp.ChangeAvailabilityRequest(0, core.AvailabilityTypeOperative); err != nil {
|
|
cp.log.DEBUG.Printf("failed configuring availability: %v", err)
|
|
}
|
|
}
|
|
|
|
// auto configuration
|
|
desiredMeasurands := "Power.Active.Import,Energy.Active.Import.Register,Current.Import,Voltage,Current.Offered,Power.Offered,SoC"
|
|
|
|
// remove offending measurands from desired values
|
|
if remove, ok := strings.CutPrefix(meterValues, "-"); ok {
|
|
desiredMeasurands = strings.Join(lo.Without(strings.Split(desiredMeasurands, ","), strings.Split(remove, ",")...), ",")
|
|
meterValues = ""
|
|
}
|
|
|
|
meterValuesSampledDataMaxLength := len(strings.Split(desiredMeasurands, ","))
|
|
|
|
resp, err := cp.GetConfigurationRequest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, opt := range resp.ConfigurationKey {
|
|
if opt.Value == nil {
|
|
continue
|
|
}
|
|
|
|
match := func(s string) bool {
|
|
return strings.EqualFold(opt.Key, s)
|
|
}
|
|
|
|
switch {
|
|
case match(KeyChargeProfileMaxStackLevel):
|
|
if val, err := strconv.Atoi(*opt.Value); err == nil {
|
|
cp.StackLevel = val
|
|
}
|
|
|
|
case match(KeyChargingScheduleAllowedChargingRateUnit):
|
|
if *opt.Value == "Power" || *opt.Value == "W" { // "W" is not allowed by spec but used by some CPs
|
|
cp.ChargingRateUnit = types.ChargingRateUnitWatts
|
|
cp.PhaseSwitching = true // assume phase switching is available for power-based charging
|
|
}
|
|
|
|
case match(KeyConnectorSwitch3to1PhaseSupported) || match(KeyChargeAmpsPhaseSwitchingSupported):
|
|
var val bool
|
|
if val, err = strconv.ParseBool(*opt.Value); err == nil {
|
|
cp.PhaseSwitching = val
|
|
}
|
|
|
|
case match(KeyMaxChargingProfilesInstalled):
|
|
if val, err := strconv.Atoi(*opt.Value); err == nil {
|
|
cp.ChargingProfileId = val
|
|
}
|
|
|
|
case match(KeyMeterValuesSampledData):
|
|
if opt.Readonly {
|
|
meterValuesSampledDataMaxLength = 0
|
|
}
|
|
cp.meterValuesSample = strings.Join(lo.Map(strings.Split(*opt.Value, ","), func(s string, _ int) string {
|
|
return strings.Trim(s, "' ")
|
|
}), ",")
|
|
|
|
case match(KeyMeterValuesSampledDataMaxLength):
|
|
if val, err := strconv.Atoi(*opt.Value); err == nil {
|
|
meterValuesSampledDataMaxLength = val
|
|
}
|
|
|
|
case match(KeyNumberOfConnectors):
|
|
if val, err := strconv.Atoi(*opt.Value); err == nil {
|
|
cp.NumberOfConnectors = val
|
|
}
|
|
|
|
case match(KeySupportedFeatureProfiles):
|
|
if !hasProperty(*opt.Value, smartcharging.ProfileName) {
|
|
cp.log.WARN.Printf("missing required SmartCharging profile")
|
|
}
|
|
// 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 match(KeyAlfenPlugAndChargeIdentifier):
|
|
cp.IdTag = *opt.Value
|
|
cp.log.DEBUG.Printf("overriding default `idTag` with Alfen-specific value: %s", cp.IdTag)
|
|
|
|
case match(KeyEvBoxSupportedMeasurands):
|
|
if meterValues == "" {
|
|
meterValues = *opt.Value
|
|
}
|
|
}
|
|
}
|
|
|
|
// BootNotification is normally received before Setup runs (we wait for it
|
|
// after WebSocket connect). Only trigger it as fallback for chargers that
|
|
// didn't send it (e.g. timeout-based connection without reboot).
|
|
cp.mu.RLock()
|
|
hasBootResult := cp.BootNotificationResult != nil
|
|
cp.mu.RUnlock()
|
|
|
|
if !hasBootResult && cp.HasRemoteTriggerFeature {
|
|
if err := cp.TriggerMessageRequest(0, core.BootNotificationFeatureName); err != nil {
|
|
cp.log.DEBUG.Printf("failed triggering BootNotification: %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(Timeout):
|
|
cp.log.DEBUG.Printf("BootNotification timeout")
|
|
case res := <-cp.bootNotificationRequestC:
|
|
cp.mu.Lock()
|
|
cp.BootNotificationResult = res
|
|
cp.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// autodetect measurands
|
|
if meterValues == "" && meterValuesSampledDataMaxLength > 0 {
|
|
sampledMeasurands := cp.tryMeasurands(desiredMeasurands, KeyMeterValuesSampledData)
|
|
meterValues = strings.Join(sampledMeasurands[:min(len(sampledMeasurands), meterValuesSampledDataMaxLength)], ",")
|
|
}
|
|
|
|
// configure measurands
|
|
if meterValues != "" {
|
|
if err := cp.ChangeConfigurationRequest(KeyMeterValuesSampledData, meterValues); err != nil {
|
|
cp.log.WARN.Printf("failed configuring %s: %v", KeyMeterValuesSampledData, err)
|
|
}
|
|
cp.meterValuesSample = meterValues
|
|
}
|
|
|
|
// trigger initial meter values
|
|
if cp.HasRemoteTriggerFeature {
|
|
if err := cp.TriggerMessageRequest(0, core.MeterValuesFeatureName); err == nil {
|
|
// wait for meter values
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(Timeout):
|
|
cp.log.WARN.Println("meter timeout")
|
|
case <-cp.meterC:
|
|
}
|
|
}
|
|
}
|
|
|
|
// configure sample rate
|
|
if meterInterval > 0 {
|
|
if err := cp.ChangeConfigurationRequest(KeyMeterValueSampleInterval, strconv.Itoa(int(meterInterval.Seconds()))); err != nil {
|
|
cp.log.WARN.Printf("failed configuring %s: %v", KeyMeterValueSampleInterval, err)
|
|
}
|
|
}
|
|
|
|
// configure websocket ping interval
|
|
if err := cp.ChangeConfigurationRequest(KeyWebSocketPingInterval, "30"); err != nil {
|
|
cp.log.DEBUG.Printf("failed configuring %s: %v", KeyWebSocketPingInterval, err)
|
|
}
|
|
|
|
if forcePowerCtrl {
|
|
cp.ChargingRateUnit = types.ChargingRateUnitWatts
|
|
cp.PhaseSwitching = true // assume phase switching is available for power-based charging
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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.SplitSeq(measurands, ",") {
|
|
if err := cp.ChangeConfigurationRequest(key, m); err == nil {
|
|
accepted = append(accepted, m)
|
|
}
|
|
}
|
|
return accepted
|
|
}
|