Skip to content

Commit

Permalink
[DA] Improve per-device addressing and topic/channel decoding strategies
Browse files Browse the repository at this point in the history
Add more details about per-device decoding to the `WanBusStrategy`, and
exercise a few topic/channel decodings, including edge cases.

Examples:

- myrealm/acme/area-42/foo-70b3d57ed005dac6
- myrealm/d/123e4567-e89b-12d3-a456-426614174000
- myrealm/dt/acme-area42-eui70b3d57ed005dac6
- myrealm/dt/eui-70b3d57ed005dac6
- myrealm/dt/acme-area42-eui70b3d57ed005dac6
- myrealm/dt/acme-area42-eui70b3d57ed005dac6-suffix
- myrealm/dt/myrealm-acme-area42-eui70b3d57ed005dac6
- mqttkit-1/d
- myrealm/dt
  • Loading branch information
amotl committed Jun 7, 2023
1 parent b7ea48c commit 55ed889
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 3 deletions.
28 changes: 25 additions & 3 deletions kotori/daq/strategy/wan.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class WanBusStrategy(StrategyBase):

# Regular expression pattern for decoding MQTT topic address segments.
channel_matcher = re.compile(r'^(?P<realm>.+?)/(?P<network>.+?)/(?P<gateway>.+?)/(?P<node>.+?)(?:/(?P<slot>.+?))?$')
device_matcher_dashed_topo = re.compile(r'^(?P<realm>.+?)/dt/(?P<device>.+?)(?:/(?P<slot>.+?))?$')
device_matcher_generic = re.compile(r'^(?P<realm>.+?)/d/(?P<device>.+?)(?:/(?P<slot>.+?))?$')
device_matcher_dashed_topo = re.compile(r'^(?P<realm>.+?)/dt/(?P<device>.+?)/(?:(?P<slot>.+?))$')
device_matcher_generic = re.compile(r'^(?P<realm>.+?)/d/(?P<device>.+?)/(?:(?P<slot>.+?))$')

def topic_to_topology(self, topic):
"""
Expand Down Expand Up @@ -59,6 +59,8 @@ def topic_to_topology(self, topic):

# Decode the topic.
address = None

# Try to match the per-device pattern.
m = self.device_matcher_generic.match(topic)
if m:
address = SmartMunch(m.groupdict())
Expand All @@ -68,21 +70,41 @@ def topic_to_topology(self, topic):
address.node = address.device
del address.device

# Try to match the per-device pattern with dashed topology encoding for topics.
if address is None:
m = self.device_matcher_dashed_topo.match(topic)
if m:
address = SmartMunch(m.groupdict())
if "device" in address:
segments = address.device.split("-")

# Compensate "too few segments": Fill up with "default" at the front.
missing_segments = 3 - len(segments)
segments = ["default"] * missing_segments + segments

# When the first topic path fragment is equal to the realm, we assume a
# slight misconfiguration, and ignore the first decoded segment completely.
if segments[0] == address.realm:
segments = segments[1:]

# Compensate "too many segments": Merge trailing segments.
if len(segments) > 3:
segments = segments[:2] + ["-".join(segments[2:])]

# Destructure three components / segments.
address.network, address.gateway, address.node = segments

# Do not propagate the `device` slot. It either has been
# dissolved, or it was propagated into the `node` slot.
del address.device

# Try to match the classic path-based WAN topic encoding scheme.
if address is None:
m = self.channel_matcher.match(topic)
if m:
address = SmartMunch(m.groupdict())

return address or {}
return address

@classmethod
def topology_to_storage(self, topology, message_type=None):
Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ markers =
hiveeyes: Tests for vendor hiveeyes.
legacy: Tests for legacy endpoints and such.
device: Device-based addressing.
strategy: Transformation strategies.
2 changes: 2 additions & 0 deletions test/settings/mqttkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class TestSettings:
device_influx_measurement_sensors = 'default_123e4567_e89b_12d3_a456_426614174000_sensors'
device_mqtt_topic_generic = 'mqttkit-1/d/123e4567-e89b-12d3-a456-426614174000/data.json'
device_mqtt_topic_dashed_topo = 'mqttkit-1/dt/itest-foo-bar/data.json'
device_http_path_generic = '/mqttkit-1/d/123e4567-e89b-12d3-a456-426614174000/data'
device_http_path_dashed_topo = '/mqttkit-1/dt/itest-foo-bar/data'


settings = TestSettings
Expand Down
166 changes: 166 additions & 0 deletions test/test_wan_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
# (c) 2023 Andreas Motl <andreas@getkotori.org>
import pytest

from kotori.daq.strategy.wan import WanBusStrategy
from kotori.util.common import SmartMunch


@pytest.mark.strategy
def test_wan_strategy_channel():
"""
Verify the classic WAN topology decoding, using a channel-based addressing scheme.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/acme/area-42/foo-70b3d57ed005dac6/data.json")
assert topology == SmartMunch(
{
"realm": "myrealm",
"network": "acme",
"gateway": "area-42",
"node": "foo-70b3d57ed005dac6",
"slot": "data.json",
}
)


