Skip to content

Commit 0c1af09

Browse files
[feature] Support dynamic mobile signal metrics for multiple interfaces
1 parent 584b29d commit 0c1af09

7 files changed

Lines changed: 146 additions & 26 deletions

File tree

.dockerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
venv/
2+
.git/
3+
.github/
4+
.tox/
5+
htmlcov/
6+
build/
7+
dist/
8+
*.egg-info/

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ services:
66
dockerfile: Dockerfile
77
ports:
88
- "8000:8000"
9+
volumes:
10+
- .:/opt/openwisp
911
depends_on:
1012
- influxdb
1113
- redis

docs/user/metrics.rst

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,25 @@ Mobile Signal Strength
207207
:target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-strength.png
208208
:align: center
209209

210-
================== =====================================
211-
**collection**: :doc:`OpenWrt Monitoring Agent
212-
</openwrt-monitoring-agent/index>`
213-
**measurement**: ``signal_strength``
210+
================== ==========================================================================
211+
**collection**: :doc:`OpenWrt Monitoring Agent </openwrt-monitoring-agent/index>`
212+
**measurement**: ``signal``
214213
**type**: ``float``
215214
**fields**: ``signal_strength``, ``signal_power``
215+
**tags**: .. code-block:: python
216+
217+
{
218+
"organization_id": "<organization-id-of-the-related-device>",
219+
"ifname": "<interface-name>",
220+
# optional
221+
"location_id": "<location-id-of-the-related-device-if-present>",
222+
"floorplan_id": "<floorplan-id-of-the-related-device-if-present>",
223+
}
216224
**configuration**: ``signal_strength``
217225
**charts**: ``signal_strength``
218-
================== =====================================
226+
================== ==========================================================================
227+
228+
If a device has multiple mobile interfaces, a separate chart will be created for each interface, with its name appended to the chart title (e.g. ``Signal Strength (RSSI): mobile0``).
219229

220230
.. _mobile_signal_quality:
221231

@@ -226,15 +236,25 @@ Mobile Signal Quality
226236
:target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-quality.png
227237
:align: center
228238

229-
================== ======================================
230-
**collection**: :doc:`OpenWrt Monitoring Agent
231-
</openwrt-monitoring-agent/index>`
232-
**measurement**: ``signal_quality``
239+
================== ==========================================================================
240+
**collection**: :doc:`OpenWrt Monitoring Agent </openwrt-monitoring-agent/index>`
241+
**measurement**: ``signal``
233242
**type**: ``float``
234-
**fields**: ``signal_quality``, ``signal_quality``
243+
**fields**: ``signal_quality``, ``snr``
244+
**tags**: .. code-block:: python
245+
246+
{
247+
"organization_id": "<organization-id-of-the-related-device>",
248+
"ifname": "<interface-name>",
249+
# optional
250+
"location_id": "<location-id-of-the-related-device-if-present>",
251+
"floorplan_id": "<floorplan-id-of-the-related-device-if-present>",
252+
}
235253
**configuration**: ``signal_quality``
236254
**charts**: ``signal_quality``
237-
================== ======================================
255+
================== ==========================================================================
256+
257+
If a device has multiple mobile interfaces, a separate chart will be created for each interface, with its name appended to the chart title (e.g. ``Signal Quality (RSRQ): mobile0``).
238258

239259
.. _mobile_access_technology_in_use:
240260

@@ -245,15 +265,25 @@ Mobile Access Technology in Use
245265
:target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/access-technology.png
246266
:align: center
247267

248-
================== ==================================
249-
**collection**: :doc:`OpenWrt Monitoring Agent
250-
</openwrt-monitoring-agent/index>`
251-
**measurement**: ``access_tech``
268+
================== ==========================================================================
269+
**collection**: :doc:`OpenWrt Monitoring Agent </openwrt-monitoring-agent/index>`
270+
**measurement**: ``signal``
252271
**type**: ``int``
253272
**fields**: ``access_tech``
273+
**tags**: .. code-block:: python
274+
275+
{
276+
"organization_id": "<organization-id-of-the-related-device>",
277+
"ifname": "<interface-name>",
278+
# optional
279+
"location_id": "<location-id-of-the-related-device-if-present>",
280+
"floorplan_id": "<floorplan-id-of-the-related-device-if-present>",
281+
}
254282
**configuration**: ``access_tech``
255283
**charts**: ``access_tech``
256-
================== ==================================
284+
================== ==========================================================================
285+
286+
If a device has multiple mobile interfaces, a separate chart will be created for each interface, with its name appended to the chart title (e.g. ``Access Technology: mobile0``).
257287

