def retrieve_asset_name(asset_id):
    """
    This method mirrors the last one in the sense that it receives an asset_id and returns the associated name, if any.
    :param asset_id: (str) A 32-byte hexadecimal string in the expected xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format. This element is configured in the database as the primary key, which means that this method should never receive multiple records.
    :raise utils.InputValidationException: If the the input argument fails the initial data type validation
    :raise mysql_utils.MySQLDatabaseException: If any errors occur when consulting the database.
    :return asset_name: (str) If an unique match was found, None otherwise
    """
    log = ambi_logger.get_logger(__name__)

    # Validate the input
    utils.validate_id(entity_id=asset_id)

    # Prepare the database access elements
    database_name = user_config.access_info['mysql_database']['database']
    table_name = proj_config.mysql_db_tables['tenant_assets']

    cnx = mysql_utils.connect_db(database_name=database_name)
    select_cursor = cnx.cursor(buffered=True)

    sql_select = """SELECT entityType, name FROM """ + str(
        table_name) + """ WHERE id = %s;"""
    select_cursor = mysql_utils.run_sql_statement(cursor=select_cursor,
                                                  sql_statement=sql_select,
                                                  data_tuple=(asset_id, ))

    # Analyse the execution
    if select_cursor.rowcount is 0:
        log.warning(
            "No records returned from {0}.{1} using asset_id = {2}".format(
                str(database_name), str(table_name), str(asset_id)))
        select_cursor.close()
        cnx.close()
        return None
    elif select_cursor.rowcount > 1:
        log.warning(
            "{0}.{1} returned {2} records using asset_id = {3}. Cannot continue..."
            .format(str(database_name), str(table_name),
                    str(select_cursor.rowcount), str(asset_id)))
        select_cursor.close()
        cnx.close()
        return None
    else:
        # A single return came back. Process it then
        record = select_cursor.fetchone()
        if record[0] != 'ASSET':
            error_msg = "The record returned from {0}.{1} using asset id = {2} has a wrong entityType. Got a {3}, expected an 'ASSET'".format(
                str(database_name), str(table_name),
                str(select_cursor.rowcount), str(record[0]))
            log.error(msg=error_msg)
            select_cursor.close()
            cnx.close()
            return None
        else:
            # All is good so far. Check if the name returned is indeed a str and return it if so
            asset_name = record[1]
            utils.validate_input_type(asset_name, str)
            select_cursor.close()
            cnx.close()
            return asset_name
Ejemplo n.º 2
0
def handleOneWayDeviceRPCRequests(deviceId, remote_method, param_dict=None):
    """POST method to place an one way RPC call to a device identified by the 'deviceId' parameter provided as argument. The RPC command needs to have a valid target in the device (client) side to have any effect. This command should be specified
    in the 'remote_method argument' and any expected arguments required for its execution should be provided in the 'param_dict' argument
    This method is equivalent to a 'fire-and-forget' routine: no replies are expected nor are going to be returned either from the client side (this service doesn't wait for a response msg in the respective topic. This means that this method
    should be use for ACTUATE only. To get any information from the device side use the two way version of this method since all device-side communications are to be initiated from "this" side (the ThingsBoard/application side) and thus a response
    is expected following a request
    @:param deviceId (str) - The typical 32-byte, dash separated hexadecimal string that uniquely identifies the device in the intended ThingsBoard installation
    @:param remote_method (str) - The name of the method that is defined client-side that is to be executed with this call
    @:param param_dict (dict) - If the remote method requires arguments to be executed, use this dictionary argument to provide them
    @:return http_status_code (int) - This method only returns the HTTP status code regarding the execution of the HTTP request and nothing more

    Usage example: Suppose that there's a device configured in the Thingsboard installation that abstracts a lighting switch that can be turned ON or OFF. The device (Raspberry Pi) that controls that switch has a continuously running method (via a
    while True for instance) that listens and reacts to messages received from the ThingsBoard server instance. One of these control methods is called "setLightSwitch" and expects a boolean action "value" passed in the message strucure (True or
    False) regarding what to do with the light switch (False = OFF, True = ON). The switch is currently OFF. To turn it ON using this method, use the following calling structure:

    handleOneWayDeviceRPCRequests(deviceId="3806ac00-5411-11ea-aa0c-135d877fb643", remote_method="setLightSwitch", param_dict={"value": True})

    The message that is going to be forward to the device as a RPC request is the following JSON structure:
    {
        "method": "setLightSwitch",
        "params":
        {
            "value": true
        }
    }

    The "method" - "params" JSON is somewhat of a format for RPC interactions, with "params" being a complex (multi-level) dictionary to allow for complex, multi-argument remote method executions
    """

    one_way_log = ambi_logger.get_logger(__name__)

    # Validate inputs
    utils.validate_input_type(deviceId, str)
    utils.validate_id(entity_id=deviceId)

    utils.validate_input_type(remote_method, str)

    if param_dict:
        utils.validate_input_type(param_dict, dict)

    # The rest is pretty much more of the same. This one gets the deviceId built in the calling URL
    service_endpoint = '/api/plugins/rpc/oneway/' + deviceId

    # Crete the data payload in the dictionary format
    data = {
        "method": str(remote_method),
        "params": param_dict
    }

    service_dict = utils.build_service_calling_info(mac.get_auth_token(user_type="tenant_admin"), service_endpoint=service_endpoint)

    # Done. Set things in motion then
    try:
        response = requests.post(url=service_dict["url"], headers=service_dict["headers"], data=json.dumps(data))
    except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout) as ce:
        error_msg = "Could not get a response from {0}...".format(str(service_dict['url']))
        one_way_log.error(error_msg)
        raise ce

    return response.status_code