@pytest.mark.strategy
def test_wan_strategy_device_generic_success():
"""
Verify the per-device WAN topology decoding, using a generic device identifier.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/d/123e4567-e89b-12d3-a456-426614174000/data.json")
assert topology == SmartMunch(
{
"realm": "myrealm",
"network": "devices",
"gateway": "default",
"node": "123e4567-e89b-12d3-a456-426614174000",
"slot": "data.json",
}
)


@pytest.mark.strategy
def test_wan_strategy_device_dashed_topo_basic():
"""
Verify the per-device WAN topology decoding, using a dashed device identifier, which translates to the topology.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/dt/acme-area42-eui70b3d57ed005dac6/data.json")
assert topology == SmartMunch(
{
"realm": "myrealm",
"network": "acme",
"gateway": "area42",
"node": "eui70b3d57ed005dac6",
"slot": "data.json",
}
)


@pytest.mark.strategy
def test_wan_strategy_device_dashed_topo_too_few_components():
"""
Verify the per-device WAN topology decoding, using a dashed device identifier with too few components.
The solution is to pad the segments with `default` labels at the front.
This specific test uses a vanilla TTN device identifier.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/dt/eui-70b3d57ed005dac6/data.json")
assert topology == SmartMunch(
{
"realm": "myrealm",
"network": "default",
"gateway": "eui",
"node": "70b3d57ed005dac6",
"slot": "data.json",
}
)


@pytest.mark.strategy
def test_wan_strategy_device_dashed_topo_three_components():
"""
Verify the per-device WAN topology decoding, using a dashed device identifier with exactly three components.
This topic will be decoded as-is.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/dt/acme-area42-eui70b3d57ed005dac6/data.json")
assert topology == SmartMunch(
{
"realm": "myrealm",
"network": "acme",
"gateway": "area42",
"node": "eui70b3d57ed005dac6",
"slot": "data.json",
}
)


@pytest.mark.strategy
def test_wan_strategy_device_dashed_topo_too_many_components_merge_suffixes():
"""
Verify the per-device WAN topology decoding, using a dashed device identifier with too many components.
The solution is to merge all trailing segments into the `node` slot, re-joining them with `-`.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/dt/acme-area42-eui70b3d57ed005dac6-suffix/data.json")
assert topology == SmartMunch(
{
"realm": "myrealm",
"network": "acme",
"gateway": "area42",
"node": "eui70b3d57ed005dac6-suffix",
"slot": "data.json",
}
)


@pytest.mark.strategy
def test_wan_strategy_device_dashed_topo_too_many_components_redundant_realm():
"""
Verify the per-device WAN topology decoding, using a dashed device identifier with too many components.
This is an edge case where the first addressing component actually equals the realm.
Strictly, it is a misconfiguration, but we pretend to be smart, and ignore that,
effectively not using the redundant information, and ignoring the `myrealm-` prefix
within the device identifier slot altogether.
Cheers, @thiasB.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/dt/myrealm-acme-area42-eui70b3d57ed005dac6/data.json")
assert topology == SmartMunch(
{
"realm": "myrealm",
"network": "acme",
"gateway": "area42",
"node": "eui70b3d57ed005dac6",
"slot": "data.json",
}
)


@pytest.mark.strategy
def test_wan_strategy_device_generic_empty():
"""
Verify the per-device WAN topology decoding, using a generic device identifier with no components.
Topic-to-topology decoding should return `None`.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/d/data.json")
assert topology is None


@pytest.mark.strategy
def test_wan_strategy_device_dashed_topo_empty():
"""
Verify the per-device WAN topology decoding, using a dashed device identifier with no components.
Topic-to-topology decoding should return `None`.
"""
strategy = WanBusStrategy()
topology = strategy.topic_to_topology("myrealm/dt/data.json")
assert topology is None

0 comments on commit 55ed889

Please sign in to comment.