import json
import os
from corebridge.timeseriesdataframe import test_data_dict_3_samples
from corebridge.core import init_console_loggingsyslog = init_console_logging(__name__, logging.DEBUG, timestamp=False)Support functions
Pop NaN values
pop_nan_values
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
build_historic_args (data:pandas.core.frame.DataFrame, history:dict|list)
Create a timeseries DataFrame from historic data defined in history.
| Type | Details | |
|---|---|---|
| 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. |
| Returns | dict | Historic data in dictionary format where keys are column names and values are the historic values as numpy array. |
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()Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
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
AICoreModuleBase (files_dir, save_dir, **kwargs)
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}"INFO 9588 root 2001759155.py 22 Init AICoreModuleBase, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
test_module.__dict__{'init_time': datetime.datetime(2025, 10, 13, 13, 55, 9, 301785, tzinfo=datetime.timezone.utc),
'aicorebridge_version': '0.6.1',
'init_args': [],
'init_kwargs': {'files_dir': '/home/fenke/repos/corebridge/nbs/cache',
'save_dir': '/home/fenke/repos/corebridge/nbs/cache'}}
Class AICoreModule
AICoreModule
AICoreModule (processor:Callable, files_dir:str, save_dir:str, **kwargs)
Initialize self. See help(type(self)) for accurate signature.
| Type | Details | |
|---|---|---|
| processor | Callable | data processing function |
| files_dir | str | path where the module can keep files |
| save_dir | str | |
| kwargs | VAR_KEYWORD |
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
AICoreModule.call_processor (calldata, **callargs)
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
AICoreModule.call (data:dict, *_, **__)
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"Corebridge version: {self.aicorebridge_version}",
]
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
AICoreModule.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 as err:
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
AICoreModule.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"INFO 9588 root 2001759155.py 22 Init AICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
get_call_data
AICoreModule.get_call_data
AICoreModule.get_call_data (data:dict|list, recordformat='records', timezone='UTC', nested=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))
calldatarecordformat: records, timezone: UTC, nested: False
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
| 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'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]class TestAICoreModule(AICoreModule):
def __init__(self, files_dir, save_dir):
super().__init__(test_function, files_dir, save_dir)class SimpleAICoreModule(AICoreModule):
def __init__(self, files_dir, save_dir):
super().__init__(test_simple_function, files_dir, save_dir)save_dir = os.path.join(os.getcwd(), 'cache')
files_dir = os.path.join(os.getcwd(), 'cache')
test_module = TestAICoreModule(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}"INFO 9588 root 2001759155.py 22 Init TestAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
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))recordformat: records, timezone: Europe/Amsterdam, nested: False
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
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 2025-10-13T13:55:09.561683+00:00, node bouwdoosje",
"Corebridge version: 0.6.1",
"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: 19.4 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_dirINFO 9588 root 2001759155.py 22 Init SimpleAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
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 2025-10-13T13:55:09.638778+00:00, node bouwdoosje",
"Corebridge version: 0.6.1",
"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
import corebridge.corefrom corebridge.aicorebridge import AICoreModuleDEBUG 9588 corebridge.aicorebridge aicorebridge.py 35 Loading corebridge.aicorebridge 0.6.1 from /home/fenke/repos/corebridge/corebridge/aicorebridge.py
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_dirINFO 9588 corebridge.aicorebridge aicorebridge.py 134 Init TestAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
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))recordformat: records, timezone: Europe/Amsterdam, nested: False
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
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 2025-10-13T13:55:09.715945+00:00, node bouwdoosje",
"Corebridge version: 0.6.1",
"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: 2.2 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 2025-10-13T13:55:09.715945+00:00, node bouwdoosje",
"Corebridge version: 0.6.1",
"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: 2.2 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()Converting nested data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
<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()Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
<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'))INFO 9588 corebridge.aicorebridge aicorebridge.py 134 Init TestNestedAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
test_result = test_nested_module.call(dict(
data=test_nested_data,
kwargs=dict(
timezone='UTC',
recordformat='records',
nested=True
)
))recordformat: records, timezone: UTC, nested: True
Converting nested data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
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 2025-10-13T13:55:09.873476+00:00, node bouwdoosje",
"Corebridge version: 0.6.1",
"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.3 ms",
"Preparation time: 4.9 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)