Ejemplo n.º 3
0
def handleTwoWayDeviceRPCRequest(deviceId, remote_method, param_dict=None):
    """POST method to execute bidirectional RPC calls to a specific method and a specific device, all identified through the argument list passed to this method. If the method to execute requires arguments itself, use the param_dict argument to
    provide a dictionary with these arguments, in a key-value scheme (key is the name of the argument with the actual value passes as... value).
    This method implements a reply-response dynamic, as opposed to the previous 'fire-and-forget' approach. The server submits a RPC requests and then listens for a response with the same requestId in the response topic. The method blocks for a
    while until a valid response can be returned.
    The usage of this method is identical to the previous one.

    @:param deviceId (str) - The typical 32-byte, dash separated hexadecimal string that uniquely identifies the device in the intended ThingsBoard installation.
    @:param remote_method (str) - The name of the method that is defined client-side that is to be executed with this call.
    @:param param_dict (dict) - If the remote method requires arguments to be executed, use this dictionary argument to provide them.
    @:raise utils.InputValidationException - If any of the input fails initial validation
    @:raise utils.ServiceEndpointException - If the request was not properly executed
    @:return response (request.models.Response) - This method returns the object that comes back from the HTTP request using the 'requests' package. This object has loads of interesting information regarding the original request that created it.
    For this context, the most relevant fields are response.status_code (int), which has the HTTP response code of the last execution, and the response.text, which contains the response of the method that was called, if any. If the remote method
    doesn't return anything, this field comes back empty.
    """
    two_way_log = ambi_logger.get_logger(__name__)

    # Validate inputs
    utils.validate_input_type(deviceId, str)
    utils.validate_id(entity_id=deviceId)

    utils.validate_input_type(remote_method, str)

    if param_dict:
        utils.validate_input_type(param_dict, dict)

    # Set the endpoint
    service_endpoint = '/api/plugins/rpc/twoway/' + deviceId

    # Create the data payload as a dictionary
    data = {
        "method": str(remote_method),
        "params": param_dict
    }

    service_dict = utils.build_service_calling_info(mac.get_auth_token(user_type="tenant_admin"), service_endpoint=service_endpoint)

    # Send the request to the server. The response, if obtained, contains the response data
    try:
        response = requests.post(url=service_dict['url'], headers=service_dict['headers'], data=json.dumps(data))
    except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout) as ce:
        error_msg = "Could not get a response from {0}...".format(str(service_dict['url']))
        two_way_log.error(error_msg)
        raise ce

    # Watch out for HTTP 408. In this project, when that code is returned (server timeout), it normally means that communication with the remote device was not established properly, by whatever reason
    if response.status_code == 408:
        error_msg = "Received a HTTP 408 - Server timed out. Could not get a response from device with id = {0}...".format(str(deviceId))
        two_way_log.error(error_msg)
        raise utils.ServiceEndpointException
    elif response.status_code == 200 and response.text == "":
        warn_msg = "Received a HTTP 200 - OK - But no response was returned..."
        two_way_log.warning(warn_msg)
        # Send back the result as a string, because that's what this method returns
        return "200"
    else:
        return response.text
Ejemplo n.º 4
0
def get_model(fn):
    """
    Get model component
    ---
    parameters:
        - in: path
          name: fn
          required: true
          schema:
              type: string
          description: Model id and part extension
          example: BSESzzACOXGTedPLzNiNklHZjdJAxTGT.glb
    """

    id, ext = fn.split('.', 1)

    if not utils.validate_id(id[:-2]):
        abort(404)

    if ext not in {"xml", "svg", "glb"}:
        abort(404)

    path = utils.storage_file_for_id_multiple(id, ext)

    if not os.path.exists(path):
        abort(404)

    return send_file(path)
Ejemplo n.º 5
0
def get_progress(id):
    if not utils.validate_id(id):
        abort(404)
    session = database.Session()
    model = session.query(
        database.model).filter(database.model.code == id).all()[0]
    return jsonify({"progress": model.progress})
Ejemplo n.º 6
0
def get_viewer(id):
    if not utils.validate_id(id):
        abort(404)
    d = utils.storage_dir_for_id(id)
    
    ifc_files = [os.path.join(d, name) for name in os.listdir(d) if os.path.isfile(os.path.join(d, name)) and name.endswith('.ifc')]
    
    if len(ifc_files) == 0:
        abort(404)
    
    failedfn = os.path.join(utils.storage_dir_for_id(id), "failed")
    if os.path.exists(failedfn):
        return render_template('error.html', id=id)

    for ifc_fn in ifc_files:
        glbfn = ifc_fn.replace(".ifc", ".glb")
        if not os.path.exists(glbfn):
            abort(404)
            
    n_files = len(ifc_files) if "_" in ifc_files[0] else None
                    
    return render_template(
        'viewer.html',
        id=id,
        n_files=n_files,
        postfix=PIPELINE_POSTFIX
    )
Ejemplo n.º 7
0
def get_log(id, ext):
    log_entry_type = namedtuple('log_entry_type',
                                ("level", "message", "instance", "product"))

    if ext not in {'html', 'json'}:
        abort(404)

    if not utils.validate_id(id):
        abort(404)
    logfn = os.path.join(utils.storage_dir_for_id(id), "log.json")
    if not os.path.exists(logfn):
        abort(404)

    if ext == 'html':
        log = []
        for ln in open(logfn):
            l = ln.strip()
            if l:
                log.append(
                    json.loads(l,
                               object_hook=lambda d: log_entry_type(*(d.get(
                                   k, '') for k in log_entry_type._fields))))
        return render_template('log.html', id=id, log=log)
    else:
        return send_file(logfn, mimetype='text/plain')
Ejemplo n.º 8
0
def get_model(fn):
    """
    Get model component
    ---
    parameters:
        - in: path
          name: fn
          required: true
          schema:
              type: string
          description: Model id and part extension
          example: BSESzzACOXGTedPLzNiNklHZjdJAxTGT.glb
    """

    id, ext = fn.split('.', 1)
    if not utils.validate_id(id):
        abort(404)

    if ext not in {"xml", "svg", "glb"}:
        abort(404)

    path = utils.storage_file_for_id(id, ext)

    if not os.path.exists(path):
        abort(404)

    if os.path.exists(path + ".gz"):
        import mimetypes
        response = make_response(
            send_file(path + ".gz",
                      mimetype=mimetypes.guess_type(fn, strict=False)[0]))
        response.headers['Content-Encoding'] = 'gzip'
        return response
    else:
        return send_file(path)
Ejemplo n.º 9
0
def get_viewer(id):
    if not utils.validate_id(id):
        abort(404)

    failedfn = os.path.join(utils.storage_dir_for_id(id), "failed")
    if os.path.exists(failedfn):
        return render_template('error.html', id=id)

    glbfn = os.path.join(utils.storage_dir_for_id(id), id + ".glb")
    if not os.path.exists(glbfn):
        abort(404)

    return render_template('viewer.html', id=id, postfix=PIPELINE_POSTFIX)
Ejemplo n.º 10
0
def get_viewer(id):
    if not utils.validate_id(id):
        abort(404)
    d = utils.storage_dir_for_id(id)
    n_files = len([
        name for name in os.listdir(d) if os.path.isfile(os.path.join(d, name))
        and os.path.join(d, name).endswith('.ifc')
    ])

    for i in range(n_files):
        glbfn = os.path.join(utils.storage_dir_for_id(id),
                             id + "_" + str(i) + ".glb")
        if not os.path.exists(glbfn):
            abort(404)

    return render_template('viewer.html', **locals())
Ejemplo n.º 11
0
def check_viewer(id):
    if not utils.validate_id(id):
        abort(404)
    return render_template('progress.html', id=id)
def getAttributes(entityType=None, entityId=None, deviceName=None, keys=None):
    """
    GET method to retrieve all server-type attributes configured for the device identified by the pair
    entityType/entityId or deviceName provided. This method requires either the entityType/entityId pair or the
    deviceName to be provided to execute
    this method. If insufficient data is provided, the relevant Exception shall be raised.
    :param entityType (str) - The entity type of the object whose attributes are to be retrieved.
    :param entityId (str) - The id string that identifies the device whose attributes are to be retrieved.
    :param deviceName (str) - The name of the device that can be used to retrieve the entityType/entityId.
    :param keys (list of str) - Each attribute returned is a key-value pair.
    Use this argument to provide a key based filter, i.e., if this list is set,
    only attributes whose keys match any of the list elements are to be returned.
    :raise utils.InputValidationException - If any of the inputs has the wrong data type or the method doesn't have the necessary data to execute.
    :raise utils.ServiceEndpointException - If problem occur when accessing the remote API
    :return attribute_dictionary (dict) - A dictionary with the retrieved attributes in the following format:
        attribute_dictionary =
        {
            'attribute_1_key': 'attribute_1_value',
            'attribute_2_key': 'attribute_2_value',
            ...
            'attribute_N_key': 'attribute_N_value'
        }
        where the keys in the dictionary are the ontology-specific names (official names) and the respective values
        are the timeseries keys being measured by the device that map straight into those ontology names.
        If the device identified by the argument data does exist but doesn't have any attributes configured,
        this method returns None instead.
    """
    log = ambi_logger.get_logger(__name__)

    # Validate inputs
    if entityId:
        utils.validate_id(entity_id=entityId)

        # The entityId seems OK but its useless unless the entityType was also provided or, at least, the deviceName, the method cannot continue
        if not entityType and not deviceName:
            error_msg = "A valid entityId was provided but no entityType nor deviceName were added. Cannot execute this method until a valid entityType/entityId or a valid deviceName is provided!"
            log.error(error_msg)
            raise utils.InputValidationException(message=error_msg)

    if entityType:
        utils.validate_entity_type(entity_type=entityType)

        # Again, as before, the method can only move forward if a corresponding entityId or deviceName was also provided
        if not entityId and not deviceName:
            error_msg = "A valid entityType was provided but no corresponding entityId nor deviceName. Cannot continue until a valid entityType/entityId or a valid deviceName is provided!"
            log.error(error_msg)
            raise utils.InputValidationException(message=error_msg)

    if deviceName:
        utils.validate_input_type(deviceName, str)

    if keys:
        utils.validate_input_type(keys, list)
        for key in keys:
            utils.validate_input_type(key, str)

    # If the code got to this point, I either have a valid entityId/entityType pair or a deviceName. Check if only the deviceName was provided and retrieve the entityId/entityType from it
    if deviceName and (not entityType or not entityId):
        # Get the entityId and entityType from the deviceName provided
        device_data = mysql_device_controller.get_device_credentials(
            device_name=deviceName)

        # Check if the previous statement returned a non-empty (not None) result. If that is the case, either the device is not (yet) configured in the device table or the table needs to be updated
        if not device_data:
            error_msg = "Cannot retrieve a pair of entityId/entityType from the device name provided: {0}. Either:" \
                        "\n1. The device is not yet configured in the database/ThingsBoard platform" \
                        "\n2. The MySQL device table needs to be updated." \
                        "\nCannot continue for now".format(str(deviceName))
            log.error(error_msg)
            raise utils.InputValidationException(message=error_msg)

        # The previous method returns a 3-element tuple in the format (entityType, entityId, timeseriesKeys). Grab the relevant data straight from it
        entityType = device_data[0]
        entityId = device_data[1]

    # Validation complete. I have all I need to execute the remote call
    service_endpoint = "/api/plugins/telemetry/{0}/{1}/values/attributes".format(
        str(entityType), str(entityId))

    # If a list of keys was provided, concatenate them to the current endpoint
    if keys:
        service_endpoint += "?keys="

        # Add all the keys to the endpoint concatenated in a single, comma separated (without any spaces in between) string
        service_endpoint += ",".join(keys)

    # Build the service dictionary from the endpoint already built
    service_dict = utils.build_service_calling_info(
        mac.get_auth_token(user_type='tenant_admin'),
        service_endpoint=service_endpoint)

    # Query the remote API
    try:
        response = requests.get(url=service_dict['url'],
                                headers=service_dict['headers'])
    except (requests.exceptions.ConnectionError,
            requests.exceptions.ConnectTimeout) as ce:
        error_msg = "Could not get a response from {0}...".format(
            str(service_dict['url']))
        log.error(error_msg)
        raise utils.ServiceEndpointException(message=ce)

    # If a response was returned, check the HTTP return code
    if response.status_code != 200:
        error_msg = "Request not successful: Received an HTTP " + str(
            eval(response.text)['status']) + " with message: " + str(
                eval(response.text)['message'])
        log.error(error_msg)
        raise utils.ServiceEndpointException(message=error_msg)
    else:
        # Got a valid result. Format the returned objects for return
        data_to_return = eval(utils.translate_postgres_to_python(
            response.text))

        if len(data_to_return) is 0:
            # Nothing to return then. Send back a None instead
            return None

        # If the request was alright, I've received the following Response Body (after eval)
        # data_to_return =
        # [
        #   {
        #       "lastUpdateTs": int,
        #       "key": str,
        #       "value": str
        #   },
        # ...
        #   {
        #       "lastUpdateTs": int,
        #       "key": str,
        #       "value": str
        #   }
        # ]
        #
        # So I need to transform this into the return structure defined above

        attribute_dictionary = {}
        for attribute_pair in data_to_return:
            # Use this opportunity to filter out any attribute returned that is not part of the measurement list desired
            if attribute_pair['value'] not in proj_config.ontology_names:
                # If the attribute value is not one in the 'official list of names', skip it
                continue
            # Create the entries defined in the man entry of this method from the list elements returned from the remote API
            attribute_dictionary[
                attribute_pair['key']] = attribute_pair['value']

        # All done. Return the attributes dictionary
        return attribute_dictionary
def retrieve_asset_id(asset_name):
    """
    This method receives the name of an asset and infers the associated id from it by consulting the respective database table.
    :param asset_name: (str) The name of the asset to retrieve from ambiosensing_thingsboard.tb_tenant_assets. This method can only implement the limited searching capabilities provided by the MySQL database. If these are not enough to retrieve an
    unique record associated to the asset name provided, the method 'fails' (even though there might be a matching record in the database but with some different uppercase/lowercase combination than the one provided) and returns 'None' in that case.
    :raise utils.InputValidationException: If the input argument fails the data type validation.
    :raise mysql_utils.MySQLDatabaseException: If any errors occur when accessing the database.
    :return asset_id: (str) If a unique match was found to the provided asset_name. None otherwise.
    """
    log = ambi_logger.get_logger(__name__)

    # Validate the input
    utils.validate_input_type(asset_name, str)

    # Prepare the database access elements
    database_name = user_config.access_info['mysql_database']['database']
    table_name = proj_config.mysql_db_tables['tenant_assets']

    cnx = mysql_utils.connect_db(database_name=database_name)
    select_cursor = cnx.cursor(buffered=True)

    sql_select = """SELECT entityType, id FROM """ + str(
        table_name) + """ WHERE name = %s;"""

    select_cursor = mysql_utils.run_sql_statement(cursor=select_cursor,
                                                  sql_statement=sql_select,
                                                  data_tuple=(asset_name, ))

    # Analyse the execution
    if select_cursor.rowcount is 0:
        log.warning(
            "No records returned from {0}.{1} using asset_name = {2}".format(
                str(database_name), str(table_name), str(asset_name)))
        select_cursor.close()
        cnx.close()
        return None
    elif select_cursor.rowcount > 1:
        log.warning(
            "{0}.{1} returned {2} records for asset_name = {3}. Cannot continue..."
            .format(str(database_name), str(table_name),
                    str(select_cursor.rowcount), str(asset_name)))
        select_cursor.close()
        cnx.close()
        return None
    else:
        # Got a single result back. Check if the entityType matches the expected one
        record = select_cursor.fetchone()
        if record[0] != 'ASSET':
            error_msg = "The record returned from {0}.{1} using asset_name = {2} has a wrong entityType. Got a {3}, expected an 'ASSET'".format(
                str(database_name), str(table_name), str(asset_name),
                str(record[0]))
            log.error(msg=error_msg)
            select_cursor.close()
            cnx.close()
            return None
        else:
            # All is well. Return the retrieved id after validation
            asset_id = record[1]
            utils.validate_id(entity_id=asset_id)

            # If the parameter returned survived the last battery of validations, all seems OK. Return the result
            select_cursor.close()
            cnx.close()
            return asset_id
