From 4786525599a35783d9736891fe0373226c2cb103 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Fri, 9 Dec 2022 16:18:35 +0100 Subject: [PATCH 01/14] Wait for EVSE status change on token authorization, create the session if the EVSE starts charging but no session exists --- internal/command/v2.1.1/mocks/mock_resolve.go | 9 +- internal/command/v2.1.1/process.go | 17 +- internal/command/v2.1.1/push_test.go | 32 ++- .../connector/v2.1.1/mocks/mock_resolve.go | 4 +- internal/connector/v2.1.1/resolve_test.go | 5 +- internal/evse/v2.1.1/resolve_test.go | 3 +- .../location/v2.1.1/mocks/mock_resolve.go | 4 +- internal/location/v2.1.1/resolve_test.go | 4 + internal/rpc/command/rpc.go | 2 +- internal/session/v2.1.1/mocks/mock_resolve.go | 4 +- internal/session/v2.1.1/process.go | 43 +++- internal/sync/htb/service.go | 3 +- internal/token/v2.1.1/mocks/mock_resolve.go | 2 + internal/token/v2.1.1/push_test.go | 16 ++ .../v2.1.1/mocks/mock_resolve.go | 23 +- internal/tokenauthorization/v2.1.1/process.go | 222 +++++++++++++++++- internal/tokenauthorization/v2.1.1/push.go | 4 +- internal/tokenauthorization/v2.1.1/resolve.go | 42 +++- 18 files changed, 389 insertions(+), 50 deletions(-) diff --git a/internal/command/v2.1.1/mocks/mock_resolve.go b/internal/command/v2.1.1/mocks/mock_resolve.go index b31eb855..f8a88cc8 100644 --- a/internal/command/v2.1.1/mocks/mock_resolve.go +++ b/internal/command/v2.1.1/mocks/mock_resolve.go @@ -6,13 +6,14 @@ import ( command "github.com/satimoto/go-ocpi/internal/command/v2.1.1" "github.com/satimoto/go-ocpi/internal/service" token "github.com/satimoto/go-ocpi/internal/token/v2.1.1/mocks" + versiondetail "github.com/satimoto/go-ocpi/internal/versiondetail/mocks" ) - func NewResolver(repositoryService *mocks.MockRepositoryService, services *service.ServiceResolver) *command.CommandResolver { return &command.CommandResolver{ - Repository: commandMocks.NewRepository(repositoryService), - OcpiService: services.OcpiService, - TokenResolver: token.NewResolver(repositoryService, services), + Repository: commandMocks.NewRepository(repositoryService), + OcpiService: services.OcpiService, + TokenResolver: token.NewResolver(repositoryService, services), + VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } } diff --git a/internal/command/v2.1.1/process.go b/internal/command/v2.1.1/process.go index 6e82d233..0a6c1949 100644 --- a/internal/command/v2.1.1/process.go +++ b/internal/command/v2.1.1/process.go @@ -32,12 +32,27 @@ func (r *CommandResolver) UpdateCommandStart(ctx context.Context, command db.Com commandParams.Status = *commandResponseDto.Result commandParams.LastUpdated = time.Now().UTC() - _, err := r.Repository.UpdateCommandStart(ctx, commandParams) + updatedCommand, err := r.Repository.UpdateCommandStart(ctx, commandParams) if err != nil { metrics.RecordError("OCPI039", "Error updating command start", err) log.Printf("OCPI039: Params=%#v", commandParams) } + + if updatedCommand.Status == db.CommandResponseTypeREJECTED && updatedCommand.AuthorizationID.Valid { + // Update the rejected commands token authorization + updateTokenAuthorizationByAuthorizationIDParams := db.UpdateTokenAuthorizationByAuthorizationIDParams{ + AuthorizationID: updatedCommand.AuthorizationID.String, + Authorized: false, + } + + _, err := r.TokenResolver.TokenAuthorizationResolver.Repository.UpdateTokenAuthorizationByAuthorizationID(ctx, updateTokenAuthorizationByAuthorizationIDParams) + + if err != nil { + metrics.RecordError("OCPI325", "Error updating token authorization", err) + log.Printf("OCPI325: Params=%#v", updateTokenAuthorizationByAuthorizationIDParams) + } + } } } diff --git a/internal/command/v2.1.1/push_test.go b/internal/command/v2.1.1/push_test.go index fd67866a..cb6916f7 100644 --- a/internal/command/v2.1.1/push_test.go +++ b/internal/command/v2.1.1/push_test.go @@ -54,6 +54,8 @@ func TestCommandReservationRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -78,6 +80,8 @@ func TestCommandReservationRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -105,6 +109,8 @@ func TestCommandReservationRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -129,7 +135,7 @@ func TestCommandReservationRequest(t *testing.T) { commandRoutes.ServeHTTP(responseRecorder, request) - commandParams, err := mockRepository.GetUpdateCommandReservationMockData() + commandParams, _ := mockRepository.GetUpdateCommandReservationMockData() paramsJson, _ := json.Marshal(commandParams) mocks.CompareJsonWithDifference(t, paramsJson, []byte(`{ @@ -154,6 +160,8 @@ func TestCommandStartRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -178,6 +186,8 @@ func TestCommandStartRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -205,6 +215,8 @@ func TestCommandStartRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -227,7 +239,7 @@ func TestCommandStartRequest(t *testing.T) { commandRoutes.ServeHTTP(responseRecorder, request) - commandParams, err := mockRepository.GetUpdateCommandStartMockData() + commandParams, _ := mockRepository.GetUpdateCommandStartMockData() paramsJson, _ := json.Marshal(commandParams) mocks.CompareJsonWithDifference(t, paramsJson, []byte(`{ @@ -250,6 +262,8 @@ func TestCommandStopRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -274,6 +288,8 @@ func TestCommandStopRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -301,6 +317,8 @@ func TestCommandStopRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -322,7 +340,7 @@ func TestCommandStopRequest(t *testing.T) { commandRoutes.ServeHTTP(responseRecorder, request) - commandParams, err := mockRepository.GetUpdateCommandStopMockData() + commandParams, _ := mockRepository.GetUpdateCommandStopMockData() paramsJson, _ := json.Marshal(commandParams) mocks.CompareJsonWithDifference(t, paramsJson, []byte(`{ @@ -345,6 +363,8 @@ func TestCommandUnlockRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -369,6 +389,8 @@ func TestCommandUnlockRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -396,6 +418,8 @@ func TestCommandUnlockRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + commandResolver := commandMocks.NewResolver(mockRepository, mockServices) commandRoutes := setupRoutes(commandResolver) responseRecorder := httptest.NewRecorder() @@ -419,7 +443,7 @@ func TestCommandUnlockRequest(t *testing.T) { commandRoutes.ServeHTTP(responseRecorder, request) - commandParams, err := mockRepository.GetUpdateCommandUnlockMockData() + commandParams, _ := mockRepository.GetUpdateCommandUnlockMockData() paramsJson, _ := json.Marshal(commandParams) mocks.CompareJsonWithDifference(t, paramsJson, []byte(`{ diff --git a/internal/connector/v2.1.1/mocks/mock_resolve.go b/internal/connector/v2.1.1/mocks/mock_resolve.go index 58a5bfdd..0707a961 100644 --- a/internal/connector/v2.1.1/mocks/mock_resolve.go +++ b/internal/connector/v2.1.1/mocks/mock_resolve.go @@ -3,11 +3,13 @@ package mocks import ( connectorMocks "github.com/satimoto/go-datastore/pkg/connector/mocks" mocks "github.com/satimoto/go-datastore/pkg/db/mocks" + party "github.com/satimoto/go-datastore/pkg/party/mocks" connector "github.com/satimoto/go-ocpi/internal/connector/v2.1.1" ) func NewResolver(repositoryService *mocks.MockRepositoryService) *connector.ConnectorResolver { return &connector.ConnectorResolver{ - Repository: connectorMocks.NewRepository(repositoryService), + Repository: connectorMocks.NewRepository(repositoryService), + PartyRepository: party.NewRepository(repositoryService), } } diff --git a/internal/connector/v2.1.1/resolve_test.go b/internal/connector/v2.1.1/resolve_test.go index 1c24437e..4499412e 100644 --- a/internal/connector/v2.1.1/resolve_test.go +++ b/internal/connector/v2.1.1/resolve_test.go @@ -64,7 +64,7 @@ func TestReplaceConnector(t *testing.T) { "standard": "IEC_62196_T2", "format": "CABLE", "powerType": "AC_3_PHASE", - "publish": true, + "isPublished": true, "voltage": 220, "amperage": 16, "wattage": 10560, @@ -121,7 +121,8 @@ func TestReplaceConnector(t *testing.T) { "standard": "IEC_62196_T2", "format": "CABLE", "powerType": "AC_3_PHASE", - "publish": true, + "isPublished": true, + "isRemoved": false, "voltage": 220, "amperage": 16, "wattage": 10560, diff --git a/internal/evse/v2.1.1/resolve_test.go b/internal/evse/v2.1.1/resolve_test.go index 5a3e1dbb..189472d9 100644 --- a/internal/evse/v2.1.1/resolve_test.go +++ b/internal/evse/v2.1.1/resolve_test.go @@ -226,7 +226,8 @@ func TestReplaceEvse(t *testing.T) { "standard": "IEC_62196_T2", "format": "CABLE", "powerType": "AC_3_PHASE", - "publish": true, + "isPublished": true, + "isRemoved": false, "voltage": 220, "amperage": 16, "wattage": 10560, diff --git a/internal/location/v2.1.1/mocks/mock_resolve.go b/internal/location/v2.1.1/mocks/mock_resolve.go index bd62e7b0..26917f77 100644 --- a/internal/location/v2.1.1/mocks/mock_resolve.go +++ b/internal/location/v2.1.1/mocks/mock_resolve.go @@ -3,6 +3,7 @@ package mocks import ( mocks "github.com/satimoto/go-datastore/pkg/db/mocks" locationMocks "github.com/satimoto/go-datastore/pkg/location/mocks" + party "github.com/satimoto/go-datastore/pkg/party/mocks" businessdetail "github.com/satimoto/go-ocpi/internal/businessdetail/mocks" displaytext "github.com/satimoto/go-ocpi/internal/displaytext/mocks" energymix "github.com/satimoto/go-ocpi/internal/energymix/mocks" @@ -21,12 +22,13 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi Repository: locationMocks.NewRepository(repositoryService), OcpiService: services.OcpiService, BusinessDetailResolver: businessdetail.NewResolver(repositoryService), + DisplayTextResolver: displaytext.NewResolver(repositoryService), EnergyMixResolver: energymix.NewResolver(repositoryService), EvseResolver: evse.NewResolver(repositoryService), - DisplayTextResolver: displaytext.NewResolver(repositoryService), GeoLocationResolver: geolocation.NewResolver(repositoryService), ImageResolver: image.NewResolver(repositoryService), OpeningTimeResolver: openingtime.NewResolver(repositoryService), + PartyRepository: party.NewRepository(repositoryService), TariffResolver: tariff.NewResolver(repositoryService, services), VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } diff --git a/internal/location/v2.1.1/resolve_test.go b/internal/location/v2.1.1/resolve_test.go index c66932db..6ee41b60 100644 --- a/internal/location/v2.1.1/resolve_test.go +++ b/internal/location/v2.1.1/resolve_test.go @@ -80,6 +80,8 @@ func TestReplaceLocation(t *testing.T) { "geoLocationID": 1, "availableEvses": 0, "totalEvses": 0, + "isIntermediateCdrCapable": false, + "isPublished": false, "isRemoteCapable": false, "isRfidCapable": false, "energyMixID": {"Int64": 0, "Valid": false}, @@ -170,6 +172,7 @@ func TestReplaceLocation(t *testing.T) { "geoLocationID": 1, "availableEvses": 0, "totalEvses": 0, + "isIntermediateCdrCapable": false, "isRemoteCapable": false, "isRfidCapable": false, "energyMixID": {"Int64": 0, "Valid": false}, @@ -258,6 +261,7 @@ func TestReplaceLocation(t *testing.T) { "geoLocationID": 1, "availableEvses": 0, "totalEvses": 0, + "isIntermediateCdrCapable": false, "isRemoteCapable": false, "isRfidCapable": false, "energyMixID": {"Int64": 0, "Valid": false}, diff --git a/internal/rpc/command/rpc.go b/internal/rpc/command/rpc.go index 7e6e6f73..cfd06a35 100644 --- a/internal/rpc/command/rpc.go +++ b/internal/rpc/command/rpc.go @@ -122,7 +122,7 @@ func (r *RpcCommandResolver) StartSession(ctx context.Context, input *ocpirpc.St locationReferencesDto.EvseUids = []*string{&input.EvseUid} } - tokenAuthorization, err := r.TokenResolver.TokenAuthorizationResolver.CreateTokenAuthorization(ctx, token, locationReferencesDto) + tokenAuthorization, err := r.TokenResolver.TokenAuthorizationResolver.CreateTokenAuthorization(ctx, credential, token, locationReferencesDto) if err != nil { metrics.RecordError("OCPI151", "Error creating token authorization", err) diff --git a/internal/session/v2.1.1/mocks/mock_resolve.go b/internal/session/v2.1.1/mocks/mock_resolve.go index 08b69a0d..3b152c4c 100644 --- a/internal/session/v2.1.1/mocks/mock_resolve.go +++ b/internal/session/v2.1.1/mocks/mock_resolve.go @@ -7,6 +7,7 @@ import ( token "github.com/satimoto/go-datastore/pkg/token/mocks" tokenauthorization "github.com/satimoto/go-datastore/pkg/tokenauthorization/mocks" chargingperiod "github.com/satimoto/go-ocpi/internal/chargingperiod/mocks" + command "github.com/satimoto/go-ocpi/internal/command/v2.1.1/mocks" location "github.com/satimoto/go-ocpi/internal/location/v2.1.1/mocks" "github.com/satimoto/go-ocpi/internal/service" session "github.com/satimoto/go-ocpi/internal/session/v2.1.1" @@ -18,10 +19,11 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi Repository: sessionMocks.NewRepository(repositoryService), OcpiService: services.OcpiService, ChargingPeriodResolver: chargingperiod.NewResolver(repositoryService), + CommandResolver: command.NewResolver(repositoryService, services), LocationResolver: location.NewResolver(repositoryService, services), NodeRepository: node.NewRepository(repositoryService), - VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), TokenRepository: token.NewRepository(repositoryService), TokenAuthorizationRepository: tokenauthorization.NewRepository(repositoryService), + VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } } diff --git a/internal/session/v2.1.1/process.go b/internal/session/v2.1.1/process.go index db347739..202a96f8 100644 --- a/internal/session/v2.1.1/process.go +++ b/internal/session/v2.1.1/process.go @@ -36,9 +36,20 @@ func (r *SessionResolver) ReplaceSessionByIdentifier(ctx context.Context, creden sessionCreated := false statusChanged := false + if err != nil && sessionDto.AuthorizationID != nil { + // Check there is no existing session with the same AuthorizationID + // This would have been created if no session was initially received + session, err = r.Repository.GetSessionByAuthorizationID(ctx, *sessionDto.AuthorizationID) + sessionDto.ID = &uid + } + if err == nil { sessionParams := param.NewUpdateSessionByUidParams(session) + if sessionDto.ID != nil { + sessionParams.Uid = *sessionDto.ID + } + if sessionDto.AuthMethod != nil { sessionParams.AuthMethod = *sessionDto.AuthMethod } @@ -178,7 +189,7 @@ func (r *SessionResolver) ReplaceSessionByIdentifier(ctx context.Context, creden go r.sendOcpiRequest(session, sessionCreated, statusChanged) if sessionCreated && session.Status == db.SessionStatusTypePENDING { - go r.waitForEvseStatus(credential, session.LocationID, session.EvseID, db.EvseStatusCHARGING, session.ID, session.Status, db.SessionStatusTypeACTIVE, 150) + go r.waitForEvseStatus(credential, session.LocationID, session.EvseID, db.EvseStatusCHARGING, session, session.Status, db.SessionStatusTypeACTIVE, 150) } return &session @@ -243,6 +254,7 @@ func (r *SessionResolver) sendOcpiRequest(session db.Session, sessionCreated, st if err != nil { metrics.RecordError("OCPI167", "Error retrieving node", err) log.Printf("OCPI167: UserID=%v", session.UserID) + return } // TODO: Handle failed RPC call more robustly @@ -267,7 +279,7 @@ func (r *SessionResolver) sendOcpiRequest(session db.Session, sessionCreated, st } } -func (r *SessionResolver) waitForEvseStatus(credential db.Credential, locationID, evseID int64, evseStatus db.EvseStatus, sessionID int64, sessionFromStatus db.SessionStatusType, sessionToStatus db.SessionStatusType, timeoutSeconds int) { +func (r *SessionResolver) waitForEvseStatus(credential db.Credential, locationID, evseID int64, evseStatus db.EvseStatus, sess db.Session, sessionFromStatus db.SessionStatusType, sessionToStatus db.SessionStatusType, timeoutSeconds int) { ctx := context.Background() deadline := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) log.Printf("Waiting for Evse status change to %v over %v seconds: LocationID=%v, EvseID=%v", evseStatus, timeoutSeconds, locationID, evseID) @@ -307,12 +319,23 @@ func (r *SessionResolver) waitForEvseStatus(credential db.Credential, locationID header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) +waitLoop: for { time.Sleep(10 * time.Second) if time.Now().After(deadline) { - log.Printf("Stopped waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) - break + log.Printf("Timeout. Stopped waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) + break waitLoop + } + + if sess.AuthorizationID.Valid { + tokenAuthorization, err := r.TokenAuthorizationRepository.GetTokenAuthorizationByAuthorizationID(ctx, sess.AuthorizationID.String) + + if err != nil && !tokenAuthorization.Authorized { + // Token authorization has been unauthorized + log.Printf("Unauthorized. Stop waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) + break waitLoop + } } response, err := r.OcpiService.Do(http.MethodGet, requestUrl.String(), header, nil) @@ -338,25 +361,25 @@ func (r *SessionResolver) waitForEvseStatus(credential db.Credential, locationID log.Printf("Evse status is %v: LocationID=%v, EvseID=%v", responseEvseStatus, locationID, evseID) if responseEvseStatus == evseStatus { - session, err := r.Repository.GetSession(ctx, sessionID) + updatedSession, err := r.Repository.GetSession(ctx, sess.ID) if err != nil { metrics.RecordError("OCPI308", "Error getting session", err) - log.Printf("OCPI308: SessionID=%v", sessionID) + log.Printf("OCPI308: SessionID=%v", sess.ID) continue } - if session.Status == sessionFromStatus { - log.Printf("Manually updating session status to %v: SessionUid=%v", sessionToStatus, session.Uid) + if updatedSession.Status == sessionFromStatus { + log.Printf("Manually updating session status to %v: SessionUid=%v", sessionToStatus, updatedSession.Uid) sessionDto := dto.SessionDto{ Status: &sessionToStatus, } - r.ReplaceSessionByIdentifier(ctx, credential, util.NilString(session.CountryCode), util.NilString(session.PartyID), session.Uid, &sessionDto) + r.ReplaceSessionByIdentifier(ctx, credential, util.NilString(updatedSession.CountryCode), util.NilString(updatedSession.PartyID), updatedSession.Uid, &sessionDto) } - break + break waitLoop } } } diff --git a/internal/sync/htb/service.go b/internal/sync/htb/service.go index 37c4121a..9ccf6b3a 100644 --- a/internal/sync/htb/service.go +++ b/internal/sync/htb/service.go @@ -90,8 +90,9 @@ func (r *HtbService) updateConnectors(ctx context.Context) { for _, connector := range connectors { connectorParams := param.NewUpdateConnectorByEvseParams(connector) connectorParams.TariffID = tariffID + connectorParams.IsPublished = tariffID.Valid - if connector.TariffID.String != connectorParams.TariffID.String { + if connector.TariffID.String != connectorParams.TariffID.String || connector.IsPublished != connectorParams.IsPublished { _, err = r.ConnectorRepository.UpdateConnectorByEvse(ctx, connectorParams) if err != nil { diff --git a/internal/token/v2.1.1/mocks/mock_resolve.go b/internal/token/v2.1.1/mocks/mock_resolve.go index e60ccf56..f0afaf1a 100644 --- a/internal/token/v2.1.1/mocks/mock_resolve.go +++ b/internal/token/v2.1.1/mocks/mock_resolve.go @@ -6,6 +6,7 @@ import ( "github.com/satimoto/go-ocpi/internal/service" token "github.com/satimoto/go-ocpi/internal/token/v2.1.1" tokenauthorization "github.com/satimoto/go-ocpi/internal/tokenauthorization/v2.1.1/mocks" + versiondetail "github.com/satimoto/go-ocpi/internal/versiondetail/mocks" ) func NewResolver(repositoryService *mocks.MockRepositoryService, services *service.ServiceResolver) *token.TokenResolver { @@ -13,5 +14,6 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi Repository: tokenMocks.NewRepository(repositoryService), OcpiService: services.OcpiService, TokenAuthorizationResolver: tokenauthorization.NewResolver(repositoryService, services), + VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } } diff --git a/internal/token/v2.1.1/push_test.go b/internal/token/v2.1.1/push_test.go index 63b90c5a..9d12b59c 100644 --- a/internal/token/v2.1.1/push_test.go +++ b/internal/token/v2.1.1/push_test.go @@ -42,6 +42,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() @@ -68,6 +70,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() @@ -119,6 +123,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() @@ -143,6 +149,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() @@ -168,6 +176,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() @@ -210,6 +220,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() @@ -257,6 +269,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() @@ -305,6 +319,8 @@ func TestTokenRequest(t *testing.T) { mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + mockRepository.SetGetCredentialByServerTokenMockData(dbMocks.CredentialMockData{Credential: db.Credential{ID: 1}}) + tokenResolver := tokenMocks.NewResolver(mockRepository, mockServices) tokenRoutes := setupRoutes(tokenResolver) responseRecorder := httptest.NewRecorder() diff --git a/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go b/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go index 9f9b5593..889a03a9 100644 --- a/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go +++ b/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go @@ -2,21 +2,32 @@ package mocks import ( mocks "github.com/satimoto/go-datastore/pkg/db/mocks" + location "github.com/satimoto/go-datastore/pkg/location/mocks" + node "github.com/satimoto/go-datastore/pkg/node/mocks" + session "github.com/satimoto/go-datastore/pkg/session/mocks" + tariff "github.com/satimoto/go-datastore/pkg/tariff/mocks" tokenauthorizationMocks "github.com/satimoto/go-datastore/pkg/tokenauthorization/mocks" user "github.com/satimoto/go-datastore/pkg/user/mocks" connector "github.com/satimoto/go-ocpi/internal/connector/v2.1.1/mocks" evse "github.com/satimoto/go-ocpi/internal/evse/v2.1.1/mocks" "github.com/satimoto/go-ocpi/internal/service" tokenauthorization "github.com/satimoto/go-ocpi/internal/tokenauthorization/v2.1.1" + versiondetail "github.com/satimoto/go-ocpi/internal/versiondetail/mocks" ) func NewResolver(repositoryService *mocks.MockRepositoryService, services *service.ServiceResolver) *tokenauthorization.TokenAuthorizationResolver { return &tokenauthorization.TokenAuthorizationResolver{ - Repository: tokenauthorizationMocks.NewRepository(repositoryService), - AsyncService: services.AsyncService, - NotificationService: services.NotificationService, - ConnectorResolver: connector.NewResolver(repositoryService), - EvseResolver: evse.NewResolver(repositoryService), - UserRepository: user.NewRepository(repositoryService), + Repository: tokenauthorizationMocks.NewRepository(repositoryService), + OcpiService: services.OcpiService, + AsyncService: services.AsyncService, + NotificationService: services.NotificationService, + ConnectorResolver: connector.NewResolver(repositoryService), + EvseResolver: evse.NewResolver(repositoryService), + LocationRepository: location.NewRepository(repositoryService), + NodeRepository: node.NewRepository(repositoryService), + SessionRepository: session.NewRepository(repositoryService), + TariffRespository: tariff.NewRepository(repositoryService), + UserRepository: user.NewRepository(repositoryService), + VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } } diff --git a/internal/tokenauthorization/v2.1.1/process.go b/internal/tokenauthorization/v2.1.1/process.go index 49ba04fb..16b61bc5 100644 --- a/internal/tokenauthorization/v2.1.1/process.go +++ b/internal/tokenauthorization/v2.1.1/process.go @@ -3,7 +3,10 @@ package tokenauthorization import ( "context" "errors" + "fmt" "log" + "net/http" + "net/url" "time" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -11,10 +14,14 @@ import ( "github.com/satimoto/go-datastore/pkg/param" "github.com/satimoto/go-datastore/pkg/util" dto "github.com/satimoto/go-ocpi/internal/dto/v2.1.1" + coreLocation "github.com/satimoto/go-ocpi/internal/location" metrics "github.com/satimoto/go-ocpi/internal/metric" + "github.com/satimoto/go-ocpi/internal/transportation" + "github.com/satimoto/go-ocpi/pkg/ocpi" + ocpiSession "github.com/satimoto/go-ocpi/pkg/ocpi/session" ) -func (r *TokenAuthorizationResolver) CreateTokenAuthorization(ctx context.Context, token db.Token, locationReferencesDto *dto.LocationReferencesDto) (*db.TokenAuthorization, error) { +func (r *TokenAuthorizationResolver) CreateTokenAuthorization(ctx context.Context, credential db.Credential, token db.Token, locationReferencesDto *dto.LocationReferencesDto) (*db.TokenAuthorization, error) { if token.Type == db.TokenTypeRFID { // Check if user is restricted, has a node and has been active user, err := r.UserRepository.GetUserByTokenID(ctx, token.ID) @@ -54,7 +61,7 @@ func (r *TokenAuthorizationResolver) CreateTokenAuthorization(ctx context.Contex return nil, errors.New("Authorization error") } - r.createTokenAuthorizationRelations(ctx, tokenAuthorization.ID, locationReferencesDto) + evseUids, _ := r.createTokenAuthorizationRelations(ctx, tokenAuthorization.ID, locationReferencesDto) if !tokenAuthorizationParams.Authorized { // Token authentication is not authorized because its initiated @@ -96,21 +103,29 @@ func (r *TokenAuthorizationResolver) CreateTokenAuthorization(ctx context.Contex updateTokenAuthorizationParams := param.NewUpdateTokenAuthorizationParams(tokenAuthorization) updateTokenAuthorizationParams.Authorized = true - r.Repository.UpdateTokenAuthorizationByAuthorizationID(ctx, updateTokenAuthorizationParams) + updatedTokenAuthorization, err := r.Repository.UpdateTokenAuthorizationByAuthorizationID(ctx, updateTokenAuthorizationParams) if err != nil { metrics.RecordError("OCPI287", "Error updating token authorization", err) log.Printf("OCPI287: Params=%#v", updateTokenAuthorizationParams) + } else { + tokenAuthorization = updatedTokenAuthorization } } + if tokenAuthorization.Authorized && locationReferencesDto.LocationID != nil { + go r.waitForEvsesStatus(credential, token, tokenAuthorization, *locationReferencesDto.LocationID, evseUids, db.EvseStatusCHARGING, 150) + } + return &tokenAuthorization, nil } -func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx context.Context, tokenAuthorizationID int64, locationReferencesDto *dto.LocationReferencesDto) { +func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx context.Context, tokenAuthorizationID int64, locationReferencesDto *dto.LocationReferencesDto) (evseUids, connectorUids []string) { if locationReferencesDto != nil { for _, evseUid := range locationReferencesDto.EvseUids { if evse, err := r.EvseResolver.Repository.GetEvseByUid(ctx, *evseUid); err == nil { + evseUids = append(evseUids, *evseUid) + setTokenAuthorizationEvseParams := db.SetTokenAuthorizationEvseParams{ TokenAuthorizationID: tokenAuthorizationID, EvseID: evse.ID, @@ -130,6 +145,8 @@ func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx conte } if connector, err := r.ConnectorResolver.Repository.GetConnectorByEvse(ctx, getConnectorByEvseParams); err == nil { + connectorUids = append(connectorUids, *connectorId) + setTokenAuthorizationConnectorParams := db.SetTokenAuthorizationConnectorParams{ TokenAuthorizationID: tokenAuthorizationID, ConnectorID: connector.ID, @@ -146,6 +163,8 @@ func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx conte } } } + + return evseUids, connectorUids } func (r *TokenAuthorizationResolver) createTokenAuthorizationSigningKey() []byte { @@ -160,3 +179,198 @@ func (r *TokenAuthorizationResolver) createTokenAuthorizationSigningKey() []byte return privateKey.Serialize() } + +func (r *TokenAuthorizationResolver) waitForEvsesStatus(credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, locationUid string, evseUids []string, evseStatus db.EvseStatus, timeoutSeconds int) { + cancelCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + for _, evseUid := range evseUids { + go r.waitForEvseStatus(credential, token, tokenAuthorization, locationUid, evseUid, evseStatus, cancelCtx, cancel, timeoutSeconds) + } + + <-cancelCtx.Done() +} + +func (r *TokenAuthorizationResolver) waitForEvseStatus(credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, locationUid, evseUid string, evseStatus db.EvseStatus, cancelCtx context.Context, cancel context.CancelFunc, timeoutSeconds int) { + defer cancel() + + ctx := context.Background() + deadline := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) + log.Printf("Waiting for Evse status change to %v over %v seconds: LocationUid=%v, EvseUid=%v", evseStatus, timeoutSeconds, locationUid, evseUid) + + versionEndpoint, err := r.VersionDetailResolver.GetVersionEndpointByIdentity(ctx, coreLocation.IDENTIFIER, credential.CountryCode, credential.PartyID) + + if err != nil { + metrics.RecordError("OCPI302", "Error getting version endpoint", err) + log.Printf("OCPI302: CountryCode=%v, PartyID=%v, Identifier=%v", credential.CountryCode, credential.PartyID, coreLocation.IDENTIFIER) + return + } + + evseUrl := fmt.Sprintf("%s/%s/%s", versionEndpoint.Url, locationUid, evseUid) + requestUrl, err := url.Parse(evseUrl) + + if err != nil { + metrics.RecordError("OCPI305", "Error parsing url", err) + log.Printf("OCPI305: Url=%v", evseUrl) + return + } + + header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) + +waitLoop: + for { + select { + case <-cancelCtx.Done(): + log.Printf("Cancelled. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + case <-time.After(10 * time.Second): + } + + if time.Now().After(deadline) { + log.Printf("Timeout. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + } + + _, err := r.SessionRepository.GetSessionByAuthorizationID(ctx, tokenAuthorization.AuthorizationID) + + if err == nil { + // Session found + log.Printf("Session. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + } + + updatedTokenAuthorization, err := r.Repository.GetTokenAuthorizationByAuthorizationID(ctx, tokenAuthorization.AuthorizationID) + + if err != nil && !updatedTokenAuthorization.Authorized { + // Token authorization has been unauthorized + log.Printf("Unauthorized. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + } + + response, err := r.OcpiService.Do(http.MethodGet, requestUrl.String(), header, nil) + + if err != nil { + metrics.RecordError("OCPI306", "Error making request", err) + log.Printf("OCPI306: Method=%v, Url=%v, Header=%#v", http.MethodGet, requestUrl.String(), header) + continue + } + + evseDto, err := r.EvseResolver.UnmarshalPullDto(response.Body) + defer response.Body.Close() + + if err != nil { + metrics.RecordError("OCPI307", "Error unmarshaling response", err) + util.LogHttpResponse("OCPI307", requestUrl.String(), response, true) + continue + } + + if evseDto.StatusCode == transportation.STATUS_CODE_OK && evseDto.Data.Status != nil { + responseEvseStatus := *evseDto.Data.Status + + log.Printf("Evse status is %v: LocationUid=%v, EvseUid=%v", responseEvseStatus, locationUid, evseUid) + + if responseEvseStatus == evseStatus { + log.Printf("Manually creating session %v", tokenAuthorization.AuthorizationID) + r.createSession(ctx, credential, token, tokenAuthorization, db.SessionStatusTypeACTIVE, locationUid, evseUid) + + break waitLoop + } + } + } +} + +func (r *TokenAuthorizationResolver) createSession(ctx context.Context, credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, status db.SessionStatusType, locationUid, evseUid string) { + timeNow := time.Now().UTC() + location, err := r.LocationRepository.GetLocationByUid(ctx, locationUid) + + if err != nil { + metrics.RecordError("OCPI317", "Error getting location", err) + log.Printf("OCPI317: LocationUid=%v", locationUid) + return + } + + evse, err := r.EvseResolver.Repository.GetEvseByUid(ctx, evseUid) + + if err != nil { + metrics.RecordError("OCPI318", "Error getting evse", err) + log.Printf("OCPI318: EvseUid=%v", evseUid) + return + } + + connectors, err := r.EvseResolver.ConnectorResolver.Repository.ListConnectors(ctx, evse.ID) + + if err != nil || len(connectors) == 0 { + metrics.RecordError("OCPI319", "Error getting connector", err) + log.Printf("OCPI319: EvseUid=%v", evseUid) + return + } + + connector := connectors[0] + + if !connector.TariffID.Valid { + metrics.RecordError("OCPI320", "Error no valid tariff", err) + log.Printf("OCPI320: ConnectorUid=%v", connector.Uid) + return + } + + tariff, err := r.TariffRespository.GetTariffByUid(ctx, connector.TariffID.String) + + if err != nil { + metrics.RecordError("OCPI321", "Error getting tariff", err) + log.Printf("OCPI321: TariffUid=%v", connector.TariffID.String) + return + } + + createSessionParams := db.CreateSessionParams{ + Uid: tokenAuthorization.AuthorizationID, + CredentialID: credential.ID, + CountryCode: location.CountryCode, + PartyID: location.PartyID, + AuthorizationID: util.SqlNullString(tokenAuthorization.AuthorizationID), + StartDatetime: timeNow, + Kwh: 0, + AuthID: token.AuthID, + AuthMethod: db.AuthMethodTypeAUTHREQUEST, + UserID: token.UserID, + TokenID: token.ID, + LocationID: location.ID, + EvseID: evse.ID, + ConnectorID: connector.ID, + Currency: tariff.Currency, + TotalCost: util.SqlNullFloat64(0), + Status: status, + LastUpdated: timeNow, + } + + session, err := r.SessionRepository.CreateSession(ctx, createSessionParams) + + if err != nil { + metrics.RecordError("OCPI322", "Error creating session", err) + log.Printf("OCPI322: Params=%#v", createSessionParams) + return + } + + go r.sendOcpiRequest(session) +} + +func (r *TokenAuthorizationResolver) sendOcpiRequest(session db.Session) { + ctx := context.Background() + node, err := r.NodeRepository.GetNodeByUserID(ctx, session.UserID) + + if err != nil { + metrics.RecordError("OCPI323", "Error retrieving node", err) + log.Printf("OCPI323: UserID=%v", session.UserID) + return + } + + // TODO: Handle failed RPC call more robustly + ocpiService := ocpi.NewService(node.LspAddr) + + sessionCreatedRequest := ocpiSession.NewSessionCreatedRequest(session) + sessionCreatedResponse, err := ocpiService.SessionCreated(ctx, sessionCreatedRequest) + + if err != nil { + metrics.RecordError("OCPI324", "Error calling RPC service", err) + log.Printf("OCPI324: Request=%#v, Response=%#v", sessionCreatedRequest, sessionCreatedResponse) + } +} diff --git a/internal/tokenauthorization/v2.1.1/push.go b/internal/tokenauthorization/v2.1.1/push.go index 5e440ce9..1b368a6e 100644 --- a/internal/tokenauthorization/v2.1.1/push.go +++ b/internal/tokenauthorization/v2.1.1/push.go @@ -8,11 +8,13 @@ import ( "github.com/satimoto/go-datastore/pkg/db" coreDto "github.com/satimoto/go-ocpi/internal/dto" dto "github.com/satimoto/go-ocpi/internal/dto/v2.1.1" + "github.com/satimoto/go-ocpi/internal/middleware" "github.com/satimoto/go-ocpi/internal/transportation" ) func (r *TokenAuthorizationResolver) AuthorizeToken(rw http.ResponseWriter, request *http.Request) { ctx := request.Context() + cred := middleware.GetCredential(ctx) token := ctx.Value("token").(db.Token) authorizationInfoDto := dto.NewAuthorizationInfoDto(token.Allowed) @@ -25,7 +27,7 @@ func (r *TokenAuthorizationResolver) AuthorizeToken(rw http.ResponseWriter, requ } // TODO: we should reject an authorization to an unknown location/evse/connector - tokenAuthorization, err := r.CreateTokenAuthorization(ctx, token, locationReferencesDto) + tokenAuthorization, err := r.CreateTokenAuthorization(ctx, *cred, token, locationReferencesDto) var displayText *coreDto.DisplayTextDto if err != nil { diff --git a/internal/tokenauthorization/v2.1.1/resolve.go b/internal/tokenauthorization/v2.1.1/resolve.go index ef64a9ff..36760467 100644 --- a/internal/tokenauthorization/v2.1.1/resolve.go +++ b/internal/tokenauthorization/v2.1.1/resolve.go @@ -2,6 +2,10 @@ package tokenauthorization import ( "github.com/satimoto/go-datastore/pkg/db" + "github.com/satimoto/go-datastore/pkg/location" + "github.com/satimoto/go-datastore/pkg/node" + "github.com/satimoto/go-datastore/pkg/session" + "github.com/satimoto/go-datastore/pkg/tariff" "github.com/satimoto/go-datastore/pkg/tokenauthorization" "github.com/satimoto/go-datastore/pkg/user" "github.com/satimoto/go-ocpi/internal/async" @@ -9,24 +13,38 @@ import ( evse "github.com/satimoto/go-ocpi/internal/evse/v2.1.1" "github.com/satimoto/go-ocpi/internal/notification" "github.com/satimoto/go-ocpi/internal/service" + "github.com/satimoto/go-ocpi/internal/transportation" + "github.com/satimoto/go-ocpi/internal/versiondetail" ) type TokenAuthorizationResolver struct { - Repository tokenauthorization.TokenAuthorizationRepository - AsyncService *async.AsyncService - NotificationService notification.Notification - ConnectorResolver *connector.ConnectorResolver - EvseResolver *evse.EvseResolver - UserRepository user.UserRepository + Repository tokenauthorization.TokenAuthorizationRepository + OcpiService *transportation.OcpiService + AsyncService *async.AsyncService + NotificationService notification.Notification + ConnectorResolver *connector.ConnectorResolver + EvseResolver *evse.EvseResolver + LocationRepository location.LocationRepository + NodeRepository node.NodeRepository + SessionRepository session.SessionRepository + TariffRespository tariff.TariffRepository + UserRepository user.UserRepository + VersionDetailResolver *versiondetail.VersionDetailResolver } func NewResolver(repositoryService *db.RepositoryService, services *service.ServiceResolver) *TokenAuthorizationResolver { return &TokenAuthorizationResolver{ - Repository: tokenauthorization.NewRepository(repositoryService), - AsyncService: services.AsyncService, - NotificationService: services.NotificationService, - ConnectorResolver: connector.NewResolver(repositoryService), - EvseResolver: evse.NewResolver(repositoryService), - UserRepository: user.NewRepository(repositoryService), + Repository: tokenauthorization.NewRepository(repositoryService), + OcpiService: services.OcpiService, + AsyncService: services.AsyncService, + NotificationService: services.NotificationService, + ConnectorResolver: connector.NewResolver(repositoryService), + EvseResolver: evse.NewResolver(repositoryService), + LocationRepository: location.NewRepository(repositoryService), + NodeRepository: node.NewRepository(repositoryService), + SessionRepository: session.NewRepository(repositoryService), + TariffRespository: tariff.NewRepository(repositoryService), + UserRepository: user.NewRepository(repositoryService), + VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } } From caff2d4316b0e910abb7855f5b4503a86acabd88 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Fri, 9 Dec 2022 21:07:51 +0100 Subject: [PATCH 02/14] Replace HasToBe tariff name spaces with underscores --- internal/sync/htb/service.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/sync/htb/service.go b/internal/sync/htb/service.go index 9ccf6b3a..30cef5b6 100644 --- a/internal/sync/htb/service.go +++ b/internal/sync/htb/service.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "strings" "sync" "time" @@ -84,7 +85,8 @@ func (r *HtbService) updateConnectors(ctx context.Context) { tariffID := util.SqlNullString(nil) if priceInformation.Conditions != nil && priceInformation.Conditions.Rate != nil { - tariffID = util.SqlNullString(fmt.Sprintf(TARIFF_UID_TEMPLATE, *priceInformation.Conditions.Rate)) + rate := strings.Replace(*priceInformation.Conditions.Rate, " ", "_", -1) + tariffID = util.SqlNullString(fmt.Sprintf(TARIFF_UID_TEMPLATE, rate)) } for _, connector := range connectors { @@ -124,7 +126,7 @@ func (r *HtbService) updateHtbTariffs(ctx context.Context, credential db.Credent } func (r *HtbService) updateTariff(ctx context.Context, credential db.Credential, htbTariff db.HtbTariff) { - tariffUid := fmt.Sprintf(TARIFF_UID_TEMPLATE, htbTariff.Name) + tariffUid := fmt.Sprintf(TARIFF_UID_TEMPLATE, strings.Replace(htbTariff.Name, " ", "_", -1)) tariff, err := r.TariffRepository.GetTariffByUid(ctx, tariffUid) if err != nil { From 4e172cb194042f8df18764a46e411605c2783eef Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Sun, 11 Dec 2022 00:41:02 +0100 Subject: [PATCH 03/14] Only wait for EVSE status when start command is accepted or token authorization is authorised, return non error response for a stop command error --- internal/command/v2.1.1/mocks/mock_resolve.go | 2 + internal/command/v2.1.1/process.go | 45 ++-- internal/command/v2.1.1/pull.go | 23 +- internal/command/v2.1.1/push.go | 4 +- internal/command/v2.1.1/resolve.go | 3 + internal/evse/v2.1.1/dto_test.go | 38 ++- internal/evse/v2.1.1/mocks/mock_resolve.go | 28 ++- internal/evse/v2.1.1/monitor.go | 208 +++++++++++++++++ internal/evse/v2.1.1/resolve.go | 48 ++-- internal/evse/v2.1.1/resolve_test.go | 31 ++- .../location/v2.1.1/mocks/mock_resolve.go | 2 +- internal/location/v2.1.1/resolve.go | 2 +- internal/rest/route_evse_2.1.1.go | 2 +- internal/rpc/command/rpc.go | 23 +- internal/rpc/token/rpc.go | 2 + .../v2.1.1/mocks/mock_resolve.go | 2 +- internal/tokenauthorization/v2.1.1/process.go | 217 +----------------- internal/tokenauthorization/v2.1.1/push.go | 4 + internal/tokenauthorization/v2.1.1/resolve.go | 2 +- 19 files changed, 417 insertions(+), 269 deletions(-) create mode 100644 internal/evse/v2.1.1/monitor.go diff --git a/internal/command/v2.1.1/mocks/mock_resolve.go b/internal/command/v2.1.1/mocks/mock_resolve.go index f8a88cc8..3fc9eddc 100644 --- a/internal/command/v2.1.1/mocks/mock_resolve.go +++ b/internal/command/v2.1.1/mocks/mock_resolve.go @@ -4,6 +4,7 @@ import ( commandMocks "github.com/satimoto/go-datastore/pkg/command/mocks" mocks "github.com/satimoto/go-datastore/pkg/db/mocks" command "github.com/satimoto/go-ocpi/internal/command/v2.1.1" + evse "github.com/satimoto/go-ocpi/internal/evse/v2.1.1/mocks" "github.com/satimoto/go-ocpi/internal/service" token "github.com/satimoto/go-ocpi/internal/token/v2.1.1/mocks" versiondetail "github.com/satimoto/go-ocpi/internal/versiondetail/mocks" @@ -13,6 +14,7 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi return &command.CommandResolver{ Repository: commandMocks.NewRepository(repositoryService), OcpiService: services.OcpiService, + EvseResolver: evse.NewResolver(repositoryService, services), TokenResolver: token.NewResolver(repositoryService, services), VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } diff --git a/internal/command/v2.1.1/process.go b/internal/command/v2.1.1/process.go index 0a6c1949..45f62694 100644 --- a/internal/command/v2.1.1/process.go +++ b/internal/command/v2.1.1/process.go @@ -26,7 +26,7 @@ func (r *CommandResolver) UpdateCommandReservation(ctx context.Context, command } } -func (r *CommandResolver) UpdateCommandStart(ctx context.Context, command db.CommandStart, commandResponseDto *dto.CommandResponseDto) { +func (r *CommandResolver) UpdateCommandStart(ctx context.Context, credential db.Credential, command db.CommandStart, commandResponseDto *dto.CommandResponseDto) { if commandResponseDto != nil { commandParams := param.NewUpdateCommandStartParams(command) commandParams.Status = *commandResponseDto.Result @@ -39,19 +39,8 @@ func (r *CommandResolver) UpdateCommandStart(ctx context.Context, command db.Com log.Printf("OCPI039: Params=%#v", commandParams) } - if updatedCommand.Status == db.CommandResponseTypeREJECTED && updatedCommand.AuthorizationID.Valid { - // Update the rejected commands token authorization - updateTokenAuthorizationByAuthorizationIDParams := db.UpdateTokenAuthorizationByAuthorizationIDParams{ - AuthorizationID: updatedCommand.AuthorizationID.String, - Authorized: false, - } - - _, err := r.TokenResolver.TokenAuthorizationResolver.Repository.UpdateTokenAuthorizationByAuthorizationID(ctx, updateTokenAuthorizationByAuthorizationIDParams) - - if err != nil { - metrics.RecordError("OCPI325", "Error updating token authorization", err) - log.Printf("OCPI325: Params=%#v", updateTokenAuthorizationByAuthorizationIDParams) - } + if updatedCommand.Status == db.CommandResponseTypeACCEPTED && updatedCommand.AuthorizationID.Valid { + go r.waitForEvseStatus(credential, updatedCommand) } } } @@ -85,3 +74,31 @@ func (r *CommandResolver) UpdateCommandUnlock(ctx context.Context, command db.Co } } } + +func (r *CommandResolver) waitForEvseStatus(credential db.Credential, command db.CommandStart) { + ctx := context.Background() + token, err := r.TokenResolver.Repository.GetToken(ctx, command.TokenID) + + if err != nil { + metrics.RecordError("OCPI325", "Error getting token", err) + log.Printf("OCPI325: CommandID=%v, TokenID=%v", command.ID, command.TokenID) + return + } + + tokenAuthorization, err := r.TokenResolver.TokenAuthorizationResolver.Repository.GetTokenAuthorizationByAuthorizationID(ctx, command.AuthorizationID.String) + + if err != nil { + metrics.RecordError("OCPI326", "Error updating command start", err) + log.Printf("OCPI326: CommandID=%v, AuthorizationID=%#v", command.ID, command.AuthorizationID) + return + } + + if command.EvseUid.Valid { + cancelCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + r.EvseResolver.WaitForEvseStatus(credential, token, tokenAuthorization, command.LocationID, command.EvseUid.String, db.EvseStatusCHARGING, cancelCtx, cancel, 150) + + <-cancelCtx.Done() + } +} diff --git a/internal/command/v2.1.1/pull.go b/internal/command/v2.1.1/pull.go index c87530ea..5c3bd2fa 100644 --- a/internal/command/v2.1.1/pull.go +++ b/internal/command/v2.1.1/pull.go @@ -151,7 +151,7 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden if err != nil { metrics.RecordError("OCPI054", "Error unmarshaling response", err) dbUtil.LogHttpResponse("OCPI054", requestUrl.String(), response, true) - return nil, errors.New("error starting reservation") + return nil, errors.New("error starting session") } if pullDto.StatusCode != transportation.STATUS_CODE_OK { @@ -164,7 +164,7 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden updateCommandStartParams.Status = db.CommandResponseTypeREJECTED r.Repository.UpdateCommandStart(ctx, updateCommandStartParams) - return nil, errors.New("error starting reservation") + return nil, errors.New("error starting session") } if pullDto.Data.Result != nil && *pullDto.Data.Result != db.CommandResponseTypeACCEPTED { @@ -201,7 +201,7 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent command, err := r.Repository.CreateCommandStop(ctx, createCommandStopParams) if err != nil { - metrics.RecordError("OCPI058", "Error creating command reservation", err) + metrics.RecordError("OCPI058", "Error creating command stop", err) log.Printf("OCPI058: Params=%#v", createCommandStopParams) return nil, errors.New("error stopping session") } @@ -231,7 +231,7 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent if err != nil { metrics.RecordError("OCPI061", "Error unmarshaling response", err) dbUtil.LogHttpResponse("OCPI062", requestUrl.String(), response, true) - return nil, errors.New("error stopping reservation") + return nil, errors.New("error stopping session") } if pullDto.StatusCode != transportation.STATUS_CODE_OK { @@ -242,9 +242,16 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent updateCommandStopParams := param.NewUpdateCommandStopParams(command) updateCommandStopParams.Status = db.CommandResponseTypeREJECTED - r.Repository.UpdateCommandStop(ctx, updateCommandStopParams) - return nil, errors.New("error stopping reservation") + if pullDto.StatusMessage == "Session unknown" { + updateCommandStopParams.Status = db.CommandResponseTypeUNKNOWNSESSION + } + + if command, err := r.Repository.UpdateCommandStop(ctx, updateCommandStopParams); err == nil { + return &command, nil + } + + return nil, errors.New("error stopping session") } if pullDto.Data.Result != nil && *pullDto.Data.Result != db.CommandResponseTypeACCEPTED { @@ -311,7 +318,7 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre if err != nil { metrics.RecordError("OCPI069", "Error unmarshaling response", err) dbUtil.LogHttpResponse("OCPI069", requestUrl.String(), response, true) - return nil, errors.New("error unlocking reservation") + return nil, errors.New("error unlocking connector") } if pullDto.StatusCode != transportation.STATUS_CODE_OK { @@ -324,7 +331,7 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre updateCommandUnlockParams.Status = db.CommandResponseTypeREJECTED r.Repository.UpdateCommandUnlock(ctx, updateCommandUnlockParams) - return nil, errors.New("error unlocking reservation") + return nil, errors.New("error unlocking connector") } if pullDto.Data.Result != nil && *pullDto.Data.Result != db.CommandResponseTypeACCEPTED { diff --git a/internal/command/v2.1.1/push.go b/internal/command/v2.1.1/push.go index 39fbc783..a1fc5fec 100644 --- a/internal/command/v2.1.1/push.go +++ b/internal/command/v2.1.1/push.go @@ -8,6 +8,7 @@ import ( "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/util" metrics "github.com/satimoto/go-ocpi/internal/metric" + "github.com/satimoto/go-ocpi/internal/middleware" "github.com/satimoto/go-ocpi/internal/transportation" ) @@ -36,6 +37,7 @@ func (r *CommandResolver) PostCommandReservationResponse(rw http.ResponseWriter, func (r *CommandResolver) PostCommandStartResponse(rw http.ResponseWriter, request *http.Request) { ctx := request.Context() + cred := middleware.GetCredential(ctx) command := ctx.Value("command").(db.CommandStart) dto, err := r.UnmarshalPushDto(request.Body) @@ -47,7 +49,7 @@ func (r *CommandResolver) PostCommandStartResponse(rw http.ResponseWriter, reque return } - r.UpdateCommandStart(ctx, command, dto) + r.UpdateCommandStart(ctx, *cred, command, dto) if err := render.Render(rw, request, transportation.OcpiSuccess(nil)); err != nil { log.Print("OCPI074", "Error updating start command") diff --git a/internal/command/v2.1.1/resolve.go b/internal/command/v2.1.1/resolve.go index 8c27a478..cfc2699e 100644 --- a/internal/command/v2.1.1/resolve.go +++ b/internal/command/v2.1.1/resolve.go @@ -3,6 +3,7 @@ package command import ( "github.com/satimoto/go-datastore/pkg/command" "github.com/satimoto/go-datastore/pkg/db" + evse "github.com/satimoto/go-ocpi/internal/evse/v2.1.1" "github.com/satimoto/go-ocpi/internal/service" token "github.com/satimoto/go-ocpi/internal/token/v2.1.1" "github.com/satimoto/go-ocpi/internal/transportation" @@ -12,6 +13,7 @@ import ( type CommandResolver struct { Repository command.CommandRepository OcpiService *transportation.OcpiService + EvseResolver *evse.EvseResolver TokenResolver *token.TokenResolver VersionDetailResolver *versiondetail.VersionDetailResolver } @@ -20,6 +22,7 @@ func NewResolver(repositoryService *db.RepositoryService, services *service.Serv return &CommandResolver{ Repository: command.NewRepository(repositoryService), OcpiService: services.OcpiService, + EvseResolver: evse.NewResolver(repositoryService, services), TokenResolver: token.NewResolver(repositoryService, services), VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } diff --git a/internal/evse/v2.1.1/dto_test.go b/internal/evse/v2.1.1/dto_test.go index 4d11360e..1d675759 100644 --- a/internal/evse/v2.1.1/dto_test.go +++ b/internal/evse/v2.1.1/dto_test.go @@ -10,6 +10,9 @@ import ( dbMocks "github.com/satimoto/go-datastore/pkg/db/mocks" "github.com/satimoto/go-datastore/pkg/util" evseMocks "github.com/satimoto/go-ocpi/internal/evse/v2.1.1/mocks" + notificationMocks "github.com/satimoto/go-ocpi/internal/notification/mocks" + serviceMocks "github.com/satimoto/go-ocpi/internal/service/mocks" + transportationMocks "github.com/satimoto/go-ocpi/internal/transportation/mocks" "github.com/satimoto/go-ocpi/test/mocks" ) @@ -18,7 +21,12 @@ func TestCreateCapabilityListDto(t *testing.T) { t.Run("Test", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) capabilities := []db.Capability{} capabilities = append(capabilities, db.Capability{Text: "Test1", Description: "Test"}) @@ -37,7 +45,12 @@ func TestCreateEvseDto(t *testing.T) { t.Run("With Status Schedules", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) statusSchedules := []db.StatusSchedule{} statusSchedules = append(statusSchedules, db.StatusSchedule{ @@ -93,7 +106,12 @@ func TestCreateEvseDto(t *testing.T) { t.Run("With Capabilities", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) capabilities := []db.Capability{} capabilities = append(capabilities, db.Capability{ @@ -123,7 +141,12 @@ func TestCreateEvseDto(t *testing.T) { t.Run("With Connectors", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) connectors := []db.Connector{} connectors = append(connectors, db.Connector{ @@ -191,7 +214,12 @@ func TestCreateEvseDto(t *testing.T) { t.Run("With Images", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) images := []db.Image{} images = append(images, db.Image{ diff --git a/internal/evse/v2.1.1/mocks/mock_resolve.go b/internal/evse/v2.1.1/mocks/mock_resolve.go index 425cc8fc..3649da8d 100644 --- a/internal/evse/v2.1.1/mocks/mock_resolve.go +++ b/internal/evse/v2.1.1/mocks/mock_resolve.go @@ -3,23 +3,37 @@ package mocks import ( mocks "github.com/satimoto/go-datastore/pkg/db/mocks" evseMocks "github.com/satimoto/go-datastore/pkg/evse/mocks" + location "github.com/satimoto/go-datastore/pkg/location/mocks" + node "github.com/satimoto/go-datastore/pkg/node/mocks" + session "github.com/satimoto/go-datastore/pkg/session/mocks" + tariff "github.com/satimoto/go-datastore/pkg/tariff/mocks" + tokenauthorization "github.com/satimoto/go-datastore/pkg/tokenauthorization/mocks" "github.com/satimoto/go-datastore/pkg/util" connector "github.com/satimoto/go-ocpi/internal/connector/v2.1.1/mocks" displaytext "github.com/satimoto/go-ocpi/internal/displaytext/mocks" evse "github.com/satimoto/go-ocpi/internal/evse/v2.1.1" geolocation "github.com/satimoto/go-ocpi/internal/geolocation/mocks" image "github.com/satimoto/go-ocpi/internal/image/mocks" + "github.com/satimoto/go-ocpi/internal/service" + versiondetail "github.com/satimoto/go-ocpi/internal/versiondetail/mocks" ) -func NewResolver(repositoryService *mocks.MockRepositoryService) *evse.EvseResolver { +func NewResolver(repositoryService *mocks.MockRepositoryService, services *service.ServiceResolver) *evse.EvseResolver { recordEvseStatusPeriods := util.GetEnvBool("RECORD_EVSE_STATUS_PERIODS", true) return &evse.EvseResolver{ - Repository: evseMocks.NewRepository(repositoryService), - ConnectorResolver: connector.NewResolver(repositoryService), - DisplayTextResolver: displaytext.NewResolver(repositoryService), - GeoLocationResolver: geolocation.NewResolver(repositoryService), - ImageResolver: image.NewResolver(repositoryService), - RecordEvseStatusPeriods: recordEvseStatusPeriods, + Repository: evseMocks.NewRepository(repositoryService), + OcpiService: services.OcpiService, + ConnectorResolver: connector.NewResolver(repositoryService), + DisplayTextResolver: displaytext.NewResolver(repositoryService), + GeoLocationResolver: geolocation.NewResolver(repositoryService), + ImageResolver: image.NewResolver(repositoryService), + LocationRepository: location.NewRepository(repositoryService), + NodeRepository: node.NewRepository(repositoryService), + SessionRepository: session.NewRepository(repositoryService), + TariffRespository: tariff.NewRepository(repositoryService), + TokenAuthorizationRepository: tokenauthorization.NewRepository(repositoryService), + VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), + RecordEvseStatusPeriods: recordEvseStatusPeriods, } } diff --git a/internal/evse/v2.1.1/monitor.go b/internal/evse/v2.1.1/monitor.go new file mode 100644 index 00000000..8036c8be --- /dev/null +++ b/internal/evse/v2.1.1/monitor.go @@ -0,0 +1,208 @@ +package evse + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "time" + + "github.com/satimoto/go-datastore/pkg/db" + "github.com/satimoto/go-datastore/pkg/util" + coreLocation "github.com/satimoto/go-ocpi/internal/location" + metrics "github.com/satimoto/go-ocpi/internal/metric" + "github.com/satimoto/go-ocpi/internal/transportation" + "github.com/satimoto/go-ocpi/pkg/ocpi" + ocpiSession "github.com/satimoto/go-ocpi/pkg/ocpi/session" +) + +func (r *EvseResolver) WaitForEvseStatus(credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, locationUid, evseUid string, evseStatus db.EvseStatus, cancelCtx context.Context, cancel context.CancelFunc, timeoutSeconds int) { + defer cancel() + + ctx := context.Background() + deadline := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) + log.Printf("Waiting for Evse status change to %v over %v seconds: LocationUid=%v, EvseUid=%v", evseStatus, timeoutSeconds, locationUid, evseUid) + + versionEndpoint, err := r.VersionDetailResolver.GetVersionEndpointByIdentity(ctx, coreLocation.IDENTIFIER, credential.CountryCode, credential.PartyID) + + if err != nil { + metrics.RecordError("OCPI302", "Error getting version endpoint", err) + log.Printf("OCPI302: CountryCode=%v, PartyID=%v, Identifier=%v", credential.CountryCode, credential.PartyID, coreLocation.IDENTIFIER) + return + } + + evseUrl := fmt.Sprintf("%s/%s/%s", versionEndpoint.Url, locationUid, evseUid) + requestUrl, err := url.Parse(evseUrl) + + if err != nil { + metrics.RecordError("OCPI305", "Error parsing url", err) + log.Printf("OCPI305: Url=%v", evseUrl) + return + } + + header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) + +waitLoop: + for { + select { + case <-cancelCtx.Done(): + log.Printf("Cancelled. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + case <-time.After(10 * time.Second): + } + + if time.Now().After(deadline) { + log.Printf("Timeout. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + } + + _, err := r.SessionRepository.GetSessionByAuthorizationID(ctx, tokenAuthorization.AuthorizationID) + + if err == nil { + // Session found + log.Printf("Session. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + } + + updatedTokenAuthorization, err := r.TokenAuthorizationRepository.GetTokenAuthorizationByAuthorizationID(ctx, tokenAuthorization.AuthorizationID) + + if err != nil && !updatedTokenAuthorization.Authorized { + // Token authorization has been unauthorized + log.Printf("Unauthorized. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) + break waitLoop + } + + response, err := r.OcpiService.Do(http.MethodGet, requestUrl.String(), header, nil) + + if err != nil { + metrics.RecordError("OCPI306", "Error making request", err) + log.Printf("OCPI306: Method=%v, Url=%v, Header=%#v", http.MethodGet, requestUrl.String(), header) + continue + } + + evseDto, err := r.UnmarshalPullDto(response.Body) + defer response.Body.Close() + + if err != nil { + metrics.RecordError("OCPI307", "Error unmarshaling response", err) + util.LogHttpResponse("OCPI307", requestUrl.String(), response, true) + continue + } + + if evseDto.StatusCode == transportation.STATUS_CODE_OK && evseDto.Data.Status != nil { + responseEvseStatus := *evseDto.Data.Status + + log.Printf("Evse status is %v: LocationUid=%v, EvseUid=%v", responseEvseStatus, locationUid, evseUid) + + if responseEvseStatus == evseStatus { + log.Printf("Manually creating session %v", tokenAuthorization.AuthorizationID) + r.createSession(ctx, credential, token, tokenAuthorization, db.SessionStatusTypeACTIVE, locationUid, evseUid) + + break waitLoop + } + } + } +} + +func (r *EvseResolver) createSession(ctx context.Context, credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, status db.SessionStatusType, locationUid, evseUid string) { + timeNow := time.Now().UTC() + location, err := r.LocationRepository.GetLocationByUid(ctx, locationUid) + + if err != nil { + metrics.RecordError("OCPI317", "Error getting location", err) + log.Printf("OCPI317: LocationUid=%v", locationUid) + return + } + + evse, err := r.Repository.GetEvseByUid(ctx, evseUid) + + if err != nil { + metrics.RecordError("OCPI318", "Error getting evse", err) + log.Printf("OCPI318: EvseUid=%v", evseUid) + return + } + + connectors, err := r.ConnectorResolver.Repository.ListConnectors(ctx, evse.ID) + + if err != nil || len(connectors) == 0 { + metrics.RecordError("OCPI319", "Error getting connector", err) + log.Printf("OCPI319: EvseUid=%v", evseUid) + return + } + + connector := connectors[0] + + if !connector.TariffID.Valid { + metrics.RecordError("OCPI320", "Error no valid tariff", err) + log.Printf("OCPI320: ConnectorUid=%v", connector.Uid) + return + } + + tariff, err := r.TariffRespository.GetTariffByUid(ctx, connector.TariffID.String) + + if err != nil { + metrics.RecordError("OCPI321", "Error getting tariff", err) + log.Printf("OCPI321: TariffUid=%v", connector.TariffID.String) + return + } + + authMethod := db.AuthMethodTypeAUTHREQUEST + + if token.Type == db.TokenTypeRFID { + authMethod = db.AuthMethodTypeWHITELIST + } + + createSessionParams := db.CreateSessionParams{ + Uid: tokenAuthorization.AuthorizationID, + CredentialID: credential.ID, + CountryCode: location.CountryCode, + PartyID: location.PartyID, + AuthorizationID: util.SqlNullString(tokenAuthorization.AuthorizationID), + StartDatetime: timeNow, + Kwh: 0, + AuthID: token.AuthID, + AuthMethod: authMethod, + UserID: token.UserID, + TokenID: token.ID, + LocationID: location.ID, + EvseID: evse.ID, + ConnectorID: connector.ID, + Currency: tariff.Currency, + TotalCost: util.SqlNullFloat64(0), + Status: status, + LastUpdated: timeNow, + } + + session, err := r.SessionRepository.CreateSession(ctx, createSessionParams) + + if err != nil { + metrics.RecordError("OCPI322", "Error creating session", err) + log.Printf("OCPI322: Params=%#v", createSessionParams) + return + } + + go r.sendOcpiRequest(session) +} + +func (r *EvseResolver) sendOcpiRequest(session db.Session) { + ctx := context.Background() + node, err := r.NodeRepository.GetNodeByUserID(ctx, session.UserID) + + if err != nil { + metrics.RecordError("OCPI323", "Error retrieving node", err) + log.Printf("OCPI323: UserID=%v", session.UserID) + return + } + + // TODO: Handle failed RPC call more robustly + ocpiService := ocpi.NewService(node.LspAddr) + + sessionCreatedRequest := ocpiSession.NewSessionCreatedRequest(session) + sessionCreatedResponse, err := ocpiService.SessionCreated(ctx, sessionCreatedRequest) + + if err != nil { + metrics.RecordError("OCPI324", "Error calling RPC service", err) + log.Printf("OCPI324: Request=%#v, Response=%#v", sessionCreatedRequest, sessionCreatedResponse) + } +} diff --git a/internal/evse/v2.1.1/resolve.go b/internal/evse/v2.1.1/resolve.go index 525cd853..03082dd3 100644 --- a/internal/evse/v2.1.1/resolve.go +++ b/internal/evse/v2.1.1/resolve.go @@ -3,31 +3,53 @@ package evse import ( "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/evse" + "github.com/satimoto/go-datastore/pkg/location" + "github.com/satimoto/go-datastore/pkg/node" + "github.com/satimoto/go-datastore/pkg/session" + "github.com/satimoto/go-datastore/pkg/tariff" + "github.com/satimoto/go-datastore/pkg/tokenauthorization" "github.com/satimoto/go-datastore/pkg/util" connector "github.com/satimoto/go-ocpi/internal/connector/v2.1.1" "github.com/satimoto/go-ocpi/internal/displaytext" "github.com/satimoto/go-ocpi/internal/geolocation" "github.com/satimoto/go-ocpi/internal/image" + "github.com/satimoto/go-ocpi/internal/service" + "github.com/satimoto/go-ocpi/internal/transportation" + "github.com/satimoto/go-ocpi/internal/versiondetail" ) type EvseResolver struct { - Repository evse.EvseRepository - ConnectorResolver *connector.ConnectorResolver - DisplayTextResolver *displaytext.DisplayTextResolver - GeoLocationResolver *geolocation.GeoLocationResolver - ImageResolver *image.ImageResolver - RecordEvseStatusPeriods bool + Repository evse.EvseRepository + OcpiService *transportation.OcpiService + ConnectorResolver *connector.ConnectorResolver + DisplayTextResolver *displaytext.DisplayTextResolver + GeoLocationResolver *geolocation.GeoLocationResolver + ImageResolver *image.ImageResolver + LocationRepository location.LocationRepository + NodeRepository node.NodeRepository + SessionRepository session.SessionRepository + TariffRespository tariff.TariffRepository + TokenAuthorizationRepository tokenauthorization.TokenAuthorizationRepository + VersionDetailResolver *versiondetail.VersionDetailResolver + RecordEvseStatusPeriods bool } -func NewResolver(repositoryService *db.RepositoryService) *EvseResolver { +func NewResolver(repositoryService *db.RepositoryService, services *service.ServiceResolver) *EvseResolver { recordEvseStatusPeriods := util.GetEnvBool("RECORD_EVSE_STATUS_PERIODS", true) return &EvseResolver{ - Repository: evse.NewRepository(repositoryService), - ConnectorResolver: connector.NewResolver(repositoryService), - DisplayTextResolver: displaytext.NewResolver(repositoryService), - GeoLocationResolver: geolocation.NewResolver(repositoryService), - ImageResolver: image.NewResolver(repositoryService), - RecordEvseStatusPeriods: recordEvseStatusPeriods, + Repository: evse.NewRepository(repositoryService), + OcpiService: services.OcpiService, + ConnectorResolver: connector.NewResolver(repositoryService), + DisplayTextResolver: displaytext.NewResolver(repositoryService), + GeoLocationResolver: geolocation.NewResolver(repositoryService), + ImageResolver: image.NewResolver(repositoryService), + LocationRepository: location.NewRepository(repositoryService), + NodeRepository: node.NewRepository(repositoryService), + SessionRepository: session.NewRepository(repositoryService), + TariffRespository: tariff.NewRepository(repositoryService), + TokenAuthorizationRepository: tokenauthorization.NewRepository(repositoryService), + VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), + RecordEvseStatusPeriods: recordEvseStatusPeriods, } } diff --git a/internal/evse/v2.1.1/resolve_test.go b/internal/evse/v2.1.1/resolve_test.go index 189472d9..bc2e45b3 100644 --- a/internal/evse/v2.1.1/resolve_test.go +++ b/internal/evse/v2.1.1/resolve_test.go @@ -12,7 +12,10 @@ import ( coreDto "github.com/satimoto/go-ocpi/internal/dto" dto "github.com/satimoto/go-ocpi/internal/dto/v2.1.1" evseMocks "github.com/satimoto/go-ocpi/internal/evse/v2.1.1/mocks" + notificationMocks "github.com/satimoto/go-ocpi/internal/notification/mocks" "github.com/satimoto/go-ocpi/internal/ocpitype" + serviceMocks "github.com/satimoto/go-ocpi/internal/service/mocks" + transportationMocks "github.com/satimoto/go-ocpi/internal/transportation/mocks" "github.com/satimoto/go-ocpi/test/mocks" ) @@ -24,7 +27,12 @@ func TestReplaceEvse(t *testing.T) { t.Run("Create Evse", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) evseStatusRESERVED := db.EvseStatusRESERVED @@ -80,7 +88,12 @@ func TestReplaceEvse(t *testing.T) { t.Run("Update Evse", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) mockRepository.SetGetEvseByUidMockData(dbMocks.EvseMockData{ Evse: db.Evse{ @@ -145,7 +158,12 @@ func TestReplaceEvse(t *testing.T) { t.Run("Update Evse with Connectors", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) mockRepository.SetGetEvseByUidMockData(dbMocks.EvseMockData{ Evse: db.Evse{ @@ -249,7 +267,12 @@ func TestReplaceEvse(t *testing.T) { t.Run("Update Evse with Coordinates", func(t *testing.T) { mockRepository := dbMocks.NewMockRepositoryService() - evseResolver := evseMocks.NewResolver(mockRepository) + mockHTTPRequester := &mocks.MockHTTPRequester{} + mockNotificationService := notificationMocks.NewService() + mockOcpiService := transportationMocks.NewOcpiService(mockHTTPRequester) + mockServices := serviceMocks.NewService(mockRepository, mockNotificationService, mockOcpiService) + + evseResolver := evseMocks.NewResolver(mockRepository, mockServices) mockRepository.SetGetEvseByUidMockData(dbMocks.EvseMockData{ Evse: db.Evse{ diff --git a/internal/location/v2.1.1/mocks/mock_resolve.go b/internal/location/v2.1.1/mocks/mock_resolve.go index 26917f77..85503df3 100644 --- a/internal/location/v2.1.1/mocks/mock_resolve.go +++ b/internal/location/v2.1.1/mocks/mock_resolve.go @@ -24,7 +24,7 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi BusinessDetailResolver: businessdetail.NewResolver(repositoryService), DisplayTextResolver: displaytext.NewResolver(repositoryService), EnergyMixResolver: energymix.NewResolver(repositoryService), - EvseResolver: evse.NewResolver(repositoryService), + EvseResolver: evse.NewResolver(repositoryService, services), GeoLocationResolver: geolocation.NewResolver(repositoryService), ImageResolver: image.NewResolver(repositoryService), OpeningTimeResolver: openingtime.NewResolver(repositoryService), diff --git a/internal/location/v2.1.1/resolve.go b/internal/location/v2.1.1/resolve.go index 4b52bb2b..b7249f54 100644 --- a/internal/location/v2.1.1/resolve.go +++ b/internal/location/v2.1.1/resolve.go @@ -39,7 +39,7 @@ func NewResolver(repositoryService *db.RepositoryService, services *service.Serv BusinessDetailResolver: businessdetail.NewResolver(repositoryService), DisplayTextResolver: displaytext.NewResolver(repositoryService), EnergyMixResolver: energymix.NewResolver(repositoryService), - EvseResolver: evse.NewResolver(repositoryService), + EvseResolver: evse.NewResolver(repositoryService, services), GeoLocationResolver: geolocation.NewResolver(repositoryService), ImageResolver: image.NewResolver(repositoryService), OpeningTimeResolver: openingtime.NewResolver(repositoryService), diff --git a/internal/rest/route_evse_2.1.1.go b/internal/rest/route_evse_2.1.1.go index b1568caf..a0b2325f 100644 --- a/internal/rest/route_evse_2.1.1.go +++ b/internal/rest/route_evse_2.1.1.go @@ -9,7 +9,7 @@ import ( ) func (rs *RestService) mountEvses() *chi.Mux { - evseResolver := evse.NewResolver(rs.RepositoryService) + evseResolver := evse.NewResolver(rs.RepositoryService, rs.ServiceResolver) router := chi.NewRouter() router.Use(middleware.Timeout(30 * time.Second)) diff --git a/internal/rpc/command/rpc.go b/internal/rpc/command/rpc.go index cfd06a35..cc424c3e 100644 --- a/internal/rpc/command/rpc.go +++ b/internal/rpc/command/rpc.go @@ -156,6 +156,11 @@ func (r *RpcCommandResolver) StartSession(ctx context.Context, input *ocpirpc.St func (r *RpcCommandResolver) StopSession(ctx context.Context, input *ocpirpc.StopSessionRequest) (*ocpirpc.StopSessionResponse, error) { if input != nil { + defaultResponse := ocpirpc.StopSessionResponse{ + Status: string(db.CommandResponseTypeACCEPTED), + AuthorizationId: input.AuthorizationId, + } + if tokenAuthorization, err := r.TokenResolver.TokenAuthorizationResolver.Repository.GetTokenAuthorizationByAuthorizationID(ctx, input.AuthorizationId); err == nil { updateTokenAuthorizationParams := param.NewUpdateTokenAuthorizationParams(tokenAuthorization) updateTokenAuthorizationParams.Authorized = false @@ -169,6 +174,19 @@ func (r *RpcCommandResolver) StopSession(ctx context.Context, input *ocpirpc.Sto } if session, err := r.SessionResolver.Repository.GetSessionByAuthorizationID(ctx, input.AuthorizationId); err == nil { + if session.Uid == session.AuthorizationID.String { + // This was a manually created session + updateSessionByUidParams := param.NewUpdateSessionByUidParams(session) + updateSessionByUidParams.Status = db.SessionStatusTypeINVALID + + if _, err := r.SessionResolver.Repository.UpdateSessionByUid(ctx, updateSessionByUidParams); err != nil { + metrics.RecordError("OCPI309", "Error updating session", err) + log.Printf("OCPI309: Params=%#v", updateSessionByUidParams) + } + + return &defaultResponse, nil + } + if session.Status == db.SessionStatusTypePENDING { updateSessionByUidParams := param.NewUpdateSessionByUidParams(session) updateSessionByUidParams.Status = db.SessionStatusTypeINVALID @@ -206,10 +224,7 @@ func (r *RpcCommandResolver) StopSession(ctx context.Context, input *ocpirpc.Sto return stopResponse, nil } - return &ocpirpc.StopSessionResponse{ - Status: string(db.CommandResponseTypeACCEPTED), - AuthorizationId: input.AuthorizationId, - }, nil + return &defaultResponse, nil } return nil, errors.New("missing request") diff --git a/internal/rpc/token/rpc.go b/internal/rpc/token/rpc.go index 5283bd09..bfdc7ef1 100644 --- a/internal/rpc/token/rpc.go +++ b/internal/rpc/token/rpc.go @@ -18,6 +18,8 @@ import ( func (r *RpcTokenResolver) CreateToken(ctx context.Context, request *ocpirpc.CreateTokenRequest) (*ocpirpc.CreateTokenResponse, error) { if request != nil { + // TODO: Handle if a token is linked by another user + // Should the token be voided for both users? tokenDto := NewCreateTokenDto(request) tokenAllowed := db.TokenAllowedTypeNOCREDIT authID, err := r.TokenResolver.GenerateAuthID(ctx) diff --git a/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go b/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go index 889a03a9..7e2c23d3 100644 --- a/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go +++ b/internal/tokenauthorization/v2.1.1/mocks/mock_resolve.go @@ -22,7 +22,7 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi AsyncService: services.AsyncService, NotificationService: services.NotificationService, ConnectorResolver: connector.NewResolver(repositoryService), - EvseResolver: evse.NewResolver(repositoryService), + EvseResolver: evse.NewResolver(repositoryService, services), LocationRepository: location.NewRepository(repositoryService), NodeRepository: node.NewRepository(repositoryService), SessionRepository: session.NewRepository(repositoryService), diff --git a/internal/tokenauthorization/v2.1.1/process.go b/internal/tokenauthorization/v2.1.1/process.go index 16b61bc5..75b97b5d 100644 --- a/internal/tokenauthorization/v2.1.1/process.go +++ b/internal/tokenauthorization/v2.1.1/process.go @@ -3,10 +3,7 @@ package tokenauthorization import ( "context" "errors" - "fmt" "log" - "net/http" - "net/url" "time" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -14,11 +11,7 @@ import ( "github.com/satimoto/go-datastore/pkg/param" "github.com/satimoto/go-datastore/pkg/util" dto "github.com/satimoto/go-ocpi/internal/dto/v2.1.1" - coreLocation "github.com/satimoto/go-ocpi/internal/location" metrics "github.com/satimoto/go-ocpi/internal/metric" - "github.com/satimoto/go-ocpi/internal/transportation" - "github.com/satimoto/go-ocpi/pkg/ocpi" - ocpiSession "github.com/satimoto/go-ocpi/pkg/ocpi/session" ) func (r *TokenAuthorizationResolver) CreateTokenAuthorization(ctx context.Context, credential db.Credential, token db.Token, locationReferencesDto *dto.LocationReferencesDto) (*db.TokenAuthorization, error) { @@ -61,7 +54,7 @@ func (r *TokenAuthorizationResolver) CreateTokenAuthorization(ctx context.Contex return nil, errors.New("Authorization error") } - evseUids, _ := r.createTokenAuthorizationRelations(ctx, tokenAuthorization.ID, locationReferencesDto) + r.createTokenAuthorizationRelations(ctx, tokenAuthorization.ID, locationReferencesDto) if !tokenAuthorizationParams.Authorized { // Token authentication is not authorized because its initiated @@ -113,19 +106,13 @@ func (r *TokenAuthorizationResolver) CreateTokenAuthorization(ctx context.Contex } } - if tokenAuthorization.Authorized && locationReferencesDto.LocationID != nil { - go r.waitForEvsesStatus(credential, token, tokenAuthorization, *locationReferencesDto.LocationID, evseUids, db.EvseStatusCHARGING, 150) - } - return &tokenAuthorization, nil } -func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx context.Context, tokenAuthorizationID int64, locationReferencesDto *dto.LocationReferencesDto) (evseUids, connectorUids []string) { +func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx context.Context, tokenAuthorizationID int64, locationReferencesDto *dto.LocationReferencesDto) { if locationReferencesDto != nil { for _, evseUid := range locationReferencesDto.EvseUids { if evse, err := r.EvseResolver.Repository.GetEvseByUid(ctx, *evseUid); err == nil { - evseUids = append(evseUids, *evseUid) - setTokenAuthorizationEvseParams := db.SetTokenAuthorizationEvseParams{ TokenAuthorizationID: tokenAuthorizationID, EvseID: evse.ID, @@ -145,8 +132,6 @@ func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx conte } if connector, err := r.ConnectorResolver.Repository.GetConnectorByEvse(ctx, getConnectorByEvseParams); err == nil { - connectorUids = append(connectorUids, *connectorId) - setTokenAuthorizationConnectorParams := db.SetTokenAuthorizationConnectorParams{ TokenAuthorizationID: tokenAuthorizationID, ConnectorID: connector.ID, @@ -163,8 +148,6 @@ func (r *TokenAuthorizationResolver) createTokenAuthorizationRelations(ctx conte } } } - - return evseUids, connectorUids } func (r *TokenAuthorizationResolver) createTokenAuthorizationSigningKey() []byte { @@ -180,197 +163,15 @@ func (r *TokenAuthorizationResolver) createTokenAuthorizationSigningKey() []byte return privateKey.Serialize() } -func (r *TokenAuthorizationResolver) waitForEvsesStatus(credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, locationUid string, evseUids []string, evseStatus db.EvseStatus, timeoutSeconds int) { - cancelCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - - for _, evseUid := range evseUids { - go r.waitForEvseStatus(credential, token, tokenAuthorization, locationUid, evseUid, evseStatus, cancelCtx, cancel, timeoutSeconds) - } - - <-cancelCtx.Done() -} - -func (r *TokenAuthorizationResolver) waitForEvseStatus(credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, locationUid, evseUid string, evseStatus db.EvseStatus, cancelCtx context.Context, cancel context.CancelFunc, timeoutSeconds int) { - defer cancel() - - ctx := context.Background() - deadline := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) - log.Printf("Waiting for Evse status change to %v over %v seconds: LocationUid=%v, EvseUid=%v", evseStatus, timeoutSeconds, locationUid, evseUid) - - versionEndpoint, err := r.VersionDetailResolver.GetVersionEndpointByIdentity(ctx, coreLocation.IDENTIFIER, credential.CountryCode, credential.PartyID) - - if err != nil { - metrics.RecordError("OCPI302", "Error getting version endpoint", err) - log.Printf("OCPI302: CountryCode=%v, PartyID=%v, Identifier=%v", credential.CountryCode, credential.PartyID, coreLocation.IDENTIFIER) - return - } - - evseUrl := fmt.Sprintf("%s/%s/%s", versionEndpoint.Url, locationUid, evseUid) - requestUrl, err := url.Parse(evseUrl) - - if err != nil { - metrics.RecordError("OCPI305", "Error parsing url", err) - log.Printf("OCPI305: Url=%v", evseUrl) - return - } - - header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) - -waitLoop: - for { - select { - case <-cancelCtx.Done(): - log.Printf("Cancelled. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) - break waitLoop - case <-time.After(10 * time.Second): - } - - if time.Now().After(deadline) { - log.Printf("Timeout. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) - break waitLoop - } - - _, err := r.SessionRepository.GetSessionByAuthorizationID(ctx, tokenAuthorization.AuthorizationID) - - if err == nil { - // Session found - log.Printf("Session. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) - break waitLoop - } - - updatedTokenAuthorization, err := r.Repository.GetTokenAuthorizationByAuthorizationID(ctx, tokenAuthorization.AuthorizationID) - - if err != nil && !updatedTokenAuthorization.Authorized { - // Token authorization has been unauthorized - log.Printf("Unauthorized. Stop waiting for Evse status change: LocationUid=%v, EvseUid=%v", locationUid, evseUid) - break waitLoop - } - - response, err := r.OcpiService.Do(http.MethodGet, requestUrl.String(), header, nil) - - if err != nil { - metrics.RecordError("OCPI306", "Error making request", err) - log.Printf("OCPI306: Method=%v, Url=%v, Header=%#v", http.MethodGet, requestUrl.String(), header) - continue - } - - evseDto, err := r.EvseResolver.UnmarshalPullDto(response.Body) - defer response.Body.Close() - - if err != nil { - metrics.RecordError("OCPI307", "Error unmarshaling response", err) - util.LogHttpResponse("OCPI307", requestUrl.String(), response, true) - continue - } - - if evseDto.StatusCode == transportation.STATUS_CODE_OK && evseDto.Data.Status != nil { - responseEvseStatus := *evseDto.Data.Status - - log.Printf("Evse status is %v: LocationUid=%v, EvseUid=%v", responseEvseStatus, locationUid, evseUid) - - if responseEvseStatus == evseStatus { - log.Printf("Manually creating session %v", tokenAuthorization.AuthorizationID) - r.createSession(ctx, credential, token, tokenAuthorization, db.SessionStatusTypeACTIVE, locationUid, evseUid) +func (r *TokenAuthorizationResolver) waitForEvsesStatus(credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, locationReferencesDto *dto.LocationReferencesDto, evseStatus db.EvseStatus, timeoutSeconds int) { + if locationReferencesDto != nil && locationReferencesDto.LocationID != nil && len(locationReferencesDto.EvseUids) > 0 { + cancelCtx, cancel := context.WithCancel(context.Background()) + defer cancel() - break waitLoop - } + for _, evseUid := range locationReferencesDto.EvseUids { + go r.EvseResolver.WaitForEvseStatus(credential, token, tokenAuthorization, *locationReferencesDto.LocationID, *evseUid, evseStatus, cancelCtx, cancel, timeoutSeconds) } - } -} - -func (r *TokenAuthorizationResolver) createSession(ctx context.Context, credential db.Credential, token db.Token, tokenAuthorization db.TokenAuthorization, status db.SessionStatusType, locationUid, evseUid string) { - timeNow := time.Now().UTC() - location, err := r.LocationRepository.GetLocationByUid(ctx, locationUid) - - if err != nil { - metrics.RecordError("OCPI317", "Error getting location", err) - log.Printf("OCPI317: LocationUid=%v", locationUid) - return - } - - evse, err := r.EvseResolver.Repository.GetEvseByUid(ctx, evseUid) - - if err != nil { - metrics.RecordError("OCPI318", "Error getting evse", err) - log.Printf("OCPI318: EvseUid=%v", evseUid) - return - } - - connectors, err := r.EvseResolver.ConnectorResolver.Repository.ListConnectors(ctx, evse.ID) - - if err != nil || len(connectors) == 0 { - metrics.RecordError("OCPI319", "Error getting connector", err) - log.Printf("OCPI319: EvseUid=%v", evseUid) - return - } - - connector := connectors[0] - - if !connector.TariffID.Valid { - metrics.RecordError("OCPI320", "Error no valid tariff", err) - log.Printf("OCPI320: ConnectorUid=%v", connector.Uid) - return - } - - tariff, err := r.TariffRespository.GetTariffByUid(ctx, connector.TariffID.String) - - if err != nil { - metrics.RecordError("OCPI321", "Error getting tariff", err) - log.Printf("OCPI321: TariffUid=%v", connector.TariffID.String) - return - } - - createSessionParams := db.CreateSessionParams{ - Uid: tokenAuthorization.AuthorizationID, - CredentialID: credential.ID, - CountryCode: location.CountryCode, - PartyID: location.PartyID, - AuthorizationID: util.SqlNullString(tokenAuthorization.AuthorizationID), - StartDatetime: timeNow, - Kwh: 0, - AuthID: token.AuthID, - AuthMethod: db.AuthMethodTypeAUTHREQUEST, - UserID: token.UserID, - TokenID: token.ID, - LocationID: location.ID, - EvseID: evse.ID, - ConnectorID: connector.ID, - Currency: tariff.Currency, - TotalCost: util.SqlNullFloat64(0), - Status: status, - LastUpdated: timeNow, - } - - session, err := r.SessionRepository.CreateSession(ctx, createSessionParams) - - if err != nil { - metrics.RecordError("OCPI322", "Error creating session", err) - log.Printf("OCPI322: Params=%#v", createSessionParams) - return - } - - go r.sendOcpiRequest(session) -} - -func (r *TokenAuthorizationResolver) sendOcpiRequest(session db.Session) { - ctx := context.Background() - node, err := r.NodeRepository.GetNodeByUserID(ctx, session.UserID) - - if err != nil { - metrics.RecordError("OCPI323", "Error retrieving node", err) - log.Printf("OCPI323: UserID=%v", session.UserID) - return - } - - // TODO: Handle failed RPC call more robustly - ocpiService := ocpi.NewService(node.LspAddr) - sessionCreatedRequest := ocpiSession.NewSessionCreatedRequest(session) - sessionCreatedResponse, err := ocpiService.SessionCreated(ctx, sessionCreatedRequest) - - if err != nil { - metrics.RecordError("OCPI324", "Error calling RPC service", err) - log.Printf("OCPI324: Request=%#v, Response=%#v", sessionCreatedRequest, sessionCreatedResponse) + <-cancelCtx.Done() } } diff --git a/internal/tokenauthorization/v2.1.1/push.go b/internal/tokenauthorization/v2.1.1/push.go index 1b368a6e..4034a6f8 100644 --- a/internal/tokenauthorization/v2.1.1/push.go +++ b/internal/tokenauthorization/v2.1.1/push.go @@ -38,6 +38,10 @@ func (r *TokenAuthorizationResolver) AuthorizeToken(rw http.ResponseWriter, requ } authorizationInfoDto = r.CreateAuthorizationInfoDto(ctx, token, tokenAuthorization, locationReferencesDto, displayText) + + if tokenAuthorization != nil && tokenAuthorization.Authorized { + go r.waitForEvsesStatus(*cred, token, *tokenAuthorization, locationReferencesDto, db.EvseStatusCHARGING, 150) + } } if err := render.Render(rw, request, transportation.OcpiSuccess(authorizationInfoDto)); err != nil { diff --git a/internal/tokenauthorization/v2.1.1/resolve.go b/internal/tokenauthorization/v2.1.1/resolve.go index 36760467..8e4fa1d1 100644 --- a/internal/tokenauthorization/v2.1.1/resolve.go +++ b/internal/tokenauthorization/v2.1.1/resolve.go @@ -39,7 +39,7 @@ func NewResolver(repositoryService *db.RepositoryService, services *service.Serv AsyncService: services.AsyncService, NotificationService: services.NotificationService, ConnectorResolver: connector.NewResolver(repositoryService), - EvseResolver: evse.NewResolver(repositoryService), + EvseResolver: evse.NewResolver(repositoryService, services), LocationRepository: location.NewRepository(repositoryService), NodeRepository: node.NewRepository(repositoryService), SessionRepository: session.NewRepository(repositoryService), From 5563254fc652b0a81bdebcebd5d19a5f41f07c35 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Mon, 12 Dec 2022 16:48:20 +0100 Subject: [PATCH 04/14] Implement async waiting for the command response so a more informed result can be returned --- go.mod | 2 +- go.sum | 6 + internal/cdr/v2.1.1/mocks/mock_resolve.go | 3 + internal/cdr/v2.1.1/monitor.go | 41 ++++ internal/cdr/v2.1.1/process.go | 2 + internal/cdr/v2.1.1/resolve.go | 6 + internal/command/constant.go | 8 + internal/command/v2.1.1/mocks/mock_resolve.go | 1 + internal/command/v2.1.1/process.go | 47 +++- internal/command/v2.1.1/pull.go | 101 ++++++++- internal/command/v2.1.1/resolve.go | 3 + internal/rest/route_command_2.1.1.go | 2 +- internal/session/v2.1.1/mocks/mock_resolve.go | 1 + internal/session/v2.1.1/monitor.go | 211 ++++++++++++++++++ internal/session/v2.1.1/process.go | 150 +------------ internal/session/v2.1.1/resolve.go | 3 + 16 files changed, 432 insertions(+), 155 deletions(-) create mode 100644 internal/cdr/v2.1.1/monitor.go create mode 100644 internal/command/constant.go create mode 100644 internal/session/v2.1.1/monitor.go diff --git a/go.mod b/go.mod index 747aa318..242c4d95 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/joho/godotenv v1.4.0 github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/prometheus/client_golang v1.13.1 - github.com/satimoto/go-datastore v0.3.1-0.20221207141414-56f9531eb6ea + github.com/satimoto/go-datastore v0.3.1-0.20221211215042-d5318660b0e1 google.golang.org/grpc v1.47.0 ) diff --git a/go.sum b/go.sum index 0d4a0682..4122ee1b 100644 --- a/go.sum +++ b/go.sum @@ -394,6 +394,12 @@ github.com/satimoto/go-datastore v0.3.1-0.20221207124238-2ae95d5797d4 h1:dV1+hbH github.com/satimoto/go-datastore v0.3.1-0.20221207124238-2ae95d5797d4/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= github.com/satimoto/go-datastore v0.3.1-0.20221207141414-56f9531eb6ea h1:CivzL7OWIhSdnZ6meu50n6NlDcYelphbquZe5930M3s= github.com/satimoto/go-datastore v0.3.1-0.20221207141414-56f9531eb6ea/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= +github.com/satimoto/go-datastore v0.3.1-0.20221211195231-cf0e5e78480d h1:wkTwNt8gWY4a9Fuvk+xZEKGIo+sWxeFsnMaeOFc9HIo= +github.com/satimoto/go-datastore v0.3.1-0.20221211195231-cf0e5e78480d/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= +github.com/satimoto/go-datastore v0.3.1-0.20221211212503-67cd89cf5af6 h1:mp63StobDDHg6sQL19afbPI55cslXodxPKNcShcA6Mg= +github.com/satimoto/go-datastore v0.3.1-0.20221211212503-67cd89cf5af6/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= +github.com/satimoto/go-datastore v0.3.1-0.20221211215042-d5318660b0e1 h1:j8SMRJQwSu7noWT2TEt+33URmPXpFw1+B/dc0aIRr4I= +github.com/satimoto/go-datastore v0.3.1-0.20221211215042-d5318660b0e1/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= github.com/satimoto/go-lsp v0.2.1-0.20221115190646-b981e98c7ba4 h1:3QKFoeOVSDU8bvrVfj/zAMDq+hg3KJBLaMgEgutJExk= github.com/satimoto/go-lsp v0.2.1-0.20221115190646-b981e98c7ba4/go.mod h1:bleUpVumUC8QAFi0CZdnbtDRGWRFKETMH5JWcLAolBs= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= diff --git a/internal/cdr/v2.1.1/mocks/mock_resolve.go b/internal/cdr/v2.1.1/mocks/mock_resolve.go index fbb4c62c..0de024d7 100644 --- a/internal/cdr/v2.1.1/mocks/mock_resolve.go +++ b/internal/cdr/v2.1.1/mocks/mock_resolve.go @@ -2,6 +2,7 @@ package mocks import ( cdrMocks "github.com/satimoto/go-datastore/pkg/cdr/mocks" + command "github.com/satimoto/go-datastore/pkg/command/mocks" mocks "github.com/satimoto/go-datastore/pkg/db/mocks" node "github.com/satimoto/go-datastore/pkg/node/mocks" session "github.com/satimoto/go-datastore/pkg/session/mocks" @@ -19,8 +20,10 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi return &cdr.CdrResolver{ Repository: cdrMocks.NewRepository(repositoryService), OcpiService: services.OcpiService, + AsyncService: services.AsyncService, CalibrationResolver: calibration.NewResolver(repositoryService), ChargingPeriodResolver: chargingperiod.NewResolver(repositoryService), + CommandRepository: command.NewRepository(repositoryService), LocationResolver: location.NewResolver(repositoryService, services), NodeRepository: node.NewRepository(repositoryService), SessionRepository: session.NewRepository(repositoryService), diff --git a/internal/cdr/v2.1.1/monitor.go b/internal/cdr/v2.1.1/monitor.go new file mode 100644 index 00000000..91fc8cd9 --- /dev/null +++ b/internal/cdr/v2.1.1/monitor.go @@ -0,0 +1,41 @@ +package cdr + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/satimoto/go-datastore/pkg/db" + "github.com/satimoto/go-ocpi/internal/async" + coreCommand "github.com/satimoto/go-ocpi/internal/command" + metrics "github.com/satimoto/go-ocpi/internal/metric" +) + +func (r *CdrResolver) updateCommand(cdr db.Cdr, session db.Session) { + ctx := context.Background() + + if cdr.AuthorizationID.Valid { + updateCommandStopBySessionIDParams := db.UpdateCommandStopBySessionIDParams{ + SessionID: session.Uid, + Status: db.CommandResponseTypeACCEPTED, + LastUpdated: time.Now().UTC(), + } + + command, err := r.CommandRepository.UpdateCommandStopBySessionID(ctx, updateCommandStopBySessionIDParams) + + if err != nil { + metrics.RecordError("OCPI329", "Error updating command stop", err) + log.Printf("OCPI329: SessionUid=%#v", session.Uid) + return + } + + asyncKey := fmt.Sprintf(coreCommand.STOP_COMMAND_ASYNC_KEY, command.ID) + asyncResult := async.AsyncResult{ + String: string(command.Status), + Bool: command.Status == db.CommandResponseTypeACCEPTED, + } + + r.AsyncService.Set(asyncKey, asyncResult) + } +} diff --git a/internal/cdr/v2.1.1/process.go b/internal/cdr/v2.1.1/process.go index 037e2e7d..41e9b6ac 100644 --- a/internal/cdr/v2.1.1/process.go +++ b/internal/cdr/v2.1.1/process.go @@ -133,6 +133,8 @@ func (r *CdrResolver) ReplaceCdrByIdentifier(ctx context.Context, credential db. metrics.RecordError("OCPI283", "Error updating session", err) log.Printf("OCPI283: Params=%#v", sessionParams) } + + r.updateCommand(cdr, session) } else { // A session was never received for this cdr, create it createSessionParams := db.CreateSessionParams{ diff --git a/internal/cdr/v2.1.1/resolve.go b/internal/cdr/v2.1.1/resolve.go index 08347893..aa5c2e1b 100644 --- a/internal/cdr/v2.1.1/resolve.go +++ b/internal/cdr/v2.1.1/resolve.go @@ -2,10 +2,12 @@ package cdr import ( "github.com/satimoto/go-datastore/pkg/cdr" + "github.com/satimoto/go-datastore/pkg/command" "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/node" "github.com/satimoto/go-datastore/pkg/session" "github.com/satimoto/go-datastore/pkg/token" + "github.com/satimoto/go-ocpi/internal/async" "github.com/satimoto/go-ocpi/internal/calibration" "github.com/satimoto/go-ocpi/internal/chargingperiod" location "github.com/satimoto/go-ocpi/internal/location/v2.1.1" @@ -18,8 +20,10 @@ import ( type CdrResolver struct { Repository cdr.CdrRepository OcpiService *transportation.OcpiService + AsyncService *async.AsyncService CalibrationResolver *calibration.CalibrationResolver ChargingPeriodResolver *chargingperiod.ChargingPeriodResolver + CommandRepository command.CommandRepository LocationResolver *location.LocationResolver NodeRepository node.NodeRepository SessionRepository session.SessionRepository @@ -32,8 +36,10 @@ func NewResolver(repositoryService *db.RepositoryService, services *service.Serv return &CdrResolver{ Repository: cdr.NewRepository(repositoryService), OcpiService: services.OcpiService, + AsyncService: services.AsyncService, CalibrationResolver: calibration.NewResolver(repositoryService), ChargingPeriodResolver: chargingperiod.NewResolver(repositoryService), + CommandRepository: command.NewRepository(repositoryService), LocationResolver: location.NewResolver(repositoryService, services), NodeRepository: node.NewRepository(repositoryService), SessionRepository: session.NewRepository(repositoryService), diff --git a/internal/command/constant.go b/internal/command/constant.go new file mode 100644 index 00000000..ccf7d5a5 --- /dev/null +++ b/internal/command/constant.go @@ -0,0 +1,8 @@ +package command + +const ( + RESERVE_NOW_ASYNC_KEY = "commands/RESERVE_NOW/%v" + START_COMMAND_ASYNC_KEY = "commands/START_COMMAND/%v" + STOP_COMMAND_ASYNC_KEY = "commands/STOP_COMMAND/%v" + UNLOCK_CONNECTOR_ASYNC_KEY = "commands/UNLOCK_CONNECTOR/%v" +) diff --git a/internal/command/v2.1.1/mocks/mock_resolve.go b/internal/command/v2.1.1/mocks/mock_resolve.go index 3fc9eddc..d0a1c2b7 100644 --- a/internal/command/v2.1.1/mocks/mock_resolve.go +++ b/internal/command/v2.1.1/mocks/mock_resolve.go @@ -14,6 +14,7 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi return &command.CommandResolver{ Repository: commandMocks.NewRepository(repositoryService), OcpiService: services.OcpiService, + AsyncService: services.AsyncService, EvseResolver: evse.NewResolver(repositoryService, services), TokenResolver: token.NewResolver(repositoryService, services), VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), diff --git a/internal/command/v2.1.1/process.go b/internal/command/v2.1.1/process.go index 45f62694..d634af0e 100644 --- a/internal/command/v2.1.1/process.go +++ b/internal/command/v2.1.1/process.go @@ -2,11 +2,14 @@ package command import ( "context" + "fmt" "log" "time" "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/param" + "github.com/satimoto/go-ocpi/internal/async" + coreCommand "github.com/satimoto/go-ocpi/internal/command" dto "github.com/satimoto/go-ocpi/internal/dto/v2.1.1" metrics "github.com/satimoto/go-ocpi/internal/metric" ) @@ -17,12 +20,21 @@ func (r *CommandResolver) UpdateCommandReservation(ctx context.Context, command commandParams.Status = *commandResponseDto.Result commandParams.LastUpdated = time.Now().UTC() - _, err := r.Repository.UpdateCommandReservation(ctx, commandParams) + updatedCommand, err := r.Repository.UpdateCommandReservation(ctx, commandParams) if err != nil { metrics.RecordError("OCPI038", "Error updating command reservation", err) log.Printf("OCPI038: Params=%#v", commandParams) } + + statusAccepted := updatedCommand.Status == db.CommandResponseTypeACCEPTED + asyncKey := fmt.Sprintf(coreCommand.RESERVE_NOW_ASYNC_KEY, updatedCommand.ID) + asyncResult := async.AsyncResult{ + String: string(updatedCommand.Status), + Bool: statusAccepted, + } + + r.AsyncService.Set(asyncKey, asyncResult) } } @@ -39,7 +51,16 @@ func (r *CommandResolver) UpdateCommandStart(ctx context.Context, credential db. log.Printf("OCPI039: Params=%#v", commandParams) } - if updatedCommand.Status == db.CommandResponseTypeACCEPTED && updatedCommand.AuthorizationID.Valid { + statusAccepted := updatedCommand.Status == db.CommandResponseTypeACCEPTED + asyncKey := fmt.Sprintf(coreCommand.START_COMMAND_ASYNC_KEY, updatedCommand.ID) + asyncResult := async.AsyncResult{ + String: string(updatedCommand.Status), + Bool: statusAccepted, + } + + r.AsyncService.Set(asyncKey, asyncResult) + + if statusAccepted && updatedCommand.AuthorizationID.Valid { go r.waitForEvseStatus(credential, updatedCommand) } } @@ -51,12 +72,21 @@ func (r *CommandResolver) UpdateCommandStop(ctx context.Context, command db.Comm commandParams.Status = *commandResponseDto.Result commandParams.LastUpdated = time.Now().UTC() - _, err := r.Repository.UpdateCommandStop(ctx, commandParams) + updatedCommand, err := r.Repository.UpdateCommandStop(ctx, commandParams) if err != nil { metrics.RecordError("OCPI040", "Error updating command stop", err) log.Printf("OCPI040: Params=%#v", commandParams) } + + statusAccepted := updatedCommand.Status == db.CommandResponseTypeACCEPTED + asyncKey := fmt.Sprintf(coreCommand.STOP_COMMAND_ASYNC_KEY, updatedCommand.ID) + asyncResult := async.AsyncResult{ + String: string(updatedCommand.Status), + Bool: statusAccepted, + } + + r.AsyncService.Set(asyncKey, asyncResult) } } @@ -66,12 +96,21 @@ func (r *CommandResolver) UpdateCommandUnlock(ctx context.Context, command db.Co commandParams.Status = *commandResponseDto.Result commandParams.LastUpdated = time.Now().UTC() - _, err := r.Repository.UpdateCommandUnlock(ctx, commandParams) + updatedCommand, err := r.Repository.UpdateCommandUnlock(ctx, commandParams) if err != nil { metrics.RecordError("OCPI041", "Error updating command unlock", err) log.Printf("OCPI041: Params=%#v", commandParams) } + + statusAccepted := updatedCommand.Status == db.CommandResponseTypeACCEPTED + asyncKey := fmt.Sprintf(coreCommand.UNLOCK_CONNECTOR_ASYNC_KEY, updatedCommand.ID) + asyncResult := async.AsyncResult{ + String: string(updatedCommand.Status), + Bool: statusAccepted, + } + + r.AsyncService.Set(asyncKey, asyncResult) } } diff --git a/internal/command/v2.1.1/pull.go b/internal/command/v2.1.1/pull.go index 5c3bd2fa..60e93fa8 100644 --- a/internal/command/v2.1.1/pull.go +++ b/internal/command/v2.1.1/pull.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "log" "net/http" "net/url" @@ -13,6 +14,7 @@ import ( "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/param" dbUtil "github.com/satimoto/go-datastore/pkg/util" + coreCommand "github.com/satimoto/go-ocpi/internal/command" metrics "github.com/satimoto/go-ocpi/internal/metric" "github.com/satimoto/go-ocpi/internal/transportation" "github.com/satimoto/go-ocpi/internal/util" @@ -46,6 +48,9 @@ func (r *CommandResolver) ReserveNow(ctx context.Context, credential db.Credenti return nil, errors.New("error requesting reservation") } + asyncKey := fmt.Sprintf(coreCommand.RESERVE_NOW_ASYNC_KEY, command.ID) + asyncChan := r.AsyncService.Add(asyncKey) + header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) commandDto := r.CreateCommandReservationDto(ctx, command) dtoBytes, err := json.Marshal(commandDto) @@ -82,7 +87,10 @@ func (r *CommandResolver) ReserveNow(ctx context.Context, credential db.Credenti updateCommandReservationParams := param.NewUpdateCommandReservationParams(command) updateCommandReservationParams.Status = db.CommandResponseTypeREJECTED - r.Repository.UpdateCommandReservation(ctx, updateCommandReservationParams) + + if command, err := r.Repository.UpdateCommandReservation(ctx, updateCommandReservationParams); err == nil { + return &command, nil + } return nil, errors.New("error requesting reservation") } @@ -96,6 +104,24 @@ func (r *CommandResolver) ReserveNow(ctx context.Context, credential db.Credenti } } + select { + case <-asyncChan: + log.Printf("Reserve command response received: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + command, err := r.Repository.GetCommandReservation(ctx, command.ID) + + if err != nil || command.Status != db.CommandResponseTypeACCEPTED { + log.Printf("Reserve command response: %v", command.Status) + return nil, errors.New("could not reserve") + } + case <-time.After(90 * time.Second): + log.Printf("Reserve command response timeout: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + return nil, errors.New("charge point unresponsive") + } + return &command, nil } @@ -126,6 +152,9 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden return nil, errors.New("error starting session") } + asyncKey := fmt.Sprintf(coreCommand.START_COMMAND_ASYNC_KEY, command.ID) + asyncChan := r.AsyncService.Add(asyncKey) + header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) commandDto := r.CreateCommandStartDto(ctx, command) dtoBytes, err := json.Marshal(commandDto) @@ -162,7 +191,10 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden updateCommandStartParams := param.NewUpdateCommandStartParams(command) updateCommandStartParams.Status = db.CommandResponseTypeREJECTED - r.Repository.UpdateCommandStart(ctx, updateCommandStartParams) + + if command, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err == nil { + return &command, nil + } return nil, errors.New("error starting session") } @@ -176,6 +208,24 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden } } + select { + case <-asyncChan: + log.Printf("Start command response received: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + command, err := r.Repository.GetCommandStart(ctx, command.ID) + + if err != nil || command.Status != db.CommandResponseTypeACCEPTED { + log.Printf("Start command response: %v", command.Status) + return nil, errors.New("could not start") + } + case <-time.After(90 * time.Second): + log.Printf("Start command response timeout: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + return nil, errors.New("charge point unresponsive") + } + return &command, nil } @@ -206,6 +256,9 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent return nil, errors.New("error stopping session") } + asyncKey := fmt.Sprintf(coreCommand.STOP_COMMAND_ASYNC_KEY, command.ID) + asyncChan := r.AsyncService.Add(asyncKey) + header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) commandDto := r.CreateCommandStopDto(ctx, command) dtoBytes, err := json.Marshal(commandDto) @@ -263,6 +316,24 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent } } + select { + case <-asyncChan: + log.Printf("Stop command response received: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + command, err := r.Repository.GetCommandStop(ctx, command.ID) + + if err != nil || command.Status != db.CommandResponseTypeACCEPTED { + log.Printf("Stop command response: %v", command.Status) + return nil, errors.New("could not stop") + } + case <-time.After(90 * time.Second): + log.Printf("Stop command response timeout: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + return nil, errors.New("charge point unresponsive") + } + return &command, nil } @@ -293,6 +364,9 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre return nil, errors.New("error unlocking connector") } + asyncKey := fmt.Sprintf(coreCommand.UNLOCK_CONNECTOR_ASYNC_KEY, command.ID) + asyncChan := r.AsyncService.Add(asyncKey) + header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) commandDto := r.CreateCommandUnlockDto(ctx, command) dtoBytes, err := json.Marshal(commandDto) @@ -329,7 +403,10 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre updateCommandUnlockParams := param.NewUpdateCommandUnlockParams(command) updateCommandUnlockParams.Status = db.CommandResponseTypeREJECTED - r.Repository.UpdateCommandUnlock(ctx, updateCommandUnlockParams) + + if command, err := r.Repository.UpdateCommandUnlock(ctx, updateCommandUnlockParams); err == nil { + return &command, nil + } return nil, errors.New("error unlocking connector") } @@ -343,5 +420,23 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre } } + select { + case <-asyncChan: + log.Printf("Unlock command response received: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + command, err := r.Repository.GetCommandUnlock(ctx, command.ID) + + if err != nil || command.Status != db.CommandResponseTypeACCEPTED { + log.Printf("Unlock command response: %v", command.Status) + return nil, errors.New("could not unlock") + } + case <-time.After(90 * time.Second): + log.Printf("Unlock command response timeout: %v", asyncKey) + r.AsyncService.Remove(asyncKey) + + return nil, errors.New("charge point unresponsive") + } + return &command, nil } diff --git a/internal/command/v2.1.1/resolve.go b/internal/command/v2.1.1/resolve.go index cfc2699e..9304e039 100644 --- a/internal/command/v2.1.1/resolve.go +++ b/internal/command/v2.1.1/resolve.go @@ -3,6 +3,7 @@ package command import ( "github.com/satimoto/go-datastore/pkg/command" "github.com/satimoto/go-datastore/pkg/db" + "github.com/satimoto/go-ocpi/internal/async" evse "github.com/satimoto/go-ocpi/internal/evse/v2.1.1" "github.com/satimoto/go-ocpi/internal/service" token "github.com/satimoto/go-ocpi/internal/token/v2.1.1" @@ -13,6 +14,7 @@ import ( type CommandResolver struct { Repository command.CommandRepository OcpiService *transportation.OcpiService + AsyncService *async.AsyncService EvseResolver *evse.EvseResolver TokenResolver *token.TokenResolver VersionDetailResolver *versiondetail.VersionDetailResolver @@ -22,6 +24,7 @@ func NewResolver(repositoryService *db.RepositoryService, services *service.Serv return &CommandResolver{ Repository: command.NewRepository(repositoryService), OcpiService: services.OcpiService, + AsyncService: services.AsyncService, EvseResolver: evse.NewResolver(repositoryService, services), TokenResolver: token.NewResolver(repositoryService, services), VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), diff --git a/internal/rest/route_command_2.1.1.go b/internal/rest/route_command_2.1.1.go index 2bd5305e..08c0d9ed 100644 --- a/internal/rest/route_command_2.1.1.go +++ b/internal/rest/route_command_2.1.1.go @@ -12,7 +12,7 @@ func (rs *RestService) mountCommands() *chi.Mux { commandResolver := command.NewResolver(rs.RepositoryService, rs.ServiceResolver) router := chi.NewRouter() - router.Use(middleware.Timeout(30 * time.Second)) + router.Use(middleware.Timeout(120 * time.Second)) router.Use(rs.CredentialContextByToken) router.Route("/RESERVE_NOW/{command_id}", func(commandRouter chi.Router) { diff --git a/internal/session/v2.1.1/mocks/mock_resolve.go b/internal/session/v2.1.1/mocks/mock_resolve.go index 3b152c4c..5fd437d9 100644 --- a/internal/session/v2.1.1/mocks/mock_resolve.go +++ b/internal/session/v2.1.1/mocks/mock_resolve.go @@ -18,6 +18,7 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi return &session.SessionResolver{ Repository: sessionMocks.NewRepository(repositoryService), OcpiService: services.OcpiService, + AsyncService: services.AsyncService, ChargingPeriodResolver: chargingperiod.NewResolver(repositoryService), CommandResolver: command.NewResolver(repositoryService, services), LocationResolver: location.NewResolver(repositoryService, services), diff --git a/internal/session/v2.1.1/monitor.go b/internal/session/v2.1.1/monitor.go new file mode 100644 index 00000000..b640941c --- /dev/null +++ b/internal/session/v2.1.1/monitor.go @@ -0,0 +1,211 @@ +package session + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "time" + + "github.com/satimoto/go-datastore/pkg/db" + "github.com/satimoto/go-datastore/pkg/util" + "github.com/satimoto/go-ocpi/internal/async" + coreCommand "github.com/satimoto/go-ocpi/internal/command" + dto "github.com/satimoto/go-ocpi/internal/dto/v2.1.1" + coreLocation "github.com/satimoto/go-ocpi/internal/location" + metrics "github.com/satimoto/go-ocpi/internal/metric" + "github.com/satimoto/go-ocpi/internal/transportation" + "github.com/satimoto/go-ocpi/pkg/ocpi" + ocpiSession "github.com/satimoto/go-ocpi/pkg/ocpi/session" +) + +func (r *SessionResolver) sendOcpiRequest(session db.Session, sessionCreated, statusChanged bool) { + ctx := context.Background() + node, err := r.NodeRepository.GetNodeByUserID(ctx, session.UserID) + + if err != nil { + metrics.RecordError("OCPI167", "Error retrieving node", err) + log.Printf("OCPI167: UserID=%v", session.UserID) + return + } + + // TODO: Handle failed RPC call more robustly + ocpiService := ocpi.NewService(node.LspAddr) + + if sessionCreated { + sessionCreatedRequest := ocpiSession.NewSessionCreatedRequest(session) + sessionCreatedResponse, err := ocpiService.SessionCreated(ctx, sessionCreatedRequest) + + if err != nil { + metrics.RecordError("OCPI168", "Error calling RPC service", err) + log.Printf("OCPI168: Request=%#v, Response=%#v", sessionCreatedRequest, sessionCreatedResponse) + } + } else if statusChanged { + sessionUpdatedRequest := ocpiSession.NewSessionUpdatedRequest(session) + sessionUpdatedResponse, err := ocpiService.SessionUpdated(ctx, sessionUpdatedRequest) + + if err != nil { + metrics.RecordError("OCPI273", "Error calling RPC service", err) + log.Printf("OCPI273: Request=%#v, Response=%#v", sessionUpdatedRequest, sessionUpdatedResponse) + } + } +} + +func (r *SessionResolver) updateCommand(session db.Session) { + ctx := context.Background() + + if session.AuthorizationID.Valid { + if session.Status == db.SessionStatusTypeACTIVE || session.Status == db.SessionStatusTypePENDING { + updateCommandStartByAuthorizationIDParams := db.UpdateCommandStartByAuthorizationIDParams{ + AuthorizationID: util.SqlNullString(session.AuthorizationID.String), + Status: db.CommandResponseTypeACCEPTED, + LastUpdated: time.Now().UTC(), + } + + command, err := r.CommandResolver.Repository.UpdateCommandStartByAuthorizationID(ctx, updateCommandStartByAuthorizationIDParams) + + if err != nil { + metrics.RecordError("OCPI327", "Error updating command start", err) + log.Printf("OCPI327: AuthorizationID=%#v", session.AuthorizationID) + return + } + + asyncKey := fmt.Sprintf(coreCommand.START_COMMAND_ASYNC_KEY, command.ID) + asyncResult := async.AsyncResult{ + String: string(command.Status), + Bool: command.Status == db.CommandResponseTypeACCEPTED, + } + + r.AsyncService.Set(asyncKey, asyncResult) + } else if session.Status == db.SessionStatusTypeCOMPLETED { + updateCommandStopBySessionIDParams := db.UpdateCommandStopBySessionIDParams{ + SessionID: session.Uid, + Status: db.CommandResponseTypeACCEPTED, + LastUpdated: time.Now().UTC(), + } + + command, err := r.CommandResolver.Repository.UpdateCommandStopBySessionID(ctx, updateCommandStopBySessionIDParams) + + if err != nil { + metrics.RecordError("OCPI328", "Error updating command stop", err) + log.Printf("OCPI328: SessionUid=%#v", session.Uid) + return + } + + asyncKey := fmt.Sprintf(coreCommand.STOP_COMMAND_ASYNC_KEY, command.ID) + asyncResult := async.AsyncResult{ + String: string(command.Status), + Bool: command.Status == db.CommandResponseTypeACCEPTED, + } + + r.AsyncService.Set(asyncKey, asyncResult) + } + } +} + +func (r *SessionResolver) waitForEvseStatus(credential db.Credential, locationID, evseID int64, evseStatus db.EvseStatus, sess db.Session, sessionFromStatus db.SessionStatusType, sessionToStatus db.SessionStatusType, timeoutSeconds int) { + ctx := context.Background() + deadline := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) + log.Printf("Waiting for Evse status change to %v over %v seconds: LocationID=%v, EvseID=%v", evseStatus, timeoutSeconds, locationID, evseID) + + versionEndpoint, err := r.VersionDetailResolver.GetVersionEndpointByIdentity(ctx, coreLocation.IDENTIFIER, credential.CountryCode, credential.PartyID) + + if err != nil { + metrics.RecordError("OCPI302", "Error getting version endpoint", err) + log.Printf("OCPI302: CountryCode=%v, PartyID=%v, Identifier=%v", credential.CountryCode, credential.PartyID, coreLocation.IDENTIFIER) + return + } + + location, err := r.LocationResolver.Repository.GetLocation(ctx, locationID) + + if err != nil { + metrics.RecordError("OCPI303", "Error getting location", err) + log.Printf("OCPI303: LocationID=%v", locationID) + return + } + + evse, err := r.LocationResolver.Repository.GetEvse(ctx, evseID) + + if err != nil { + metrics.RecordError("OCPI304", "Error getting evse", err) + log.Printf("OCPI304: EvseID=%v", evseID) + return + } + + evseUrl := fmt.Sprintf("%s/%s/%s", versionEndpoint.Url, location.Uid, evse.Uid) + requestUrl, err := url.Parse(evseUrl) + + if err != nil { + metrics.RecordError("OCPI305", "Error parsing url", err) + log.Printf("OCPI305: Url=%v", evseUrl) + return + } + + header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) + +waitLoop: + for { + time.Sleep(10 * time.Second) + + if time.Now().After(deadline) { + log.Printf("Timeout. Stopped waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) + break waitLoop + } + + if sess.AuthorizationID.Valid { + tokenAuthorization, err := r.TokenAuthorizationRepository.GetTokenAuthorizationByAuthorizationID(ctx, sess.AuthorizationID.String) + + if err != nil && !tokenAuthorization.Authorized { + // Token authorization has been unauthorized + log.Printf("Unauthorized. Stop waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) + break waitLoop + } + } + + response, err := r.OcpiService.Do(http.MethodGet, requestUrl.String(), header, nil) + + if err != nil { + metrics.RecordError("OCPI306", "Error making request", err) + log.Printf("OCPI306: Method=%v, Url=%v, Header=%#v", http.MethodGet, requestUrl.String(), header) + continue + } + + evseDto, err := r.LocationResolver.EvseResolver.UnmarshalPullDto(response.Body) + defer response.Body.Close() + + if err != nil { + metrics.RecordError("OCPI307", "Error unmarshaling response", err) + util.LogHttpResponse("OCPI307", requestUrl.String(), response, true) + continue + } + + if evseDto.StatusCode == transportation.STATUS_CODE_OK && evseDto.Data.Status != nil { + responseEvseStatus := *evseDto.Data.Status + + log.Printf("Evse status is %v: LocationID=%v, EvseID=%v", responseEvseStatus, locationID, evseID) + + if responseEvseStatus == evseStatus { + updatedSession, err := r.Repository.GetSession(ctx, sess.ID) + + if err != nil { + metrics.RecordError("OCPI308", "Error getting session", err) + log.Printf("OCPI308: SessionID=%v", sess.ID) + continue + } + + if updatedSession.Status == sessionFromStatus { + log.Printf("Manually updating session status to %v: SessionUid=%v", sessionToStatus, updatedSession.Uid) + + sessionDto := dto.SessionDto{ + Status: &sessionToStatus, + } + + r.ReplaceSessionByIdentifier(ctx, credential, util.NilString(updatedSession.CountryCode), util.NilString(updatedSession.PartyID), updatedSession.Uid, &sessionDto) + } + + break waitLoop + } + } + } +} diff --git a/internal/session/v2.1.1/process.go b/internal/session/v2.1.1/process.go index 202a96f8..4f4c18d1 100644 --- a/internal/session/v2.1.1/process.go +++ b/internal/session/v2.1.1/process.go @@ -2,22 +2,14 @@ package session import ( "context" - "fmt" "log" - "net/http" - "net/url" - "time" "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/param" "github.com/satimoto/go-datastore/pkg/util" dto "github.com/satimoto/go-ocpi/internal/dto/v2.1.1" evse "github.com/satimoto/go-ocpi/internal/evse/v2.1.1" - coreLocation "github.com/satimoto/go-ocpi/internal/location" metrics "github.com/satimoto/go-ocpi/internal/metric" - "github.com/satimoto/go-ocpi/internal/transportation" - "github.com/satimoto/go-ocpi/pkg/ocpi" - ocpiSession "github.com/satimoto/go-ocpi/pkg/ocpi/session" ) func (r *SessionResolver) ReplaceSession(ctx context.Context, credential db.Credential, uid string, sessionDto *dto.SessionDto) *db.Session { @@ -188,6 +180,10 @@ func (r *SessionResolver) ReplaceSessionByIdentifier(ctx context.Context, creden // Send a session created/update RPC message to LSP go r.sendOcpiRequest(session, sessionCreated, statusChanged) + if sessionCreated || statusChanged { + go r.updateCommand(session) + } + if sessionCreated && session.Status == db.SessionStatusTypePENDING { go r.waitForEvseStatus(credential, session.LocationID, session.EvseID, db.EvseStatusCHARGING, session, session.Status, db.SessionStatusTypeACTIVE, 150) } @@ -246,141 +242,3 @@ func (r *SessionResolver) replaceTokenAuthorization(ctx context.Context, country r.TokenAuthorizationRepository.UpdateTokenAuthorizationByAuthorizationID(ctx, tokenAuthorizationParams) } - -func (r *SessionResolver) sendOcpiRequest(session db.Session, sessionCreated, statusChanged bool) { - ctx := context.Background() - node, err := r.NodeRepository.GetNodeByUserID(ctx, session.UserID) - - if err != nil { - metrics.RecordError("OCPI167", "Error retrieving node", err) - log.Printf("OCPI167: UserID=%v", session.UserID) - return - } - - // TODO: Handle failed RPC call more robustly - ocpiService := ocpi.NewService(node.LspAddr) - - if sessionCreated { - sessionCreatedRequest := ocpiSession.NewSessionCreatedRequest(session) - sessionCreatedResponse, err := ocpiService.SessionCreated(ctx, sessionCreatedRequest) - - if err != nil { - metrics.RecordError("OCPI168", "Error calling RPC service", err) - log.Printf("OCPI168: Request=%#v, Response=%#v", sessionCreatedRequest, sessionCreatedResponse) - } - } else if statusChanged { - sessionUpdatedRequest := ocpiSession.NewSessionUpdatedRequest(session) - sessionUpdatedResponse, err := ocpiService.SessionUpdated(ctx, sessionUpdatedRequest) - - if err != nil { - metrics.RecordError("OCPI273", "Error calling RPC service", err) - log.Printf("OCPI273: Request=%#v, Response=%#v", sessionUpdatedRequest, sessionUpdatedResponse) - } - } -} - -func (r *SessionResolver) waitForEvseStatus(credential db.Credential, locationID, evseID int64, evseStatus db.EvseStatus, sess db.Session, sessionFromStatus db.SessionStatusType, sessionToStatus db.SessionStatusType, timeoutSeconds int) { - ctx := context.Background() - deadline := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) - log.Printf("Waiting for Evse status change to %v over %v seconds: LocationID=%v, EvseID=%v", evseStatus, timeoutSeconds, locationID, evseID) - - versionEndpoint, err := r.VersionDetailResolver.GetVersionEndpointByIdentity(ctx, coreLocation.IDENTIFIER, credential.CountryCode, credential.PartyID) - - if err != nil { - metrics.RecordError("OCPI302", "Error getting version endpoint", err) - log.Printf("OCPI302: CountryCode=%v, PartyID=%v, Identifier=%v", credential.CountryCode, credential.PartyID, coreLocation.IDENTIFIER) - return - } - - location, err := r.LocationResolver.Repository.GetLocation(ctx, locationID) - - if err != nil { - metrics.RecordError("OCPI303", "Error getting location", err) - log.Printf("OCPI303: LocationID=%v", locationID) - return - } - - evse, err := r.LocationResolver.Repository.GetEvse(ctx, evseID) - - if err != nil { - metrics.RecordError("OCPI304", "Error getting evse", err) - log.Printf("OCPI304: EvseID=%v", evseID) - return - } - - evseUrl := fmt.Sprintf("%s/%s/%s", versionEndpoint.Url, location.Uid, evse.Uid) - requestUrl, err := url.Parse(evseUrl) - - if err != nil { - metrics.RecordError("OCPI305", "Error parsing url", err) - log.Printf("OCPI305: Url=%v", evseUrl) - return - } - - header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, nil, nil) - -waitLoop: - for { - time.Sleep(10 * time.Second) - - if time.Now().After(deadline) { - log.Printf("Timeout. Stopped waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) - break waitLoop - } - - if sess.AuthorizationID.Valid { - tokenAuthorization, err := r.TokenAuthorizationRepository.GetTokenAuthorizationByAuthorizationID(ctx, sess.AuthorizationID.String) - - if err != nil && !tokenAuthorization.Authorized { - // Token authorization has been unauthorized - log.Printf("Unauthorized. Stop waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) - break waitLoop - } - } - - response, err := r.OcpiService.Do(http.MethodGet, requestUrl.String(), header, nil) - - if err != nil { - metrics.RecordError("OCPI306", "Error making request", err) - log.Printf("OCPI306: Method=%v, Url=%v, Header=%#v", http.MethodGet, requestUrl.String(), header) - continue - } - - evseDto, err := r.LocationResolver.EvseResolver.UnmarshalPullDto(response.Body) - defer response.Body.Close() - - if err != nil { - metrics.RecordError("OCPI307", "Error unmarshaling response", err) - util.LogHttpResponse("OCPI307", requestUrl.String(), response, true) - continue - } - - if evseDto.StatusCode == transportation.STATUS_CODE_OK && evseDto.Data.Status != nil { - responseEvseStatus := *evseDto.Data.Status - - log.Printf("Evse status is %v: LocationID=%v, EvseID=%v", responseEvseStatus, locationID, evseID) - - if responseEvseStatus == evseStatus { - updatedSession, err := r.Repository.GetSession(ctx, sess.ID) - - if err != nil { - metrics.RecordError("OCPI308", "Error getting session", err) - log.Printf("OCPI308: SessionID=%v", sess.ID) - continue - } - - if updatedSession.Status == sessionFromStatus { - log.Printf("Manually updating session status to %v: SessionUid=%v", sessionToStatus, updatedSession.Uid) - - sessionDto := dto.SessionDto{ - Status: &sessionToStatus, - } - - r.ReplaceSessionByIdentifier(ctx, credential, util.NilString(updatedSession.CountryCode), util.NilString(updatedSession.PartyID), updatedSession.Uid, &sessionDto) - } - - break waitLoop - } - } - } -} diff --git a/internal/session/v2.1.1/resolve.go b/internal/session/v2.1.1/resolve.go index 27261c2d..5170a0c3 100644 --- a/internal/session/v2.1.1/resolve.go +++ b/internal/session/v2.1.1/resolve.go @@ -6,6 +6,7 @@ import ( "github.com/satimoto/go-datastore/pkg/session" "github.com/satimoto/go-datastore/pkg/token" "github.com/satimoto/go-datastore/pkg/tokenauthorization" + "github.com/satimoto/go-ocpi/internal/async" "github.com/satimoto/go-ocpi/internal/chargingperiod" command "github.com/satimoto/go-ocpi/internal/command/v2.1.1" location "github.com/satimoto/go-ocpi/internal/location/v2.1.1" @@ -17,6 +18,7 @@ import ( type SessionResolver struct { Repository session.SessionRepository OcpiService *transportation.OcpiService + AsyncService *async.AsyncService ChargingPeriodResolver *chargingperiod.ChargingPeriodResolver CommandResolver *command.CommandResolver LocationResolver *location.LocationResolver @@ -30,6 +32,7 @@ func NewResolver(repositoryService *db.RepositoryService, services *service.Serv return &SessionResolver{ Repository: session.NewRepository(repositoryService), OcpiService: services.OcpiService, + AsyncService: services.AsyncService, ChargingPeriodResolver: chargingperiod.NewResolver(repositoryService), CommandResolver: command.NewResolver(repositoryService, services), LocationResolver: location.NewResolver(repositoryService, services), From 53c44f8d812af2f73f2a6361153b56d8330a1700 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Mon, 12 Dec 2022 17:09:51 +0100 Subject: [PATCH 05/14] Fix variable assignment issues --- internal/command/v2.1.1/pull.go | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/command/v2.1.1/pull.go b/internal/command/v2.1.1/pull.go index 60e93fa8..24e20abb 100644 --- a/internal/command/v2.1.1/pull.go +++ b/internal/command/v2.1.1/pull.go @@ -88,8 +88,8 @@ func (r *CommandResolver) ReserveNow(ctx context.Context, credential db.Credenti updateCommandReservationParams := param.NewUpdateCommandReservationParams(command) updateCommandReservationParams.Status = db.CommandResponseTypeREJECTED - if command, err := r.Repository.UpdateCommandReservation(ctx, updateCommandReservationParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandReservation(ctx, updateCommandReservationParams); err == nil { + return &updatedCommand, nil } return nil, errors.New("error requesting reservation") @@ -99,8 +99,8 @@ func (r *CommandResolver) ReserveNow(ctx context.Context, credential db.Credenti updateCommandReservationParams := param.NewUpdateCommandReservationParams(command) updateCommandReservationParams.Status = *pullDto.Data.Result - if command, err = r.Repository.UpdateCommandReservation(ctx, updateCommandReservationParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandReservation(ctx, updateCommandReservationParams); err == nil { + return &updatedCommand, nil } } @@ -109,7 +109,7 @@ func (r *CommandResolver) ReserveNow(ctx context.Context, credential db.Credenti log.Printf("Reserve command response received: %v", asyncKey) r.AsyncService.Remove(asyncKey) - command, err := r.Repository.GetCommandReservation(ctx, command.ID) + command, err = r.Repository.GetCommandReservation(ctx, command.ID) if err != nil || command.Status != db.CommandResponseTypeACCEPTED { log.Printf("Reserve command response: %v", command.Status) @@ -192,8 +192,8 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden updateCommandStartParams := param.NewUpdateCommandStartParams(command) updateCommandStartParams.Status = db.CommandResponseTypeREJECTED - if command, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err == nil { + return &updatedCommand, nil } return nil, errors.New("error starting session") @@ -203,8 +203,8 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden updateCommandStartParams := param.NewUpdateCommandStartParams(command) updateCommandStartParams.Status = *pullDto.Data.Result - if command, err = r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err == nil { + return &updatedCommand, nil } } @@ -213,7 +213,7 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden log.Printf("Start command response received: %v", asyncKey) r.AsyncService.Remove(asyncKey) - command, err := r.Repository.GetCommandStart(ctx, command.ID) + command, err = r.Repository.GetCommandStart(ctx, command.ID) if err != nil || command.Status != db.CommandResponseTypeACCEPTED { log.Printf("Start command response: %v", command.Status) @@ -300,8 +300,8 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent updateCommandStopParams.Status = db.CommandResponseTypeUNKNOWNSESSION } - if command, err := r.Repository.UpdateCommandStop(ctx, updateCommandStopParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandStop(ctx, updateCommandStopParams); err == nil { + return &updatedCommand, nil } return nil, errors.New("error stopping session") @@ -311,8 +311,8 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent updateCommandStopParams := param.NewUpdateCommandStopParams(command) updateCommandStopParams.Status = *pullDto.Data.Result - if command, err = r.Repository.UpdateCommandStop(ctx, updateCommandStopParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandStop(ctx, updateCommandStopParams); err == nil { + return &updatedCommand, nil } } @@ -321,7 +321,7 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent log.Printf("Stop command response received: %v", asyncKey) r.AsyncService.Remove(asyncKey) - command, err := r.Repository.GetCommandStop(ctx, command.ID) + command, err = r.Repository.GetCommandStop(ctx, command.ID) if err != nil || command.Status != db.CommandResponseTypeACCEPTED { log.Printf("Stop command response: %v", command.Status) @@ -404,8 +404,8 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre updateCommandUnlockParams := param.NewUpdateCommandUnlockParams(command) updateCommandUnlockParams.Status = db.CommandResponseTypeREJECTED - if command, err := r.Repository.UpdateCommandUnlock(ctx, updateCommandUnlockParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandUnlock(ctx, updateCommandUnlockParams); err == nil { + return &updatedCommand, nil } return nil, errors.New("error unlocking connector") @@ -415,8 +415,8 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre updateCommandUnlockParams := param.NewUpdateCommandUnlockParams(command) updateCommandUnlockParams.Status = *pullDto.Data.Result - if command, err = r.Repository.UpdateCommandUnlock(ctx, updateCommandUnlockParams); err == nil { - return &command, nil + if updatedCommand, err := r.Repository.UpdateCommandUnlock(ctx, updateCommandUnlockParams); err == nil { + return &updatedCommand, nil } } @@ -425,7 +425,7 @@ func (r *CommandResolver) UnlockConnector(ctx context.Context, credential db.Cre log.Printf("Unlock command response received: %v", asyncKey) r.AsyncService.Remove(asyncKey) - command, err := r.Repository.GetCommandUnlock(ctx, command.ID) + command, err = r.Repository.GetCommandUnlock(ctx, command.ID) if err != nil || command.Status != db.CommandResponseTypeACCEPTED { log.Printf("Unlock command response: %v", command.Status) From d3c537812dd42e3fc26d280ae6e2bd82ddc40f02 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Mon, 12 Dec 2022 21:29:23 +0100 Subject: [PATCH 06/14] Monitor session status and cancel EVSE check if status changes --- internal/rpc/command/rpc.go | 26 +++++++++----------------- internal/session/v2.1.1/monitor.go | 27 +++++++++++++-------------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/internal/rpc/command/rpc.go b/internal/rpc/command/rpc.go index cc424c3e..879454cd 100644 --- a/internal/rpc/command/rpc.go +++ b/internal/rpc/command/rpc.go @@ -157,7 +157,7 @@ func (r *RpcCommandResolver) StartSession(ctx context.Context, input *ocpirpc.St func (r *RpcCommandResolver) StopSession(ctx context.Context, input *ocpirpc.StopSessionRequest) (*ocpirpc.StopSessionResponse, error) { if input != nil { defaultResponse := ocpirpc.StopSessionResponse{ - Status: string(db.CommandResponseTypeACCEPTED), + Status: string(db.CommandResponseTypeACCEPTED), AuthorizationId: input.AuthorizationId, } @@ -174,26 +174,18 @@ func (r *RpcCommandResolver) StopSession(ctx context.Context, input *ocpirpc.Sto } if session, err := r.SessionResolver.Repository.GetSessionByAuthorizationID(ctx, input.AuthorizationId); err == nil { - if session.Uid == session.AuthorizationID.String { - // This was a manually created session + if session.Uid == session.AuthorizationID.String || session.Status == db.SessionStatusTypePENDING { + // This was a manually created session or the status is still pending updateSessionByUidParams := param.NewUpdateSessionByUidParams(session) updateSessionByUidParams.Status = db.SessionStatusTypeINVALID if _, err := r.SessionResolver.Repository.UpdateSessionByUid(ctx, updateSessionByUidParams); err != nil { metrics.RecordError("OCPI309", "Error updating session", err) - log.Printf("OCPI309: Params=%#v", updateSessionByUidParams) + log.Printf("OCPI309: Params=%#v", updateSessionByUidParams) } - return &defaultResponse, nil - } - - if session.Status == db.SessionStatusTypePENDING { - updateSessionByUidParams := param.NewUpdateSessionByUidParams(session) - updateSessionByUidParams.Status = db.SessionStatusTypeINVALID - - if _, err := r.SessionResolver.Repository.UpdateSessionByUid(ctx, updateSessionByUidParams); err != nil { - metrics.RecordError("OCPI309", "Error updating session", err) - log.Printf("OCPI309: Params=%#v", updateSessionByUidParams) + if session.Uid == session.AuthorizationID.String { + return &defaultResponse, nil } } @@ -204,15 +196,15 @@ func (r *RpcCommandResolver) StopSession(ctx context.Context, input *ocpirpc.Sto log.Printf("OCPI154: CredentialID=%v", session.CredentialID) return nil, errors.New("credential not found") } - + if !credential.ClientToken.Valid || len(credential.ClientToken.String) == 0 { metrics.RecordError("OCPI155", "Error invalid credential", err) log.Printf("OCPI155: CredentialID=%v, Token=%v", credential.ID, credential.ClientToken) return nil, errors.New("invalid credential token") } - + command, err := r.CommandResolver.StopSession(ctx, credential, session.Uid) - + if err != nil { metrics.RecordError("OCPI156", "Error requesting stop", err) log.Printf("OCPI156: Input=%#v", input) diff --git a/internal/session/v2.1.1/monitor.go b/internal/session/v2.1.1/monitor.go index b640941c..5cc907fb 100644 --- a/internal/session/v2.1.1/monitor.go +++ b/internal/session/v2.1.1/monitor.go @@ -153,6 +153,15 @@ waitLoop: break waitLoop } + if updatedSession, err := r.Repository.GetSession(ctx, sess.ID); err == nil { + sess = updatedSession + } + + if sess.Status != sessionFromStatus { + log.Printf("Session. Stop waiting for Evse status change: LocationID=%v, EvseID=%v", locationID, evseID) + break waitLoop + } + if sess.AuthorizationID.Valid { tokenAuthorization, err := r.TokenAuthorizationRepository.GetTokenAuthorizationByAuthorizationID(ctx, sess.AuthorizationID.String) @@ -186,23 +195,13 @@ waitLoop: log.Printf("Evse status is %v: LocationID=%v, EvseID=%v", responseEvseStatus, locationID, evseID) if responseEvseStatus == evseStatus { - updatedSession, err := r.Repository.GetSession(ctx, sess.ID) + log.Printf("Manually updating session status to %v: SessionUid=%v", sessionToStatus, sess.Uid) - if err != nil { - metrics.RecordError("OCPI308", "Error getting session", err) - log.Printf("OCPI308: SessionID=%v", sess.ID) - continue + sessionDto := dto.SessionDto{ + Status: &sessionToStatus, } - if updatedSession.Status == sessionFromStatus { - log.Printf("Manually updating session status to %v: SessionUid=%v", sessionToStatus, updatedSession.Uid) - - sessionDto := dto.SessionDto{ - Status: &sessionToStatus, - } - - r.ReplaceSessionByIdentifier(ctx, credential, util.NilString(updatedSession.CountryCode), util.NilString(updatedSession.PartyID), updatedSession.Uid, &sessionDto) - } + r.ReplaceSessionByIdentifier(ctx, credential, util.NilString(sess.CountryCode), util.NilString(sess.PartyID), sess.Uid, &sessionDto) break waitLoop } From 339b2b5089952e8b3420da7f0ac62ab632507d5f Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Mon, 12 Dec 2022 22:53:37 +0100 Subject: [PATCH 07/14] Return error responses for not accepted start commands --- internal/command/v2.1.1/pull.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/command/v2.1.1/pull.go b/internal/command/v2.1.1/pull.go index 24e20abb..735491f8 100644 --- a/internal/command/v2.1.1/pull.go +++ b/internal/command/v2.1.1/pull.go @@ -192,20 +192,24 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden updateCommandStartParams := param.NewUpdateCommandStartParams(command) updateCommandStartParams.Status = db.CommandResponseTypeREJECTED - if updatedCommand, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err == nil { - return &updatedCommand, nil + if _, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err != nil { + metrics.RecordError("OCPI308", "Error updating command start", err) + dbUtil.LogHttpResponse("OCPI308", requestUrl.String(), response, true) } - return nil, errors.New("error starting session") + return nil, errors.New("charge point unresponsive") } if pullDto.Data.Result != nil && *pullDto.Data.Result != db.CommandResponseTypeACCEPTED { updateCommandStartParams := param.NewUpdateCommandStartParams(command) updateCommandStartParams.Status = *pullDto.Data.Result - if updatedCommand, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err == nil { - return &updatedCommand, nil + if _, err := r.Repository.UpdateCommandStart(ctx, updateCommandStartParams); err != nil { + metrics.RecordError("OCPI330", "Error updating command start", err) + dbUtil.LogHttpResponse("OCPI330", requestUrl.String(), response, true) } + + return nil, errors.New("could not start charge") } select { @@ -217,7 +221,7 @@ func (r *CommandResolver) StartSession(ctx context.Context, credential db.Creden if err != nil || command.Status != db.CommandResponseTypeACCEPTED { log.Printf("Start command response: %v", command.Status) - return nil, errors.New("could not start") + return nil, errors.New("could not start charge") } case <-time.After(90 * time.Second): log.Printf("Start command response timeout: %v", asyncKey) @@ -325,7 +329,7 @@ func (r *CommandResolver) StopSession(ctx context.Context, credential db.Credent if err != nil || command.Status != db.CommandResponseTypeACCEPTED { log.Printf("Stop command response: %v", command.Status) - return nil, errors.New("could not stop") + return nil, errors.New("could not stop charge") } case <-time.After(90 * time.Second): log.Printf("Stop command response timeout: %v", asyncKey) From 3b2f51d682f9a2fbc5f4018e2bb832996fd4ca90 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Tue, 13 Dec 2022 18:07:18 +0100 Subject: [PATCH 08/14] Log command responses --- internal/command/v2.1.1/push.go | 27 ++++++++++++++++++--------- internal/command/v2.1.1/push_test.go | 2 ++ internal/rpc/command/rpc.go | 8 ++++---- internal/util/request.go | 22 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 internal/util/request.go diff --git a/internal/command/v2.1.1/push.go b/internal/command/v2.1.1/push.go index a1fc5fec..a91157b8 100644 --- a/internal/command/v2.1.1/push.go +++ b/internal/command/v2.1.1/push.go @@ -6,20 +6,23 @@ import ( "github.com/go-chi/render" "github.com/satimoto/go-datastore/pkg/db" - "github.com/satimoto/go-datastore/pkg/util" + dbUtil "github.com/satimoto/go-datastore/pkg/util" metrics "github.com/satimoto/go-ocpi/internal/metric" "github.com/satimoto/go-ocpi/internal/middleware" "github.com/satimoto/go-ocpi/internal/transportation" + "github.com/satimoto/go-ocpi/internal/util" ) func (r *CommandResolver) PostCommandReservationResponse(rw http.ResponseWriter, request *http.Request) { ctx := request.Context() command := ctx.Value("command").(db.CommandReservation) + + util.DebugRequest(request) dto, err := r.UnmarshalPushDto(request.Body) if err != nil { metrics.RecordError("OCPI071", "Error unmarshaling request", err) - util.LogHttpRequest("OCPI071", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI071", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) return @@ -29,7 +32,7 @@ func (r *CommandResolver) PostCommandReservationResponse(rw http.ResponseWriter, if err := render.Render(rw, request, transportation.OcpiSuccess(nil)); err != nil { log.Print("OCPI072", "Error updating reservation command") - util.LogHttpRequest("OCPI072", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI072", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) } @@ -39,11 +42,13 @@ func (r *CommandResolver) PostCommandStartResponse(rw http.ResponseWriter, reque ctx := request.Context() cred := middleware.GetCredential(ctx) command := ctx.Value("command").(db.CommandStart) + + util.DebugRequest(request) dto, err := r.UnmarshalPushDto(request.Body) if err != nil { metrics.RecordError("OCPI073", "Error unmarshaling request", err) - util.LogHttpRequest("OCPI073", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI073", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) return @@ -53,7 +58,7 @@ func (r *CommandResolver) PostCommandStartResponse(rw http.ResponseWriter, reque if err := render.Render(rw, request, transportation.OcpiSuccess(nil)); err != nil { log.Print("OCPI074", "Error updating start command") - util.LogHttpRequest("OCPI074", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI074", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) } @@ -62,11 +67,13 @@ func (r *CommandResolver) PostCommandStartResponse(rw http.ResponseWriter, reque func (r *CommandResolver) PostCommandStopResponse(rw http.ResponseWriter, request *http.Request) { ctx := request.Context() command := ctx.Value("command").(db.CommandStop) + + util.DebugRequest(request) dto, err := r.UnmarshalPushDto(request.Body) if err != nil { metrics.RecordError("OCPI075", "Error unmarshaling request", err) - util.LogHttpRequest("OCPI075", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI075", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) return @@ -76,7 +83,7 @@ func (r *CommandResolver) PostCommandStopResponse(rw http.ResponseWriter, reques if err := render.Render(rw, request, transportation.OcpiSuccess(nil)); err != nil { log.Print("OCPI076", "Error updating stop command") - util.LogHttpRequest("OCPI076", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI076", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) } @@ -85,11 +92,13 @@ func (r *CommandResolver) PostCommandStopResponse(rw http.ResponseWriter, reques func (r *CommandResolver) PostCommandUnlockResponse(rw http.ResponseWriter, request *http.Request) { ctx := request.Context() command := ctx.Value("command").(db.CommandUnlock) + + util.DebugRequest(request) dto, err := r.UnmarshalPushDto(request.Body) if err != nil { metrics.RecordError("OCPI077", "Error unmarshaling request", err) - util.LogHttpRequest("OCPI077", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI077", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) return @@ -99,7 +108,7 @@ func (r *CommandResolver) PostCommandUnlockResponse(rw http.ResponseWriter, requ if err := render.Render(rw, request, transportation.OcpiSuccess(nil)); err != nil { log.Print("OCPI078", "Error updating unlock command") - util.LogHttpRequest("OCPI078", request.URL.String(), request, true) + dbUtil.LogHttpRequest("OCPI078", request.URL.String(), request, true) render.Render(rw, request, transportation.OcpiServerError(nil, err.Error())) } diff --git a/internal/command/v2.1.1/push_test.go b/internal/command/v2.1.1/push_test.go index cb6916f7..bd55197b 100644 --- a/internal/command/v2.1.1/push_test.go +++ b/internal/command/v2.1.1/push_test.go @@ -251,6 +251,8 @@ func TestCommandStartRequest(t *testing.T) { "status_code": 1000, "status_message": "Success" }`), jsondiff.SupersetMatch) + + t.Fatal("Creating 'POST /START_SESSION/{command_id}' request failed!") }) } diff --git a/internal/rpc/command/rpc.go b/internal/rpc/command/rpc.go index 879454cd..a07a4556 100644 --- a/internal/rpc/command/rpc.go +++ b/internal/rpc/command/rpc.go @@ -63,7 +63,7 @@ func (r *RpcCommandResolver) ReserveNow(ctx context.Context, input *ocpirpc.Rese if err != nil { metrics.RecordError("OCPI145", "Error requesting reservation", err) log.Printf("OCPI145: Input=%#v", input) - return nil, errors.New("error requesting reservation") + return nil, err } reserveNowResponse := ocpiCommand.NewCommandReservationResponse(*command) @@ -143,7 +143,7 @@ func (r *RpcCommandResolver) StartSession(ctx context.Context, input *ocpirpc.St if err != nil { metrics.RecordError("OCPI152", "Error requesting start", err) log.Printf("OCPI152: Input=%#v", input) - return nil, errors.New("error starting session") + return nil, err } startResponse := ocpiCommand.NewCommandStartResponse(*command, verificationKey) @@ -208,7 +208,7 @@ func (r *RpcCommandResolver) StopSession(ctx context.Context, input *ocpirpc.Sto if err != nil { metrics.RecordError("OCPI156", "Error requesting stop", err) log.Printf("OCPI156: Input=%#v", input) - return nil, errors.New("error stopping session") + return nil, err } stopResponse := ocpiCommand.NewCommandStopResponse(*command) @@ -251,7 +251,7 @@ func (r *RpcCommandResolver) UnlockConnector(ctx context.Context, input *ocpirpc if err != nil { metrics.RecordError("OCPI160", "Error requesting unlock", err) log.Printf("OCPI160: Input=%#v", input) - return nil, errors.New("error unlocking connector") + return nil, err } unlockResponse := ocpiCommand.NewCommandUnlockResponse(*command) diff --git a/internal/util/request.go b/internal/util/request.go new file mode 100644 index 00000000..838cf910 --- /dev/null +++ b/internal/util/request.go @@ -0,0 +1,22 @@ +package util + +import ( + "bytes" + "io/ioutil" + "log" + "net/http" +) + +func DebugRequest(request *http.Request) { + buf, err := ioutil.ReadAll(request.Body) + + if err != nil { + log.Printf("Error reading request body: %s", err.Error()) + return + } + + log.Printf("Request body: %s", string(buf)) + + reader := ioutil.NopCloser(bytes.NewBuffer(buf)) + request.Body = reader +} \ No newline at end of file From 58ac0453e833d243d685a9139004f0f437f40e8f Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Fri, 16 Dec 2022 19:32:31 +0100 Subject: [PATCH 09/14] Create a party if none exists when updating a location, add extra logging to token authorization RPC, sync each party one-by-one setting locations to removed before syncing --- go.mod | 4 +- go.sum | 2 + .../connector/v2.1.1/mocks/mock_resolve.go | 6 +- internal/connector/v2.1.1/process.go | 8 +- internal/connector/v2.1.1/resolve.go | 10 +-- internal/evse/v2.1.1/process.go | 2 + .../location/v2.1.1/mocks/mock_resolve.go | 4 +- internal/location/v2.1.1/process.go | 8 +- internal/location/v2.1.1/resolve.go | 6 +- internal/location/v2.1.1/sync.go | 18 +++- internal/party/mocks/mock_resolve.go | 13 +++ internal/party/process.go | 49 ++++++++++ internal/party/resolve.go | 16 ++++ internal/rpc/tokenauthorization/rpc.go | 6 ++ internal/sync/mocks/mock_resolve.go | 2 + internal/sync/process.go | 89 ++++++++++++------- internal/sync/resolver.go | 3 + .../environments/mainnet/terraform.tfvars | 2 +- 18 files changed, 186 insertions(+), 62 deletions(-) create mode 100644 internal/party/mocks/mock_resolve.go create mode 100644 internal/party/process.go create mode 100644 internal/party/resolve.go diff --git a/go.mod b/go.mod index 242c4d95..0e9d9b74 100644 --- a/go.mod +++ b/go.mod @@ -13,12 +13,10 @@ require ( github.com/joho/godotenv v1.4.0 github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/prometheus/client_golang v1.13.1 - github.com/satimoto/go-datastore v0.3.1-0.20221211215042-d5318660b0e1 + github.com/satimoto/go-datastore v0.3.1-0.20221215215552-467363aef4a9 google.golang.org/grpc v1.47.0 ) -require github.com/satimoto/go-lsp v0.2.1-0.20221115190646-b981e98c7ba4 // indirect - require ( github.com/99designs/gqlgen v0.17.2 // indirect github.com/appleboy/go-fcm v0.1.5 diff --git a/go.sum b/go.sum index 4122ee1b..128d4302 100644 --- a/go.sum +++ b/go.sum @@ -400,6 +400,8 @@ github.com/satimoto/go-datastore v0.3.1-0.20221211212503-67cd89cf5af6 h1:mp63Sto github.com/satimoto/go-datastore v0.3.1-0.20221211212503-67cd89cf5af6/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= github.com/satimoto/go-datastore v0.3.1-0.20221211215042-d5318660b0e1 h1:j8SMRJQwSu7noWT2TEt+33URmPXpFw1+B/dc0aIRr4I= github.com/satimoto/go-datastore v0.3.1-0.20221211215042-d5318660b0e1/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= +github.com/satimoto/go-datastore v0.3.1-0.20221215215552-467363aef4a9 h1:vmWEl6/uFLQND2y0IE47Ym8iBDRhfF/z8AdFH9ls8LI= +github.com/satimoto/go-datastore v0.3.1-0.20221215215552-467363aef4a9/go.mod h1:SvM8losYPwH6hJgUKLkjNtQwVv06lZgCMWl8cGdllCM= github.com/satimoto/go-lsp v0.2.1-0.20221115190646-b981e98c7ba4 h1:3QKFoeOVSDU8bvrVfj/zAMDq+hg3KJBLaMgEgutJExk= github.com/satimoto/go-lsp v0.2.1-0.20221115190646-b981e98c7ba4/go.mod h1:bleUpVumUC8QAFi0CZdnbtDRGWRFKETMH5JWcLAolBs= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= diff --git a/internal/connector/v2.1.1/mocks/mock_resolve.go b/internal/connector/v2.1.1/mocks/mock_resolve.go index 0707a961..500768a6 100644 --- a/internal/connector/v2.1.1/mocks/mock_resolve.go +++ b/internal/connector/v2.1.1/mocks/mock_resolve.go @@ -3,13 +3,13 @@ package mocks import ( connectorMocks "github.com/satimoto/go-datastore/pkg/connector/mocks" mocks "github.com/satimoto/go-datastore/pkg/db/mocks" - party "github.com/satimoto/go-datastore/pkg/party/mocks" connector "github.com/satimoto/go-ocpi/internal/connector/v2.1.1" + party "github.com/satimoto/go-ocpi/internal/party/mocks" ) func NewResolver(repositoryService *mocks.MockRepositoryService) *connector.ConnectorResolver { return &connector.ConnectorResolver{ - Repository: connectorMocks.NewRepository(repositoryService), - PartyRepository: party.NewRepository(repositoryService), + Repository: connectorMocks.NewRepository(repositoryService), + PartyResolver: party.NewResolver(repositoryService), } } diff --git a/internal/connector/v2.1.1/process.go b/internal/connector/v2.1.1/process.go index 9aeb9388..a1c229ee 100644 --- a/internal/connector/v2.1.1/process.go +++ b/internal/connector/v2.1.1/process.go @@ -17,13 +17,7 @@ func (r *ConnectorResolver) ReplaceConnector(ctx context.Context, credential db. publish := connectorDto.TariffID != nil if !publish { - getPartyByCredentialParams := db.GetPartyByCredentialParams{ - CredentialID: credential.ID, - CountryCode: dbUtil.DefaultString(location.CountryCode, credential.CountryCode), - PartyID: dbUtil.DefaultString(location.PartyID, credential.PartyID), - } - - if party, err := r.PartyRepository.GetPartyByCredential(ctx, getPartyByCredentialParams); err == nil { + if party, err := r.PartyResolver.GetParty(ctx, credential, dbUtil.NilString(location.CountryCode), dbUtil.NilString(location.PartyID)); err == nil { publish = party.PublishNullTariff } } diff --git a/internal/connector/v2.1.1/resolve.go b/internal/connector/v2.1.1/resolve.go index 21b77886..2cd9fe58 100644 --- a/internal/connector/v2.1.1/resolve.go +++ b/internal/connector/v2.1.1/resolve.go @@ -3,17 +3,17 @@ package connector import ( "github.com/satimoto/go-datastore/pkg/connector" "github.com/satimoto/go-datastore/pkg/db" - "github.com/satimoto/go-datastore/pkg/party" + "github.com/satimoto/go-ocpi/internal/party" ) type ConnectorResolver struct { - Repository connector.ConnectorRepository - PartyRepository party.PartyRepository + Repository connector.ConnectorRepository + PartyResolver *party.PartyResolver } func NewResolver(repositoryService *db.RepositoryService) *ConnectorResolver { return &ConnectorResolver{ - Repository: connector.NewRepository(repositoryService), - PartyRepository: party.NewRepository(repositoryService), + Repository: connector.NewRepository(repositoryService), + PartyResolver: party.NewResolver(repositoryService), } } diff --git a/internal/evse/v2.1.1/process.go b/internal/evse/v2.1.1/process.go index b6bd8629..06530ae2 100644 --- a/internal/evse/v2.1.1/process.go +++ b/internal/evse/v2.1.1/process.go @@ -305,6 +305,7 @@ func (r *EvseResolver) updateLocationAvailability(ctx context.Context, location IsIntermediateCdrCapable: location.IsIntermediateCdrCapable, IsRemoteCapable: false, IsRfidCapable: false, + IsRemoved: true, LastUpdated: location.LastUpdated, } @@ -318,6 +319,7 @@ func (r *EvseResolver) updateLocationAvailability(ctx context.Context, location for _, evse := range evses { if evse.Status != db.EvseStatusREMOVED { + updateLocationAvailabilityParams.IsRemoved = false updateLocationAvailabilityParams.TotalEvses++ if evse.Status == db.EvseStatusAVAILABLE { diff --git a/internal/location/v2.1.1/mocks/mock_resolve.go b/internal/location/v2.1.1/mocks/mock_resolve.go index 85503df3..a1cf889b 100644 --- a/internal/location/v2.1.1/mocks/mock_resolve.go +++ b/internal/location/v2.1.1/mocks/mock_resolve.go @@ -3,7 +3,6 @@ package mocks import ( mocks "github.com/satimoto/go-datastore/pkg/db/mocks" locationMocks "github.com/satimoto/go-datastore/pkg/location/mocks" - party "github.com/satimoto/go-datastore/pkg/party/mocks" businessdetail "github.com/satimoto/go-ocpi/internal/businessdetail/mocks" displaytext "github.com/satimoto/go-ocpi/internal/displaytext/mocks" energymix "github.com/satimoto/go-ocpi/internal/energymix/mocks" @@ -12,6 +11,7 @@ import ( image "github.com/satimoto/go-ocpi/internal/image/mocks" location "github.com/satimoto/go-ocpi/internal/location/v2.1.1" openingtime "github.com/satimoto/go-ocpi/internal/openingtime/mocks" + party "github.com/satimoto/go-ocpi/internal/party/mocks" "github.com/satimoto/go-ocpi/internal/service" tariff "github.com/satimoto/go-ocpi/internal/tariff/v2.1.1/mocks" versiondetail "github.com/satimoto/go-ocpi/internal/versiondetail/mocks" @@ -28,7 +28,7 @@ func NewResolver(repositoryService *mocks.MockRepositoryService, services *servi GeoLocationResolver: geolocation.NewResolver(repositoryService), ImageResolver: image.NewResolver(repositoryService), OpeningTimeResolver: openingtime.NewResolver(repositoryService), - PartyRepository: party.NewRepository(repositoryService), + PartyResolver: party.NewResolver(repositoryService), TariffResolver: tariff.NewResolver(repositoryService, services), VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } diff --git a/internal/location/v2.1.1/process.go b/internal/location/v2.1.1/process.go index 73e8fa00..d09d38f4 100644 --- a/internal/location/v2.1.1/process.go +++ b/internal/location/v2.1.1/process.go @@ -124,13 +124,7 @@ func (r *LocationResolver) ReplaceLocationByIdentifier(ctx context.Context, cred isIntermediateCdrCapable := false publish := false - getPartyByCredentialParams := db.GetPartyByCredentialParams{ - CredentialID: credential.ID, - CountryCode: util.DefaultString(countryCode, credential.CountryCode), - PartyID: util.DefaultString(partyID, credential.PartyID), - } - - if party, err := r.PartyRepository.GetPartyByCredential(ctx, getPartyByCredentialParams); err == nil { + if party, err := r.PartyResolver.GetParty(ctx, credential, countryCode, partyID); err == nil { isIntermediateCdrCapable = party.IsIntermediateCdrCapable publish = party.PublishLocation } diff --git a/internal/location/v2.1.1/resolve.go b/internal/location/v2.1.1/resolve.go index b7249f54..6f0adf3c 100644 --- a/internal/location/v2.1.1/resolve.go +++ b/internal/location/v2.1.1/resolve.go @@ -3,7 +3,6 @@ package location import ( "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/location" - "github.com/satimoto/go-datastore/pkg/party" "github.com/satimoto/go-ocpi/internal/businessdetail" "github.com/satimoto/go-ocpi/internal/displaytext" "github.com/satimoto/go-ocpi/internal/energymix" @@ -11,6 +10,7 @@ import ( "github.com/satimoto/go-ocpi/internal/geolocation" "github.com/satimoto/go-ocpi/internal/image" "github.com/satimoto/go-ocpi/internal/openingtime" + "github.com/satimoto/go-ocpi/internal/party" "github.com/satimoto/go-ocpi/internal/service" tariff "github.com/satimoto/go-ocpi/internal/tariff/v2.1.1" "github.com/satimoto/go-ocpi/internal/transportation" @@ -27,7 +27,7 @@ type LocationResolver struct { GeoLocationResolver *geolocation.GeoLocationResolver ImageResolver *image.ImageResolver OpeningTimeResolver *openingtime.OpeningTimeResolver - PartyRepository party.PartyRepository + PartyResolver *party.PartyResolver TariffResolver *tariff.TariffResolver VersionDetailResolver *versiondetail.VersionDetailResolver } @@ -43,7 +43,7 @@ func NewResolver(repositoryService *db.RepositoryService, services *service.Serv GeoLocationResolver: geolocation.NewResolver(repositoryService), ImageResolver: image.NewResolver(repositoryService), OpeningTimeResolver: openingtime.NewResolver(repositoryService), - PartyRepository: party.NewRepository(repositoryService), + PartyResolver: party.NewResolver(repositoryService), TariffResolver: tariff.NewResolver(repositoryService, services), VersionDetailResolver: versiondetail.NewResolver(repositoryService, services), } diff --git a/internal/location/v2.1.1/sync.go b/internal/location/v2.1.1/sync.go index 99ff2c83..869eb05b 100644 --- a/internal/location/v2.1.1/sync.go +++ b/internal/location/v2.1.1/sync.go @@ -39,7 +39,23 @@ func (r *LocationResolver) SyncByIdentifier(ctx context.Context, credential db.C header := transportation.NewOcpiRequestHeader(&credential.ClientToken.String, countryCode, partyID) query := requestUrl.Query() - if !fullSync { + if fullSync { + if countryCode != nil && partyID != nil { + log.Printf("Setting all locations to removed") + updateLocationsRemovedByPartyAndCountryCodeParams := db.UpdateLocationsRemovedByPartyAndCountryCodeParams{ + CountryCode: util.SqlNullString(countryCode), + PartyID: util.SqlNullString(partyID), + IsRemoved: true, + } + + err := r.Repository.UpdateLocationsRemovedByPartyAndCountryCode(ctx, updateLocationsRemovedByPartyAndCountryCodeParams) + + if err != nil { + metrics.RecordError("OCPI331", "Error updating locations", err) + log.Printf("OCPI331: Param=%#v", updateLocationsRemovedByPartyAndCountryCodeParams) + } + } + } else { if lastUpdated != nil { query.Set("date_from", lastUpdated.UTC().Format(time.RFC3339)) } else if location, err := r.GetLastLocationByIdentity(ctx, &credential.ID, countryCode, partyID); err == nil { diff --git a/internal/party/mocks/mock_resolve.go b/internal/party/mocks/mock_resolve.go new file mode 100644 index 00000000..616b2788 --- /dev/null +++ b/internal/party/mocks/mock_resolve.go @@ -0,0 +1,13 @@ +package mocks + +import ( + mocks "github.com/satimoto/go-datastore/pkg/db/mocks" + partyMocks "github.com/satimoto/go-datastore/pkg/party/mocks" + "github.com/satimoto/go-ocpi/internal/party" +) + +func NewResolver(repositoryService *mocks.MockRepositoryService) *party.PartyResolver { + return &party.PartyResolver{ + Repository: partyMocks.NewRepository(repositoryService), + } +} diff --git a/internal/party/process.go b/internal/party/process.go new file mode 100644 index 00000000..2c44ef5d --- /dev/null +++ b/internal/party/process.go @@ -0,0 +1,49 @@ +package party + +import ( + "context" + "errors" + + "github.com/satimoto/go-datastore/pkg/db" +) + +func (r *PartyResolver) GetParty(ctx context.Context, credential db.Credential, countryCode, partyID *string) (*db.Party, error) { + if countryCode != nil && partyID != nil { + getPartyByCredentialParams := db.GetPartyByCredentialParams{ + CredentialID: credential.ID, + CountryCode: *countryCode, + PartyID: *partyID, + } + + if party, err := r.Repository.GetPartyByCredential(ctx, getPartyByCredentialParams); err == nil { + return &party, nil + } + } + + getPartyByCredentialParams := db.GetPartyByCredentialParams{ + CredentialID: credential.ID, + CountryCode: credential.CountryCode, + PartyID: credential.PartyID, + } + + if credentialParty, err := r.Repository.GetPartyByCredential(ctx, getPartyByCredentialParams); err == nil { + if countryCode != nil && partyID != nil { + createPartyParams := db.CreatePartyParams{ + CredentialID: credential.ID, + CountryCode: *countryCode, + PartyID: *partyID, + IsIntermediateCdrCapable: credentialParty.IsIntermediateCdrCapable, + PublishLocation: credentialParty.PublishLocation, + PublishNullTariff: credentialParty.PublishNullTariff, + } + + if party, err := r.Repository.CreateParty(ctx, createPartyParams); err == nil { + return &party, nil + } + } + + return &credentialParty, err + } + + return nil, errors.New("Error getting party") +} diff --git a/internal/party/resolve.go b/internal/party/resolve.go new file mode 100644 index 00000000..0e586e9f --- /dev/null +++ b/internal/party/resolve.go @@ -0,0 +1,16 @@ +package party + +import ( + "github.com/satimoto/go-datastore/pkg/db" + "github.com/satimoto/go-datastore/pkg/party" +) + +type PartyResolver struct { + Repository party.PartyRepository +} + +func NewResolver(repositoryService *db.RepositoryService) *PartyResolver { + return &PartyResolver{ + Repository: party.NewRepository(repositoryService), + } +} diff --git a/internal/rpc/tokenauthorization/rpc.go b/internal/rpc/tokenauthorization/rpc.go index ff89df5e..abcdbc9a 100644 --- a/internal/rpc/tokenauthorization/rpc.go +++ b/internal/rpc/tokenauthorization/rpc.go @@ -29,6 +29,8 @@ func (r *RpcTokenAuthorizationResolver) UpdateTokenAuthorization(ctx context.Con return &ocpirpc.UpdateTokenAuthorizationResponse{Ok: false}, nil } + log.Printf("Update token authorization: %v", request.AuthorizationId) + if token.Type == db.TokenTypeRFID { // Update token authorization using async channel asyncResult := async.AsyncResult{ @@ -38,6 +40,8 @@ func (r *RpcTokenAuthorizationResolver) UpdateTokenAuthorization(ctx context.Con ok := r.AsyncService.Set(request.AuthorizationId, asyncResult) + log.Printf("Async result authorize=%v ok=%v", request.Authorize, ok) + return &ocpirpc.UpdateTokenAuthorizationResponse{Ok: ok}, nil } else if _, err := r.SessionRepository.GetSessionByAuthorizationID(ctx, tokenAuthorization.AuthorizationID); err != nil { // Only update token authorization if session is not yet created @@ -46,6 +50,8 @@ func (r *RpcTokenAuthorizationResolver) UpdateTokenAuthorization(ctx context.Con Authorized: request.Authorize, } + log.Printf("Token authorise=%v", request.Authorize) + _, err := r.TokenAuthorizationRepository.UpdateTokenAuthorizationByAuthorizationID(ctx, updateTokenAuthorizationParams) if err != nil { diff --git a/internal/sync/mocks/mock_resolve.go b/internal/sync/mocks/mock_resolve.go index 62db31ad..755e033f 100644 --- a/internal/sync/mocks/mock_resolve.go +++ b/internal/sync/mocks/mock_resolve.go @@ -3,6 +3,7 @@ package mocks import ( credential "github.com/satimoto/go-datastore/pkg/credential/mocks" mocks "github.com/satimoto/go-datastore/pkg/db/mocks" + party "github.com/satimoto/go-datastore/pkg/party/mocks" sync "github.com/satimoto/go-ocpi/internal/sync" "github.com/satimoto/go-ocpi/internal/transportation" version "github.com/satimoto/go-ocpi/internal/version/mocks" @@ -14,6 +15,7 @@ func NewService(repositoryService *mocks.MockRepositoryService, ocpiRequester *t return &sync.SyncService{ Repository: repo, CredentialRepository: credential.NewRepository(repositoryService), + PartyRepository: party.NewRepository(repositoryService), VersionResolver: version.NewResolver(repositoryService, ocpiRequester), } } diff --git a/internal/sync/process.go b/internal/sync/process.go index bcff0254..ead44983 100644 --- a/internal/sync/process.go +++ b/internal/sync/process.go @@ -8,54 +8,83 @@ import ( "github.com/satimoto/go-datastore/pkg/db" "github.com/satimoto/go-datastore/pkg/util" + coreCdr "github.com/satimoto/go-ocpi/internal/cdr" coreLocation "github.com/satimoto/go-ocpi/internal/location" metrics "github.com/satimoto/go-ocpi/internal/metric" + coreSession "github.com/satimoto/go-ocpi/internal/session" coreTariff "github.com/satimoto/go-ocpi/internal/tariff" ) func (r *SyncService) SynchronizeCredential(credential db.Credential, fullSync, withTariffs bool, lastUpdated *time.Time, countryCode *string, partyID *string) { if credential.VersionID.Valid { - activeSyncsKey := fmt.Sprintf("%s*%s", credential.CountryCode, credential.PartyID) + log.Printf("Start credential sync Url=%v LastUpdated=%v CountryCode=%v PartyID=%v", credential.Url, lastUpdated, util.DefaultString(countryCode, ""), util.DefaultString(partyID, "")) + + ctx := context.Background() + version, err := r.VersionResolver.Repository.GetVersion(ctx, credential.VersionID.Int64) + + if err != nil { + metrics.RecordError("OCPI270", "Error retrieving credential version", err) + log.Printf("OCPI270: CredentialID=%v, VersionID=%v", credential.ID, credential.VersionID) + return + } if countryCode != nil && partyID != nil { - activeSyncsKey = fmt.Sprintf("%s*%s", *countryCode, *partyID) + // Single party sync + r.synchronizeParty(ctx, credential, version, fullSync, lastUpdated, *countryCode, *partyID) + } else { + // Full sync all parties + parties, _ := r.PartyRepository.ListPartiesByCredentialID(ctx, credential.ID) + + for _, party := range parties { + r.synchronizeParty(ctx, credential, version, fullSync, lastUpdated, party.CountryCode, party.PartyID) + } + + r.synchronizeSessionsAndCdrs(ctx, credential, version, fullSync, lastUpdated, countryCode, partyID) } - if _, ok := r.activeSyncs[activeSyncsKey]; !ok { - r.activeSyncs[activeSyncsKey] = true - ctx := context.Background() + if withTariffs { + r.synchronizeTariffs(ctx, credential, version, fullSync, lastUpdated, countryCode, partyID) + } + } +} - log.Printf("Start credential sync %v Url=%v LastUpdated=%v CountryCode=%v PartyID=%v", - activeSyncsKey, credential.Url, lastUpdated, util.DefaultString(countryCode, ""), util.DefaultString(partyID, "")) - version, err := r.VersionResolver.Repository.GetVersion(ctx, credential.VersionID.Int64) +func (r *SyncService) synchronizeParty(ctx context.Context, credential db.Credential, version db.Version, fullSync bool, lastUpdated *time.Time, countryCode, partyID string) { + activeSyncsKey := fmt.Sprintf("%s*%s", countryCode, partyID) - if err != nil { - metrics.RecordError("OCPI270", "Error retrieving credential version", err) - log.Printf("OCPI270: CredentialID=%v, VersionID=%v", credential.ID, credential.VersionID) - return - } + if _, ok := r.activeSyncs[activeSyncsKey]; !ok { + r.activeSyncs[activeSyncsKey] = true - for _, syncerHandler := range r.syncerHandlers { - if syncerHandler.Version == version.Version { - if countryCode == nil || syncerHandler.Identifier == coreLocation.IDENTIFIER || syncerHandler.Identifier == coreTariff.IDENTIFIER { - if withTariffs && syncerHandler.Identifier == coreTariff.IDENTIFIER && !r.tariffsSyncing { - // Only sync tariffs one at a time - r.tariffsSyncing = true - syncerHandler.Syncer.SyncByIdentifier(ctx, credential, fullSync, lastUpdated, countryCode, partyID) - r.tariffsSyncing = false - } else if syncerHandler.Identifier != coreTariff.IDENTIFIER { - syncerHandler.Syncer.SyncByIdentifier(ctx, credential, fullSync, lastUpdated, countryCode, partyID) - } - } + log.Printf("Start party sync %v Url=%v LastUpdated=%v CountryCode=%v PartyID=%v", activeSyncsKey, credential.Url, lastUpdated, countryCode, partyID) + + for _, syncerHandler := range r.syncerHandlers { + if syncerHandler.Version == version.Version { + if syncerHandler.Identifier == coreLocation.IDENTIFIER { + syncerHandler.Syncer.SyncByIdentifier(ctx, credential, fullSync, lastUpdated, &countryCode, &partyID) } } + } - delete(r.activeSyncs, activeSyncsKey) - log.Printf("End credential sync %v", activeSyncsKey) - return + delete(r.activeSyncs, activeSyncsKey) + log.Printf("End party sync %v", activeSyncsKey) + } +} + +func (r *SyncService) synchronizeSessionsAndCdrs(ctx context.Context, credential db.Credential, version db.Version, fullSync bool, lastUpdated *time.Time, countryCode *string, partyID *string) { + for _, syncerHandler := range r.syncerHandlers { + if syncerHandler.Version == version.Version && (syncerHandler.Identifier == coreSession.IDENTIFIER || syncerHandler.Identifier == coreCdr.IDENTIFIER) { + syncerHandler.Syncer.SyncByIdentifier(ctx, credential, fullSync, lastUpdated, countryCode, partyID) } + } +} - log.Printf("Ignore credential sync %v Url=%v LastUpdated=%v CountryCode=%v PartyID=%v", - activeSyncsKey, credential.Url, lastUpdated, util.DefaultString(countryCode, ""), util.DefaultString(partyID, "")) +func (r *SyncService) synchronizeTariffs(ctx context.Context, credential db.Credential, version db.Version, fullSync bool, lastUpdated *time.Time, countryCode *string, partyID *string) { + if !r.tariffsSyncing { + for _, syncerHandler := range r.syncerHandlers { + if syncerHandler.Version == version.Version && syncerHandler.Identifier == coreTariff.IDENTIFIER { + r.tariffsSyncing = true + syncerHandler.Syncer.SyncByIdentifier(ctx, credential, fullSync, lastUpdated, countryCode, partyID) + r.tariffsSyncing = false + } + } } } diff --git a/internal/sync/resolver.go b/internal/sync/resolver.go index f2a0a60c..bb35fb80 100644 --- a/internal/sync/resolver.go +++ b/internal/sync/resolver.go @@ -6,6 +6,7 @@ import ( "github.com/satimoto/go-datastore/pkg/credential" "github.com/satimoto/go-datastore/pkg/db" + "github.com/satimoto/go-datastore/pkg/party" "github.com/satimoto/go-ocpi/internal/sync/htb" "github.com/satimoto/go-ocpi/internal/sync/nlcon" "github.com/satimoto/go-ocpi/internal/transportation" @@ -19,6 +20,7 @@ type SyncService struct { HtbService *htb.HtbService NlConService *nlcon.NlConService CredentialRepository credential.CredentialRepository + PartyRepository party.PartyRepository VersionResolver *version.VersionResolver syncerHandlers []*SyncerHandler shutdownCtx context.Context @@ -35,6 +37,7 @@ func NewService(repositoryService *db.RepositoryService, ocpiService *transporta HtbService: htb.NewService(repositoryService), NlConService: nlcon.NewService(repositoryService), CredentialRepository: credential.NewRepository(repositoryService), + PartyRepository: party.NewRepository(repositoryService), VersionResolver: version.NewResolver(repositoryService, ocpiService), activeSyncs: make(map[string]bool), tariffsSyncing: false, diff --git a/terraform/environments/mainnet/terraform.tfvars b/terraform/environments/mainnet/terraform.tfvars index a1f419e0..a974d23e 100644 --- a/terraform/environments/mainnet/terraform.tfvars +++ b/terraform/environments/mainnet/terraform.tfvars @@ -50,4 +50,4 @@ env_record_evse_status_periods = false env_shutdown_timeout = 20 -env_token_authorization_timeout = 5 +env_token_authorization_timeout = 9 From fb75e2a985a0500712f5524bbbd0a0c02408d9f5 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Fri, 16 Dec 2022 23:09:47 +0100 Subject: [PATCH 10/14] Move connector published check to create only --- internal/connector/v2.1.1/process.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/connector/v2.1.1/process.go b/internal/connector/v2.1.1/process.go index a1c229ee..b39f6a14 100644 --- a/internal/connector/v2.1.1/process.go +++ b/internal/connector/v2.1.1/process.go @@ -14,14 +14,6 @@ import ( func (r *ConnectorResolver) ReplaceConnector(ctx context.Context, credential db.Credential, location db.Location, evse db.Evse, uid string, connectorDto *dto.ConnectorDto) *db.Connector { if connectorDto != nil { - publish := connectorDto.TariffID != nil - - if !publish { - if party, err := r.PartyResolver.GetParty(ctx, credential, dbUtil.NilString(location.CountryCode), dbUtil.NilString(location.PartyID)); err == nil { - publish = party.PublishNullTariff - } - } - connector, err := r.Repository.GetConnectorByEvse(ctx, db.GetConnectorByEvseParams{ EvseID: evse.ID, Uid: uid, @@ -52,6 +44,7 @@ func (r *ConnectorResolver) ReplaceConnector(ctx context.Context, credential db. if connectorDto.TariffID != nil { connectorParams.TariffID = dbUtil.SqlNullString(connectorDto.TariffID) + connectorParams.IsPublished = true } if connectorDto.LastUpdated != nil { @@ -59,7 +52,6 @@ func (r *ConnectorResolver) ReplaceConnector(ctx context.Context, credential db. } connectorParams.Identifier = dbUtil.SqlNullString(GetConnectorIdentifier(evse, connectorDto)) - connectorParams.IsPublished = publish connectorParams.Wattage = util.CalculateWattage(connectorParams.PowerType, connectorParams.Voltage, connectorParams.Amperage) updatedConnector, err := r.Repository.UpdateConnectorByEvse(ctx, connectorParams) @@ -71,6 +63,14 @@ func (r *ConnectorResolver) ReplaceConnector(ctx context.Context, credential db. connector = updatedConnector } else { + publish := connectorDto.TariffID != nil + + if !publish { + if party, err := r.PartyResolver.GetParty(ctx, credential, dbUtil.NilString(location.CountryCode), dbUtil.NilString(location.PartyID)); err == nil { + publish = party.PublishNullTariff + } + } + connectorParams := NewCreateConnectorParams(evse, connectorDto) connectorParams.IsPublished = publish connector, err = r.Repository.CreateConnector(ctx, connectorParams) From f033332232beedb9edd54272ea92710ca6875f26 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Wed, 21 Dec 2022 17:41:40 +0100 Subject: [PATCH 11/14] Set FCM message priority to high --- internal/tokenauthorization/v2.1.1/notification.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/tokenauthorization/v2.1.1/notification.go b/internal/tokenauthorization/v2.1.1/notification.go index b7e3fe65..7e3de062 100644 --- a/internal/tokenauthorization/v2.1.1/notification.go +++ b/internal/tokenauthorization/v2.1.1/notification.go @@ -19,6 +19,7 @@ func (r *TokenAuthorizationResolver) sendNotification(user db.User, data notific message := &fcm.Message{ To: user.DeviceToken.String, ContentAvailable: true, + Priority: "high", Data: data, } From df5cc88e8f50197900a7f7f8b16255aeb388bdd7 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Fri, 23 Dec 2022 21:45:13 +0100 Subject: [PATCH 12/14] Fix initialisation of tokenAuthorizationClient --- pkg/ocpi/tokenauthorization.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/ocpi/tokenauthorization.go b/pkg/ocpi/tokenauthorization.go index c9c33f1b..bc6b4e58 100644 --- a/pkg/ocpi/tokenauthorization.go +++ b/pkg/ocpi/tokenauthorization.go @@ -12,7 +12,7 @@ func (s *OcpiService) UpdateTokenAuthorization(ctx context.Context, in *ocpirpc. } func (s *OcpiService) getTokenAuthorizationClient() ocpirpc.TokenAuthorizationServiceClient { - if s.tokenClient == nil { + if s.tokenAuthorizationClient == nil { client := ocpirpc.NewTokenAuthorizationServiceClient(s.clientConn) s.tokenAuthorizationClient = &client } From 0d606ab8a31a78210558e3ac3225d8146d19467b Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Mon, 2 Jan 2023 08:47:39 +0100 Subject: [PATCH 13/14] Add request duration logging --- pkg/ocpi/cdr.go | 10 +++++++++- pkg/ocpi/command.go | 18 ++++++++++++++++-- pkg/ocpi/credential.go | 34 ++++++++++++++++++++++++++++++---- pkg/ocpi/rpc.go | 18 ++++++++++++++++-- pkg/ocpi/session.go | 18 ++++++++++++++++-- pkg/ocpi/token.go | 18 ++++++++++++++++-- pkg/ocpi/tokenauthorization.go | 10 +++++++++- 7 files changed, 112 insertions(+), 14 deletions(-) diff --git a/pkg/ocpi/cdr.go b/pkg/ocpi/cdr.go index a0abd65e..b696e2bd 100644 --- a/pkg/ocpi/cdr.go +++ b/pkg/ocpi/cdr.go @@ -2,13 +2,21 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-ocpi/ocpirpc" "google.golang.org/grpc" ) func (s *OcpiService) CdrCreated(ctx context.Context, in *ocpirpc.CdrCreatedRequest, opts ...grpc.CallOption) (*ocpirpc.CdrCreatedResponse, error) { - return s.getCdrClient().CdrCreated(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getCdrClient().CdrCreated(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("CdrCreated responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) getCdrClient() ocpirpc.CdrServiceClient { diff --git a/pkg/ocpi/command.go b/pkg/ocpi/command.go index 26ce166c..ff212ad0 100644 --- a/pkg/ocpi/command.go +++ b/pkg/ocpi/command.go @@ -2,17 +2,31 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-ocpi/ocpirpc" "google.golang.org/grpc" ) func (s *OcpiService) StartSession(ctx context.Context, in *ocpirpc.StartSessionRequest, opts ...grpc.CallOption) (*ocpirpc.StartSessionResponse, error) { - return s.getCommandClient().StartSession(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getCommandClient().StartSession(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("StartSession responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) StopSession(ctx context.Context, in *ocpirpc.StopSessionRequest, opts ...grpc.CallOption) (*ocpirpc.StopSessionResponse, error) { - return s.getCommandClient().StopSession(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getCommandClient().StopSession(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("StopSession responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) getCommandClient() ocpirpc.CommandServiceClient { diff --git a/pkg/ocpi/credential.go b/pkg/ocpi/credential.go index 8015c2f5..c801a5a3 100644 --- a/pkg/ocpi/credential.go +++ b/pkg/ocpi/credential.go @@ -2,25 +2,51 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-ocpi/ocpirpc" "google.golang.org/grpc" ) func (s *OcpiService) CreateCredential(ctx context.Context, in *ocpirpc.CreateCredentialRequest, opts ...grpc.CallOption) (*ocpirpc.CreateCredentialResponse, error) { - return s.getCredentialClient().CreateCredential(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getCredentialClient().CreateCredential(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("CreateCredential responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) RegisterCredential(ctx context.Context, in *ocpirpc.RegisterCredentialRequest, opts ...grpc.CallOption) (*ocpirpc.RegisterCredentialResponse, error) { - return s.getCredentialClient().RegisterCredential(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getCredentialClient().RegisterCredential(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("RegisterCredential responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) SyncCredential(ctx context.Context, in *ocpirpc.SyncCredentialRequest, opts ...grpc.CallOption) (*ocpirpc.SyncCredentialResponse, error) { - return s.getCredentialClient().SyncCredential(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getCredentialClient().SyncCredential(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("SyncCredential responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) UnregisterCredential(ctx context.Context, in *ocpirpc.UnregisterCredentialRequest, opts ...grpc.CallOption) (*ocpirpc.UnregisterCredentialResponse, error) { - return s.getCredentialClient().UnregisterCredential(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getCredentialClient().UnregisterCredential(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("UnregisterCredential responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) getCredentialClient() ocpirpc.CredentialServiceClient { diff --git a/pkg/ocpi/rpc.go b/pkg/ocpi/rpc.go index 64b324d5..9ba5369a 100644 --- a/pkg/ocpi/rpc.go +++ b/pkg/ocpi/rpc.go @@ -2,17 +2,31 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-ocpi/ocpirpc" "google.golang.org/grpc" ) func (s *OcpiService) TestConnection(ctx context.Context, in *ocpirpc.TestConnectionRequest, opts ...grpc.CallOption) (*ocpirpc.TestConnectionResponse, error) { - return s.getRpcClient().TestConnection(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getRpcClient().TestConnection(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("TestConnection responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) TestMessage(ctx context.Context, in *ocpirpc.TestMessageRequest, opts ...grpc.CallOption) (*ocpirpc.TestMessageResponse, error) { - return s.getRpcClient().TestMessage(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getRpcClient().TestMessage(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("TestMessage responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) getRpcClient() ocpirpc.RpcServiceClient { diff --git a/pkg/ocpi/session.go b/pkg/ocpi/session.go index 75678faa..4b8f5e86 100644 --- a/pkg/ocpi/session.go +++ b/pkg/ocpi/session.go @@ -2,17 +2,31 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-ocpi/ocpirpc" "google.golang.org/grpc" ) func (s *OcpiService) SessionCreated(ctx context.Context, in *ocpirpc.SessionCreatedRequest, opts ...grpc.CallOption) (*ocpirpc.SessionCreatedResponse, error) { - return s.getSessionClient().SessionCreated(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getSessionClient().SessionCreated(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("SessionCreated responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) SessionUpdated(ctx context.Context, in *ocpirpc.SessionUpdatedRequest, opts ...grpc.CallOption) (*ocpirpc.SessionUpdatedResponse, error) { - return s.getSessionClient().SessionUpdated(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getSessionClient().SessionUpdated(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("SessionUpdated responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) getSessionClient() ocpirpc.SessionServiceClient { diff --git a/pkg/ocpi/token.go b/pkg/ocpi/token.go index 348771a5..0fc4cc43 100644 --- a/pkg/ocpi/token.go +++ b/pkg/ocpi/token.go @@ -2,17 +2,31 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-ocpi/ocpirpc" "google.golang.org/grpc" ) func (s *OcpiService) CreateToken(ctx context.Context, in *ocpirpc.CreateTokenRequest, opts ...grpc.CallOption) (*ocpirpc.CreateTokenResponse, error) { - return s.getTokenClient().CreateToken(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getTokenClient().CreateToken(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("CreateToken responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) UpdateTokens(ctx context.Context, in *ocpirpc.UpdateTokensRequest, opts ...grpc.CallOption) (*ocpirpc.UpdateTokensResponse, error) { - return s.getTokenClient().UpdateTokens(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getTokenClient().UpdateTokens(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("UpdateTokens responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) getTokenClient() ocpirpc.TokenServiceClient { diff --git a/pkg/ocpi/tokenauthorization.go b/pkg/ocpi/tokenauthorization.go index bc6b4e58..a8fe4f5a 100644 --- a/pkg/ocpi/tokenauthorization.go +++ b/pkg/ocpi/tokenauthorization.go @@ -2,13 +2,21 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-ocpi/ocpirpc" "google.golang.org/grpc" ) func (s *OcpiService) UpdateTokenAuthorization(ctx context.Context, in *ocpirpc.UpdateTokenAuthorizationRequest, opts ...grpc.CallOption) (*ocpirpc.UpdateTokenAuthorizationResponse, error) { - return s.getTokenAuthorizationClient().UpdateTokenAuthorization(ctx, in, opts...) + timerStart := time.Now() + response, err := s.getTokenAuthorizationClient().UpdateTokenAuthorization(ctx, in, opts...) + timerStop := time.Now() + + log.Printf("UpdateTokenAuthorization responded in %f seconds", timerStop.Sub(timerStart).Seconds()) + + return response, err } func (s *OcpiService) getTokenAuthorizationClient() ocpirpc.TokenAuthorizationServiceClient { From 03fa3215a2e3e217902e1c977558f7b4f97c19a0 Mon Sep 17 00:00:00 2001 From: Ross Savage Date: Mon, 2 Jan 2023 09:04:46 +0100 Subject: [PATCH 14/14] Add OCPI dialing duration logging --- pkg/ocpi/service.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/ocpi/service.go b/pkg/ocpi/service.go index 3991281a..a78f1e05 100644 --- a/pkg/ocpi/service.go +++ b/pkg/ocpi/service.go @@ -2,6 +2,8 @@ package ocpi import ( "context" + "log" + "time" "github.com/satimoto/go-datastore/pkg/util" "github.com/satimoto/go-ocpi/ocpirpc" @@ -44,8 +46,12 @@ type OcpiService struct { } func NewService(address string) Ocpi { + timerStart := time.Now() clientConn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials())) + timerStop := time.Now() + util.PanicOnError("OCPI001", "Error connecting to OCPI RPC address", err) + log.Printf("OCPI %v dialed in %f seconds", address, timerStop.Sub(timerStart).Seconds()) return &OcpiService{ clientConn: clientConn,