Example #1
0
class TestTimeSeries(unittest.TestCase):
    """
    Test class for time series api client
    """
    def setUp(self) -> None:
        """
        Setup test data
        Returns:
            None
        """
        self.fiware_header = FiwareHeader(
            service=settings.FIWARE_SERVICE,
            service_path=settings.FIWARE_SERVICEPATH)
        self.ql_client = QuantumLeapClient(url=settings.QL_URL,
                                           fiware_header=self.fiware_header)

        self.cb_client = ContextBrokerClient(url=settings.CB_URL,
                                             fiware_header=self.fiware_header)

    def test_meta_endpoints(self) -> None:
        """
        Test meta data endpoints
        Returns:
            None
        """
        with QuantumLeapClient(
                url=settings.QL_URL,
                fiware_header=self.fiware_header) \
                as client:
            self.assertIsNotNone(client.get_version())
            self.assertIsNotNone(client.get_health())

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL,
                ql_url=settings.QL_URL)
    def test_input_endpoints(self) -> None:
        """
        Test input endpoint
        Returns:
            None
        """
        entities = create_entities()
        for entity in entities:
            self.cb_client.post_entity(entity)

        with QuantumLeapClient(
                url=settings.QL_URL,
                fiware_header=self.fiware_header) \
                as client:
            notification_message = Message(data=entities,
                                           subscriptionId="test")
            client.post_subscription(cb_url=settings.CB_URL,
                                     ql_url=settings.QL_URL,
                                     entity_id=entities[0].id)
            client.post_notification(notification_message)
        time.sleep(1)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                ql_url=settings.QL_URL)
    def test_entity_context(self) -> None:
        """
        Test entities endpoint
        Returns:
            None
        """
        entities = create_entities()
        with QuantumLeapClient(
                url=settings.QL_URL,
                fiware_header=self.fiware_header) \
                as client:
            notification_message = Message(data=entities,
                                           subscriptionId="test")
            client.post_notification(notification_message)

            time.sleep(1)
            entities = client.get_entities(entity_type=entities[0].type)
            for entity in entities:
                logger.debug(entity.json(indent=2))

    def test_query_endpoints_by_id(self) -> None:
        """
        Test queries with default values

        Returns:
            None
        """
        with QuantumLeapClient(
                url=settings.QL_URL,
                fiware_header=self.fiware_header.copy(
                    update={'service_path': '/static'})) \
                as client:

            entities = create_entities()

            with self.assertRaises(requests.RequestException):
                client.get_entity_by_id(entity_id=entities[0].id,
                                        entity_type='MyType')
            for entity in entities:
                # get by id
                attrs_id = client.get_entity_by_id(entity_id=entity.id,
                                                   aggr_period='minute',
                                                   aggr_method='avg',
                                                   attrs='temperature,co2')
                logger.debug(attrs_id.json(indent=2))
                logger.debug(attrs_id.to_pandas())

                attrs_values_id = client.get_entity_values_by_id(
                    entity_id=entity.id)
                logger.debug(attrs_values_id.to_pandas())
                self.assertEqual(len(attrs_values_id.index), 10000)

                attr_id = client.get_entity_attr_by_id(entity_id=entity.id,
                                                       attr_name="temperature")
                logger.debug(attr_id.to_pandas())
                self.assertEqual(len(attr_id.index), 10000)

                attr_values_id = client.get_entity_attr_values_by_id(
                    entity_id=entity.id, attr_name="temperature")
                logger.debug(attr_values_id.to_pandas())
                self.assertEqual(len(attrs_values_id.index), 10000)

    def test_query_endpoints_by_type(self) -> None:
        """
        Test queries by type with default values

        Returns:
            None
        """
        with QuantumLeapClient(
                url=settings.QL_URL,
                fiware_header=self.fiware_header.copy(
                    update={'service_path': '/static'})) \
                as client:

            entities = create_entities()

            for entity in entities:
                # get by type
                attrs_type = client.get_entity_by_type(entity_type=entity.type)
                for entity_id in attrs_type:
                    logger.debug(entity_id.to_pandas())

                self.assertEqual(
                    sum([len(entity_id.index) for entity_id in attrs_type]),
                    10000)

                attrs_values_type = client.get_entity_values_by_type(
                    entity_type=entity.type, )
                for entity_id in attrs_values_type:
                    logger.debug(entity_id.to_pandas())
                self.assertEqual(
                    sum([
                        len(entity_id.index) for entity_id in attrs_values_type
                    ]), 10000)

                attr_type = client.get_entity_attr_by_type(
                    entity_type=entity.type, attr_name="temperature")
                for entity_id in attr_type:
                    logger.debug(entity_id.to_pandas())
                self.assertEqual(
                    sum([len(entity_id.index) for entity_id in attr_type]),
                    10000)

                attr_values_type = client.get_entity_attr_values_by_type(
                    entity_type=entity.type, attr_name="temperature")
                for entity_id in attr_values_type:
                    logger.debug(entity_id.to_pandas())
                self.assertEqual(
                    sum([
                        len(entity_id.index) for entity_id in attr_values_type
                    ]), 10000)

    def test_test_query_endpoints_with_args(self) -> None:
        """
        Test arguments for queries

        Returns:
            None
        """
        with QuantumLeapClient(
                url=settings.QL_URL,
                fiware_header=self.fiware_header.copy(
                    update={'service_path': '/static'})) \
                as client:

            for entity in create_entities():
                # test limit
                for limit in range(5000, 25000, 5000):
                    records = client.get_entity_by_id(entity_id=entity.id,
                                                      attrs='temperature,co2',
                                                      limit=limit)

                    logger.debug(records.json(indent=2))
                    logger.debug(records.to_pandas())
                    self.assertEqual(len(records.index), limit)

                # test last_n
                for last_n in range(5000, 25000, 5000):
                    limit = 15000
                    last_n_records = client.get_entity_by_id(
                        entity_id=entity.id,
                        attrs='temperature,co2',
                        limit=limit,
                        last_n=last_n)
                    self.assertGreater(last_n_records.index[0],
                                       records.index[0])
                    self.assertEqual(len(last_n_records.index),
                                     min(last_n, limit))

                # test offset
                old_records = None
                for offset in range(5000, 25000, 5000):
                    # with limit
                    records = client.get_entity_by_id(entity_id=entity.id,
                                                      attrs='temperature,co2',
                                                      offset=offset)

                    if old_records:
                        self.assertLess(old_records.index[0], records.index[0])
                    old_records = records

                old_records = None
                for offset in range(5000, 25000, 5000):
                    # test with last_n
                    records = client.get_entity_by_id(entity_id=entity.id,
                                                      attrs='temperature,co2',
                                                      offset=offset,
                                                      last_n=5)
                    if old_records:
                        self.assertGreater(old_records.index[0],
                                           records.index[0])
                    old_records = records

    def tearDown(self) -> None:
        """
        Clean up server
        Returns:
            None
        """
        clear_all(fiware_header=self.fiware_header,
                  cb_url=settings.CB_URL,
                  ql_url=settings.QL_URL)

        self.ql_client.close()
        self.cb_client.close()
