Skip to content

Commit b2ab31f

Browse files
committed
Add decoder for acquiring data in NDJSON format
1 parent bef0e8b commit b2ab31f

7 files changed

Lines changed: 97 additions & 1 deletion

File tree

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ in progress
88
- **InfluxDB:** Added capability to acquire bulk readings in JSON format
99
- **MQTT:** Added capability to acquire bulk readings in compact JSON format,
1010
with timestamps as keys
11+
- **InfluxDB:** Added decoder for acquiring data in NDJSON format
1112

1213
.. _kotori-0.28.0:
1314

kotori/daq/decoder/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# (c) 2019-2021 Andreas Motl <andreas@getkotori.org>
33
from kotori.daq.decoder.airrohr import AirrohrDecoder
44
from kotori.daq.decoder.json import CompactTimestampedJsonDecoder
5+
from kotori.daq.decoder.ndjson import NdJsonDecoder
56
from kotori.daq.decoder.tasmota import TasmotaSensorDecoder, TasmotaStateDecoder
67
from kotori.daq.decoder.schema import MessageType
78
from kotori.daq.decoder.tts_ttn import TheThingsStackDecoder
@@ -25,6 +26,12 @@ def probe(self, payload: str = None):
2526
if 'slot' not in self.topology:
2627
return False
2728

29+
# NDJSON format
30+
if self.topology.slot.endswith('data.ndjson'):
31+
self.info.message_type = MessageType.DATA_CONTAINER
32+
self.info.decoder = NdJsonDecoder
33+
return True
34+
2835
# Compact JSON format, with timestamps as keys
2936
if self.topology.slot.endswith('tc.json'):
3037
self.info.message_type = MessageType.DATA_CONTAINER

kotori/daq/decoder/ndjson.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# -*- coding: utf-8 -*-
2+
# (c) 2023 Andreas Motl <andreas@getkotori.org>
3+
4+
5+
class NdJsonDecoder:
6+
"""
7+
Decode NDJSON payloads. NDJSON is a newline-delimited JSON format.
8+
It is suitable for submitting multiple JSON records in bulk, or for
9+
streaming them.
10+
11+
NDJSON has been called LDJSON, and is also known as JSON Lines, see
12+
also JSON streaming.
13+
14+
- http://ndjson.org/
15+
- https://jsonlines.org/
16+
- https://en.wikipedia.org/wiki/JSON_streaming
17+
18+
Documentation
19+
=============
20+
- https://getkotori.org/docs/handbook/decoders/ndjson.html (not yet)
21+
22+
Example
23+
=======
24+
::
25+
26+
{"temperature":21.42,"humidity":41.55}
27+
{"temperature":42.84,"humidity":83.1}
28+
29+
"""
30+
31+
@staticmethod
32+
def decode(payload):
33+
34+
# Decode from NDJSON, using pandas.
35+
import pandas as pd
36+
df = pd.read_json(payload, lines=True)
37+
38+
# Transform to records again.
39+
data = df.to_dict(orient="records")
40+
return data

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@
130130
#'txmongo==16.3.0',
131131
'pymongo>=3.11.0,<5',
132132
],
133+
'daq_ndjson': [
134+
'pandas<4',
135+
'numpy<3',
136+
],
133137
'daq_geospatial': [
134138
'Geohash>=1.0,<2',
135139
'geopy>=1.12.0,<3',

test/settings/mqttkit.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class TestSettings:
2828
mqtt_topic_event = 'mqttkit-1/itest/foo/bar/event.json'
2929
mqtt_topic_homie = 'mqttkit-1/itest/foo/bar/data/__json__'
3030
mqtt_topic_json_compact = 'mqttkit-1/itest/foo/bar/tc.json'
31+
mqtt_topic_ndjson = 'mqttkit-1/itest/foo/bar/data.ndjson'
3132
mqtt_topic_json_legacy = 'mqttkit-1/itest/foo/bar/message-json'
3233

3334
# HTTP channel settings.

test/test_daq_mqtt.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from twisted.internet import threads
99

1010
from test.settings.mqttkit import settings, influx_sensors, PROCESS_DELAY_MQTT, device_influx_sensors
11-
from test.util import mqtt_json_sensor, sleep, mqtt_sensor
11+
from test.util import mqtt_json_sensor, sleep, mqtt_sensor, mqtt_ndjson_sensor
1212

1313
logger = logging.getLogger(__name__)
1414

@@ -110,6 +110,42 @@ def test_mqtt_to_influxdb_json_compact_bulk(machinery, create_influxdb, reset_in
110110
assert record == {u'time': '2021-01-19T18:56:08Z', u'temperature': 42.84, u'humidity': 83.1}
111111

112112

113+
@pytest_twisted.inlineCallbacks
114+
@pytest.mark.mqtt
115+
def test_mqtt_to_influxdb_ndjson_bulk(machinery, create_influxdb, reset_influxdb):
116+
"""
117+
Publish multiple readings in NDJSON format to MQTT broker
118+
and proof they are stored in the InfluxDB database.
119+
120+
TODO: Grafana provisioning failed!
121+
"""
122+
123+
# Submit multiple measurements, without timestamp.
124+
data = [
125+
{
126+
'temperature': 21.42,
127+
'humidity': 41.55,
128+
},
129+
{
130+
'temperature': 42.84,
131+
'humidity': 83.1,
132+
},
133+
]
134+
yield threads.deferToThread(mqtt_ndjson_sensor, settings.mqtt_topic_ndjson, data)
135+
136+
# Wait for some time to process the message.
137+
yield sleep(PROCESS_DELAY_MQTT)
138+
139+
# Proof that data arrived in InfluxDB.
140+
record = influx_sensors.get_record(index=0)
141+
del record['time']
142+
assert record == {u'temperature': 21.42, u'humidity': 41.55}
143+
144+
record = influx_sensors.get_record(index=1)
145+
del record['time']
146+
assert record == {u'temperature': 42.84, u'humidity': 83.1}
147+
148+
113149
@pytest_twisted.inlineCallbacks
114150
@pytest.mark.mqtt
115151
@pytest.mark.legacy

test/util.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
import typing as t
1010

11+
import pandas as pd
1112
import pytest
1213
import requests
1314
from influxdb import InfluxDBClient
@@ -205,6 +206,12 @@ def mqtt_json_sensor(topic, data):
205206
return mqtt_sensor(topic, payload)
206207

207208

209+
def mqtt_ndjson_sensor(topic, data):
210+
df = pd.DataFrame.from_records(data)
211+
payload = df.to_json(orient="records", lines=True)
212+
return mqtt_sensor(topic, payload)
213+
214+
208215
def mqtt_sensor(topic, payload):
209216

210217
logger.info('MQTT: Submitting reading')

0 commit comments

Comments
 (0)