Wat is OCPP?
OCPP is ontwikkeld door een samenwerking door meerdere partijen, waaronder ElaadNL. Elaad omschrijft het protocol als volgt:
Het Open Charge Point Protocol (OCPP) is een protocol waarin beschreven staat hoe de communicatie tussen het oplaadpunt voor elektrische auto’s en het backofficesysteem moet plaatsvinden. – elaad.nl
Het backoffice is een term die in de EV-wereld veel wordt gebruikt om een (cloud)dienst te omschrijven die op afstand laadpalen beheert. Via OCPP-berichten kan het backoffice met een laadpaal communiceren, om bijvoorbeeld laaddata op te halen of transacties te accepteren. Dit is een belangrijke ontwikkeling geweest, aangezien andere vormen van laadpaalcommunicatie erg variëren tussen modellen of zelfs slecht gedocumenteerd zijn. Waar we vroeger dus overgelaten zouden zijn aan obscure forums om onze laadpaal aan de praat te krijgen, kunnen we nu gebruik maken van het uitgebreid gedocumenteerde en breed ondersteunde OCPP. Wat voor ons nog belangrijker is, is dat er ook OCPP-berichten zijn om in te stellen met welk vermogen er geladen mag worden. Via dit bericht kunnen we een laadprofiel instellen in de laadpaal. Dit laadprofiel geeft aan wanneer en hoeveel geladen mag worden.
Hoe zien OCPP-berichten er uit?
De berichten die binnen OCPP worden verstuurd hebben het JSON-format. Het JSON-bericht om de maximale laadpaalcapaciteit in te stellen ziet er zo uit:
{ "connectorId": 0, "csChargingProfiles": { "chargingProfileId": 1, "stackLevel": 0, "chargingProfilePurpose": "ChargePointMaxProfile", "chargingProfileKind": "Absolute", "chargingSchedule": { "chargingRateUnit": "Amps", "chargingSchedulePeriod": [ { "startPeriod": 0, "limit": 16 } ], "duration": 86400 } } }
Kort gezegd sturen we hier een laadprofiel om de maximale laadpaalcapaciteit in te stellen (ChargePointMaxProfile
). Dit laadprofiel is absolute
en gaat daarom gelijk in en stopt niet wanneer een laadsessie eindigt. Daarnaast zetten we met dit profiel een limiet van 16 Ampère en dit limiet duurt 86400 seconden (24 uur). Dit is maar een voorbeeld van een OCPP-bericht, aangezien er nog veel meer types zijn om verschillende vormen van informatie uit te wisselen.
OCPP in Python
Om deze berichten naar je eigen laadpaal te sturen zullen we in een aantal stappen een Python script maken dat een server opzet waarmee de laadpaal kan verbinden. Dit vereist wel dat de laadpaal op hetzelfde (lokale) netwerk is aangesloten als de computer waar we deze server op gaan laten draaien.
Voor de implementatie van OCPP maken we gebruik van een gelijknamige Python package, OCPP (GitHub). Deze kunnen we makkelijk installeren met PIP:
pip3 install ocpp
Aangezien OCPP het Websocket-protocol gebruikt om een verbinding op te zetten tussen het backoffice en de laadpaal is dit ook een vereiste voor ons project. Voor Websocket is gelukkig ook een simpele package beschikbaar die ook te installeren is met PIP:
pip install websockets
Websocket server opzetten
Wanneer we OCPP en Websocket hebben geïnstalleerd kunnen we een nieuw Python script maken, ocpp_server.py
dat een OCPP server opzet waar de laadpaal mee kan verbinden:
import asyncio import websockets # We define the main function which is the entry point of this script async def main(): server = await websockets.serve( on_connect, # Handles an incoming connection, we will define this next '0.0.0.0', 9000, # Port on which we run the server subprotocols=['ocpp1.6'], # We use OCPP 1.6 in this demo ) await server.wait_closed() if __name__ == '__main__': # Start the server asyncio.run(main())
Hierboven definiëren we dat we een Websocket-server opzetten die draait op poort 9000. In deze demo maken we gebruik van OCPP 1.6. OCPP 2 is al uit, maar minder breed ondersteund.
Nieuwe verbindingen afhandelen
Nu we de server hebben kunnen we de OCPP package gebruiken om te bepalen wat er gebeurt wanneer er een laadpaal verbind:
# Set the logging configuration to give some insights into what happens when running import logging logging.basicConfig(level=logging.DEBUG) from datetime import datetime, timezone import asyncio from ocpp.routing import on from ocpp.v16 import ChargePoint as OCPPChargePoint from ocpp.v16 import call_result from ocpp.v16.enums import Action, RegistrationStatus class ChargePoint(OCPPChargePoint): """Defines how OCPP messages from the charge point to our server are handled.""" @on(Action.BootNotification) async def on_boot_notification(self, charge_point_vendor, charge_point_model, **kwargs): """Handles the boot notification which the charge point uses to register itself to our server.""" return call_result.BootNotification( current_time=datetime.now(timezone.utc).isoformat(), interval=10, status=RegistrationStatus.accepted ) # Define what happens when a charging station connects async def on_connect(websocket, path): await websocket.send('Connection made successfully.') logging.info(f'Charge point {path} connected') charge_point_id = path.strip('/') cp = ChargePoint(charge_point_id, websocket) # Create a new OCPP ChargePoint instance await asyncio.gather(cp.start(), set_limit(cp, 16)) # Start the ChargePoint and set the limit, we will define this function later
Hierboven definiëren we eerst een laadpaal object. Die is nu nog wat kaal, maar kan later uitgebreid worden met extra acties voor onze server om af te handelen. Op dit moment wordt alleen de BootNotification
afgehandeld. Deze ontvangen we van de laadpaal wanneer hij zichzelf registreert bij onze server en is het eerste bericht dat wordt gestuurd. Na ChargePoint
definiëren we on_connect
waar wordt bepaald wat er gebeurt wanneer een laadpaal probeert te verbinden met onze server. Eerst sturen we dat de connectie succesvol was, waarna we het ChargePoint
object opzetten om de communicatie tussen de laadpaal en onze server te beheren. Als laatste starten we de ChargePoint met .start()
zodat deze gaat luisteren naar nieuwe berichten en roepen we set_limit
aan om een limiet van 16 A te zetten.
Sturen van een laadprofiel
De laatste stap is het toevoegen van de functionaliteit om een laadprofiel te sturen naar de laadpaal:
from ocpp.v16 import call from ocpp.v16 import call_result from ocpp.v16.enums import ChargingProfileKindType, ChargingProfilePurposeType, \ ChargingRateUnitType async def set_limit(cp: ChargePoint, amps: float) -> None: request = call.SetChargingProfile( connector_id=0, cs_charging_profiles={ "charging_profile_id": 1, "stack_level": 0, "charging_profile_purpose": ChargingProfilePurposeType.charge_point_max_profile, "charging_profile_kind": ChargingProfileKindType.absolute, "charging_schedule": { "charging_rate_unit": ChargingRateUnitType.amps, "charging_schedule_period": [{ "start_period": 0, "limit": amps, }], "duration": 86400 } } ) response = await cp.call(request) if not isinstance(response, call_result.SetChargingProfile): logging.error("Response was not a SetChargingProfile response!") return if response.status != "Accepted": logging.error("Charging profile was not accepted!") return logging.info("Charging profile accepted!")
Hierboven definiëren we de functie, set_limit
, die op het aangemaakte ChargePoint-object een limiet zet door een laadprofiel te sturen. Dit is het laadprofiel dat we eerder hebben besproken, alleen met iets andere syntax om te voldoen aan hoe de OCPP package het verwacht. Wanneer we het bericht hebben gespecificeerd, sturen we deze naar de laadpaal met cp.call()
. Met twee checks verifiëren we daarna of de laadpaal het bericht goed heeft ontvangen.
Het starten van de server
We kunnen de server nu starten door het volgende commando uit te voeren:
python3 ocpp_server.py
De server draait als het goed is nu op poort 9000 (check of je firewall dit toelaat!), maar de laadpaal zal hier nog niet automatisch mee verbinden. Eerst moeten we de laadpaal vertellen waar onze server draait. Hiervoor hebben we het lokale IP-adres van de computer waar de server op draait nodig. Deze kun je vinden met de commando’s ifconfig
op macOS en Linux, of ipconfig
op Windows. Het adres heeft meestal het format 192.168.x.x
.
Dit IP-adres, samen met de poort waarop onze server draait, maken het Websocket adres en ziet er zo uit: ws://192.168.1.10:9000
(in het geval het IP-adres van je computer 192.168.1.10
is). De volgende stap is om dit in je laadpaal te configureren. Hoe dit precies moet verschilt per merk, maar het komt er op neer dat het backofficeadres (ook wel eMSP-adres genoemd) aangepast moet worden in de systeemconfiguratie.
Als dit is gelukt en je je laadpaal opnieuw opstart zul je als het goed is in je server logs zien dat je laadpaal verbind en het laadprofiel wordt gestuurd. Wat mij betreft een moment om een biertje te pakken en dit te vieren.
Conclusie
In deze demo hebben we een OCPP-server gebouwd om te communiceren met een laadpaal. De functionaliteit van de server is nog niet heel veel soeps, maar aangezien we de volledige kracht van Python tot onze beschikking hebben is het makkelijk om deze uit te breiden. Denk bijvoorbeeld aan het ophalen van energieprijzen om alleen te laden wanneer de stroom goedkoop is, of om energiemetingen te gebruiken om alleen te laden wanneer je zonnepanelen genoeg zonnestroom genereren. De weg ligt open om je eigen Home Energy Management System (HEMS) te bouwen!