Example #2
0
# ## Main script
if __name__ == '__main__':
    # create a fiware header object
    fiware_header = FiwareHeader(service=SERVICE, service_path=SERVICE_PATH)
    # clear the state of your service and scope
    clear_iot_agent(url=IOTA_URL, fiware_header=fiware_header)
    clear_context_broker(url=CB_URL, fiware_header=fiware_header)

    # Create clients and restore devices and groups from file
    groups = parse_file_as(List[ServiceGroup], READ_GROUPS_FILEPATH)
    devices = parse_file_as(List[Device], READ_DEVICES_FILEPATH)
    entities = parse_file_as(List[ContextEntity], READ_ENTITIES_FILEPATH)
    cbc = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header)
    for entity in entities:
        cbc.post_entity(entity=entity)

    iotac = IoTAClient(url=IOTA_URL, fiware_header=fiware_header)
    iotac.post_groups(service_groups=groups)
    iotac.post_devices(devices=devices)

    # ToDo: Retrieve all iot resources from the IoT-Agent
    # Get the group and device configurations from the server
    group = iotac.get_group(resource="/iot/json", apikey=APIKEY)
    weather_station = iotac.get_device(device_id="device:001")
    zone_temperature_sensor = iotac.get_device(device_id="device:002")
    heater = iotac.get_device(device_id="device:003")

    # ToDo: Get context entities from the Context Broker
    #  (exclude the IoT device ones)
    building = cbc.get_entity(entity_id="urn:ngsi-ld:building:001",
    # ## 2 Interact with QL
    #
    # ### 2.1 Create a ContextEntity to work with
    #
    # for more details see: e01_ngsi_v2_context_basics.py
    hall = {
        "id": "Hall_1",
        "type": "Room",
        "temperature": {
            "value": random.randint(0, 100),
            "type": "Integer"
        },
    }

    hall_entity = ContextEntity(**hall)
    cb_client.post_entity(hall_entity)

    # ### 2.2 Manage subscriptions
    #
    # create a subscription
    # Note: that the IP must be the ones that orion and quantumleap can access,
    # e.g. service name or static IP, localhost will not work here.
    ql_client.post_subscription(entity_id=hall_entity.id,
                                cb_url="http://orion:1026",
                                ql_url="http://quantumleap:8668",
                                throttling=0)

    # Get all subscriptions
    subscription_list = cb_client.get_subscription_list()

    # Notify QL manually
    # ### 2.1.2 Using the constructor and interfaces
    #
    room2_entity = ContextEntity(id="Room2", type="Room")
    temp_attr = NamedContextAttribute(name="temperature",
                                      value=22,
                                      type=DataType.FLOAT)
    pressure_attr = NamedContextAttribute(name="pressure",
                                          value=222,
                                          type="Integer")
    room2_entity.add_attributes([temp_attr, pressure_attr])

    # ## 2.2 Post Entities
    #
    print(cb_client.get_entity_list())
    cb_client.post_entity(entity=room1_entity)
    cb_client.post_entity(entity=room2_entity)

    # # 3 Access entities in Fiware
    #
    # Get all entities from context broker
    logger.info(cb_client.get_entity_list())

    # Get entities by id
    logger.info(cb_client.get_entity_list(entity_ids=["Room1"]))

    # Get entities by type
    logger.info(cb_client.get_entity_list(entity_types=["Room"]))

    # Get entities by id pattern
    logger.info(cb_client.get_entity_list(id_pattern="^Room[2-5]"))
    # ToDo: create a `description` property for your building
    building_description = NamedContextAttribute(name="description",
                                                 type="Text",
                                                 value="Small office building "
                                                 "with good insulation "
                                                 "standard")

    # add all properties to your building using the
    # `add_attribute` function of your building object
    building.add_attributes(attrs=[building_description, category, address])

    # ToDo: Create a context broker client and add the fiware_header
    cbc = ContextBrokerClient(url=CB_URL, fiware_header=fiware_header)
    # ToDo: Send your building model to the context broker. Check the client
    #  for the proper function
    cbc.post_entity(entity=building)

    # Update your local building model with the one from the server
    building = cbc.get_entity(entity_id=building.id, entity_type=building.type)

    # print your `building model` as json
    print(f"This is your building model: \n {building.json(indent=2)} \n")

    # ToDo: create a `opening hours` property and add it to the building object
    #  in the context broker. Do not update the whole entity! In real
    #  scenarios it might have been modified by other users.
    opening_hours = NamedContextAttribute(
        name="openingHours",
        type="array",
        value=["Mo-Fr 10:00-19:00", "Sa closed", "Su closed"])
class Control:
    """PID controller with FIWARE interface"""
    def __init__(self):
        """Initialization"""
        # # use envirnment variables
        # os.environ["CONFIG_FILE"] = "False"
        # define parameters
        self.params = {}
        self.params['controller_name'] = os.getenv("CONTROLLER_NAME", 'PID_1')
        self.params['type'] = "PID_Controller"
        self.params['setpoint'] = float(os.getenv("SETPOINT", '293.15'))
        # reverse mode can be activated by passing negative tunings to controller
        self.params['Kp'] = float(os.getenv("KP", '1.0'))
        self.params['Ki'] = float(os.getenv("KI", '0'))
        self.params['Kd'] = float(os.getenv("KD", '0'))
        self.params['lim_low'] = float(os.getenv("LIM_LOW", '0'))
        self.params['lim_upper'] = float(os.getenv("LIM_UPPER", '100'))
        self.params['pause_time'] = float(os.getenv("PAUSE_TIME", 0.2))
        self.params['sensor_entity_name'] = os.getenv("SENSOR_ENTITY_NAME", '')
        self.params['sensor_type'] = os.getenv("SENSOR_TYPE", None)
        self.params['sensor_attr'] = os.getenv("SENSOR_ATTR", '')
        self.params['actuator_entity_name'] = os.getenv(
            "ACTUATOR_ENTITY_NAME", '')
        self.params['actuator_type'] = os.getenv("ACTUATOR_TYPE", '')
        self.params['actuator_command'] = os.getenv("ACTUATOR_COMMAND", '')
        self.params['actuator_command_value'] = self.params[
            'actuator_command'] + '_info'
        self.params['service'] = os.getenv("FIWARE_SERVICE", '')
        self.params['service_path'] = os.getenv("FIWARE_SERVICE_PATH", '')
        self.params['cb_url'] = os.getenv("CB_URL", "http://localhost:1026")

        # settings for security mode
        self.security_mode = os.getenv("SECURITY_MODE",
                                       'False').lower() in ('true', '1', 'yes')
        self.params['token'] = (None, None)
        # Get token from keycloak in security mode
        if self.security_mode:
            self.kp = KeycloakPython()
            self.params['token'] = self.kp.get_access_token()

        # Create simple pid instance
        self.pid = PID(self.params['Kp'],
                       self.params['Ki'],
                       self.params['Kd'],
                       setpoint=self.params['setpoint'],
                       output_limits=(self.params['lim_low'],
                                      self.params['lim_upper']))

        # Additional parameters
        self.auto_mode = True
        self.u = None  # control variable u
        self.y_act = self.params[
            'setpoint']  # set the initial measurement to set point

        # Create the fiware header
        fiware_header = FiwareHeader(service=self.params['service'],
                                     service_path=self.params['service_path'])

        # Create orion context broker client
        self.ORION_CB = ContextBrokerClient(url=self.params['cb_url'],
                                            fiware_header=fiware_header)

    def create_entity(self):
        """Creates entitiy of PID controller in orion context broker"""
        try:
            self.ORION_CB.get_entity(entity_id=self.params['controller_name'],
                                     entity_type=self.params['type'])
            print('Entity name already assigned')
        except requests.exceptions.HTTPError as err:
            msg = err.args[0]
            if "NOT FOUND" not in msg.upper():
                raise  # throw other errors except "entity not found"
            print('[INFO]: Create new PID entity')
            pid_entity = ContextEntity(id=f"{self.params['controller_name']}",
                                       type=self.params['type'])
            cb_attrs = []
            for attr in ['Kp', 'Ki', 'Kd', 'lim_low', 'lim_upper', 'setpoint']:
                cb_attrs.append(
                    NamedContextAttribute(name=attr,
                                          type="Number",
                                          value=self.params[attr]))
            pid_entity.add_attributes(attrs=cb_attrs)
            self.ORION_CB.post_entity(entity=pid_entity, update=True)

    def update_params(self):
        """Read PID parameters of entity in context broker and updates PID control parameters"""
        # read PID parameters from context broker
        for attr in ['Kp', 'Ki', 'Kd', 'lim_low', 'lim_upper', 'setpoint']:
            self.params[attr] = float(
                self.ORION_CB.get_attribute_value(
                    entity_id=self.params['controller_name'],
                    entity_type=self.params['type'],
                    attr_name=attr))
        # update PID parameters
        self.pid.tunings = (self.params['Kp'], self.params['Ki'],
                            self.params['Kd'])
        self.pid.output_limits = (self.params['lim_low'],
                                  self.params['lim_upper'])
        self.pid.setpoint = self.params['setpoint']
        # read measured values from CB
        try:
            # read the current actuator value u (synchronize the value with the actuator)
            self.u = self.ORION_CB.get_attribute_value(
                entity_id=self.params['actuator_entity_name'],
                entity_type=self.params['actuator_type'],
                attr_name=self.params['actuator_command_value'])
            if not isinstance(self.u, (int, float)):
                self.u = None

            # read the value of process variable y from sensor
            y = self.ORION_CB.get_attribute_value(
                entity_id=self.params['sensor_entity_name'],
                entity_type=self.params['sensor_type'],
                attr_name=self.params['sensor_attr'])
            # set 0 if empty
            if y == " ":
                y = '0'
            # convert to float
            self.y_act = float(y)
        except requests.exceptions.HTTPError as err:
            msg = err.args[0]
            if "NOT FOUND" not in msg.upper():
                raise
            self.auto_mode = False
            print("Controller connection fails")
        else:
            # If no errors are raised
            self.auto_mode = True

        # Update the actual actuator value to allow warm star after interruption
        self.pid.set_auto_mode(self.auto_mode, last_output=self.u)

    def run(self):
        """Calculation of PID output"""
        try:
            if self.auto_mode:  # if connection is good, auto_mode = True -> controller active
                # calculate PID output
                self.u = self.pid(self.y_act)
                # build command
                command = NamedCommand(name=self.params['actuator_command'],
                                       value=round(self.u, 3))
                self.ORION_CB.post_command(
                    entity_id=self.params['actuator_entity_name'],
                    entity_type=self.params['actuator_type'],
                    command=command)
        except requests.exceptions.HTTPError as err:
            msg = err.args[0]
            if "NOT FOUND" not in msg.upper():
                raise
            self.auto_mode = False
            print("Controller connection fails")

    def update_token(self):
        """
        Update the token if necessary. Write the latest token into the
        header of CB client.
        """
        token = self.kp.check_update_token_validity(
            input_token=self.params['token'], min_valid_time=60)
        if all(token):  # if a valid token is returned
            self.params['token'] = token
        # Update the header with token
        self.ORION_CB.headers.update(
            {"Authorization": f"Bearer {self.params['token'][0]}"})

    def control_loop(self):
        """The control loop"""
        try:
            while True:
                # Update token if run in security mode
                if self.security_mode:
                    self.update_token()
                else:
                    pass
                self.update_params()
                self.run()
                time.sleep(self.params['pause_time'])
        finally:
            print("control loop fails")
            os.abort()
Example #7
0
class TestContextBroker(unittest.TestCase):
    """
    Test class for ContextBrokerClient
    """

    def setUp(self) -> None:
        """
        Setup test data
        Returns:
            None
        """
        self.fiware_header = FiwareHeader(
            service=settings.FIWARE_SERVICE,
            service_path=settings.FIWARE_SERVICEPATH)
        clear_all(fiware_header=self.fiware_header,
                  cb_url=settings.CB_URL,
                  iota_url=settings.IOTA_JSON_URL)
        self.resources = {
            "entities_url": "/v2/entities",
            "types_url": "/v2/types",
            "subscriptions_url": "/v2/subscriptions",
            "registrations_url": "/v2/registrations"
        }
        self.attr = {'temperature': {'value': 20.0,
                                     'type': 'Number'}}
        self.entity = ContextEntity(id='MyId', type='MyType', **self.attr)


        self.client = ContextBrokerClient(
            url=settings.CB_URL,
            fiware_header=self.fiware_header)
        self.subscription = Subscription.parse_obj({
            "description": "One subscription to rule them all",
            "subject": {
                "entities": [
                    {
                        "idPattern": ".*",
                        "type": "Room"
                    }
                ],
                "condition": {
                    "attrs": [
                        "temperature"
                    ],
                    "expression": {
                        "q": "temperature>40"
                    }
                }
            },
            "notification": {
                "http": {
                    "url": "http://localhost:1234"
                },
                "attrs": [
                    "temperature",
                    "humidity"
                ]
            },
            "expires": datetime.now(),
            "throttling": 0
        })

    def test_management_endpoints(self):
        """
        Test management functions of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            self.assertIsNotNone(client.get_version())
            self.assertEqual(client.get_resources(), self.resources)

    def test_statistics(self):
        """
        Test statistics of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            self.assertIsNotNone(client.get_statistics())

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL,
                iota_url=settings.IOTA_JSON_URL)
    def test_pagination(self):
        """
        Test pagination of context broker client
        Test pagination. only works if enough entities are available
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            entities_a = [ContextEntity(id=str(i),
                                        type=f'filip:object:TypeA') for i in
                          range(0, 1000)]
            client.update(action_type=ActionType.APPEND, entities=entities_a)
            entities_b = [ContextEntity(id=str(i),
                                        type=f'filip:object:TypeB') for i in
                          range(1000, 2001)]
            client.update(action_type=ActionType.APPEND, entities=entities_b)
            self.assertLessEqual(len(client.get_entity_list(limit=1)), 1)
            self.assertLessEqual(len(client.get_entity_list(limit=999)), 999)
            self.assertLessEqual(len(client.get_entity_list(limit=1001)), 1001)
            self.assertLessEqual(len(client.get_entity_list(limit=2001)), 2001)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL)
    def test_entity_filtering(self):
        """
        Test filter operations of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            # test patterns
            with self.assertRaises(ValueError):
                client.get_entity_list(id_pattern='(&()?')
            with self.assertRaises(ValueError):
                client.get_entity_list(type_pattern='(&()?')
            entities_a = [ContextEntity(id=str(i),
                                        type=f'filip:object:TypeA') for i in
                          range(0, 5)]

            client.update(action_type=ActionType.APPEND, entities=entities_a)
            entities_b = [ContextEntity(id=str(i),
                                        type=f'filip:object:TypeB') for i in
                          range(6, 10)]

            client.update(action_type=ActionType.APPEND, entities=entities_b)

            entities_all = client.get_entity_list()
            entities_by_id_pattern = client.get_entity_list(
                id_pattern='.*[1-5]')
            self.assertLess(len(entities_by_id_pattern), len(entities_all))

            entities_by_type_pattern = client.get_entity_list(
                type_pattern=".*TypeA$")
            self.assertLess(len(entities_by_type_pattern), len(entities_all))

            qs = QueryString(qs=[('presentValue', '>', 0)])
            entities_by_query = client.get_entity_list(q=qs)
            self.assertLess(len(entities_by_query), len(entities_all))

            # test options
            for opt in list(AttrsFormat):
                entities_by_option = client.get_entity_list(response_format=opt)
                self.assertEqual(len(entities_by_option), len(entities_all))
                self.assertEqual(client.get_entity(
                    entity_id='0',
                    response_format=opt),
                    entities_by_option[0])
            with self.assertRaises(ValueError):
                client.get_entity_list(response_format='not in AttrFormat')

            client.update(action_type=ActionType.DELETE, entities=entities_a)

            client.update(action_type=ActionType.DELETE, entities=entities_b)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL)
    def test_entity_operations(self):
        """
        Test entity operations of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            client.post_entity(entity=self.entity, update=True)
            res_entity = client.get_entity(entity_id=self.entity.id)
            client.get_entity(entity_id=self.entity.id, attrs=['temperature'])
            self.assertEqual(client.get_entity_attributes(
                entity_id=self.entity.id), res_entity.get_properties(
                response_format='dict'))
            res_entity.temperature.value = 25
            client.update_entity(entity=res_entity)
            self.assertEqual(client.get_entity(entity_id=self.entity.id),
                             res_entity)
            res_entity.add_attributes({'pressure': ContextAttribute(
                type='Number', value=1050)})
            client.update_entity(entity=res_entity)
            self.assertEqual(client.get_entity(entity_id=self.entity.id),
                             res_entity)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL)
    def test_attribute_operations(self):
        """
        Test attribute operations of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            entity = self.entity
            attr_txt = NamedContextAttribute(name='attr_txt',
                                             type='Text',
                                             value="Test")
            attr_bool = NamedContextAttribute(name='attr_bool',
                                              type='Boolean',
                                              value=True)
            attr_float = NamedContextAttribute(name='attr_float',
                                               type='Number',
                                               value=round(random.random(), 5))
            attr_list = NamedContextAttribute(name='attr_list',
                                              type='StructuredValue',
                                              value=[1, 2, 3])
            attr_dict = NamedContextAttribute(name='attr_dict',
                                              type='StructuredValue',
                                              value={'key': 'value'})
            entity.add_attributes([attr_txt,
                                   attr_bool,
                                   attr_float,
                                   attr_list,
                                   attr_dict])

            self.assertIsNotNone(client.post_entity(entity=entity,
                                                    update=True))
            res_entity = client.get_entity(entity_id=entity.id)

            for attr in entity.get_properties():
                self.assertIn(attr, res_entity.get_properties())
                res_attr = client.get_attribute(entity_id=entity.id,
                                                attr_name=attr.name)

                self.assertEqual(type(res_attr.value), type(attr.value))
                self.assertEqual(res_attr.value, attr.value)
                value = client.get_attribute_value(entity_id=entity.id,
                                                   attr_name=attr.name)
                # unfortunately FIWARE returns an int for 20.0 although float
                # is expected
                if isinstance(value, int) and not isinstance(value, bool):
                    value = float(value)
                self.assertEqual(type(value), type(attr.value))
                self.assertEqual(value, attr.value)

            for attr_name, attr in entity.get_properties(
                    response_format='dict').items():

                client.update_entity_attribute(entity_id=entity.id,
                                               attr_name=attr_name,
                                               attr=attr)
                value = client.get_attribute_value(entity_id=entity.id,
                                                   attr_name=attr_name)
                # unfortunately FIWARE returns an int for 20.0 although float
                # is expected
                if isinstance(value, int) and not isinstance(value, bool):
                    value = float(value)
                self.assertEqual(type(value), type(attr.value))
                self.assertEqual(value, attr.value)

            new_value = 1337.0
            client.update_attribute_value(entity_id=entity.id,
                                          attr_name='temperature',
                                          value=new_value)
            attr_value = client.get_attribute_value(entity_id=entity.id,
                                                    attr_name='temperature')
            self.assertEqual(attr_value, new_value)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL)
    def test_type_operations(self):
        """
        Test type operations of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            self.assertIsNotNone(client.post_entity(entity=self.entity,
                                                    update=True))
            client.get_entity_types()
            client.get_entity_types(options='count')
            client.get_entity_types(options='values')
            client.get_entity_type(entity_type='MyType')
            client.delete_entity(entity_id=self.entity.id,
                                 entity_type=self.entity.type)

    @unittest.skip('Does currently not work in CI')
    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL)
    def test_subscriptions(self):
        """
        Test subscription operations of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            sub_id = client.post_subscription(subscription=self.subscription,
                                              skip_initial_notification=True)
            sub_res = client.get_subscription(subscription_id=sub_id)
            time.sleep(1)
            sub_update = sub_res.copy(
                update={'expires': datetime.now() + timedelta(days=1)})
            client.update_subscription(subscription=sub_update)
            sub_res_updated = client.get_subscription(subscription_id=sub_id)
            self.assertNotEqual(sub_res.expires, sub_res_updated.expires)
            self.assertEqual(sub_res.id, sub_res_updated.id)
            self.assertGreaterEqual(sub_res_updated.expires, sub_res.expires)

            # test duplicate prevention and update
            sub = self.subscription.copy()
            id1 = client.post_subscription(sub)
            sub_first_version = client.get_subscription(id1)
            sub.description = "This subscription shall not pass"

            id2 = client.post_subscription(sub, update=False)
            self.assertEqual(id1, id2)
            sub_second_version = client.get_subscription(id2)
            self.assertEqual(sub_first_version.description,
                             sub_second_version.description)

            id2 = client.post_subscription(sub, update=True)
            self.assertEqual(id1, id2)
            sub_second_version = client.get_subscription(id2)
            self.assertNotEqual(sub_first_version.description,
                                sub_second_version.description)

            # test that duplicate prevention does not prevent to much
            sub2 = self.subscription.copy()
            sub2.description = "Take this subscription to Fiware"
            sub2.subject.entities[0] = {
                "idPattern": ".*",
                "type": "Building"
            }
            id3 = client.post_subscription(sub2)
            self.assertNotEqual(id1, id3)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL,
                iota_url=settings.IOTA_JSON_URL)
    def test_subscription_set_status(self):
        """
        Test subscription operations of context broker client
        """
        sub = self.subscription.copy(
            update={'expires': datetime.now() + timedelta(days=1)})
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            sub_id = client.post_subscription(subscription=sub)
            sub_res = client.get_subscription(subscription_id=sub_id)
            self.assertEqual(sub_res.status, Status.ACTIVE)

            sub_inactive = sub_res.copy(update={'status': Status.INACTIVE})
            client.update_subscription(subscription=sub_inactive)
            sub_res_inactive = client.get_subscription(subscription_id=sub_id)
            self.assertEqual(sub_res_inactive.status, Status.INACTIVE)

            sub_active = sub_res_inactive.copy(update={'status': Status.ACTIVE})
            client.update_subscription(subscription=sub_active)
            sub_res_active = client.get_subscription(subscription_id=sub_id)
            self.assertEqual(sub_res_active.status, Status.ACTIVE)

            sub_expired = sub_res_active.copy(
                update={'expires': datetime.now() - timedelta(days=365)})
            client.update_subscription(subscription=sub_expired)
            sub_res_expired = client.get_subscription(subscription_id=sub_id)
            self.assertEqual(sub_res_expired.status, Status.EXPIRED)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL,
                iota_url=settings.IOTA_JSON_URL)
    def test_mqtt_subscriptions(self):
        mqtt_url = settings.MQTT_BROKER_URL
        mqtt_topic = ''.join([settings.FIWARE_SERVICE,
                              settings.FIWARE_SERVICEPATH])
        notification = self.subscription.notification.copy(
            update={'http': None, 'mqtt': Mqtt(url=mqtt_url,
                                               topic=mqtt_topic)})
        subscription = self.subscription.copy(
            update={'notification': notification,
                    'description': 'MQTT test subscription',
                    'expires': None})
        entity = ContextEntity(id='myID', type='Room', **self.attr)

        self.client.post_entity(entity=entity)
        sub_id = self.client.post_subscription(subscription)

        sub_message = None

        def on_connect(client, userdata, flags, reasonCode, properties=None):
            if reasonCode != 0:
                logger.error(f"Connection failed with error code: "
                             f"'{reasonCode}'")
                raise ConnectionError
            else:
                logger.info("Successfully, connected with result code " + str(
                    reasonCode))
            client.subscribe(mqtt_topic)

        def on_subscribe(client, userdata, mid, granted_qos, properties=None):
            logger.info("Successfully subscribed to with QoS: %s", granted_qos)

        def on_message(client, userdata, msg):
            logger.info(msg.topic + " " + str(msg.payload))
            nonlocal sub_message
            sub_message = Message.parse_raw(msg.payload)

        def on_disconnect(client, userdata, reasonCode, properties=None):
            logger.info("MQTT client disconnected with reasonCode "
                        + str(reasonCode))

        import paho.mqtt.client as mqtt
        mqtt_client = mqtt.Client(userdata=None,
                                  protocol=mqtt.MQTTv5,
                                  transport="tcp")
        # add our callbacks to the client
        mqtt_client.on_connect = on_connect
        mqtt_client.on_subscribe = on_subscribe
        mqtt_client.on_message = on_message
        mqtt_client.on_disconnect = on_disconnect

        # connect to the server
        mqtt_url = urlparse(mqtt_url)
        mqtt_client.connect(host=mqtt_url.hostname,
                            port=mqtt_url.port,
                            keepalive=60,
                            bind_address="",
                            bind_port=0,
                            clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY,
                            properties=None)

        # create a non-blocking thread for mqtt communication
        mqtt_client.loop_start()
        new_value = 50

        self.client.update_attribute_value(entity_id=entity.id,
                                           attr_name='temperature',
                                           value=new_value,
                                           entity_type=entity.type)
        time.sleep(5)

        # test if the subscriptions arrives and the content aligns with updates
        self.assertIsNotNone(sub_message)
        self.assertEqual(sub_id, sub_message.subscriptionId)
        self.assertEqual(new_value, sub_message.data[0].temperature.value)
        mqtt_client.loop_stop()
        mqtt_client.disconnect()
        time.sleep(1)

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL)
    def test_batch_operations(self):
        """
        Test batch operations of context broker client
        """
        with ContextBrokerClient(
                url=settings.CB_URL,
                fiware_header=self.fiware_header) as client:
            entities = [ContextEntity(id=str(i),
                                      type=f'filip:object:TypeA') for i in
                        range(0, 1000)]
            client.update(entities=entities, action_type=ActionType.APPEND)
            entities = [ContextEntity(id=str(i),
                                      type=f'filip:object:TypeB') for i in
                        range(0, 1000)]
            client.update(entities=entities, action_type=ActionType.APPEND)
            entity = EntityPattern(idPattern=".*", typePattern=".*TypeA$")
            query = Query.parse_obj(
                {"entities": [entity.dict(exclude_unset=True)]})
            self.assertEqual(1000,
                             len(client.query(query=query,
                                              response_format='keyValues')))

    @clean_test(fiware_service=settings.FIWARE_SERVICE,
                fiware_servicepath=settings.FIWARE_SERVICEPATH,
                cb_url=settings.CB_URL,
                iota_url=settings.IOTA_JSON_URL)
    def test_command_with_mqtt(self):
        """
        Test if a command can be send to a device in FIWARE

        To test this a virtual device is created and provisioned to FIWARE and
        a hosted MQTT Broker

        This test only works if the address of a running MQTT Broker is given in
        the environment ('MQTT_BROKER_URL')

        The main part of this test was taken out of the iot_mqtt_example, see
        there for a complete documentation
        """
        mqtt_broker_url = settings.MQTT_BROKER_URL

        device_attr1 = DeviceAttribute(name='temperature',
                                       object_id='t',
                                       type="Number",
                                       metadata={
                                           "unit":
                                               {"type": "Unit",
                                                "value": {
                                                    "name": {
                                                        "type": "Text",
                                                        "value": "degree "
                                                                 "Celsius"
                                                    }
                                                }}
                                       })

        # creating a static attribute that holds additional information
        static_device_attr = StaticDeviceAttribute(name='info',
                                                   type="Text",
                                                   value="Filip example for "
                                                         "virtual IoT device")
        # creating a command that the IoT device will liston to
        device_command = DeviceCommand(name='heater', type="Boolean")

        device = Device(device_id='MyDevice',
                        entity_name='MyDevice',
                        entity_type='Thing2',
                        protocol='IoTA-JSON',
                        transport='MQTT',
                        apikey=settings.FIWARE_SERVICEPATH.strip('/'),
                        attributes=[device_attr1],
                        static_attributes=[static_device_attr],
                        commands=[device_command])

        device_attr2 = DeviceAttribute(name='humidity',
                                       object_id='h',
                                       type="Number",
                                       metadata={
                                           "unitText":
                                               {"value": "percent",
                                                "type": "Text"}})

        device.add_attribute(attribute=device_attr2)

        # Send device configuration to FIWARE via the IoT-Agent. We use the
        # general ngsiv2 httpClient for this.
        service_group = ServiceGroup(
            service=self.fiware_header.service,
            subservice=self.fiware_header.service_path,
            apikey=settings.FIWARE_SERVICEPATH.strip('/'),
            resource='/iot/json')

        # create the Http client node that once sent the device cannot be posted
        # again and you need to use the update command
        config = HttpClientConfig(cb_url=settings.CB_URL,
                                  iota_url=settings.IOTA_JSON_URL)
        client = HttpClient(fiware_header=self.fiware_header, config=config)
        client.iota.post_group(service_group=service_group, update=True)
        client.iota.post_device(device=device, update=True)

        time.sleep(0.5)

        # check if the device is correctly configured. You will notice that
        # unfortunately the iot API does not return all the metadata. However,
        # it will still appear in the context-entity
        device = client.iota.get_device(device_id=device.device_id)

        # check if the data entity is created in the context broker
        entity = client.cb.get_entity(entity_id=device.device_id,
                                      entity_type=device.entity_type)

        # create a mqtt client that we use as representation of an IoT device
        # following the official documentation of Paho-MQTT.
        # https://www.eclipse.org/paho/index.php?page=clients/python/
        # docs/index.php

        # The callback for when the mqtt client receives a CONNACK response from
        # the server. All callbacks need to have this specific arguments,
        # Otherwise the client will not be able to execute them.
        def on_connect(client, userdata, flags, reasonCode, properties=None):
            client.subscribe(f"/{device.apikey}/{device.device_id}/cmd")

        # Callback when the command topic is succesfully subscribed
        def on_subscribe(client, userdata, mid, granted_qos, properties=None):
            pass

        # NOTE: We need to use the apikey of the service-group to send the
        # message to the platform
        def on_message(client, userdata, msg):
            data = json.loads(msg.payload)
            res = {k: v for k, v in data.items()}
            client.publish(topic=f"/json/{service_group.apikey}"
                                 f"/{device.device_id}/cmdexe",
                           payload=json.dumps(res))

        def on_disconnect(client, userdata, reasonCode, properties=None):
            pass

        mqtt_client = mqtt.Client(client_id="filip-test",
                                  userdata=None,
                                  protocol=mqtt.MQTTv5,
                                  transport="tcp")

        # add our callbacks to the client
        mqtt_client.on_connect = on_connect
        mqtt_client.on_subscribe = on_subscribe
        mqtt_client.on_message = on_message
        mqtt_client.on_disconnect = on_disconnect

        # extract the form the environment
        mqtt_broker_url = urlparse(mqtt_broker_url)

        mqtt_client.connect(host=mqtt_broker_url.hostname,
                            port=mqtt_broker_url.port,
                            keepalive=60,
                            bind_address="",
                            bind_port=0,
                            clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY,
                            properties=None)
        # create a non-blocking thread for mqtt communication
        mqtt_client.loop_start()

        for attr in device.attributes:
            mqtt_client.publish(
                topic=f"/json/{service_group.apikey}/{device.device_id}/attrs",
                payload=json.dumps({attr.object_id: random.randint(0, 9)}))

        time.sleep(5)
        entity = client.cb.get_entity(entity_id=device.device_id,
                                      entity_type=device.entity_type)

        # create and send a command via the context broker
        context_command = NamedCommand(name=device_command.name,
                                       value=False)
        client.cb.post_command(entity_id=entity.id,
                               entity_type=entity.type,
                               command=context_command)

        time.sleep(5)
        # check the entity the command attribute should now show OK
        entity = client.cb.get_entity(entity_id=device.device_id,
                                      entity_type=device.entity_type)

        # The main part of this test, for all this setup was done
        self.assertEqual("OK", entity.heater_status.value)

        # close the mqtt listening thread
        mqtt_client.loop_stop()
        # disconnect the mqtt device
        mqtt_client.disconnect()

    def test_patch_entity(self) -> None:
        """
        Test the methode: patch_entity
        Returns:
           None
        """

        # setup test-entity
        entity = ContextEntity(id="test_id1", type="test_type1")
        attr1 = NamedContextAttribute(name="attr1", value="1")
        attr1.metadata["m1"] = \
            NamedMetadata(name="meta1", type="metatype", value="2")
        attr2 = NamedContextAttribute(name="attr2", value="2")
        attr1.metadata["m2"] = \
            NamedMetadata(name="meta2", type="metatype", value="3")
        entity.add_attributes([attr1, attr2])

        # sub-Test1: Post new
        self.client.patch_entity(entity=entity)
        self.assertEqual(entity,
                         self.client.get_entity(entity_id=entity.id))
        self.tearDown()

        # sub-Test2: ID/type of old_entity changed
        self.client.post_entity(entity=entity)
        test_entity = ContextEntity(id="newID", type="newType")
        test_entity.add_attributes([attr1, attr2])
        self.client.patch_entity(test_entity, old_entity=entity)
        self.assertEqual(test_entity,
                         self.client.get_entity(entity_id=test_entity.id))
        self.assertRaises(RequestException, self.client.get_entity,
                          entity_id=entity.id)
        self.tearDown()

        # sub-Test3: a non valid old_entity is provided, entity exists
        self.client.post_entity(entity=entity)
        old_entity = ContextEntity(id="newID", type="newType")

        self.client.patch_entity(entity, old_entity=old_entity)
        self.assertEqual(entity, self.client.get_entity(entity_id=entity.id))
        self.tearDown()

        # sub-Test4: no old_entity provided, entity is new
        old_entity = ContextEntity(id="newID", type="newType")
        self.client.patch_entity(entity, old_entity=old_entity)
        self.assertEqual(entity, self.client.get_entity(entity_id=entity.id))
        self.tearDown()

        # sub-Test5: no old_entity provided, entity is new
        old_entity = ContextEntity(id="newID", type="newType")
        self.client.patch_entity(entity, old_entity=old_entity)
        self.assertEqual(entity, self.client.get_entity(entity_id=entity.id))
        self.tearDown()

        # sub-Test6: New attr, attr del, and attr changed. No Old_entity given
        self.client.post_entity(entity=entity)
        test_entity = ContextEntity(id="test_id1", type="test_type1")
        attr1_changed = NamedContextAttribute(name="attr1", value="2")
        attr1_changed.metadata["m4"] = \
            NamedMetadata(name="meta3", type="metatype5", value="4")
        attr3 = NamedContextAttribute(name="attr3", value="3")
        test_entity.add_attributes([attr1_changed, attr3])
        self.client.patch_entity(test_entity)

        self.assertEqual(test_entity,
                         self.client.get_entity(entity_id=entity.id))
        self.tearDown()

        # sub-Test7: Attr changes, concurrent changes in Fiware,
        #            old_entity given

        self.client.post_entity(entity=entity)

        concurrent_entity = ContextEntity(id="test_id1", type="test_type1")
        attr1_changed = copy.deepcopy(attr1)
        attr1_changed.metadata["m1"].value = "3"
        attr1_changed.value = "4"
        concurrent_entity.add_attributes([attr1_changed, attr2])
        self.client.patch_entity(concurrent_entity)

        user_entity = copy.deepcopy(entity)
        attr3 = NamedContextAttribute(name="attr3", value="3")
        user_entity.add_attributes([attr3])
        self.client.patch_entity(user_entity, old_entity=entity)

        result_entity = concurrent_entity
        result_entity.add_attributes([attr2, attr3])

        self.assertEqual(result_entity,
                         self.client.get_entity(entity_id=entity.id))
        self.tearDown()

    def tearDown(self) -> None:
        """
        Cleanup test server
        """
        self.client.close()
        clear_all(fiware_header=self.fiware_header,
                  cb_url=settings.CB_URL,
                  iota_url=settings.IOTA_JSON_URL)