Exemplo n.º 1
0
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")
Exemplo n.º 2
0
def test_validate_payload_with_non_existing_schema():
    """
    Test if correct exception is raised when a validation schema cannot be
    found.
    """
    with pytest.raises(ValidationError):
        validate_payload({}, "NonExistingAction", 2)
Exemplo n.º 3
0
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")
Exemplo n.º 4
0
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({}, "BootNotification", -1)
Exemplo n.º 5
0
    async def call(self, *, message: Any, context: RouterContext):
        ocpp_version = subprotocol_to_ocpp_version(self.subprotocol)

        camel_case_payload = snake_to_camel_case(asdict(message))

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

        validate_payload(call, ocpp_version)

        await self._send(message=call.to_json(),
                         is_response=False,
                         context=context)
        self.subscriptions[call.unique_id] = context.queue
        try:
            response = await asyncio.wait_for(context.queue.get(),
                                              self._response_timeout)
        except asyncio.TimeoutError:
            del self.subscriptions[call.unique_id]
            raise

        if response.message_type_id == MessageType.CallError:
            log.warning("Received a CALLError: %s'", response)
            raise response.to_exception()
        else:
            response.action = call.action
            validate_payload(response, ocpp_version)

        snake_case_payload = camel_to_snake_case(response.payload)
        call_result = context.ocpp_adapter.call_result
        cls = getattr(call_result, message.__class__.__name__)
        return cls(**snake_case_payload)
Exemplo n.º 6
0
def test_validate_get_composite_profile_payload():
    """" Test if payloads with floats are validated correctly.

    This test uses the value of 15.2, which is internally represented as
    15.19999999999999857891452847979962825775146484375.
    You can verify this using `decimal.Decimal(15.2)`
    """
    message = CallResult(unique_id="1234",
                         action="GetCompositeSchedule",
                         payload={
                             'status': 'Accepted',
                             'connectorId': 1,
                             'scheduleStart': '2021-06-15T14:01:32Z',
                             'chargingSchedule': {
                                 'duration':
                                 60,
                                 'chargingRateUnit':
                                 'A',
                                 'chargingSchedulePeriod': [{
                                     'startPeriod': 0,
                                     'limit': 15.2
                                 }]
                             }
                         })

    validate_payload(message, ocpp_version="1.6")
Exemplo n.º 7
0
    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)
Exemplo n.º 8
0
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")
Exemplo n.º 9
0
def message_to_payload(message: str, action: str) -> Any:
    response = unpack(message)
    response.action = action
    validate_payload(response, "1.6")
    snake_case_payload = camel_to_snake_case(response.payload)
    cls = getattr(call_result, f"{action}Payload")
    payload = cls(**snake_case_payload)
    return payload
Exemplo n.º 10
0
def test_validate_payload_with_invalid_payload():
    """
    Test if validate_payload raises ValidationError when validation of
    payload failes.
    """
    with pytest.raises(ValidationError):
        validate_payload(
            {'invalid_key': True}, 'Heartbeat', MessageType.CallResult,
        )
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
def test_validate_payload_with_valid_payload():
    """
    Test if validate_payload doesn't return any exceptions when it's
    validating a valid payload.
    """
    validate_payload(
            {'currentTime': datetime.now().isoformat()},
            'Heartbeat',
            MessageType.CallResult
    )
Exemplo n.º 13
0
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")
Exemplo n.º 14
0
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")
Exemplo n.º 15
0
def test_validate_payload_with_invalid_additional_properties_payload():
    """
    Test if validate_payload raises FormatViolationError when validation of
    payload with unrequested properties fails.
    """
    message = CallResult(
        unique_id="1234",
        action="Heartbeat",
        payload={'invalid_key': True},
    )

    with pytest.raises(FormatViolationError):
        validate_payload(message, ocpp_version="1.6")
Exemplo n.º 16
0
def test_validate_set_maxlength_violation_payload():
    """
    Test if payloads that violate maxLength raise a
    TypeConstraintViolationError
    """
    message = Call(
        unique_id="1234",
        action="StartTransaction",
        payload={
            "idTag": "012345678901234567890",
            "connectorId": 1,
        },
    )

    with pytest.raises(TypeConstraintViolationError):
        validate_payload(message, ocpp_version="1.6")
Exemplo n.º 17
0
def test_validate_payload_with_invalid_missing_property_payload():
    """
    Test if validate_payload raises ProtocolError when validation of
    payload with missing properties fails.
    """
    message = Call(
        unique_id="1234",
        action="StartTransaction",
        payload={
            'connectorId': 1,
            'idTag': "okTag",
            # meterStart is purposely missing
            'timestamp': '2022-01-25T19:18:30.018Z'
            },
    )

    with pytest.raises(ProtocolError):
        validate_payload(message, ocpp_version="1.6")
Exemplo n.º 18
0
def test_validate_payload_with_invalid_type_payload():
    """
    Test if validate_payload raises TypeConstraintViolationError when
    validation of payload with mismatched type fails.
    """
    message = Call(
        unique_id="1234",
        action="StartTransaction",
        payload={
            'connectorId': 1,
            'idTag': "okTag",
            'meterStart': "invalid_type",
            'timestamp': '2022-01-25T19:18:30.018Z'
            },
    )

    with pytest.raises(TypeConstraintViolationError):
        validate_payload(message, ocpp_version="1.6")
    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
Exemplo n.º 20
0
    async def _handle_call(self, msg, *, context: RouterContext = None):
        """
        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.

        """
        ocpp_version = subprotocol_to_ocpp_version(self.subprotocol)

        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, 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.")

        handler_context = HandlerContext(
            charging_station_id=context.charging_station_id,
            _router_context=context,
            _router=self,
        )
        # Convert message to correct Call instance
        class_ = getattr(context.ocpp_adapter.call, f"{msg.action}Payload")
        payload = class_(**snake_case_payload)
        try:
            response = handler(payload=payload, context=handler_context)
            if inspect.isawaitable(response):
                response = await response
        except Exception as e:
            log.exception("Error while handling request '%s'", msg)
            response = msg.create_call_error(e).to_json()
            await self._send(message=response,
                             is_response=True,
                             context=context)

        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, ocpp_version)

        await self._send(message=response.to_json(),
                         is_response=True,
                         context=context)

        try:
            handler = handlers["_after_action"]
            response = handler(payload=payload, context=handler_context)
            if inspect.isawaitable(response):
                if self._create_task:
                    # Create task to avoid blocking when making a call
                    # inside the after handler
                    asyncio.ensure_future(response)
                else:
                    await response
        except KeyError:
            # '_on_after' hooks are not required. Therefore ignore exception
            # when no '_on_after' hook is installed.
            pass