233 lines
5.1 KiB
Go
233 lines
5.1 KiB
Go
package ocpp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/evcc-io/evcc/util"
|
|
ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6"
|
|
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
|
|
"github.com/lorenzodonini/ocpp-go/ws"
|
|
)
|
|
|
|
type registration struct {
|
|
mu sync.RWMutex
|
|
setup sync.RWMutex // serialises chargepoint setup
|
|
cp *CP // guarded by setup and CS mutexes
|
|
status map[int]*core.StatusNotificationRequest // guarded by mu mutex
|
|
}
|
|
|
|
func newRegistration() *registration {
|
|
return ®istration{status: make(map[int]*core.StatusNotificationRequest)}
|
|
}
|
|
|
|
type CS struct {
|
|
ocpp16.CentralSystem
|
|
mu sync.Mutex
|
|
log *util.Logger
|
|
regs map[string]*registration // guarded by mu mutex
|
|
txnId atomic.Int64
|
|
publishFunc func()
|
|
server ws.Server // raw server, used by the forwarder to write frames
|
|
}
|
|
|
|
// Write sends a raw OCPP frame to the charger with the given station ID.
|
|
func (cs *CS) Write(id string, data []byte) error {
|
|
return cs.server.Write(id, data)
|
|
}
|
|
|
|
type stationStatus struct {
|
|
ID string `json:"id"`
|
|
Status StationStatus `json:"status"`
|
|
}
|
|
|
|
// Status represents the runtime OCPP status
|
|
type Status struct {
|
|
ExternalUrl string `json:"externalUrl,omitempty"`
|
|
Stations []stationStatus `json:"stations"`
|
|
}
|
|
|
|
// status returns the OCPP runtime status
|
|
func (cs *CS) status() Status {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
|
|
stations := []stationStatus{}
|
|
|
|
for id, reg := range cs.regs {
|
|
if id == "" {
|
|
continue // skip anonymous registrations
|
|
}
|
|
|
|
state := StationStatusUnknown
|
|
if cp := reg.cp; cp != nil {
|
|
if cp.Connected() {
|
|
state = StationStatusConnected
|
|
} else {
|
|
state = StationStatusConfigured
|
|
}
|
|
}
|
|
|
|
stations = append(stations, stationStatus{
|
|
ID: id,
|
|
Status: state,
|
|
})
|
|
}
|
|
|
|
return Status{
|
|
ExternalUrl: ExternalUrl(),
|
|
Stations: stations,
|
|
}
|
|
}
|
|
|
|
// SetUpdated sets a callback function that is called when the status changes
|
|
func (cs *CS) SetUpdated(f func()) {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
cs.publishFunc = f
|
|
}
|
|
|
|
// errorHandler logs error channel
|
|
func (cs *CS) errorHandler(errC <-chan error) {
|
|
for err := range errC {
|
|
cs.log.ERROR.Println(err)
|
|
}
|
|
}
|
|
|
|
// publish triggers the publish callback if set
|
|
func (cs *CS) publish() {
|
|
if cs.publishFunc != nil {
|
|
cs.publishFunc()
|
|
}
|
|
}
|
|
|
|
func (cs *CS) ChargepointByID(id string) (*CP, error) {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
|
|
reg, ok := cs.regs[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown charge point: %s", id)
|
|
}
|
|
if reg.cp == nil {
|
|
return nil, fmt.Errorf("charge point not configured: %s", id)
|
|
}
|
|
return reg.cp, nil
|
|
}
|
|
|
|
func (cs *CS) WithConnectorStatus(id string, connector int, fun func(status *core.StatusNotificationRequest)) {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
|
|
if reg, ok := cs.regs[id]; ok {
|
|
reg.mu.RLock()
|
|
if status, ok := reg.status[connector]; ok {
|
|
fun(status)
|
|
}
|
|
reg.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
// RegisterChargepoint registers a charge point with the central system of returns an already registered charge point
|
|
func (cs *CS) RegisterChargepoint(id string, newfun func() *CP, init func(*CP) error) (*CP, error) {
|
|
cs.mu.Lock()
|
|
|
|
// prepare shadow state
|
|
reg, registered := cs.regs[id]
|
|
if !registered {
|
|
reg = newRegistration()
|
|
cs.regs[id] = reg
|
|
}
|
|
|
|
cs.mu.Unlock()
|
|
cs.publish()
|
|
|
|
// serialise on chargepoint id
|
|
reg.setup.Lock()
|
|
defer reg.setup.Unlock()
|
|
|
|
cs.mu.Lock()
|
|
cp := reg.cp
|
|
cs.mu.Unlock()
|
|
|
|
// setup already completed?
|
|
if cp != nil {
|
|
// duplicate registration of id empty
|
|
if id == "" {
|
|
return nil, errors.New("cannot have >1 charge point with empty station id")
|
|
}
|
|
|
|
return cp, nil
|
|
}
|
|
|
|
// first time- create the charge point
|
|
cp = newfun()
|
|
|
|
cs.mu.Lock()
|
|
reg.cp = cp
|
|
cs.mu.Unlock()
|
|
|
|
if registered {
|
|
cp.onTransportConnect()
|
|
}
|
|
|
|
return cp, init(cp)
|
|
}
|
|
|
|
// NewChargePoint implements ocpp16.ChargePointConnectionHandler
|
|
func (cs *CS) NewChargePoint(chargePoint ocpp16.ChargePointConnection) {
|
|
cs.mu.Lock()
|
|
|
|
// check for configured charge point
|
|
reg, ok := cs.regs[chargePoint.ID()]
|
|
if ok {
|
|
cs.log.DEBUG.Printf("charge point connected: %s", chargePoint.ID())
|
|
|
|
// wait for BootNotification before marking as connected
|
|
if cp := reg.cp; cp != nil {
|
|
cp.onTransportConnect()
|
|
}
|
|
|
|
cs.mu.Unlock()
|
|
cs.publish()
|
|
return
|
|
}
|
|
|
|
// check for configured anonymous charge point
|
|
reg, ok = cs.regs[""]
|
|
if ok && reg.cp != nil {
|
|
cp := reg.cp
|
|
cs.log.INFO.Printf("charge point connected, registering: %s", chargePoint.ID())
|
|
|
|
// update id
|
|
cp.RegisterID(chargePoint.ID())
|
|
cs.regs[chargePoint.ID()] = reg
|
|
delete(cs.regs, "")
|
|
|
|
cp.onTransportConnect()
|
|
|
|
cs.mu.Unlock()
|
|
cs.publish()
|
|
return
|
|
}
|
|
|
|
// register unknown charge point
|
|
cs.regs[chargePoint.ID()] = newRegistration()
|
|
cs.log.INFO.Printf("unknown charge point connected: %s", chargePoint.ID())
|
|
|
|
cs.mu.Unlock()
|
|
cs.publish()
|
|
}
|
|
|
|
// ChargePointDisconnected implements ocpp16.ChargePointConnectionHandler
|
|
func (cs *CS) ChargePointDisconnected(chargePoint ocpp16.ChargePointConnection) {
|
|
cs.log.DEBUG.Printf("charge point disconnected: %s", chargePoint.ID())
|
|
|
|
if cp, err := cs.ChargepointByID(chargePoint.ID()); err == nil {
|
|
cp.connect(false)
|
|
}
|
|
|
|
cs.publish()
|
|
}
|