Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ help:
@echo " If no version is provided, poetry outputs the current project version"
@echo " test run all the tests and linting"
@echo " update updates the dependencies in poetry.lock"
@echo " v21-central-system-example Run the example implementing an OCPP 2.1 central system.
@echo " v21-charge-point-example Run the example implementing an OCPP 2.1 charger.
@echo " update updates the dependencies in poetry.lock"
@echo ""
@echo "Check the Makefile to know exactly what each target is doing."

Expand Down Expand Up @@ -55,3 +58,9 @@ release: .install-poetry

deploy: update tests
poetry publish --build

v21-central-system-example:
PYTHONPATH=ocpp:$$PYTHONPATH poetry run python examples/v21/central_system.py

v21-charge-point-example:
PYTHONPATH=ocpp:$$PYTHONPATH poetry run python examples/v21/charge_point.py
58 changes: 58 additions & 0 deletions examples/v21/central_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import asyncio
import logging
import pathlib
from datetime import datetime, timezone

try:
import websockets
except ModuleNotFoundError:
print("This example relies on the 'websockets' package.")
print("Please install it by running: ")
print()
print(" $ pip install websockets")
import sys

sys.exit(1)

from ocpp.messages import SchemaValidator
from ocpp.routing import on
from ocpp.v21 import ChargePoint as cp
from ocpp.v21 import call_result
from ocpp.v21.enums import RegistrationStatus

logging.basicConfig(level=logging.INFO)

# The ocpp package doesn't come with the JSON schemas for OCPP 2.1.
# See https://github.com/mobilityhouse/ocpp/issues/458 for more details.
schemas_dir = str(pathlib.Path(__file__).parent.joinpath("schemas").resolve())
validator = SchemaValidator(schemas_dir)


class ChargePoint(cp):
@on("BootNotification")
def on_boot_notification(self, reason: str, charging_station: str, **kwargs):
return call_result.BootNotification(
current_time=datetime.now(timezone.utc).isoformat(),
interval=10,
status=RegistrationStatus.accepted,
)


async def on_connect(websocket, path):
charge_point_id = path.strip("/")
cp = ChargePoint(charge_point_id, websocket, validator)

await cp.start()


async def main():
server = await websockets.serve(
on_connect, "0.0.0.0", 9000, subprotocols=["ocpp2.1"]
)

logging.info("Server Started listening to new connections...")
await server.wait_closed()


if __name__ == "__main__":
asyncio.run(main())
54 changes: 54 additions & 0 deletions examples/v21/charge_point.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import asyncio
import logging
import pathlib

try:
import websockets
except ModuleNotFoundError:
print("This example relies on the 'websockets' package.")
print("Please install it by running: ")
print()
print(" $ pip install websockets")
import sys

sys.exit(1)

from ocpp.messages import SchemaValidator
from ocpp.v21 import ChargePoint as cp
from ocpp.v21 import call, call_result
from ocpp.v21.datatypes import ChargingStation
from ocpp.v21.enums import BootReason, RegistrationStatus

logging.basicConfig(level=logging.INFO)

schemas_dir = pathlib.Path(__file__).parent.joinpath("schemas").resolve()
validator = SchemaValidator(str(schemas_dir))


class ChargePoint(cp):
async def send_boot_notification(self):
request = call.BootNotification(
reason=BootReason.power_up,
charging_station=ChargingStation(
model="Virtual Charge Point",
vendor_name="y",
),
)

response: call_result.BootNotification = await self.call(request)

if response.status == RegistrationStatus.accepted:
print("Connected to central system.")


async def main():
async with websockets.connect(
"ws://localhost:9000/CP_1", subprotocols=["ocpp2.1"]
) as ws:
cp = ChargePoint("CP_1", ws, validator)

await asyncio.gather(cp.start(), cp.send_boot_notification())


if __name__ == "__main__":
asyncio.run(main())
105 changes: 105 additions & 0 deletions examples/v21/schemas/BootNotificationRequest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$id": "urn:OCPP:Cp:2:2023:5:BootNotificationRequest",
"comment": "OCPP 2.1 Draft 1, Copyright Open Charge Alliance",
"definitions": {
"CustomDataType": {
"description": "This class does not get 'AdditionalProperties = false' in the schema generation, so it can be extended with arbitrary JSON properties to allow adding custom data.",
"javaType": "CustomData",
"type": "object",
"properties": {
"vendorId": {
"type": "string",
"maxLength": 255
}
},
"required": [
"vendorId"
]
},
"BootReasonEnumType": {
"javaType": "BootReasonEnum",
"type": "string",
"additionalProperties": false,
"enum": [
"ApplicationReset",
"FirmwareUpdate",
"LocalReset",
"PowerUp",
"RemoteReset",
"ScheduledReset",
"Triggered",
"Unknown",
"Watchdog"
]
},
"ChargingStationType": {
"javaType": "ChargingStation",
"type": "object",
"additionalProperties": false,
"properties": {
"customData": {
"$ref": "#/definitions/CustomDataType"
},
"serialNumber": {
"type": "string",
"maxLength": 25
},
"model": {
"type": "string",
"maxLength": 20
},
"modem": {
"$ref": "#/definitions/ModemType"
},
"vendorName": {
"type": "string",
"maxLength": 50
},
"firmwareVersion": {
"type": "string",
"maxLength": 50
}
},
"required": [
"model",
"vendorName"
]
},
"ModemType": {
"javaType": "Modem",
"type": "object",
"additionalProperties": false,
"properties": {
"customData": {
"$ref": "#/definitions/CustomDataType"
},
"iccid": {
"type": "string",
"maxLength": 20
},
"imsi": {
"type": "string",
"maxLength": 20
}
}
}
},
"type": "object",
"additionalProperties": false,
"properties": {
"customData": {
"$ref": "#/definitions/CustomDataType"
},
"chargingStation": {
"$ref": "#/definitions/ChargingStationType"
},
"reason": {
"$ref": "#/definitions/BootReasonEnumType"
}
},
"required": [
"reason",
"chargingStation"
]
}
79 changes: 79 additions & 0 deletions examples/v21/schemas/BootNotificationResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$id": "urn:OCPP:Cp:2:2023:5:BootNotificationResponse",
"comment": "OCPP 2.1 Draft 1, Copyright Open Charge Alliance",
"definitions": {
"CustomDataType": {
"description": "This class does not get 'AdditionalProperties = false' in the schema generation, so it can be extended with arbitrary JSON properties to allow adding custom data.",
"javaType": "CustomData",
"type": "object",
"properties": {
"vendorId": {
"type": "string",
"maxLength": 255
}
},
"required": [
"vendorId"
]
},
"RegistrationStatusEnumType": {
"javaType": "RegistrationStatusEnum",
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Pending",
"Rejected"
]
},
"StatusInfoType": {
"javaType": "StatusInfo",
"type": "object",
"additionalProperties": false,
"properties": {
"customData": {
"$ref": "#/definitions/CustomDataType"
},
"reasonCode": {
"type": "string",
"maxLength": 20
},
"additionalInfo": {
"type": "string",
"maxLength": 512
}
},
"required": [
"reasonCode"
]
}
},
"type": "object",
"additionalProperties": false,
"properties": {
"customData": {
"$ref": "#/definitions/CustomDataType"
},
"currentTime": {
"type": "string",
"format": "date-time"
},
"interval": {
"type": "integer",
"minimum": -2147483648.0,
"maximum": 2147483647.0
},
"status": {
"$ref": "#/definitions/RegistrationStatusEnumType"
},
"statusInfo": {
"$ref": "#/definitions/StatusInfoType"
}
},
"required": [
"currentTime",
"interval",
"status"
]
}
24 changes: 17 additions & 7 deletions ocpp/charge_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Dict, List, Union

from ocpp.exceptions import NotSupportedError, OCPPError
from ocpp.messages import Call, MessageType, unpack, validate_payload
from ocpp.messages import Call, MessageType, SchemaValidator, unpack, validate_payload
from ocpp.routing import create_route_map

LOGGER = logging.getLogger("ocpp")
Expand Down Expand Up @@ -85,7 +85,7 @@ class ChargePoint:
initiated and received by the Central System
"""

def __init__(self, id, connection, response_timeout=30):
def __init__(self, id, connection, validator: SchemaValidator, response_timeout=30):
"""

Args:
Expand All @@ -98,6 +98,8 @@ def __init__(self, id, connection, response_timeout=30):
"""
self.id = id

self._validator = validator

# The maximum time in seconds it may take for a CP to respond to a
# CALL. An asyncio.TimeoutError will be raised if this limit has been
# exceeded.
Expand Down Expand Up @@ -178,7 +180,7 @@ async def _handle_call(self, msg):
)

if not handlers.get("_skip_schema_validation", False):
validate_payload(msg, self._ocpp_version)
validate_payload(msg, self._validator)
# OCPP uses camelCase for the keys in the payload. It's more pythonic
# to use snake_case for keyword arguments. Therefore the keys must be
# 'translated'. Some examples:
Expand Down Expand Up @@ -221,7 +223,7 @@ async def _handle_call(self, msg):
response = msg.create_call_result(camel_case_payload)

if not handlers.get("_skip_schema_validation", False):
validate_payload(response, self._ocpp_version)
validate_payload(msg, self._validator)

await self._send(response.to_json())

Expand Down Expand Up @@ -265,13 +267,21 @@ async def call(self, payload, suppress=True, unique_id=None):
unique_id if unique_id is not None else str(self._unique_id_generator())
)

action = payload.__class__.__name__

# The call and call_result classes of OCPP 1.6, 2.0 and 2.0.1 are suffixed with 'Payload'.
# E.g. call_result.BootNotificationPayload. The suffixed doesn't make much sense and is removed
# as of OCPP 2.1.
if payload.__class__.__name__.endswith("Payload"):
action = payload.__class__.__name__[:-7]

call = Call(
unique_id=unique_id,
action=payload.__class__.__name__[:-7],
action=action,
payload=remove_nones(camel_case_payload),
)

validate_payload(call, self._ocpp_version)
validate_payload(call, self._validator)

# Use a lock to prevent make sure that only 1 message can be send at a
# a time.
Expand All @@ -294,7 +304,7 @@ async def call(self, payload, suppress=True, unique_id=None):
raise response.to_exception()
else:
response.action = call.action
validate_payload(response, self._ocpp_version)
validate_payload(response, self._validator)

snake_case_payload = camel_to_snake_case(response.payload)
# Create the correct Payload instance based on the received payload. If
Expand Down
Loading