258288
.. _iperf3:
259289

openwisp_monitoring/device/tests/test_api.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,71 @@ def test_mobile_charts(self):
10081008
)
10091009
self.assertEqual(self.chart_queryset.count(), charts_count + 3)
10101010

1011+
@tag("flaky_with_udp_writes")
1012+
def test_multiple_mobile_charts(self):
1013+
org = self._create_org()
1014+
device = self._create_device(organization=org)
1015+
charts_count = self.chart_queryset.count()
1016+
data = {
1017+
"type": "DeviceMonitoring",
1018+
"interfaces": [
1019+
{
1020+
"name": "mobile0",
1021+
"mac": "00:00:00:00:00:00",
1022+
"mtu": 1900,
1023+
"multicast": True,
1024+
"txqueuelen": 1000,
1025+
"type": "modem-manager",
1026+
"up": True,
1027+
"mobile": {
1028+
"connection_status": "connected",
1029+
"imei": "300000001234567",
1030+
"manufacturer": "Sierra Wireless, Incorporated",
1031+
"model": "MC7430",
1032+
"operator_code": "50502",
1033+
"operator_name": "YES OPTUS",
1034+
"power_status": "on",
1035+
"signal": {
1036+
"lte": {"rsrp": -75, "rsrq": -8, "rssi": -51, "snr": 13},
1037+
},
1038+
},
1039+
},
1040+
{
1041+
"name": "mobile1",
1042+
"mac": "00:00:00:00:00:01",
1043+
"mtu": 1900,
1044+
"multicast": True,
1045+
"txqueuelen": 1000,
1046+
"type": "modem-manager",
1047+
"up": True,
1048+
"mobile": {
1049+
"connection_status": "connected",
1050+
"imei": "300000001234568",
1051+
"manufacturer": "Sierra Wireless, Incorporated",
1052+
"model": "MC7430",
1053+
"operator_code": "50502",
1054+
"operator_name": "YES OPTUS",
1055+
"power_status": "on",
1056+
"signal": {
1057+
"lte": {"rsrp": -80, "rsrq": -10, "rssi": -55, "snr": 11},
1058+
},
1059+
},
1060+
},
1061+
],
1062+
}
1063+
self._post_data(device.id, device.key, data)
1064+
response = self.client.get(self._url(device.pk.hex, device.key))
1065+
self.assertEqual(response.status_code, 200)
1066+
charts = response.data["charts"]
1067+
self.assertEqual(self.chart_queryset.count(), charts_count + 6)
1068+
chart_titles = [chart["title"] for chart in charts]
1069+
self.assertIn("Signal Strength (RSSI): mobile0", chart_titles)
1070+
self.assertIn("Signal Strength (RSSI): mobile1", chart_titles)
1071+
self.assertIn("Signal Quality (RSRQ): mobile0", chart_titles)
1072+
self.assertIn("Signal Quality (RSRQ): mobile1", chart_titles)
1073+
self.assertIn("Access Technology: mobile0", chart_titles)
1074+
self.assertIn("Access Technology: mobile1", chart_titles)
1075+
10111076
# This test reads chart summaries immediately after posting data. That is
10121077
# unreliable with UDP writes.
10131078
@tag("flaky_with_udp_writes")

openwisp_monitoring/device/writer.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ def write(self, data, time=None, current=False):
7777
ifname = interface["name"]
7878
if "mobile" in interface:
7979
self._write_mobile_signal(
80-
interface, ifname, ct, self.device_data.pk, current, time=time
80+
interface,
81+
ifname,
82+
ct,
83+
self.device_data.pk,
84+
current,
85+
time=time,
86+
extra_tags=device_extra_tags,
8187
)
8288
ifstats = interface.get("statistics", {})
8389
# Explicitly stated None to avoid skipping in case the stats are zero
@@ -192,7 +198,9 @@ def _get_mobile_signal_type(self, signal):
192198
if tech in signal:
193199
return tech
194200

