OCPP: clear stale transaction state on Available status (#30247)
This commit is contained in:
parent
a0a110580d
commit
e407006e03
2 changed files with 95 additions and 0 deletions
|
|
@ -28,15 +28,26 @@ func (conn *Connector) OnStatusNotification(request *core.StatusNotificationRequ
|
|||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
|
||||
var applied bool
|
||||
if conn.status == nil {
|
||||
conn.status = request
|
||||
close(conn.statusC) // signal initial status received
|
||||
applied = true
|
||||
} else if request.Timestamp == nil || conn.timestampValid(request.Timestamp.Time) {
|
||||
conn.status = request
|
||||
applied = true
|
||||
} else {
|
||||
conn.log.TRACE.Printf("ignoring status: %s < %s", request.Timestamp.Time, conn.status.Timestamp)
|
||||
}
|
||||
|
||||
// Available means cable unplugged and any prior transaction is stale
|
||||
if applied && request.Status == core.ChargePointStatusAvailable && conn.txnId != 0 {
|
||||
conn.log.DEBUG.Printf("clearing stale transaction %d on Available status", conn.txnId)
|
||||
conn.txnId = 0
|
||||
conn.idTag = ""
|
||||
conn.assumeMeterStopped()
|
||||
}
|
||||
|
||||
if conn.isWaitingForAuth() {
|
||||
if conn.remoteIdTag != "" {
|
||||
// dispatch asynchronously: RemoteStartTransactionRequest issues a
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/benbjohnson/clock"
|
||||
"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"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
|
@ -152,6 +153,89 @@ func (suite *connTestSuite) TestConnectorMeasurementsRunningTxn() {
|
|||
suite.NoError(err, "Voltages")
|
||||
}
|
||||
|
||||
// TestOnStatusNotificationClearsStaleTxn ensures that a transaction left over
|
||||
// from a previous session (e.g. because the charger never sent StopTransaction,
|
||||
// like the Zaptec Go 2 in local OCPP mode) is cleared when the connector
|
||||
// returns to Available, so the next Preparing can trigger RemoteStartTransaction.
|
||||
func (suite *connTestSuite) TestOnStatusNotificationClearsStaleTxn() {
|
||||
suite.conn.remoteIdTag = "evcc"
|
||||
suite.conn.txnId = 42
|
||||
suite.conn.idTag = "stale"
|
||||
|
||||
_, err := suite.conn.OnStatusNotification(&core.StatusNotificationRequest{
|
||||
ConnectorId: 1,
|
||||
Status: core.ChargePointStatusAvailable,
|
||||
ErrorCode: core.NoError,
|
||||
Timestamp: types.NewDateTime(suite.clock.Now()),
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, suite.conn.txnId, "txnId should be cleared on Available")
|
||||
suite.Equal("", suite.conn.idTag, "idTag should be cleared on Available")
|
||||
|
||||
// next Preparing notification must now satisfy NeedsAuthentication
|
||||
_, err = suite.conn.OnStatusNotification(&core.StatusNotificationRequest{
|
||||
ConnectorId: 1,
|
||||
Status: core.ChargePointStatusPreparing,
|
||||
ErrorCode: core.NoError,
|
||||
Timestamp: types.NewDateTime(suite.clock.Now().Add(time.Second)),
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.True(suite.conn.NeedsAuthentication(), "Preparing after Available should require authentication")
|
||||
}
|
||||
|
||||
// TestOnStatusNotificationKeepsActiveTxn ensures that an active transaction is
|
||||
// not cleared by transient status notifications other than Available.
|
||||
func (suite *connTestSuite) TestOnStatusNotificationKeepsActiveTxn() {
|
||||
suite.conn.txnId = 42
|
||||
suite.conn.idTag = "active"
|
||||
|
||||
for _, status := range []core.ChargePointStatus{
|
||||
core.ChargePointStatusCharging,
|
||||
core.ChargePointStatusSuspendedEV,
|
||||
core.ChargePointStatusSuspendedEVSE,
|
||||
core.ChargePointStatusFinishing,
|
||||
} {
|
||||
_, err := suite.conn.OnStatusNotification(&core.StatusNotificationRequest{
|
||||
ConnectorId: 1,
|
||||
Status: status,
|
||||
ErrorCode: core.NoError,
|
||||
Timestamp: types.NewDateTime(suite.clock.Now()),
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.Equalf(42, suite.conn.txnId, "txnId must survive %s", status)
|
||||
suite.Equalf("active", suite.conn.idTag, "idTag must survive %s", status)
|
||||
suite.clock.Add(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnStatusNotificationKeepsTxnOnIgnoredAvailable ensures we do not clear
|
||||
// transaction state when an Available notification is rejected due to an
|
||||
// outdated timestamp (i.e. the cached status remains the current one).
|
||||
func (suite *connTestSuite) TestOnStatusNotificationKeepsTxnOnIgnoredAvailable() {
|
||||
// prime with a recent Charging status
|
||||
_, err := suite.conn.OnStatusNotification(&core.StatusNotificationRequest{
|
||||
ConnectorId: 1,
|
||||
Status: core.ChargePointStatusCharging,
|
||||
ErrorCode: core.NoError,
|
||||
Timestamp: types.NewDateTime(suite.clock.Now()),
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.conn.txnId = 42
|
||||
suite.conn.idTag = "active"
|
||||
|
||||
// out-of-order Available with an older timestamp must be ignored
|
||||
// and must not clear the running transaction
|
||||
_, err = suite.conn.OnStatusNotification(&core.StatusNotificationRequest{
|
||||
ConnectorId: 1,
|
||||
Status: core.ChargePointStatusAvailable,
|
||||
ErrorCode: core.NoError,
|
||||
Timestamp: types.NewDateTime(suite.clock.Now().Add(-time.Minute)),
|
||||
})
|
||||
suite.NoError(err)
|
||||
suite.Equal(42, suite.conn.txnId, "txnId must survive ignored Available")
|
||||
suite.Equal("active", suite.conn.idTag, "idTag must survive ignored Available")
|
||||
}
|
||||
|
||||
func (suite *connTestSuite) TestOnStopTransactionResetsReportedPower() {
|
||||
suite.conn.meterUpdated = suite.clock.Now()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue