import json
import os
from corebridge.timeseriesdataframe import test_data_dict_3_samplesSupport 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_dirassave_dirand 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_argsdef 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'].annotationfloat | 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 npdef test_function(data:pd.DataFrame, anumber:float|np.ndarray=0):
return data * anumberdef 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'].annotationpandas.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_dir2026-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_paramTrue
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_class2026-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_dir2026-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 jsontest_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_datadictdf_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 dataclass 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)