evcc-io/charger/ocpp/cp.go

240 lines
5.5 KiB
Go

package ocpp
import (
"fmt"
"strconv"
"sync"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
"github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
)
const (
// Core profile keys
KeyNumberOfConnectors = "NumberOfConnectors"
// Meter profile keys
KeyMeterValuesSampledData = "MeterValuesSampledData"
KeyMeterValueSampleInterval = "MeterValueSampleInterval"
// Smart Charging profile keys
KeyChargeProfileMaxStackLevel = "ChargeProfileMaxStackLevel"
KeyChargingScheduleAllowedChargingRateUnit = "ChargingScheduleAllowedChargingRateUnit"
KeyChargingScheduleMaxPeriods = "ChargingScheduleMaxPeriods"
KeyConnectorSwitch3to1PhaseSupported = "ConnectorSwitch3to1PhaseSupported"
KeyMaxChargingProfilesInstalled = "MaxChargingProfilesInstalled"
)
type CP struct {
mu sync.Mutex
log *util.Logger
once sync.Once
id string
connectC, statusC chan struct{}
updated time.Time
status *core.StatusNotificationRequest
timeout time.Duration
meterUpdated time.Time
measurements map[string]types.SampledValue
txnCount int // change initial value to the last known global transaction. Needs persistence
txnId int
}
func NewChargePoint(log *util.Logger, id string, timeout time.Duration) *CP {
return &CP{
log: log,
id: id,
connectC: make(chan struct{}),
statusC: make(chan struct{}),
measurements: make(map[string]types.SampledValue),
timeout: timeout,
}
}
func (cp *CP) ID() string {
cp.mu.Lock()
defer cp.mu.Unlock()
return cp.id
}
func (cp *CP) RegisterID(id string) {
cp.mu.Lock()
defer cp.mu.Unlock()
if cp.id != "" {
panic("ocpp: cannot re-register id")
}
cp.id = id
}
func (cp *CP) Connect() {
cp.mu.Lock()
defer cp.mu.Unlock()
cp.once.Do(func() {
close(cp.connectC)
})
}
func (cp *CP) HasConnected() <-chan struct{} {
return cp.connectC
}
func (cp *CP) Initialized(timeout time.Duration) bool {
cp.log.DEBUG.Printf("waiting for chargepoint status: %v", timeout)
// trigger status
time.AfterFunc(5*time.Second, func() {
select {
case <-cp.statusC:
return
default:
Instance().TriggerMessageRequest(cp.ID(), core.StatusNotificationFeatureName)
}
})
// wait for status
select {
case <-cp.statusC:
cp.update()
return true
case <-time.After(timeout):
return false
}
}
func (cp *CP) WatchDog(timeout time.Duration) {
cp.timeout = timeout
go func() {
for ; true; <-time.NewTicker(timeout).C {
cp.mu.Lock()
update := cp.txnId != 0 && time.Since(cp.meterUpdated) > timeout
cp.mu.Unlock()
if update {
Instance().TriggerMessageRequest(cp.ID(), core.MeterValuesFeatureName)
}
}
}()
}
// TransactionID returns the current transaction id
func (cp *CP) TransactionID() int {
cp.mu.Lock()
defer cp.mu.Unlock()
return cp.txnId
}
func (cp *CP) Status() (api.ChargeStatus, error) {
cp.mu.Lock()
defer cp.mu.Unlock()
res := api.StatusNone
if time.Since(cp.updated) > cp.timeout {
return res, api.ErrTimeout
}
if cp.status.ErrorCode != core.NoError {
return res, fmt.Errorf("%s: %s", cp.status.ErrorCode, cp.status.Info)
}
switch cp.status.Status {
case core.ChargePointStatusAvailable, // "Available"
core.ChargePointStatusUnavailable: // "Unavailable"
res = api.StatusA
case
core.ChargePointStatusPreparing, // "Preparing"
core.ChargePointStatusSuspendedEVSE, // "SuspendedEVSE"
core.ChargePointStatusSuspendedEV, // "SuspendedEV"
core.ChargePointStatusFinishing: // "Finishing"
res = api.StatusB
case core.ChargePointStatusCharging: // "Charging"
res = api.StatusC
case core.ChargePointStatusReserved, // "Reserved"
core.ChargePointStatusFaulted: // "Faulted"
return api.StatusF, fmt.Errorf("chargepoint status: %s", cp.status.ErrorCode)
default:
return api.StatusNone, fmt.Errorf("invalid chargepoint status: %s", cp.status.Status)
}
return res, nil
}
var _ api.Meter = (*CP)(nil)
func (cp *CP) CurrentPower() (float64, error) {
cp.mu.Lock()
defer cp.mu.Unlock()
if cp.timeout > 0 && time.Since(cp.meterUpdated) > cp.timeout {
return 0, api.ErrNotAvailable
}
if power, ok := cp.measurements[string(types.MeasurandPowerActiveImport)]; ok {
return strconv.ParseFloat(power.Value, 64)
}
return 0, api.ErrNotAvailable
}
var _ api.MeterEnergy = (*CP)(nil)
func (cp *CP) TotalEnergy() (float64, error) {
cp.mu.Lock()
defer cp.mu.Unlock()
if cp.timeout > 0 && time.Since(cp.meterUpdated) > cp.timeout {
return 0, api.ErrNotAvailable
}
if power, ok := cp.measurements[string(types.MeasurandEnergyActiveImportRegister)]; ok {
f, err := strconv.ParseFloat(power.Value, 64)
return f / 1e3, err
}
return 0, api.ErrNotAvailable
}
func getKeyCurrentPhase(phase int) string {
return string(types.MeasurandCurrentImport) + "@L" + strconv.Itoa(phase)
}
var _ api.MeterCurrent = (*CP)(nil)
func (cp *CP) Currents() (float64, float64, float64, error) {
cp.mu.Lock()
defer cp.mu.Unlock()
if cp.timeout > 0 && time.Since(cp.meterUpdated) > cp.timeout {
return 0, 0, 0, api.ErrNotAvailable
}
currents := make([]float64, 0, 3)
for phase := 1; phase <= 3; phase++ {
current, ok := cp.measurements[getKeyCurrentPhase(phase)]
if !ok {
return 0, 0, 0, api.ErrNotAvailable
}
f, err := strconv.ParseFloat(current.Value, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid current for phase %d: %w", phase, err)
}
currents = append(currents, f)
}
return currents[0], currents[1], currents[2], nil
}