def get_asset_env_data(start_date,
                       end_date,
                       variable_list,
                       asset_name=None,
                       asset_id=None,
                       filter_nones=True):
    """
    Use this method to retrieve the environmental data between two dates specified by the pair base_date and time_interval, for each of the variables indicated in the variables list and for an asset identified by at least one of the elements in
    the pair asset_name/asset_id.
    :param start_date: (datetime.datetime) A datetime.datetime object denoting the beginning (the oldest date element) of the time window for data  retrieval
    :param end_date: (datetime.datetime) A datetime.datetime object denoting the end (the newest date element) of the time window for data retrieval.
    :param variable_list: (list of str) A list with the variable names (ontology names) that are to be retrieved from the respective database table. Each one of the elements in the list provided is going to be validated against the 'official'
    list
    in proj_config.ontology_names.
    :param asset_name: (str) The name of the asset entity from where the data is to be retrieved from. This method expects either this parameter or the respective id to be provided and does not execute unless at least one of them is present.
    :param asset_id: (str) The id string associated to an asset element in the database, i.e., the 32 byte hexadecimal string in the usual 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' format. This method expects either this element or the asset name to
    be provided before continuing. If none are present, the respective Exception is raised.
    :param filter_nones: (bool) Set this flag to True to exclude any None values from the final result dictionary. Otherwise the method returns all values, including NULL/None ones.
    :raise utils.InputValidationException: If any of the inputs fails the initial data type validation or if none of the asset identifiers (name or id) are provided.
    :raise mysql_utils.MySQLDatabaseException: If any errors occur during the database accesses.
    :return response (dict): This method returns a response dictionary in a format that is expected to be serialized and returned as a REST API response further on. For this method, the response dictionary has the following format:
        response =
            {
                env_variable_1: [
                    {
                        timestamp_1: <str>,
                        value_1: <str>
                    },
                    {
                        timestamp_2: <str>.
                        value_2: <str>
                    },
                    ...,
                    {
                        timestamp_N: <str>,
                        value_N: <str>
                    }
                ],
                env_variable_2: [
                    {
                        timestamp_1: <str>,
                        value_1: <str>
                    },
                    {
                        timestamp_2: <str>.
                        value_2: <str>
                    },
                    ...,
                    {
                        timestamp_N: <str>,
                        value_N: <str>
                    }
                ],
                ...
                env_variable_N: [
                    {
                        timestamp_1: <str>,
                        value_1: <str>
                    },
                    {
                        timestamp_2: <str>.
                        value_2: <str>
                    },
                    ...,
                    {
                        timestamp_N: <str>,
                        value_N: <str>
                    }
                ]
            }
    """
    log = ambi_logger.get_logger(__name__)

    # Validate inputs

    # Check if at least one element from the pair asset_name/asset_id was provided
    if not asset_name and not asset_id:
        error_msg = "Missing both asset name and asset id from the input parameters. Cannot continue until at least one of these is provided."
        log.error(error_msg)
        raise utils.InputValidationException(message=error_msg)

    if asset_name:
        utils.validate_input_type(asset_name, str)

    if asset_id:
        utils.validate_id(entity_id=asset_id)

    utils.validate_input_type(start_date, datetime.datetime)
    utils.validate_input_type(end_date, datetime.datetime)

    if start_date > datetime.datetime.now():
        error_msg = "The start date provided: {0} is invalid! Please provide a past date to continue...".format(
            str(start_date))
        log.error(error_msg)
        raise utils.InputValidationException(message=error_msg)

    if end_date > datetime.datetime.now():
        error_msg = "The end date provided: {0} is invalid! Please provide a past or current date to continue....".format(
            str(end_date))
        log.error(error_msg)
        raise utils.InputValidationException(message=error_msg)

    if start_date >= end_date:
        error_msg = "Invalid time window for data retrieval provided: {0} -> {1}. Cannot continue until a start_date < end_date is provided!".format(
            str(start_date), str(end_date))
        log.error(msg=error_msg)
        raise utils.InputValidationException(message=error_msg)

    utils.validate_input_type(variable_list, list)
    for i in range(0, len(variable_list)):
        # Check inf the element is indeed a str as expected
        utils.validate_input_type(variable_list[i], str)
        # Take the change to normalize it to all lowercase characters
        variable_list[i] = variable_list[i].lower()

        # And check if it is indeed a valid element
        if variable_list[i] not in proj_config.ontology_names:
            log.warning(
                "Attention: the environmental variable name provided: {0} is not among the ones supported:\n{1}\nRemoving it from the variable list..."
                .format(str(variable_list[i]),
                        str(proj_config.ontology_names)))
            variable_list.remove(variable_list[i])

    # Check if the last operation didn't emptied the whole environmental variable list
    if len(variable_list) == 0:
        error_msg = "The variable list is empty! Cannot continue until at least one valid environmental variable is provided"
        log.error(error_msg)
        raise utils.InputValidationException(message=error_msg)

    # The asset_id is one of the most important parameters in this case. Use the provided arguments to either obtain it or make sure the one provided is a valid one. If both parameters were provided (asset_id and asset_name)
    if asset_id:
        # If an asset id was provided, use it to obtain the associated name
        asset_name_db = retrieve_asset_name(asset_id=asset_id)

        # Check if any names were obtained above and, if so, check if it matches any asset name also provided
        if asset_name:
            if asset_name != asset_name_db:
                log.warning(
                    "The asset name obtained from {0}.{1}: {2} does not matches the one provided: {3}. Defaulting to {2}..."
                    .format(
                        str(user_config.access_info['mysql_database']
                            ['database']),
                        str(proj_config.mysql_db_tables['tenant_assets']),
                        str(asset_name_db), str(asset_name)))
                asset_name = asset_name_db

    if not asset_id and asset_name:
        # Another case: only the asset name was provided but no associated id. Use the respective method to retrieve the asset id from the name
        asset_id = retrieve_asset_id(asset_name=asset_name)

        # Check if a valid id was indeed returned (not None)
        if not asset_id:
            error_msg = "Invalid asset id returned from {0}.{1} using asset_name = {2}. Cannot continue...".format(
                str(user_config.access_info['mysql_database']['database']),
                str(proj_config.mysql_db_tables['tenant_assets']),
                str(asset_name))
            log.error(msg=error_msg)
            raise utils.InputValidationException(message=error_msg)

    utils.validate_input_type(filter_nones, bool)

    # Initial input validation cleared. Before moving any further, implement the database access objects and use them to retrieve a unique, single name/id pair for the asset in question
    database_name = user_config.access_info['mysql_database']['database']
    asset_device_table_name = proj_config.mysql_db_tables['asset_devices']
    device_data_table_name = proj_config.mysql_db_tables['device_data']

    cnx = mysql_utils.connect_db(database_name=database_name)
    select_cursor = cnx.cursor(buffered=True)

    # All ready for data retrieval. First, retrieve the device_id for every device associated to the given asset
    sql_select = """SELECT toId, toName FROM """ + str(
        asset_device_table_name
    ) + """ WHERE fromEntityType = %s AND fromId = %s AND toEntityType = %s;"""

    select_cursor = mysql_utils.run_sql_statement(cursor=select_cursor,
                                                  sql_statement=sql_select,
                                                  data_tuple=('ASSET',
                                                              asset_id,
                                                              'DEVICE'))

    # Analyse the execution results
    if select_cursor.rowcount is 0:
        error_msg = "Asset (asset_name = {0}, asset_id = {1}) has no devices associated to it! Cannot continue...".format(
            str(asset_name), str(asset_id))
        log.error(msg=error_msg)
        select_cursor.close()
        cnx.close()
        raise mysql_utils.MySQLDatabaseException(message=error_msg)
    else:
        log.info(
            "Asset (asset_name = {0}, asset_id = {1}) has {2} devices associated."
            .format(str(asset_name), str(asset_id),
                    str(select_cursor.rowcount)))

    # Extract the devices id's to a list for easier iteration later on
    record = select_cursor.fetchone()
    device_id_list = []

    while record:
        device_id_list.append(record[0])

        # Grab another one
        record = select_cursor.fetchone()

    # Prepare a mash up of all device_id retrieved so far separated by OR statements to replace the last element in the SQL SELECT statement to execute later on
    device_id_string = []

    for _ in device_id_list:
        device_id_string.append("deviceId = %s")

    # And now connect them all into a single string stitched together with 'OR's
    device_where_str = """ OR """.join(device_id_string)

    # Store the full results in this dictionary
    result_dict = {}

    # Prepare the SQL SELECT to retrieve data from
    for i in range(0, len(variable_list)):
        # And the partial results in this one
        sql_select = """SELECT timestamp, value FROM """ + str(
            device_data_table_name) + """ WHERE ontologyId = %s AND (""" + str(
                device_where_str
            ) + """) AND (timestamp >= %s AND timestamp <= %s);"""

        # Prepare the data tuple by joining together the current ontologyId with all the deviceIds retrieved from before
        data_tuple = tuple([variable_list[i]] + device_id_list + [start_date] +
                           [end_date])

        select_cursor = mysql_utils.run_sql_statement(cursor=select_cursor,
                                                      sql_statement=sql_select,
                                                      data_tuple=data_tuple)

        # Analyse the execution outcome
        if select_cursor.rowcount > 0:
            # Results came back for this particular ontologyId. For this method, the information of the device that made the measurement is irrelevant. Create a dictionary entry for the ontologyId parameter and populate the list of dictionaries
            # with the data retrieved
            result_dict[variable_list[i]] = []

            # Process the database records
            record = select_cursor.fetchone()

            while record:
                # Check if the filtering flag is set
                if filter_nones:
                    # And if so, check if the current record has a None as its value
                    if record[1] is None:
                        # If so, grab the next record and skip the rest of this cycle
                        record = select_cursor.fetchone()
                        continue

                result_dict[variable_list[i]].append({
                    "timestamp":
                    str(int(record[0].timestamp())),
                    "value":
                    str(record[1])
                })

                # Grab the next record in line
                record = select_cursor.fetchone()

    # All done it seems. Close down the database access elements and return the results so far
    select_cursor.close()
    cnx.close()
    return result_dict