Revert "Move setup from charger to chargepoint (part 1)"
This reverts commit fc3f563df2.
This commit is contained in:
parent
fc3f563df2
commit
28ba92e50d
4 changed files with 258 additions and 286 deletions
327
charger/ocpp.go
327
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},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue