def test_validate_meter_values_hertz(): """ Tests that a unit of measure called "Hertz" is permitted in validation. This was missing from the original 1.6 spec, but was added as an errata (see the OCPP 1.6 Errata sheet, v4.0 Release, 2019-10-23, page 34). """ message = Call(unique_id="1234", action="MeterValues", payload={ 'connectorId': 1, 'transactionId': 123456789, 'meterValue': [{ 'timestamp': '2020-02-21T13:48:45.459756Z', 'sampledValue': [{ "value": "50.0", "measurand": "Frequency", "unit": "Hertz", }] }] }) validate_payload(message, ocpp_version="1.6")
def test_validate_payload_with_invalid_message_type_id(): """ Test if validate_payload raises ValidationError when it is called with a message type id other than 2, Call, or 3, CallError. """ with pytest.raises(ValidationError): validate_payload(dict(), ocpp_version="1.6")
def test_validate_set_charging_profile_payload(): """" Test if payloads with floats are validated correctly. This test uses the value of 21.4, which is internally represented as 21.39999999999999857891452847979962825775146484375. You can verify this using `decimal.Decimal(21.4)` """ message = Call(unique_id="1234", action="SetChargingProfile", payload={ 'connectorId': 1, 'csChargingProfiles': { 'chargingProfileId': 1, 'stackLevel': 0, 'chargingProfilePurpose': 'TxProfile', 'chargingProfileKind': 'Relative', 'chargingSchedule': { 'chargingRateUnit': 'A', 'chargingSchedulePeriod': [{ 'startPeriod': 0, 'limit': 21.4 }] }, 'transactionId': 123456789, } }) validate_payload(message, ocpp_version="1.6")
def test_validate_payload_with_valid_payload(ocpp_version): """ Test if validate_payload doesn't return any exceptions when it's validating a valid payload. """ message = CallResult(unique_id="1234", action="Heartbeat", payload={'currentTime': datetime.now().isoformat()}) validate_payload(message, ocpp_version=ocpp_version)
def test_validate_payload_with_non_existing_schema(): """ Test if correct exception is raised when a validation schema cannot be found. """ message = CallResult( unique_id="1234", action="MagicSpell", payload={'invalid_key': True}, ) with pytest.raises(ValidationError): validate_payload(message, ocpp_version="1.6")
def test_validate_payload_with_invalid_payload(): """ Test if validate_payload raises ValidationError when validation of payload failes. """ message = CallResult( unique_id="1234", action="Heartbeat", payload={'invalid_key': True}, ) with pytest.raises(ValidationError): validate_payload(message, ocpp_version="1.6")
async def call(self, payload, suppress=True): """ Send Call message to client and return payload of response. The given payload is transformed into a Call object by looking at the type of the payload. A payload of type BootNotificationPayload will turn in a Call with Action BootNotification, a HeartbeatPayload will result in a Call with Action Heartbeat etc. A timeout is raised when no response has arrived before expiring of the configured timeout. When waiting for a response no other Call message can be send. So this function will wait before response arrives or response timeout has expired. This is in line the OCPP specification Suppress is used to maintain backwards compatibility. When set to True, if response is a CallError, then this call will be suppressed. When set to False, an exception will be raised for users to handle this CallError. """ camel_case_payload = snake_to_camel_case(asdict(payload)) call = Call( unique_id=str(self._unique_id_generator()), action=payload.__class__.__name__[:-7], payload=remove_nones(camel_case_payload) ) validate_payload(call, self._ocpp_version) # Use a lock to prevent make sure that only 1 message can be send at a # a time. async with self._call_lock: await self._send(call.to_json()) try: response = \ await self._get_specific_response(call.unique_id, self._response_timeout) except asyncio.TimeoutError: raise asyncio.TimeoutError( f"Waited {self._response_timeout}s for response on " f"{call.to_json()}." ) if response.message_type_id == MessageType.CallError: LOGGER.warning("Received a CALLError: %s'", response) if suppress: return raise response.to_exception() else: response.action = call.action validate_payload(response, self._ocpp_version) snake_case_payload = camel_to_snake_case(response.payload) # Create the correct Payload instance based on the received payload. If # this method is called with a call.BootNotificationPayload, then it # will create a call_result.BootNotificationPayload. If this method is # called with a call.HeartbeatPayload, then it will create a # call_result.HeartbeatPayload etc. cls = getattr(self._call_result, payload.__class__.__name__) # noqa return cls(**snake_case_payload)
async def _handle_call(self, msg): """ Execute all hooks installed for based on the Action of the message. First the '_on_action' hook is executed and its response is returned to the client. If there is no '_on_action' hook for Action in the message a CallError with a NotImplemtendError is returned. Next the '_after_action' hook is executed. """ try: handlers = self.route_map[msg.action] except KeyError: raise NotImplementedError(f"No handler for '{msg.action}' " "registered.") if not handlers.get('_skip_schema_validation', False): validate_payload(msg, self._ocpp_version) # 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: # # * chargePointVendor becomes charge_point_vendor # * firmwareVersion becomes firmwareVersion snake_case_payload = camel_to_snake_case(msg.payload) try: handler = handlers['_on_action'] except KeyError: raise NotImplementedError(f"No handler for '{msg.action}' " "registered.") try: response = handler(**snake_case_payload) if inspect.isawaitable(response): response = await response except Exception as e: LOGGER.exception("Error while handling request '%s'", msg) response = msg.create_call_error(e).to_json() await self._send(response) return temp_response_payload = asdict(response) # Remove nones ensures that we strip out optional arguments # which were not set and have a default value of None response_payload = remove_nones(temp_response_payload) # The response payload must be 'translated' from snake_case to # camelCase. So: # # * charge_point_vendor becomes chargePointVendor # * firmware_version becomes firmwareVersion camel_case_payload = snake_to_camel_case(response_payload) response = msg.create_call_result(camel_case_payload) if not handlers.get('_skip_schema_validation', False): validate_payload(response, self._ocpp_version) await self._send(response.to_json()) try: handler = handlers['_after_action'] # Create task to avoid blocking when making a call inside the # after handler response = handler(**snake_case_payload) if inspect.isawaitable(response): asyncio.ensure_future(response) except KeyError: # '_on_after' hooks are not required. Therefore ignore exception # when no '_on_after' hook is installed. pass