Skip to content

Commit 228fd02

Browse files
raymar9Copilot
andcommitted
add MqttRlDspSink based on VSE industry document RL-DSP;
with a modified MQTT topic and payload structure Co-authored-by: Copilot <copilot@github.com>
1 parent 77c1c56 commit 228fd02

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

smartmeter_datacollector/sinks/mqtt_sink.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,23 @@ def data_point_to_mqtt_json(data_point: MeterDataPoint, timestamp: int) -> str:
192192
"obis": data_point.obis.to_short_str(),
193193
}
194194
)
195+
196+
197+
class MqttSinkRlDsp(MqttDataSink):
198+
def __init__(self, config: MqttConfig) -> None:
199+
super().__init__(config)
200+
self._group = "building"
201+
202+
async def send(self, data_bundle: MeterDataBundle) -> None:
203+
topic = self.build_topic_name(data_bundle)
204+
payload = self.to_mqtt_payload(data_bundle)
205+
await self._publish_with_retries(topic, payload, retries=self.RETRIES)
206+
207+
def build_topic_name(self, data_bundle: MeterDataBundle) -> str:
208+
return f"dt/{self._group}/{data_bundle.source}/ds"
209+
210+
@staticmethod
211+
def to_mqtt_payload(data_bundle: MeterDataBundle) -> str:
212+
meter: dict[str, str | float] = {"ts": data_bundle.timestamp.isoformat()}
213+
meter.update({dp.obis.to_short_str(): dp.value for dp in data_bundle.data_points})
214+
return json.dumps({"meter": meter})

tests/test_mqtt_sink.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
from aiomqtt import MqttCodeError
1616
from paho.mqtt.client import MQTT_ERR_NO_CONN
1717

18-
from smartmeter_datacollector.sinks.mqtt_sink import MqttConfig, MqttDataSink
18+
from smartmeter_datacollector.sinks.mqtt_sink import MqttConfig, MqttDataSink, MqttSinkRlDsp
1919
from smartmeter_datacollector.smartmeter.meter_data import MeterDataBundle, MeterDataPoint, MeterDataPointType
2020
from smartmeter_datacollector.smartmeter.obis import OBISCode
2121

2222
TEST_DATA_POINT_TYPE = MeterDataPointType("TEST_TYPE", "test type", "unit")
2323
TEST_OBIS = OBISCode(0, 1, 2, 3, 4, 5)
24+
TEST_OBIS_2 = OBISCode(1, 1, 2, 8, 0, 255)
2425

2526

2627
@pytest.fixture(autouse=True)
@@ -190,3 +191,84 @@ def test_mqtt_config_encrypted_authorized_client_cert(_: mock.MagicMock):
190191

191192
with mock.patch("smartmeter_datacollector.sinks.mqtt_sink.ssl"):
192193
sink = MqttDataSink(cfg)
194+
195+
196+
@pytest.mark.asyncio
197+
async def test_mqtt_sink_rldsp_send_publishes_single_message(mocked_mqtt_client: mock.MagicMock):
198+
config = MqttConfig("localhost")
199+
sink = MqttSinkRlDsp(config)
200+
201+
timestamp = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
202+
data_point = MeterDataPoint(TEST_DATA_POINT_TYPE, 42.0, TEST_OBIS)
203+
data_bundle = MeterDataBundle("meter1", timestamp, [data_point])
204+
205+
await sink.send(data_bundle)
206+
207+
assert mocked_mqtt_client.publish.await_count == 1
208+
209+
210+
@pytest.mark.asyncio
211+
async def test_mqtt_sink_rldsp_send_uses_correct_topic(mocked_mqtt_client: mock.MagicMock):
212+
config = MqttConfig("localhost")
213+
sink = MqttSinkRlDsp(config)
214+
215+
timestamp = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
216+
data_point = MeterDataPoint(TEST_DATA_POINT_TYPE, 42.0, TEST_OBIS)
217+
data_bundle = MeterDataBundle("meter1", timestamp, [data_point])
218+
expected_topic = "dt/building/meter1/ds"
219+
220+
await sink.send(data_bundle)
221+
222+
mocked_mqtt_client.publish.assert_awaited_once_with(expected_topic, mock.ANY)
223+
224+
225+
@pytest.mark.asyncio
226+
async def test_mqtt_sink_rldsp_send_multiple_datapoints_single_publish(mocked_mqtt_client: mock.MagicMock):
227+
config = MqttConfig("localhost")
228+
sink = MqttSinkRlDsp(config)
229+
230+
timestamp = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
231+
data_points = [
232+
MeterDataPoint(TEST_DATA_POINT_TYPE, 100.0, TEST_OBIS),
233+
MeterDataPoint(TEST_DATA_POINT_TYPE, 200.0, TEST_OBIS_2),
234+
]
235+
data_bundle = MeterDataBundle("meter1", timestamp, data_points)
236+
237+
await sink.send(data_bundle)
238+
239+
assert mocked_mqtt_client.publish.await_count == 1
240+
241+
242+
def test_mqtt_sink_rldsp_to_mqtt_payload_contains_timestamp():
243+
timestamp = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
244+
data_point = MeterDataPoint(TEST_DATA_POINT_TYPE, 42.0, TEST_OBIS)
245+
data_bundle = MeterDataBundle("meter1", timestamp, [data_point])
246+
247+
payload = json.loads(MqttSinkRlDsp.to_mqtt_payload(data_bundle))
248+
249+
assert payload["meter"]["ts"] == timestamp.isoformat()
250+
251+
252+
def test_mqtt_sink_rldsp_to_mqtt_payload_contains_obis_values():
253+
timestamp = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
254+
data_points = [
255+
MeterDataPoint(TEST_DATA_POINT_TYPE, 100.0, TEST_OBIS),
256+
MeterDataPoint(TEST_DATA_POINT_TYPE, 200.0, TEST_OBIS_2),
257+
]
258+
data_bundle = MeterDataBundle("meter1", timestamp, data_points)
259+
260+
payload = json.loads(MqttSinkRlDsp.to_mqtt_payload(data_bundle))
261+
262+
assert payload["meter"][TEST_OBIS.to_short_str()] == 100.0
263+
assert payload["meter"][TEST_OBIS_2.to_short_str()] == 200.0
264+
265+
266+
def test_mqtt_sink_rldsp_build_topic_name():
267+
config = MqttConfig("localhost")
268+
sink = MqttSinkRlDsp(config)
269+
timestamp = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
270+
data_bundle = MeterDataBundle("my_meter", timestamp, [])
271+
272+
topic = sink.build_topic_name(data_bundle)
273+
274+
assert topic == "dt/building/my_meter/ds"

0 commit comments

Comments
 (0)