mobilityhouse / ocpp

Python implementation of the Open Charge Point Protocol (OCPP).
MIT License
766 stars 298 forks source link

Inject dependencies in handlers #571

Open OrangeTux opened 7 months ago

OrangeTux commented 7 months ago

The current routing API provides very little flexibility. The current approach with the @on() decorator has a few limitations.

Issues

  1. The interface of a handlers is limited to receive primitive types like str, int, list or dict. It would be great if the framework allow handlers taking composite types. Compare the first 'old' style of routing with the second example that uses composite types.
class ChargePoint:
  @on("TransactionEvent")
  async def on_transaction_event(
    self, 
    event_type: str,
    datetime: str,
    meter_value: Dict,
    **kwargs
):
    ...
from datetime import datetime
from ocpp.v201.enums import EventType
from ocpp.v201.datatypes import Metervalue

class ChargePoint:
  @on("TransactionEvent")
  async def on_transaction_event(
    self, 
    event_type: EventType
    datetime: datetime,
    meter_value: MeterValue,
    **kwargs
):
   ...
  1. Handlers only receive data that's available in the Call's payload. It would be great if also other dependencies could be injected into a handler. For example, the unique id of message. See #545 . Or maybe a database connection. Or the IP address of the charger.

  2. Handlers are tightly coupled an instance of ocpp.v16.ChargePoint or ocpp.v201.ChargePoint. You can't register plain function as handlers. E.g., this is not possible:


@on("Heartbeat")
async fn on_heartbeat(**kwargs):
   ...

In my experience, binding the handlers to a a class leads to large and complex classes. Where each handler mutates a piece of state within the class.

Possible solution

I'm wondering if we can implement a dependency mechanism that allows users a lot more flexibility. FastAPI has an interesting Dependency Injection mechanism. And I'm keen to figure out if we can implement something similar.

FastAPI allows handlers to receive any type, as long as that type can be constructed from an HTTP request. This library could implement a similar approach. Below are three examples of how dependency injection could look like:

class ChargePoint():
   @on("TransactionEvent")
   async def on_transaction_event(
      self,
      event_type: EventType = Depends(EventType),
      datetime: datetime = Depends(datetime),
      meter_value: MeterValue = Depends(MeterValue),
  ):
    ...
# Filter the unique_id from a call
def unique_id(call: Call) -> str:
   ...

class ChargePoint():
   @on("heartbeat")
   async def on_heartbeat(
      self,
      unique_id: str = Depends(unique_id),
  ):
    ...

# Returns the EVSE ID of a connection
def evse_id(...) -> str:
   ...

# Returns a connection to a database
def get_db() -> DbConnection:
   ...

@on("BootNotification")
async def on_boot_notification(
   evse_id: str = Depends(evse_id),
   db: DbConnection = Depends(get_db),
):
   """ Look up EVSE ID in database and to verify if it is allowed to connect.  """
  ...