Skip to content

Commit

Permalink
Merge pull request #85 from andacata/feature/charging_profiles_module
Browse files Browse the repository at this point in the history
feat(chargingprofiles): Implementation of the ChargingProfiles module
  • Loading branch information
lilgallon authored Mar 11, 2024
2 parents 257bb47 + a8e1200 commit 0230204
Show file tree
Hide file tree
Showing 25 changed files with 861 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ fun generateUUIDv4Token(): String {
}

typealias CiString = String
typealias URL = String
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.izivia.ocpi.toolkit.modules.chargingProfiles

import com.izivia.ocpi.toolkit.common.*
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.ActiveChargingProfile
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.ActiveChargingProfileResult
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.ChargingProfileResult
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.ClearProfileResult
import com.izivia.ocpi.toolkit.modules.credentials.repositories.PartnerRepository
import com.izivia.ocpi.toolkit.modules.versions.domain.ModuleID
import com.izivia.ocpi.toolkit.transport.TransportClient
import com.izivia.ocpi.toolkit.transport.TransportClientBuilder
import com.izivia.ocpi.toolkit.transport.domain.HttpException
import com.izivia.ocpi.toolkit.transport.domain.HttpMethod
import com.izivia.ocpi.toolkit.transport.domain.HttpRequest
import com.izivia.ocpi.toolkit.transport.domain.HttpStatus

/**
* Send calls to the SCSP
*
* @property transportClientBuilder used to build transport client
* @property serverVersionsEndpointUrl used to know which partner to communicate with
* @property partnerRepository used to get information about the partner (endpoint, token)
* @property callbackBaseUrl used to build the callback URL sent to the other partner
*/
class ChargingProfilesCpoClient(
private val transportClientBuilder: TransportClientBuilder,
private val serverVersionsEndpointUrl: String,
private val partnerRepository: PartnerRepository,
private val callbackBaseUrl: String
) {

private fun buildCallbackTransport(): TransportClient =
transportClientBuilder.build(callbackBaseUrl)

private suspend fun buildTransport(): TransportClient = transportClientBuilder
.buildFor(
module = ModuleID.chargingprofiles,
partnerUrl = serverVersionsEndpointUrl,
partnerRepository = partnerRepository
)

suspend fun postCallbackActiveChargingProfile(
responseUrl: String,
result: ActiveChargingProfileResult
): OcpiResponseBody<Any> = with(buildCallbackTransport()) {
send(
HttpRequest(
method = HttpMethod.POST,
path = responseUrl,
body = mapper.writeValueAsString(result)
)
.withRequiredHeaders(
requestId = generateRequestId(),
correlationId = generateCorrelationId()
)
.authenticate(partnerRepository = partnerRepository, partnerUrl = serverVersionsEndpointUrl)
)
.also {
if (it.status != HttpStatus.OK) throw HttpException(it.status, "status should be ${HttpStatus.OK}")
}
.parseBody()
}

suspend fun postCallbackChargingProfile(
responseUrl: String,
result: ChargingProfileResult
): OcpiResponseBody<Any> = with(buildCallbackTransport()) {
send(
HttpRequest(
method = HttpMethod.POST,
path = responseUrl,
body = mapper.writeValueAsString(result)
)
.withRequiredHeaders(
requestId = generateRequestId(),
correlationId = generateCorrelationId()
)
.authenticate(partnerRepository = partnerRepository, partnerUrl = serverVersionsEndpointUrl)
)
.also {
if (it.status != HttpStatus.OK) throw HttpException(it.status, "status should be ${HttpStatus.OK}")
}
.parseBody()
}

suspend fun postCallbackClearProfile(
responseUrl: String,
result: ClearProfileResult
): OcpiResponseBody<Any> = with(buildCallbackTransport()) {
send(
HttpRequest(
method = HttpMethod.POST,
path = responseUrl,
body = mapper.writeValueAsString(result)
)
.withRequiredHeaders(
requestId = generateRequestId(),
correlationId = generateCorrelationId()
)
.authenticate(partnerRepository = partnerRepository, partnerUrl = serverVersionsEndpointUrl)
)
.also {
if (it.status != HttpStatus.OK) throw HttpException(it.status, "status should be ${HttpStatus.OK}")
}
.parseBody()
}

suspend fun putActiveChargingProfile(
sessionId: CiString,
activeChargingProfile: ActiveChargingProfile
): OcpiResponseBody<Any> = with(buildTransport()) {
send(
HttpRequest(
method = HttpMethod.PUT,
path = "/$sessionId",
body = mapper.writeValueAsString(activeChargingProfile)
)
.withRequiredHeaders(
requestId = generateRequestId(),
correlationId = generateCorrelationId()
)
.authenticate(partnerRepository = partnerRepository, partnerUrl = serverVersionsEndpointUrl)
)
.also {
if (it.status != HttpStatus.OK) throw HttpException(it.status, "status should be ${HttpStatus.OK}")
}
.parseBody()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.izivia.ocpi.toolkit.modules.chargingProfiles

import com.izivia.ocpi.toolkit.common.CiString
import com.izivia.ocpi.toolkit.common.OcpiResponseBody
import com.izivia.ocpi.toolkit.common.URL
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.ChargingProfileResponse
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.SetChargingProfile

/**
* The ChargingProfiles module consists of two interfaces: a Receiver interface that enables a Sender (and its clients)
* to send ChargingProfiles to a Location/EVSE, and an Sender interface to receive the response from the Location/EVSE
* asynchronously.
*
* This is the receiver interface
*
* Method: Description
* - GET: Gets the ActiveChargingProfile for a specific charging session.
* - POST: n/a (use PUT)
* - PUT: Creates/updates a ChargingProfile for a specific charging session.
* - PATCH: n/a (use PUT)
* - DELETE: Cancels an existing ChargingProfile for a specific charging session.
*/
interface ChargingProfilesCpoInterface {

/**
* Retrieves the ActiveChargingProfile as it is currently planned for the the given session.
*
* Endpoint structure definition:
* - /ocpi/cpo/2.2.1/chargingprofiles/{session_id}?duration={duration}&response_url={url}
*
* @param sessionId (max-length=36) The unique id that identifies the session in the CPO platform.
* @param duration Length of the requested ActiveChargingProfile in seconds Duration in seconds.
* @param responseUrl (max-length=255) URL that the ActiveChargingProfileResult POST should be send to. This URL
* might contain an unique ID to be able to distinguish between GET ActiveChargingProfile requests.
*/
suspend fun getActiveChargingProfile(
sessionId: CiString,
duration: Int,
responseUrl: URL
): OcpiResponseBody<ChargingProfileResponse>

/**
* Creates a new ChargingProfile on a session, or replaces an existing ChargingProfile on the EVSE.
*
* Endpoint structure definition:
* - /ocpi/cpo/2.2.1/chargingprofiles/{session_id}
*
* @param sessionId (max-length=36) The unique id that identifies the session in the CPO platform.
*/
suspend fun putChargingProfile(
sessionId: CiString,
setChargingProfile: SetChargingProfile
): OcpiResponseBody<ChargingProfileResponse>

/**
* Clears the ChargingProfile set by the eMSP on the given session.
*
* Endpoint structure definition:
* - /ocpi/cpo/2.2.1/chargingprofiles/{session_id}?response_url={url}
*
* @param sessionId (max-length=36) The unique id that identifies the session in the CPO platform.
* @param responseUrl (max-length=255) URL that the ClearProfileResult POST should be send to. This URL might
* contain an unique ID to be able to distinguish between DELETE ChargingProfile requests.
*/
suspend fun deleteChargingProfile(
sessionId: CiString,
responseUrl: URL
): OcpiResponseBody<ChargingProfileResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.izivia.ocpi.toolkit.modules.chargingProfiles

import com.izivia.ocpi.toolkit.common.OcpiSelfRegisteringModuleServer
import com.izivia.ocpi.toolkit.common.httpResponse
import com.izivia.ocpi.toolkit.common.mapper
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.SetChargingProfile
import com.izivia.ocpi.toolkit.modules.versions.domain.InterfaceRole
import com.izivia.ocpi.toolkit.modules.versions.domain.ModuleID
import com.izivia.ocpi.toolkit.modules.versions.domain.VersionNumber
import com.izivia.ocpi.toolkit.modules.versions.repositories.MutableVersionsRepository
import com.izivia.ocpi.toolkit.transport.TransportServer
import com.izivia.ocpi.toolkit.transport.domain.HttpMethod
import com.izivia.ocpi.toolkit.transport.domain.VariablePathSegment

class ChargingProfilesCpoServer(
private val service: ChargingProfilesCpoInterface,
versionsRepository: MutableVersionsRepository? = null,
basePathOverride: String? = null
) : OcpiSelfRegisteringModuleServer(
ocpiVersion = VersionNumber.V2_2_1,
moduleID = ModuleID.chargingprofiles,
interfaceRole = InterfaceRole.RECEIVER,
versionsRepository = versionsRepository,
basePathOverride = basePathOverride
) {

override suspend fun doRegisterOn(transportServer: TransportServer) {
transportServer.handle(
method = HttpMethod.GET,
path = basePathSegments + listOf(
VariablePathSegment("sessionId")
),
queryParams = listOf("duration", "response_url")
) { req ->
req.httpResponse {
service
.getActiveChargingProfile(
sessionId = req.pathParams["sessionId"]!!,
duration = req.queryParams["duration"]!!.toInt(),
responseUrl = req.queryParams["response_url"]!!
)
}
}

transportServer.handle(
method = HttpMethod.PUT,
path = basePathSegments + listOf(
VariablePathSegment("sessionId")
)
) { req ->
req.httpResponse {
service
.putChargingProfile(
sessionId = req.pathParams["sessionId"]!!,
setChargingProfile = mapper.readValue(req.body, SetChargingProfile::class.java)
)
}
}

transportServer.handle(
method = HttpMethod.DELETE,
path = basePathSegments + listOf(
VariablePathSegment("sessionId")
),
queryParams = listOf("response_url")
) { req ->
req.httpResponse {
service
.deleteChargingProfile(
sessionId = req.pathParams["sessionId"]!!,
responseUrl = req.queryParams["response_url"]!!
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.izivia.ocpi.toolkit.modules.chargingProfiles

import com.izivia.ocpi.toolkit.common.*
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.ChargingProfile
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.ChargingProfileResponse
import com.izivia.ocpi.toolkit.modules.chargingProfiles.domain.SetChargingProfile
import com.izivia.ocpi.toolkit.modules.credentials.repositories.PartnerRepository
import com.izivia.ocpi.toolkit.modules.versions.domain.ModuleID
import com.izivia.ocpi.toolkit.transport.TransportClient
import com.izivia.ocpi.toolkit.transport.TransportClientBuilder
import com.izivia.ocpi.toolkit.transport.domain.HttpException
import com.izivia.ocpi.toolkit.transport.domain.HttpMethod
import com.izivia.ocpi.toolkit.transport.domain.HttpRequest
import com.izivia.ocpi.toolkit.transport.domain.HttpStatus

class ChargingProfilesScspClient(
private val transportClientBuilder: TransportClientBuilder,
private val serverVersionsEndpointUrl: String,
private val partnerRepository: PartnerRepository,
private val callbackBaseUrl: URL
) {

private suspend fun buildTransport(): TransportClient = transportClientBuilder
.buildFor(
module = ModuleID.chargingprofiles,
partnerUrl = serverVersionsEndpointUrl,
partnerRepository = partnerRepository
)

suspend fun getActiveChargingProfile(
sessionId: CiString,
duration: Int,
requestId: String
): OcpiResponseBody<ChargingProfileResponse> = with(buildTransport()) {
send(
HttpRequest(
method = HttpMethod.GET,
path = "/$sessionId",
queryParams = mapOf(
"duration" to duration.toString(),
"response_url" to "$callbackBaseUrl/" +
"${ChargingProfilesScspServer.ACTIVE_CHARGING_PROFILE_CALLBACK_URL}/$requestId"
)
)
.withRequiredHeaders(
requestId = generateRequestId(),
correlationId = generateCorrelationId()
)
.authenticate(partnerRepository = partnerRepository, partnerUrl = serverVersionsEndpointUrl)
)
.also {
if (it.status != HttpStatus.OK) throw HttpException(it.status, "status should be ${HttpStatus.OK}")
}
.parseBody()
}

suspend fun putChargingProfile(
sessionId: CiString,
chargingProfile: ChargingProfile,
requestId: String
): OcpiResponseBody<ChargingProfileResponse> = with(buildTransport()) {
send(
HttpRequest(
method = HttpMethod.PUT,
path = "/$sessionId",
body = mapper.writeValueAsString(
SetChargingProfile(
chargingProfile = chargingProfile,
responseUrl = "$callbackBaseUrl/" +
"${ChargingProfilesScspServer.CHARGING_PROFILE_CALLBACK_URL}/$requestId"
)
)
)
.withRequiredHeaders(
requestId = generateRequestId(),
correlationId = generateCorrelationId()
)
.authenticate(partnerRepository = partnerRepository, partnerUrl = serverVersionsEndpointUrl)
)
.also {
if (it.status != HttpStatus.OK && it.status != HttpStatus.CREATED) {
throw HttpException(it.status, "status should be ${HttpStatus.OK} or ${HttpStatus.CREATED}")
}
}
.parseBody()
}

suspend fun deleteChargingProfile(
sessionId: CiString,
requestId: String
): OcpiResponseBody<ChargingProfileResponse> = with(buildTransport()) {
send(
HttpRequest(
method = HttpMethod.DELETE,
path = "/$sessionId",
queryParams = mapOf(
"response_url" to "$callbackBaseUrl/" +
"${ChargingProfilesScspServer.CLEAR_PROFILE_CALLBACK_URL}/$requestId"
)
)
.withRequiredHeaders(
requestId = generateRequestId(),
correlationId = generateCorrelationId()
)
.authenticate(partnerRepository = partnerRepository, partnerUrl = serverVersionsEndpointUrl)
)
.also {
if (it.status != HttpStatus.OK) throw HttpException(it.status, "status should be ${HttpStatus.OK}")
}
.parseBody()
}
}
Loading

0 comments on commit 0230204

Please sign in to comment.