195-
def _write_mobile_signal(self, interface, ifname, ct, pk, current=False, time=None):
201+
def _write_mobile_signal(
202+
self, interface, ifname, ct, pk, current=False, time=None, extra_tags=None
203+
):
196204
access_type = self._get_mobile_signal_type(interface["mobile"].get("signal"))
197205
if not access_type:
198206
return
@@ -210,13 +218,15 @@ def _write_mobile_signal(self, interface, ifname, ct, pk, current=False, time=No
210218
if signal_strength is not None:
211219
signal_strength = float(signal_strength)
212220
if signal_strength is not None or signal_power is not None:
221+
name = f"{ifname} signal strength"
213222
metric, created = Metric._get_or_create(
214223
object_id=self.device_data.pk,
215224
content_type_id=ct.id,
216225
configuration="signal_strength",
217-
name="signal strength",
226+
name=name,
218227
key="signal",
219228
main_tags={"ifname": Metric._makekey(ifname)},
229+
extra_tags=extra_tags,
220230
)
221231
self._append_metric_data(
222232
metric, signal_strength, current, time=time, extra_values=extra_values
@@ -239,27 +249,31 @@ def _write_mobile_signal(self, interface, ifname, ct, pk, current=False, time=No
239249
if signal_quality is not None:
240250
signal_quality = float(signal_quality)
241251
if snr is not None or signal_quality is not None:
252+
name = f"{ifname} signal quality"
242253
metric, created = Metric._get_or_create(
243254
object_id=self.device_data.pk,
244255
content_type_id=ct.id,
245256
configuration="signal_quality",
246-
name="signal quality",
257+
name=name,
247258
key="signal",
248259
main_tags={"ifname": Metric._makekey(ifname)},
260+
extra_tags=extra_tags,
249261
)
250262
self._append_metric_data(
251263
metric, signal_quality, current, time=time, extra_values=extra_values
252264
)
253265
if created:
254266
self._create_signal_quality_chart(metric)
255267
# create access technology chart
268+
name = f"{ifname} access technology"
256269
metric, created = Metric._get_or_create(
257270
object_id=self.device_data.pk,
258271
content_type_id=ct.id,
259272
configuration="access_tech",
260-
name="access technology",
273+
name=name,
261274
key="signal",
262275
main_tags={"ifname": Metric._makekey(ifname)},
276+
extra_tags=extra_tags,
263277
)
264278
self._append_metric_data(
265279
metric,

openwisp_monitoring/monitoring/configuration.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ def _get_access_tech():
572572
"type": "scatter",
573573
"fill": "none",
574574
"yaxis": {"zeroline": False},
575-
"title": _("Signal Strength (RSSI)"),
575+
"title": _("Signal Strength (RSSI): {ifname}"),
576576
"colors": (DEFAULT_COLORS[3], DEFAULT_COLORS[0]),
577577
"description": _(
578578
"Signal Strength (RSSI) and Signal Power (RSRP), measured in dBm."
@@ -598,7 +598,7 @@ def _get_access_tech():
598598
"type": "scatter",
599599
"fill": "none",
600600
"yaxis": {"zeroline": False},
601-
"title": _("Signal Quality (RSRQ)"),
601+
"title": _("Signal Quality (RSRQ): {ifname}"),
602602
"colors": (DEFAULT_COLORS[3], DEFAULT_COLORS[0]),
603603
"description": _(
604604
_(
@@ -623,7 +623,7 @@ def _get_access_tech():
623623
"charts": {
624624
"access_tech": {
625625
"type": "bar",
626-
"title": _("Access Technology"),
626+
"title": _("Access Technology: {ifname}"),
627627
"description": _(
628628
_(
629629
"Shows the access technology (LTE, UTMS, CDMA1x, etc.) "

openwisp_monitoring/monitoring/tests/test_models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,14 +337,15 @@ def test_metric_pre_write_signals_emitted(self):
337337

338338
def test_metric_post_write_signals_emitted(self):
339339
om = self._create_object_metric()
340+
now = timezone.now()
340341
with catch_signal(post_metric_write) as handler:
341-
om.write(3, current=True, time=start_time)
342+
om.write(3, current=True, time=now)
342343
handler.assert_called_once_with(
343344
sender=Metric,
344345
metric=om,
345346
values={om.field_name: 3},
346347
signal=post_metric_write,
347-
time=start_time.isoformat(),
348+
time=now.isoformat(),
348349
current=True,
349350
)
350351

0 commit comments

Comments
 (0)