AICore-Bridge

Bridge between Univia AICore framework and Wodan/Conan processor modules
import json
import os

from corebridge.timeseriesdataframe import test_data_dict_3_samples

Support functions

Pop NaN values


pop_nan_values


def pop_nan_values(
    data
):

Recursively pop keys with nan values from dict or lists with dicts. Use just before handing data to AICore for further processing since it explodes when encountering NaN values.

Args: data (Union[list, dict]): The data to be processed.

Returns: Union[list, dict]: The processed data with keys with nan values removed.

test_data_with_nan = test_data_dict_3_samples.copy() + [
   {
      "time":"2023-05-04T11:44:53.000Z",
      "value":np.nan
   }

]
print(json.dumps(test_data_with_nan, indent=3))
[
   {
      "time": "2023-05-04T10:04:49.000Z",
      "value": 16.72
   },
   {
      "time": "2023-05-04T10:24:51.000Z",
      "value": 16.65
   },
   {
      "time": "2023-05-04T10:44:53.000Z",
      "value": 16.55
   },
   {
      "time": "2023-05-04T11:44:53.000Z",
      "value": NaN
   }
]
print(json.dumps(pop_nan_values(test_data_with_nan), indent=3))
[
   {
      "time": "2023-05-04T10:04:49.000Z",
      "value": 16.72
   },
   {
      "time": "2023-05-04T10:24:51.000Z",
      "value": 16.65
   },
   {
      "time": "2023-05-04T10:44:53.000Z",
      "value": 16.55
   },
   {
      "time": "2023-05-04T11:44:53.000Z"
   }
]

Build historic args


build_historic_args


def build_historic_args(
    data:DataFrame, # The input time-series DataFrame.
    history:dict | list, # Historic data definition, each item in the list is a dictionary with 
a startDate key to set the start of a section of historic data in the 
result and a column-value pair for each of the columns.
)->dict: # Historic data in dictionary format where keys are column names and 
values are the historic values as numpy array.

Create a timeseries DataFrame from historic data defined in history.

test_data=set_time_index_zone(timeseries_dataframe_from_datadict(
   [
      {
         "time":"2023-05-04T10:04:49",
         "value":16.72
      },
      {
         "time":"2023-05-04T10:44:53",
         "value":16.55
      },
      {
         "time":"2023-05-04T10:24:51",
         "value":16.65
      }
   ], ['datetimeMeasure', 'time'], 'records'), 'UTC').sort_index()
test_data
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
history_arg = [
                dict(justANumber=1.0),
                dict(startDate="2023-05-04T10:25:00+00:00", justANumber=2.0)
            ]
build_historic_args(test_data,history_arg)
justANumber
time
2023-05-04 10:04:49+00:00 1.0
2023-05-04 10:24:51+00:00 1.0
2023-05-04 10:44:53+00:00 2.0
assert len(test_data) == len(build_historic_args(test_data,history_arg)['justANumber']), "build_historic_args failed to build historic data"

Class AICoreModuleBase

In the third iteration of the AICore module the initializer signature changes from

    def __init__(
        self, 
        save_dir:str, # path where the module can keep files 
        assets_dir:str, # path to support files (scripts, metadata, etc)
        *args, **kwargs
    ):

to the almost identical signature


    def __init__(
        self, 
        files_dir, 
        save_dir
    ):

Note how the order of the folder arguments is reversed. We can adapt by

  • treating original assets_dir as save_dir and vice versa OR
  • rewrite the method to match the new signature and change the signatures of the derived classes in each module

AICoreModuleBase


def AICoreModuleBase(
    files_dir, save_dir, kwargs:VAR_KEYWORD
):

Initialize self. See help(type(self)) for accurate signature.

Exported source
class AICoreModuleBase:

    def __init__(
        self, 
        files_dir, 
        save_dir,
        **kwargs
    ):
        
        self.init_time = datetime.datetime.now(datetime.UTC)
        self.aicorebridge_version = __version__

        self.init_args = []
        self.init_kwargs = dict(
            files_dir=files_dir,
            save_dir=save_dir,
            **kwargs
        )


        syslog.info(f"Init {self.__class__.__name__}, version {self.aicorebridge_version}, files directory {files_dir}, save dir {save_dir} on {platform.node()}")
save_dir = os.path.join(os.getcwd(), 'cache')
files_dir = os.path.join(os.getcwd(), 'cache')
test_module = AICoreModuleBase(files_dir, save_dir)

assert test_module.init_kwargs['save_dir'] == save_dir, f"init_kwargs['save_dir'] should be {save_dir}"
assert test_module.init_kwargs['files_dir'] == files_dir, f"init_kwargs['files_dir'] should be {files_dir}"
2026-01-29T14:38:35+0100 INFO   8596    __main__    2001759155.py   __init__    22  Init AICoreModuleBase, version 0.6.6, files directory c:\Users\fenke\repos\corebridge\nbs\cache, save dir c:\Users\fenke\repos\corebridge\nbs\cache on werkdoos
test_module.__dict__
{'init_time': datetime.datetime(2026, 1, 29, 13, 38, 35, 902376, tzinfo=datetime.timezone.utc),
 'aicorebridge_version': '0.6.6',
 'init_args': [],
 'init_kwargs': {'files_dir': 'c:\\Users\\fenke\\repos\\corebridge\\nbs\\cache',
  'save_dir': 'c:\\Users\\fenke\\repos\\corebridge\\nbs\\cache'}}

Class AICoreModule


AICoreModule


def AICoreModule(
    processor:Callable, # data processing function
    files_dir:str, # path where the module can keep files
    save_dir:str, kwargs:VAR_KEYWORD
):

Initialize self. See help(type(self)) for accurate signature.

Exported source
class AICoreModule(AICoreModuleBase):
    def __init__(self, 
        processor:typing.Callable, # data processing function
        files_dir:str,              # path where the module can keep files 
        save_dir:str,
        **kwargs
    ):
    
        super().__init__(files_dir, save_dir, **kwargs)
        self._init_processor(processor)

AICoreModule.call_processor


def call_processor(
    calldata, callargs:VAR_KEYWORD
):
Exported source
# TODO: Refactor into Processor classes to handle different funtion types

@patch
def _init_processor(
        self:AICoreModule, 
        processor:typing.Callable):
    """Initializes processor related variables on self"""
    
    self.processor = processor
    self.processor_signature = inspect.signature(self.processor)
    self.processor_params = dict(self.processor_signature.parameters)
    self.return_param = self.processor_params.pop('return', None)
    
    self.data_param, *self.call_params = list(self.processor_params.keys())

    if not (
        self.processor_params[self.data_param].annotation == pd.DataFrame
        or self.processor_params[self.data_param].annotation == np.ndarray

    ):

        self.data_param = None
        self.call_params = list(self.processor_params.keys())
Exported source
# can be overloaded
@patch
def call_processor(self:AICoreModule, calldata, **callargs):
    if self.data_param:
        return self.processor(calldata, **callargs)
    else:
        return self.processor(**callargs)

call

The new entry point from AICore with the following signature

def call(self, data, files)

This method, originally called by earlier versions of AICore, is responsible for processing the data and parameters request recieved by AICore. Infer takes a data parameter which contains the contents of the data key in the request body. Additionally an optional list of files that were send with the request - these are currently ignored - and finally the contents of the kwargs key in the request body.


AICoreModule.call


def call(
    data:dict, _:VAR_POSITIONAL, __:VAR_KEYWORD
):

Infer the data using the processor function.

Exported source
@patch
def call(self:AICoreModule, data:dict, *_, **__):
    """Infer the data using the processor function."""

    payload_data = data

    msg=[
        f"Startup time: {self.init_time.isoformat()}, node {platform.node()}",
        f"Call time: {datetime.datetime.now(datetime.UTC).isoformat()}",
        f"Python version: {sys.version}",
        f"Corebridge version: {self.aicorebridge_version}",
        f"CPU times: {', '.join([f'{k}: {v}' for k, v in psutil.cpu_times_percent()._asdict().items() ])}",
        f"Memory: {', '.join([f'{k}: {v}' for k, v in psutil.virtual_memory()._asdict().items() ])}",

    ]

    try:
        t00 = time.perf_counter_ns()

        kwargs = payload_data.get('kwargs', {})
        data = payload_data.get('data', {})

        msg+=[
            f"{self.processor.__name__}({self.processor_signature})",  
            f"Data: {type(data)} length: {len(data)}",    
            f"kwargs {list(kwargs.keys())}",  
            #f"init_args: {self.init_args}, init_kwargs: {self.init_kwargs}",
        ]

        # Pickup params, pop those that are not intended for the processor
        lastSeen = kwargs.pop('lastSeen', False)
        recordformat = kwargs.pop('format', "records").lower()
        timezone = kwargs.get('timezone', 'UTC')
        nested = kwargs.pop('nested', False)
        msg.append(f"lastSeen: {lastSeen}, timezone: {timezone}, recordformat: {recordformat}, nested: {nested}")

        samplerPeriod = kwargs.pop('samplerPeriod', self.init_kwargs.get('samplerPeriod','h'))
        samplerMethod = kwargs.pop('samplerMethod', self.init_kwargs.get('samplerMethod',None))
        reversed = kwargs.pop('reversed', False)

        calldata = self.get_call_data(
            data, 
            recordformat=recordformat,
            timezone=timezone,
            nested=nested,)
        
        history = build_historic_args(calldata, kwargs.pop('history', {}))
        callargs = self.get_callargs(kwargs, history)

        # for arg, val in callargs.items():
        #     msg.append(f"{arg}: {val}")
        
        t02 = time.perf_counter_ns()
        calculated_result = self.call_processor(
            calldata, 
            **callargs
        )
        t03 = time.perf_counter_ns()
        msg.append(f"Processing time: {(t03-t02)/1e6:.1f} ms")
        msg.append(f"Preparation time: {(t02-t00)/1e6:.1f} ms")

        if isinstance(calculated_result, dict):
            msg.append(f"return-data ictionary keys: {calculated_result.keys()}")
            return {
                'msg':msg,
                'data': [calculated_result]
            }
        elif isinstance(calculated_result, list):
            msg.append(f"return-data list length: {len(calculated_result)}")
            return {
                'msg':msg,
                'data': calculated_result
            }

        try:
            result = timeseries_dataframe(
                calculated_result, 
                timezone=timezone)
            
            msg.append(f"result shape: {result.shape}")

            if samplerMethod:
                msg.append(f"Sampler: {samplerMethod}, period: {samplerPeriod}")
                result = timeseries_dataframe_resample(result, samplerPeriod, samplerMethod)

            msg.append(f"return-data shape: {result.shape}")

            if reversed:
                result = result[::-1]

            return {
                'msg':msg,
                'data': pop_nan_values( timeseries_dataframe_to_datadict(
                    result if not lastSeen else result[-1:],
                    recordformat=recordformat,
                    timezone=timezone))
            }
        
        # tries dataframe return
        except Exception as err:
            msg.append(f"No timeseries data, error={err}")
        
        df = pd.DataFrame(calculated_result)
        df
        df.columns = [f"value_{str(c)}" if isinstance(c, int) else str(c) for c in list(df.columns)]
        df.reset_index().to_dict(orient='records')
        return {
            'msg':msg,
            'data': pop_nan_values( df.reset_index().to_dict(orient='records') )
        }

    
    # function try-catch
    except Exception as err:
        msg.append(''.join(traceback.format_exception(None, err, err.__traceback__)))
        return {
            'msg': msg,
            'data': []
        }

get_callargs

Exported source
# Specialized types for initializing annotated parameters
# Add types by adding a tuple with the type name and a builder function
annotated_arg_builders = {
    str(B[0]):B[1] for B in [
        (np.ndarray, lambda X: np.array(X, dtype=X.dtype))
    ]
}
annotated_arg_builders
{"<class 'numpy.ndarray'>": <function __main__.<lambda>(X)>}

AICoreModule.init_annotated_param


def init_annotated_param(
    param_name, value
):

Initialize argument for the processor call

param_name: name of the parameter to be initialized value: value of the parameter read from infer data to be used for initialization

Exported source
@patch
def init_annotated_param(self:AICoreModule, param_name, value):
    """
    Initialize argument for the processor call
    
    param_name: name of the parameter to be initialized
    value: value of the parameter read from infer data to be used for initialization
    
    """

    annotation = self.processor_signature.parameters[param_name].annotation
    #print(f"param_name: {param_name}, value: {value}, annotation: {annotation}")

    # try to convert value to one of the types in the builders of annotated_arg_builders
    for T in typing.get_args(annotation):
        try:
            builder = annotated_arg_builders.get(str(T), lambda X:T(X))
            return builder(value)
        
        except TypeError:
            continue

    try:
        return annotation(value)
    
    except TypeError as err:
        syslog.exception(f"Exception {str(err)} in fallback conversion to {annotation} of {type(value)}")

AICoreModule.get_callargs


def get_callargs(
    kwargs, history
):

Get arguments for the processor call

Exported source
@patch
def get_callargs(self:AICoreModule, kwargs, history):
    "Get arguments for the processor call"

    # Remove null / None values
    kwargs = {k:v for k,v in kwargs.items() if v is not None}
    
    call_args = {
        K:self.init_annotated_param(
            K,
            history.get(
                K,
                kwargs.get(
                    K,
                    self.init_kwargs.get(
                        K, 
                        history.get(
                            snake_case_to_camel_case(K),
                            kwargs.get(
                                snake_case_to_camel_case(K),
                                self.init_kwargs.get(
                                    snake_case_to_camel_case(K), 
                                    self.processor_signature.parameters[K].default
                                )
                            )
                        )
                    )
                )
            )
        )
        for K in self.call_params
    }

    return call_args
def processor_function(data:pd.DataFrame, just_a_number:float|np.ndarray):
    return just_a_number * data

test_module = AICoreModule(processor_function, os.path.join(os.getcwd(), 'cache'), os.path.join(os.getcwd(), 'cache'))
assert 'just_a_number' in test_module.get_callargs(
    {
        'justANumber': 2
    },
    {}
   
), "get_callargs failed to translate camel-case processor argument to snake-case kwargs argument"
2026-01-29T14:38:35+0100 INFO   8596    __main__    2001759155.py   __init__    22  Init AICoreModule, version 0.6.6, files directory c:\Users\fenke\repos\corebridge\nbs\cache, save dir c:\Users\fenke\repos\corebridge\nbs\cache on werkdoos

get_call_data


AICoreModule.get_call_data


def get_call_data(
    data:dict | list, recordformat:str='records', timezone:str='UTC', nested:bool=False
):

Convert data to the processor signature

Exported source
@patch
def get_call_data(
        self:AICoreModule, 
        data:dict|list, 
        recordformat='records', 
        timezone='UTC',
        nested=False):
    
    "Convert data to the processor signature"
    
    if not self.data_param:
        return None
    
    #print(f"recordformat: {recordformat}, timezone: {timezone}, nested: {nested}" )

    df = set_time_index_zone(timeseries_dataframe_from_datadict(
        data, ['datetimeMeasure', 'time'], recordformat=recordformat, nested=nested), timezone)

    df.sort_index(inplace=True)

    if self.processor_params[self.data_param].annotation == pd.DataFrame:
        return df
    elif len(df.columns) > 1:
        df.index = (df.index - datetime.datetime(1970,1,1, tzinfo=datetime.timezone.utc)) / datetime.timedelta(seconds=1)
        return df.to_records(index=True)
    else:
        df.index = (df.index - datetime.datetime(1970,1,1, tzinfo=datetime.timezone.utc)) / datetime.timedelta(seconds=1)
        return df.reset_index().to_numpy()
test_data
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
timeseries_dataframe_to_datadict(test_data)
[{'time': '2023-05-04T10:04:49Z', 'value': 16.72},
 {'time': '2023-05-04T10:24:51Z', 'value': 16.65},
 {'time': '2023-05-04T10:44:53Z', 'value': 16.55}]
calldata = test_module.get_call_data(timeseries_dataframe_to_datadict(test_data))
calldata
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
history = build_historic_args(calldata,history_arg)
history
justANumber
time
2023-05-04 10:04:49+00:00 2.0
2023-05-04 10:24:51+00:00 2.0
2023-05-04 10:44:53+00:00 2.0
calldata
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
print(test_module.get_callargs(calldata, history))
{'just_a_number': array([2., 2., 2.])}
np.array(history['justANumber'])
array([2., 2., 2.])
history
justANumber
time
2023-05-04 10:04:49+00:00 2.0
2023-05-04 10:24:51+00:00 2.0
2023-05-04 10:44:53+00:00 2.0
test_module.init_annotated_param(
    'just_a_number',
    12.34
)
12.34
test_module.processor_signature.parameters['just_a_number'].annotation
float | numpy.ndarray
np.array(history['justANumber'])
array([2., 2., 2.])
annotated_arg_builders[str(np.ndarray)](history['justANumber'])
array([2., 2., 2.])
assert True, 'stop'

Dynamic Module Class

Until now every time Univia changed their AICore framework, we had to modify the AICoreModule class for the module. By letting a class factory handle the class creation - class, not object creation - we can avoid ging through evey module and changing it, however little work this appears to be.


create_basic_module_class


def create_basic_module_class(
    processing_function, class_name:str='Module'
):

Tests

import os, pandas as pd, numpy as np
def test_function(data:pd.DataFrame, anumber:float|np.ndarray=0):
    return data * anumber
def test_simple_function(anumber:float, another:float):
    return [another * anumber]
Module = create_basic_module_class(test_function)
# class TestAICoreModule(AICoreModule):
#     def __init__(self, files_dir, save_dir):
#         super().__init__(test_function, files_dir, save_dir)
2026-01-29T14:38:36+0100 INFO   8596    __main__    3690511148.py   create_basic_module_class   4   create_basic_module_class(test_function, Module)
SimpleAICoreModule = create_basic_module_class(test_simple_function, "SimpleAICoreModule")

# class SimpleAICoreModule(AICoreModule):
#     def __init__(self, files_dir, save_dir):
#         super().__init__(test_simple_function, files_dir, save_dir)
2026-01-29T14:38:36+0100 INFO   8596    __main__    3690511148.py   create_basic_module_class   4   create_basic_module_class(test_simple_function, SimpleAICoreModule)
save_dir = os.path.join(os.getcwd(), 'cache')
files_dir = os.path.join(os.getcwd(), 'cache')

test_module = Module(files_dir, save_dir)

assert test_module.init_kwargs['save_dir'] == save_dir, f"init_kwargs['save_dir'] should be {save_dir}"
assert test_module.init_kwargs['files_dir'] == files_dir, f"init_kwargs['files_dir'] should be {files_dir}"
2026-01-29T14:38:36+0100 INFO   8596    __main__    2001759155.py   __init__    22  Init Module, version 0.6.6, files directory c:\Users\fenke\repos\corebridge\nbs\cache, save dir c:\Users\fenke\repos\corebridge\nbs\cache on werkdoos
test_data = [
    dict(datetimeMeasure='2020-04-01T00:01:11.123Z', value=1.1),
    dict(datetimeMeasure='2020-04-02T00:20:00Z', value=2.3),
]
result = test_module.call(dict(data=test_data, kwargs=dict(timezone='Europe/Amsterdam', anumber=2)))

print("Test Data\n", json.dumps(test_data, indent=2))
print("Result Message\n", json.dumps(result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(result['data'], indent=2, cls=NumpyEncoder))
Test Data
 [
  {
    "datetimeMeasure": "2020-04-01T00:01:11.123Z",
    "value": 1.1
  },
  {
    "datetimeMeasure": "2020-04-02T00:20:00Z",
    "value": 2.3
  }
]
Result Message
 [
  "Startup time: 2026-01-29T13:38:36.210166+00:00, node werkdoos",
  "Call time: 2026-01-29T13:38:36.225219+00:00",
  "Python version: 3.12.7 (tags/v3.12.7:0b05ead, Oct  1 2024, 03:06:41) [MSC v.1941 64 bit (AMD64)]",
  "Corebridge version: 0.6.6",
  "CPU times: user: 6.0, system: 3.5, idle: 90.0, interrupt: 0.3, dpc: 0.2",
  "Memory: total: 68425105408, available: 54549725184, percent: 20.3, used: 13875380224, free: 54549725184",
  "test_function((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 2",
  "kwargs ['timezone', 'anumber']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.2 ms",
  "Preparation time: 110.8 ms",
  "result shape: (2, 1)",
  "return-data shape: (2, 1)"
]
Result Data
 [
  {
    "time": "2020-04-01T02:01:11.123+02:00",
    "value": 2.2
  },
  {
    "time": "2020-04-02T02:20:00.000+02:00",
    "value": 4.6
  }
]
test_module.processor_signature.parameters['data'].annotation
pandas.core.frame.DataFrame
annotation = test_module.processor_signature.parameters['anumber'].annotation
print(typing.get_args(annotation))
(<class 'float'>, <class 'numpy.ndarray'>)
for T in typing.get_args(annotation):
    print(T(0))
0.0
[]

Simple module

simple_module = SimpleAICoreModule(files_dir,save_dir)

assert simple_module.init_kwargs['save_dir'] == save_dir
2026-01-29T14:38:36+0100 INFO   8596    __main__    2001759155.py   __init__    22  Init SimpleAICoreModule, version 0.6.6, files directory c:\Users\fenke\repos\corebridge\nbs\cache, save dir c:\Users\fenke\repos\corebridge\nbs\cache on werkdoos
not simple_module.data_param
True
simple_module.call_params
['anumber', 'another']
result = simple_module.call(dict(data=[], kwargs=dict(timezone='Europe/Amsterdam', anumber=2, another=11))) #dict(data=[], kwargs=dict(timezone='Europe/Amsterdam', anumber=2, another=11)
print("Result Message\n", json.dumps(result['msg'], indent=2))
print("Result Data\n", json.dumps(result['data'], indent=2))
Result Message
 [
  "Startup time: 2026-01-29T13:38:36.402453+00:00, node werkdoos",
  "Call time: 2026-01-29T13:38:36.435320+00:00",
  "Python version: 3.12.7 (tags/v3.12.7:0b05ead, Oct  1 2024, 03:06:41) [MSC v.1941 64 bit (AMD64)]",
  "Corebridge version: 0.6.6",
  "CPU times: user: 18.4, system: 10.1, idle: 70.3, interrupt: 0.6, dpc: 0.6",
  "Memory: total: 68425105408, available: 54538469376, percent: 20.3, used: 13886636032, free: 54538469376",
  "test_simple_function((anumber: float, another: float))",
  "Data: <class 'list'> length: 0",
  "kwargs ['timezone', 'anumber', 'another']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.0 ms",
  "Preparation time: 0.1 ms",
  "return-data list length: 1"
]
Result Data
 [
  22.0
]

Tests with library module

from corebridge.aicorebridge import AICoreModule, create_basic_module_class
2026-01-29T13:48:09+0000 INFO   2393    corebridge.aicorebridge core.py init_console_logging    53  Installed <StreamHandler (INFO)> for corebridge.aicorebridge
2026-01-29T13:48:09+0000 INFO   2393    corebridge.aicorebridge aicorebridge.py <module>    35  Loading corebridge.aicorebridge 0.6.6 from /home/runner/work/corebridge/corebridge/corebridge/aicorebridge.py
TestAICoreModule = create_basic_module_class(test_function, "CreateTestAICoreModule")
# class TestAICoreModule(AICoreModule):
#     def __init__(self, files_dir, save_dir):
#         super().__init__(test_function, files_dir, save_dir)
        
test_module = TestAICoreModule(files_dir, save_dir)

assert test_module.init_kwargs['save_dir'] == save_dir
assert test_module.init_kwargs['files_dir'] == files_dir
2026-01-29T14:38:36+0100 INFO   8596    corebridge.aicorebridge aicorebridge.py create_basic_module_class   421 create_basic_module_class(test_function, CreateTestAICoreModule)
2026-01-29T14:38:36+0100 INFO   8596    corebridge.aicorebridge aicorebridge.py __init__    135 Init CreateTestAICoreModule, version 0.6.6, files directory c:\Users\fenke\repos\corebridge\nbs\cache, save dir c:\Users\fenke\repos\corebridge\nbs\cache on werkdoos
test_data = [
    dict(datetimeMeasure='2020-04-01T00:01:11.123Z', value=1.1),
    dict(datetimeMeasure='2020-04-02T00:20:00Z', value=2.3),
]
result = test_module.call(dict(data=test_data, kwargs=dict(timezone='Europe/Amsterdam', anumber=2)))

print("Test Data\n", json.dumps(test_data, indent=2))
print("Result Message\n", json.dumps(result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(result['data'], indent=2, cls=NumpyEncoder))
Test Data
 [
  {
    "datetimeMeasure": "2020-04-01T00:01:11.123Z",
    "value": 1.1
  },
  {
    "datetimeMeasure": "2020-04-02T00:20:00Z",
    "value": 2.3
  }
]
Result Message
 [
  "Startup time: 2026-01-29T13:38:36.467584+00:00, node werkdoos",
  "Call time: 2026-01-29T13:38:36.478068+00:00",
  "Corebridge version: 0.6.6",
  "CPU times: user: 10.9, system: 9.4, idle: 35.9, interrupt: 1.6, dpc: 0.0",
  "Memory: total: 68425105408, available: 54537240576, percent: 20.3, used: 13887864832, free: 54537240576",
  "test_function((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 2",
  "kwargs ['timezone', 'anumber']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.1 ms",
  "Preparation time: 1.7 ms",
  "result shape: (2, 1)",
  "return-data shape: (2, 1)"
]
Result Data
 [
  {
    "time": "2020-04-01T02:01:11.123+02:00",
    "value": 2.2
  },
  {
    "time": "2020-04-02T02:20:00.000+02:00",
    "value": 4.6
  }
]
print("Test Data\n", json.dumps(test_data, indent=2))
print("Result Message\n", json.dumps(result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(result['data'], indent=2, cls=NumpyEncoder))
Test Data
 [
  {
    "datetimeMeasure": "2020-04-01T00:01:11.123Z",
    "value": 1.1
  },
  {
    "datetimeMeasure": "2020-04-02T00:20:00Z",
    "value": 2.3
  }
]
Result Message
 [
  "Startup time: 2026-01-29T13:38:36.467584+00:00, node werkdoos",
  "Call time: 2026-01-29T13:38:36.478068+00:00",
  "Corebridge version: 0.6.6",
  "CPU times: user: 10.9, system: 9.4, idle: 35.9, interrupt: 1.6, dpc: 0.0",
  "Memory: total: 68425105408, available: 54537240576, percent: 20.3, used: 13887864832, free: 54537240576",
  "test_function((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 2",
  "kwargs ['timezone', 'anumber']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.1 ms",
  "Preparation time: 1.7 ms",
  "result shape: (2, 1)",
  "return-data shape: (2, 1)"
]
Result Data
 [
  {
    "time": "2020-04-01T02:01:11.123+02:00",
    "value": 2.2
  },
  {
    "time": "2020-04-02T02:20:00.000+02:00",
    "value": 4.6
  }
]

Various experiments

import json
test_nested_data = json.loads("""
[
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:46:33.313Z",
        "datetimeSource": "2025-07-21T09:46:33.313Z",
        "datetimeAcquisition": "2025-07-21T09:46:33.691Z",
        "connector": "lora.kpn",
        "value": "0d01b4613d2ab820ec21e42a912a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": 0,
                "spreadingFactor": 11,
                "frequency": 868.1,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": 0,
                        "location": {
                            "latitude": 51.556896,
                            "longitude": 5.865362
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1319,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:36:33.332Z",
        "datetimeSource": "2025-07-21T09:36:33.332Z",
        "datetimeAcquisition": "2025-07-21T09:36:33.697Z",
        "connector": "lora.kpn",
        "value": "0d0174603d2a9e20bf21dc2a802a",
        "metadata": {
            "connection": {
                "rssi": -104,
                "snr": 5,
                "spreadingFactor": 11,
                "frequency": 867.3,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -104,
                        "snr": 5,
                        "location": {
                            "latitude": 51.5569,
                            "longitude": 5.865385
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1318,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:26:33.350Z",
        "datetimeSource": "2025-07-21T09:26:33.350Z",
        "datetimeAcquisition": "2025-07-21T09:26:33.705Z",
        "connector": "lora.kpn",
        "value": "0d01b45e3d2aa120ad21d42a5d2a",
        "metadata": {
            "connection": {
                "rssi": -105,
                "snr": 9,
                "spreadingFactor": 11,
                "frequency": 868.5,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -105,
                        "snr": 9,
                        "location": {
                            "latitude": 51.556892,
                            "longitude": 5.865356
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1317,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:16:33.368Z",
        "datetimeSource": "2025-07-21T09:16:33.368Z",
        "datetimeAcquisition": "2025-07-21T09:16:33.734Z",
        "connector": "lora.kpn",
        "value": "0d01745d3d2ac320a621d12a5b2a",
        "metadata": {
            "connection": {
                "rssi": -109,
                "snr": 2,
                "spreadingFactor": 11,
                "frequency": 866.6,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -109,
                        "snr": 2,
                        "location": {
                            "latitude": 51.556892,
                            "longitude": 5.865359
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1316,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:06:33.386Z",
        "datetimeSource": "2025-07-21T09:06:33.386Z",
        "datetimeAcquisition": "2025-07-21T09:06:33.748Z",
        "connector": "lora.kpn",
        "value": "0d01345c3d2aa920b821d02a612a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": -2,
                "spreadingFactor": 11,
                "frequency": 868.5,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": -2,
                        "location": {
                            "latitude": 51.556858,
                            "longitude": 5.865352
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1315,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:56:33.405Z",
        "datetimeSource": "2025-07-21T08:56:33.405Z",
        "datetimeAcquisition": "2025-07-21T08:56:33.754Z",
        "connector": "lora.kpn",
        "value": "0d01f45a3d2aa020c821d22a7d2a",
        "metadata": {
            "connection": {
                "rssi": -115,
                "snr": -6.25,
                "spreadingFactor": 11,
                "frequency": 866.1,
                "gateways": [
                    {
                        "id": "FF010323",
                        "rssi": -115,
                        "snr": -6.25,
                        "location": {
                            "latitude": 51.516491,
                            "longitude": 5.884403
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1314,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:46:33.423Z",
        "datetimeSource": "2025-07-21T08:46:33.423Z",
        "datetimeAcquisition": "2025-07-21T08:46:33.778Z",
        "connector": "lora.kpn",
        "value": "0d01b4593d2ab920d121cf2a922a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": 8,
                "spreadingFactor": 11,
                "frequency": 868.5,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": 8,
                        "location": {
                            "latitude": 51.556862,
                            "longitude": 5.865373
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1313,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:36:33.441Z",
        "datetimeSource": "2025-07-21T08:36:33.441Z",
        "datetimeAcquisition": "2025-07-21T08:36:33.821Z",
        "connector": "lora.kpn",
        "value": "0d0174583d2ac020e221cb2ada2a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": 2,
                "spreadingFactor": 11,
                "frequency": 866.4,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": 2,
                        "location": {
                            "latitude": 51.55687,
                            "longitude": 5.86535
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1312,
                "counterDown": 26,
                "errorRate": 6
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:26:33.460Z",
        "datetimeSource": "2025-07-21T08:26:33.460Z",
        "datetimeAcquisition": "2025-07-21T08:26:33.810Z",
        "connector": "lora.kpn",
        "value": "0d01b4563d2aa220db21c72aba2a",
        "metadata": {
            "connection": {
                "rssi": -103,
                "snr": -2,
                "spreadingFactor": 11,
                "frequency": 868.1,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -103,
                        "snr": -2,
                        "location": {
                            "latitude": 51.556854,
                            "longitude": 5.865371
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1311,
                "counterDown": 26,
                "errorRate": 6
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:16:33.560Z",
        "datetimeSource": "2025-07-21T08:16:33.560Z",
        "datetimeAcquisition": "2025-07-21T08:16:33.962Z",
        "connector": "lora.kpn",
        "value": "0d0174553d2ac220d821c52ad92a",
        "metadata": {
            "connection": {
                "rssi": -104,
                "snr": -7,
                "spreadingFactor": 11,
                "frequency": 865.1,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -104,
                        "snr": -7,
                        "location": {
                            "latitude": 51.556866,
                            "longitude": 5.865384
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1310,
                "counterDown": 26,
                "errorRate": 6
            }
        }
    }
]


  """)
import pandas as pd
from corebridge.timeseriesdataframe import timeseries_dataframe_from_datadict
df_normalized = pd.json_normalize(
    test_nested_data, 
    sep='.',
    #record_prefix='metadata.'
)
df_normalized.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10 entries, 0 to 9
Data columns (total 15 columns):
 #   Column                               Non-Null Count  Dtype  
---  ------                               --------------  -----  
 0   deviceId                             10 non-null     object 
 1   datetimeMeasure                      10 non-null     object 
 2   datetimeSource                       10 non-null     object 
 3   datetimeAcquisition                  10 non-null     object 
 4   connector                            10 non-null     object 
 5   value                                10 non-null     object 
 6   metadata.connection.rssi             10 non-null     int64  
 7   metadata.connection.snr              10 non-null     float64
 8   metadata.connection.spreadingFactor  10 non-null     int64  
 9   metadata.connection.frequency        10 non-null     float64
 10  metadata.connection.gateways         10 non-null     object 
 11  metadata.frame.port                  10 non-null     int64  
 12  metadata.frame.counterUp             10 non-null     int64  
 13  metadata.frame.counterDown           10 non-null     int64  
 14  metadata.frame.errorRate             10 non-null     int64  
dtypes: float64(2), int64(6), object(7)
memory usage: 1.3+ KB
dfn = timeseries_dataframe_from_datadict(test_nested_data, ['datetimeMeasure', 'time'], recordformat='records', nested=True).dropna()
dfn.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 10 entries, 2025-07-21 09:46:33.313000+00:00 to 2025-07-21 08:16:33.560000+00:00
Data columns (total 14 columns):
 #   Column                               Non-Null Count  Dtype  
---  ------                               --------------  -----  
 0   deviceId                             10 non-null     object 
 1   datetimeSource                       10 non-null     object 
 2   datetimeAcquisition                  10 non-null     object 
 3   connector                            10 non-null     object 
 4   value                                10 non-null     object 
 5   metadata.connection.rssi             10 non-null     int64  
 6   metadata.connection.snr              10 non-null     float64
 7   metadata.connection.spreadingFactor  10 non-null     int64  
 8   metadata.connection.frequency        10 non-null     float64
 9   metadata.connection.gateways         10 non-null     object 
 10  metadata.frame.port                  10 non-null     int64  
 11  metadata.frame.counterUp             10 non-null     int64  
 12  metadata.frame.counterDown           10 non-null     int64  
 13  metadata.frame.errorRate             10 non-null     int64  
dtypes: float64(2), int64(6), object(6)
memory usage: 1.2+ KB
dfn
deviceId datetimeSource datetimeAcquisition connector value metadata.connection.rssi metadata.connection.snr metadata.connection.spreadingFactor metadata.connection.frequency metadata.connection.gateways metadata.frame.port metadata.frame.counterUp metadata.frame.counterDown metadata.frame.errorRate
time
2025-07-21 09:46:33.313000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:46:33.313Z 2025-07-21T09:46:33.691Z lora.kpn 0d01b4613d2ab820ec21e42a912a -106 0.00 11 868.1 [{'id': 'FF01055A', 'rssi': -106, 'snr': 0, 'l... 2 1319 26 4
2025-07-21 09:36:33.332000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:36:33.332Z 2025-07-21T09:36:33.697Z lora.kpn 0d0174603d2a9e20bf21dc2a802a -104 5.00 11 867.3 [{'id': 'FF01055A', 'rssi': -104, 'snr': 5, 'l... 2 1318 26 4
2025-07-21 09:26:33.350000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:26:33.350Z 2025-07-21T09:26:33.705Z lora.kpn 0d01b45e3d2aa120ad21d42a5d2a -105 9.00 11 868.5 [{'id': 'FF01055A', 'rssi': -105, 'snr': 9, 'l... 2 1317 26 4
2025-07-21 09:16:33.368000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:16:33.368Z 2025-07-21T09:16:33.734Z lora.kpn 0d01745d3d2ac320a621d12a5b2a -109 2.00 11 866.6 [{'id': 'FF01055A', 'rssi': -109, 'snr': 2, 'l... 2 1316 26 4
2025-07-21 09:06:33.386000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:06:33.386Z 2025-07-21T09:06:33.748Z lora.kpn 0d01345c3d2aa920b821d02a612a -106 -2.00 11 868.5 [{'id': 'FF01055A', 'rssi': -106, 'snr': -2, '... 2 1315 26 4
2025-07-21 08:56:33.405000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:56:33.405Z 2025-07-21T08:56:33.754Z lora.kpn 0d01f45a3d2aa020c821d22a7d2a -115 -6.25 11 866.1 [{'id': 'FF010323', 'rssi': -115, 'snr': -6.25... 2 1314 26 4
2025-07-21 08:46:33.423000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:46:33.423Z 2025-07-21T08:46:33.778Z lora.kpn 0d01b4593d2ab920d121cf2a922a -106 8.00 11 868.5 [{'id': 'FF01055A', 'rssi': -106, 'snr': 8, 'l... 2 1313 26 4
2025-07-21 08:36:33.441000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:36:33.441Z 2025-07-21T08:36:33.821Z lora.kpn 0d0174583d2ac020e221cb2ada2a -106 2.00 11 866.4 [{'id': 'FF01055A', 'rssi': -106, 'snr': 2, 'l... 2 1312 26 6
2025-07-21 08:26:33.460000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:26:33.460Z 2025-07-21T08:26:33.810Z lora.kpn 0d01b4563d2aa220db21c72aba2a -103 -2.00 11 868.1 [{'id': 'FF01055A', 'rssi': -103, 'snr': -2, '... 2 1311 26 6
2025-07-21 08:16:33.560000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:16:33.560Z 2025-07-21T08:16:33.962Z lora.kpn 0d0174553d2ac220d821c52ad92a -104 -7.00 11 865.1 [{'id': 'FF01055A', 'rssi': -104, 'snr': -7, '... 2 1310 26 6
dfm = timeseries_dataframe_from_datadict(test_nested_data, ['datetimeMeasure', 'time'], recordformat='records', nested=False).dropna()
dfm.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 10 entries, 2025-07-21 09:46:33.313000+00:00 to 2025-07-21 08:16:33.560000+00:00
Data columns (total 6 columns):
 #   Column               Non-Null Count  Dtype 
---  ------               --------------  ----- 
 0   deviceId             10 non-null     object
 1   datetimeSource       10 non-null     object
 2   datetimeAcquisition  10 non-null     object
 3   connector            10 non-null     object
 4   value                10 non-null     object
 5   metadata             10 non-null     object
dtypes: object(6)
memory usage: 560.0+ bytes
def test_nested_data_processing(data:pd.DataFrame, anumber:float|np.ndarray=0):
    print (f"Processing {len(data)} rows of data")
    print(data.columns)
    return data
class TestNestedAICoreModule(AICoreModule):
    def __init__(self, files_dir, save_dir):
        super().__init__(test_nested_data_processing, files_dir, save_dir)

test_nested_module = TestNestedAICoreModule(os.path.join(os.getcwd(), 'cache'),os.path.join(os.getcwd(), 'cache'))
2026-01-29T14:38:36+0100 INFO   8596    corebridge.aicorebridge aicorebridge.py __init__    135 Init TestNestedAICoreModule, version 0.6.6, files directory c:\Users\fenke\repos\corebridge\nbs\cache, save dir c:\Users\fenke\repos\corebridge\nbs\cache on werkdoos
test_result = test_nested_module.call(dict(
    data=test_nested_data,
    kwargs=dict(
        timezone='UTC',
        recordformat='records',
        nested=True
    )
))
Processing 10 rows of data
Index(['deviceId', 'datetimeSource', 'datetimeAcquisition', 'connector',
       'value', 'metadata.connection.rssi', 'metadata.connection.snr',
       'metadata.connection.spreadingFactor', 'metadata.connection.frequency',
       'metadata.connection.gateways', 'metadata.frame.port',
       'metadata.frame.counterUp', 'metadata.frame.counterDown',
       'metadata.frame.errorRate'],
      dtype='object')
print("Result Message\n", json.dumps(test_result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(test_result['data'], indent=2, cls=NumpyEncoder))
Result Message
 [
  "Startup time: 2026-01-29T13:38:36.605275+00:00, node werkdoos",
  "Call time: 2026-01-29T13:38:36.616701+00:00",
  "Corebridge version: 0.6.6",
  "CPU times: user: 21.3, system: 7.4, idle: 71.3, interrupt: 0.0, dpc: 0.0",
  "Memory: total: 68425105408, available: 54528323584, percent: 20.3, used: 13896781824, free: 54528323584",
  "test_nested_data_processing((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 10",
  "kwargs ['timezone', 'recordformat', 'nested']",
  "lastSeen: False, timezone: UTC, recordformat: records, nested: True",
  "Processing time: 0.4 ms",
  "Preparation time: 3.5 ms",
  "result shape: (10, 14)",
  "return-data shape: (10, 14)"
]
Result Data
 [
  {
    "time": "2025-07-21T08:16:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:16:33.560Z",
    "datetimeAcquisition": "2025-07-21T08:16:33.962Z",
    "connector": "lora.kpn",
    "value": "0d0174553d2ac220d821c52ad92a",
    "metadata.connection.rssi": -104,
    "metadata.connection.snr": -7.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 865.1,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -104,
        "snr": -7,
        "location": {
          "latitude": 51.556866,
          "longitude": 5.865384
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1310,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 6
  },
  {
    "time": "2025-07-21T08:26:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:26:33.460Z",
    "datetimeAcquisition": "2025-07-21T08:26:33.810Z",
    "connector": "lora.kpn",
    "value": "0d01b4563d2aa220db21c72aba2a",
    "metadata.connection.rssi": -103,
    "metadata.connection.snr": -2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.1,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -103,
        "snr": -2,
        "location": {
          "latitude": 51.556854,
          "longitude": 5.865371
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1311,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 6
  },
  {
    "time": "2025-07-21T08:36:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:36:33.441Z",
    "datetimeAcquisition": "2025-07-21T08:36:33.821Z",
    "connector": "lora.kpn",
    "value": "0d0174583d2ac020e221cb2ada2a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": 2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 866.4,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": 2,
        "location": {
          "latitude": 51.55687,
          "longitude": 5.86535
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1312,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 6
  },
  {
    "time": "2025-07-21T08:46:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:46:33.423Z",
    "datetimeAcquisition": "2025-07-21T08:46:33.778Z",
    "connector": "lora.kpn",
    "value": "0d01b4593d2ab920d121cf2a922a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": 8.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.5,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": 8,
        "location": {
          "latitude": 51.556862,
          "longitude": 5.865373
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1313,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T08:56:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:56:33.405Z",
    "datetimeAcquisition": "2025-07-21T08:56:33.754Z",
    "connector": "lora.kpn",
    "value": "0d01f45a3d2aa020c821d22a7d2a",
    "metadata.connection.rssi": -115,
    "metadata.connection.snr": -6.25,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 866.1,
    "metadata.connection.gateways": [
      {
        "id": "FF010323",
        "rssi": -115,
        "snr": -6.25,
        "location": {
          "latitude": 51.516491,
          "longitude": 5.884403
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1314,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:06:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:06:33.386Z",
    "datetimeAcquisition": "2025-07-21T09:06:33.748Z",
    "connector": "lora.kpn",
    "value": "0d01345c3d2aa920b821d02a612a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": -2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.5,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": -2,
        "location": {
          "latitude": 51.556858,
          "longitude": 5.865352
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1315,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:16:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:16:33.368Z",
    "datetimeAcquisition": "2025-07-21T09:16:33.734Z",
    "connector": "lora.kpn",
    "value": "0d01745d3d2ac320a621d12a5b2a",
    "metadata.connection.rssi": -109,
    "metadata.connection.snr": 2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 866.6,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -109,
        "snr": 2,
        "location": {
          "latitude": 51.556892,
          "longitude": 5.865359
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1316,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:26:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:26:33.350Z",
    "datetimeAcquisition": "2025-07-21T09:26:33.705Z",
    "connector": "lora.kpn",
    "value": "0d01b45e3d2aa120ad21d42a5d2a",
    "metadata.connection.rssi": -105,
    "metadata.connection.snr": 9.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.5,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -105,
        "snr": 9,
        "location": {
          "latitude": 51.556892,
          "longitude": 5.865356
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1317,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:36:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:36:33.332Z",
    "datetimeAcquisition": "2025-07-21T09:36:33.697Z",
    "connector": "lora.kpn",
    "value": "0d0174603d2a9e20bf21dc2a802a",
    "metadata.connection.rssi": -104,
    "metadata.connection.snr": 5.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 867.3,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -104,
        "snr": 5,
        "location": {
          "latitude": 51.5569,
          "longitude": 5.865385
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1318,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:46:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:46:33.313Z",
    "datetimeAcquisition": "2025-07-21T09:46:33.691Z",
    "connector": "lora.kpn",
    "value": "0d01b4613d2ab820ec21e42a912a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": 0.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.1,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": 0,
        "location": {
          "latitude": 51.556896,
          "longitude": 5.865362
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1319,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  }
]
print(test_result['msg'][-1])
return-data shape: (10, 14)

References