evcc-io/charger/ocpp.go

367 lines
10 KiB
Go

package charger
import (
"errors"
"fmt"
"strconv"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/ocpp"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
"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
id string
connector int
idtag string
phases int
current float64
phaseSwitchingSupported bool
}
func init() {
registry.Add("ocpp", NewOCPPFromConfig)
}
// 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
Meter bool
MeterInterval time.Duration
InitialReset core.ResetType
}{
Connector: 1,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
switch cc.InitialReset {
case
"",
core.ResetTypeSoft,
core.ResetTypeHard:
default:
return nil, fmt.Errorf("unknown configuration option detected for reset: %s", cc.InitialReset)
}
ocpp, err := NewOCPP(cc.StationId, cc.Connector, cc.IdTag, cc.Meter, cc.MeterInterval, cc.InitialReset)
if err != nil {
return ocpp, err
}
var (
meter func() (float64, error)
meterCurrent func() (float64, float64, float64, error)
chargeRater func() (float64, error)
)
if cc.Meter {
meter = ocpp.currentPower
meterCurrent = ocpp.currents
chargeRater = ocpp.chargedEnergy
}
return decorateOCPP(ocpp, meter, meterCurrent, chargeRater), 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.MeterCurrent,Currents,func() (float64, float64, float64, error)" -t "api.ChargeRater,ChargedEnergy,func() (float64, error)"
// NewOCPP creates OCPP charger
func NewOCPP(id string, connector int, idtag string, hasMeter bool, meterInterval time.Duration, initialReset core.ResetType) (*OCPP, error) {
cp, err := ocpp.Instance().Register(id, hasMeter)
if err != nil {
return nil, err
}
logstr := "-charger"
if id != "" {
logstr = fmt.Sprintf("-%s", id)
}
c := &OCPP{
log: util.NewLogger(fmt.Sprintf("ocpp%s:%d", logstr, connector)),
cp: cp,
id: id,
connector: connector,
idtag: idtag,
}
if err := cp.Boot(); err != nil {
return nil, err
}
var (
rc = make(chan error, 1)
options []core.ConfigurationKey
meterValuesSampledDataString string
meterValuesSampleInterval string
)
err = ocpp.Instance().GetConfiguration(id, func(resp *core.GetConfigurationConfirmation, err error) {
options = resp.ConfigurationKey
for _, opt := range options {
c.log.TRACE.Printf("%s (%t): %s", opt.Key, opt.Readonly, *opt.Value)
switch opt.Key {
case ocpp.KeyMeterValuesSampledData:
meterValuesSampledDataString = *opt.Value
case ocpp.KeyMeterValueSampleInterval:
meterValuesSampleInterval = *opt.Value
case string(ocpp.KeyConnectorSwitch3to1PhaseSupported):
// Detection of 1 phase charging/switching support
b, err := strconv.ParseBool(*opt.Value)
if err != nil {
rc <- err
return
}
c.phaseSwitchingSupported = b
}
}
rc <- err
}, []string{})
if err := c.wait(err, rc); err != nil {
return nil, err
}
if err := cp.DetectCapabilities(options); err != nil {
return nil, err
}
{ // Check supported connectors of charge point
supported := cp.GetNumberOfSupportedConnectors()
if c.connector > supported {
return nil, fmt.Errorf("configured connector is not available, max available connectors %d", supported)
}
}
if hasMeter {
if meterValuesSampledDataString != "Current.Import,Current.Offered,Energy.Active.Import.Register,Power.Active.Import,Temperature" {
c.log.TRACE.Printf("Current values \n\t%s != \n\t%+v", ocpp.ValuePreferedMeterValuesSampleData, meterValuesSampledDataString)
rc = make(chan error, 1)
err = ocpp.Instance().ChangeConfiguration(id, func(resp *core.ChangeConfigurationConfirmation, err error) {
c.log.TRACE.Printf("ChangeMeterConfigurationRequest %T: %+v", resp, resp)
if resp.Status == core.ConfigurationStatusRejected {
rc <- fmt.Errorf("configuration change rejected")
}
rc <- err
}, ocpp.KeyMeterValuesSampledData, ocpp.ValuePreferedMeterValuesSampleData)
if err := c.wait(err, rc); err != nil {
return nil, err
}
}
{
intervalStr := fmt.Sprintf("%d", int(meterInterval.Seconds()))
if meterValuesSampleInterval != intervalStr {
rc = make(chan error, 1)
err := ocpp.Instance().ChangeConfiguration(id, func(resp *core.ChangeConfigurationConfirmation, err error) {
c.log.TRACE.Printf("ChangeSampleMeterValueInterval %T: %v", resp, resp)
if resp.Status == core.ConfigurationStatusRejected {
rc <- fmt.Errorf("configuration of meter interval rejected: %w", err)
}
rc <- err
}, ocpp.KeyMeterValueSampleInterval, intervalStr)
if err := c.wait(err, rc); err != nil {
return nil, err
}
}
}
// get initial meter values
if hasMeter {
ocpp.Instance().TriggerMeterValueRequest(cp)
}
}
if initialReset != "" {
t := core.ResetTypeSoft
if initialReset == core.ResetTypeHard {
t = core.ResetTypeHard
}
ocpp.Instance().TriggerResetRequest(cp, t)
}
// TODO: check for running transaction
return c, nil
}
// Enabled implements the api.Charger interface
func (c *OCPP) Enabled() (bool, error) {
current, err := c.cp.Status()
if current == api.StatusC {
return true, err
}
return false, err
}
func (c *OCPP) wait(err error, rc chan error) error {
if err == nil {
select {
case err = <-rc:
close(rc)
case <-time.After(request.Timeout):
err = api.ErrTimeout
}
}
return err
}
// Enable implements the api.Charger interface
func (c *OCPP) Enable(enable bool) error {
var err error
rc := make(chan error, 1)
if enable {
err = ocpp.Instance().RemoteStartTransaction(c.id, func(resp *core.RemoteStartTransactionConfirmation, err error) {
c.log.TRACE.Printf("RemoteStartTransaction %T: %+v", resp, resp)
if err == nil && resp != nil && resp.Status != types.RemoteStartStopStatusAccepted {
err = errors.New(string(resp.Status))
}
rc <- err
}, c.idtag, func(request *core.RemoteStartTransactionRequest) {
request.ConnectorId = &c.connector
})
} else {
err = ocpp.Instance().RemoteStopTransaction(c.id, func(resp *core.RemoteStopTransactionConfirmation, err error) {
c.log.TRACE.Printf("RemoteStopTransaction %T: %+v", resp, resp)
if err == nil && resp != nil && resp.Status != types.RemoteStartStopStatusAccepted {
err = errors.New(string(resp.Status))
}
rc <- err
}, c.cp.TransactionID())
}
return c.wait(err, rc)
}
func (c *OCPP) setChargingProfile(connectorid int, profile *types.ChargingProfile) error {
c.log.TRACE.Printf("SetChargingProfileRequest %T: %+v", profile, profile)
c.log.TRACE.Printf("SetChargingProfileRequest %T: %+v", profile.ChargingSchedule, profile.ChargingSchedule)
rc := make(chan error, 1)
err := ocpp.Instance().SetChargingProfile(c.id, func(resp *smartcharging.SetChargingProfileConfirmation, err error) {
c.log.TRACE.Printf("SetChargingProfileResponse %T: %+v", resp, resp)
if err == nil && resp != nil && resp.Status != smartcharging.ChargingProfileStatusAccepted {
err = errors.New(string(resp.Status))
}
rc <- err
}, 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
}
// connectorID: 0 - profile will be applied to all connectors
err := c.setChargingProfile(0, getMaxCharginProfile(period))
if err != nil {
c.log.TRACE.Printf("failed to set charging profile: %s", err)
}
return err
}
func getMaxCharginProfile(period types.ChargingSchedulePeriod) *types.ChargingProfile {
return &types.ChargingProfile{
ChargingProfileId: 1,
StackLevel: 1,
ChargingProfilePurpose: types.ChargingProfilePurposeChargePointMaxProfile,
ChargingProfileKind: types.ChargingProfileKindAbsolute,
ChargingSchedule: &types.ChargingSchedule{
StartSchedule: types.NewDateTime(time.Now().Add(-1 * time.Hour)),
ChargingRateUnit: types.ChargingRateUnitAmperes,
ChargingSchedulePeriod: []types.ChargingSchedulePeriod{period},
},
}
}
// MaxCurrent implements the api.Charger interface
func (c *OCPP) MaxCurrent(current int64) error {
return c.MaxCurrentMillis(float64(current))
}
// MaxCurrentMillis implements the api.ChargerEx interface
func (c *OCPP) MaxCurrentMillis(current float64) error {
err := c.setPeriod(current, c.phases)
if err == nil {
c.current = current
}
return err
}
// Status implements the api.Charger interface
func (c *OCPP) Status() (api.ChargeStatus, error) {
return c.cp.Status()
}
// TODO: Phases1p3p implements the api.ChargePhases interface
// func (c *OCPP) Phases1p3p(phases int) error {
// if !c.phaseSwitchingSupported {
// return fmt.Errorf("phase switching is not supported by the charger")
// }
// err := c.setPeriod(c.current, phases)
// if err == nil {
// c.phases = phases
// }
// return err
// }
// CurrentPower implements the api.Meter interface
func (c *OCPP) currentPower() (float64, error) {
return c.cp.CurrentPower()
}
// ChargedEnergy implements the api.ChargeRater interface
func (c *OCPP) chargedEnergy() (float64, error) {
return c.cp.ChargedEnergy()
}
// Currents implements the api.MeterCurrent interface
func (c *OCPP) currents() (float64, float64, float64, error) {
return c.cp.Currents()
}
// // Identify implements the api.Identifier interface
// func (c *OCPP) Identify() (string, error) {
// return "", errors.New("not implemented")
// }