lorenzodonini/ocpp-go

Concurrent usage of OCPP 1.6 and OCPP 2.0.1

tretmar opened this issue · 4 comments

Hello there!

My company already uses this OCPP go library for OCCP 1.6.
We now want to support OCPP 2.0.1 as well which means we need to able to provide OCPP 1.6 and OCPP 2.0.1 concurrently.
We've already tested the OCPP 2.0.1 server against a client simulation and it worked like a charm.

Problem
We have a lot of custom logic in our code handling the connections, monitoring, and controlling of charging stations.
In many cases, we directly depend on the OCPP 1.6 packages from this library.
Right now, the OCPP 2.0.1 implementation was put in a new directory and thus, there are new packages for every feature.

Do you have any idea how we could face this issue? We really want to prevent to just copy/paste our current implementation and use the OCPP 2.0.1 packages.
Maybe, there there is a way to have a common CentralSystem or CSMS Interface?

Any help is appreciated. Thank you very much.

Hello,

interesting question and not completely unexpected.

If you take a look at all the messages in the callbacks, you'll realize that most of the messages and/or attributes are different between v1.6 and v2.0.1, so it wasn't really possible to unify the two versions from a library perspective, hence the two separate packages. It's literally two different protocols, not just a "version bump".

Take GetConfiguration (v1.6) as an example. It was basically replaced by the GetVariables message (v2.0.1), which uses an entirely new data model. I honestly don't see how it could be possible to create an abstraction (except for a raw map[string]interface{} type), that would allow to map two heterogenous types under a common interface/parent class.

Adapters?

My understanding is that any vendor or service provider who wishes to support both protocol versions will have to rewrite their handlers anyways (with lots of copy paste), as more data and custom behavior is involved. I expect DB schemas to also change between versions.

If you want to leverage shared logic, you could either:

  • convert the v1.6 messages internally to match the v2 schema
  • delegate all business logic to external functions, which take in primitive parameters or custom types -> this would be an adapter on your end basically

Here's a naive approach, just to show you what would be involved for a "simple" message:

// v1.6 handler

func (handler *V16Handler) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (confirmation *core.BootNotificationConfirmation, err error) {
	logDefault(chargePointId, request.GetFeatureName()).Infof("boot confirmed")
	// Forward to v2 handler, converting to the data model expected by v2
	res, err := v2Handler.OnBootNotification(chargePointId, &provisioning.BootNotificationRequest{
		Reason: provisioning.BootReasonUnknown, // This did not exist in 1.6, so you will have to either assume or
		ChargingStation: provisioning.ChargingStationType{
			SerialNumber:    request.ChargePointSerialNumber,
			Model:           request.ChargePointModel,
			VendorName:      request.ChargePointVendor,
			FirmwareVersion: request.FirmwareVersion,
			Modem: &provisioning.ModemType{
				Iccid: request.Iccid,
				Imsi:  request.Imsi,
			},
		},
	})
	// Re-convert to v1.6 format
	return &core.BootNotificationConfirmation{
		CurrentTime: types.NewDateTime(res.CurrentTime.Time),
		Interval:    res.Interval,
		Status:      core.RegistrationStatus(res.Status),
	}, nil
}

// v2.0.1 handler (different file, but can be in the same package to reuse all your custom functions)

func (c *V2Handler) OnBootNotification(chargingStationID string, request *provisioning.BootNotificationRequest) (response *provisioning.BootNotificationResponse, err error) {
	logDefault(chargingStationID, request.GetFeatureName()).Infof("boot confirmed for %v %v, serial: %v, firmare version: %v, reason: %v",
		request.ChargingStation.VendorName, request.ChargingStation.Model, request.ChargingStation.SerialNumber, request.ChargingStation.FirmwareVersion, request.Reason)
	response = provisioning.NewBootNotificationResponse(types.NewDateTime(time.Now()), defaultHeartbeatInterval, provisioning.RegistrationStatusAccepted)
	return
}

Just this example should show you that these conversions can be problematic, since information could be lost in both directions.
It's up to you to decide whether an adapter would be preferable for your setup, instead of a clean separation.

Just a shared interface?

If you just wish to have all the callbacks from v1.6 and v2.0.1 in a single package, this could probably be achieved, although with a different naming scheme (e.g. OnV16BootNotification along OnV2BootNotification). This would not solve the issues given by the underlying protocols being different, but it could simplify your implementation a bit. I would have to investigate this.

TLDR

As much as I understand your pain, I don't plan on writing an adapter between the two protocol versions, because it would require making some dangerous assumptions which might do more harm than good in the long run.
If you come up with a brilliant idea to solve this, merge requests are definitely welcome!

Instead, I could investigate if offering the two interfaces combined into a single one would offer any major benefits (see my point above).

Hi @lorenzodonini .
Thank you very much for your really detailed and well structured answer.

It helps us to know that these two versions can be seen as new protocols on a certain level. Even if we can unify them, it will probably cause problems sooner or later. Thus, we are going to try separating the version-specific code parts from our custom logic.

We will let you know when have a brilliant idee to solve this!

Cant we handle this with websocket sub protocol and run them in different ports, or have a subprotocol based switching server in front of service

@ysaakpr you can definitely do that on a networking level, but it won't solve the fact that you need to implement both v1.6 and v2.0.1 callbacks in your code.

Btw the networking layer currently doesn't pass any information about the negotiated subprotocol to the upper layers -> that's why the protocol version needs to be known at startup time.
In other words the current implementation forces you to spin up two processes, one for v1.6 and one for v2.0.1, if you need to handle both concurrently.