Skip to content

Commit 26b4dc1

Browse files
dee077pandafynemesifier
committed
[feature] Added indoor maps #564 #662
- Allow to drill down to indoor map from geographic map - Added REST API endpoints for getting device details and indoor coordinates for specific locations - Allow searching and filtering by status in geographic map locations Closes #564 Closes #662 --------- Co-authored-by: Gagan Deep <[email protected]> Co-authored-by: Federico Capoano <[email protected]>
1 parent fa80622 commit 26b4dc1

File tree

19 files changed

+1512
-103
lines changed

19 files changed

+1512
-103
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ jobs:
8383
pip install -r requirements-test.txt
8484
pip install -U -I -e .
8585
pip uninstall -y Django
86+
# TODO: Remove before merging - install branch build of openwisp-utils for CI testing
87+
pip install -U -I "openwisp-utils @ https://github.com/openwisp/openwisp-utils/tarball/gsoc25-map-adjustments"
8688
pip install -U ${{ matrix.django-version }}
8789
sudo npm install -g prettier
8890

docs/user/rest-api.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,52 @@ Here's a few examples:
226226
GET /api/v1/monitoring/device/{pk}/nearby-devices/?model={model1,model2}
227227
GET /api/v1/monitoring/device/{pk}/nearby-devices/?distance__lte={distance}
228228
229+
List Devices in a Location
230+
~~~~~~~~~~~~~~~~~~~~~~~~~~
231+
232+
.. code-block:: text
233+
234+
GET /api/v1/monitoring/location/{pk}/device/
235+
236+
Returns a list of network devices deployed in the specified location.
237+
238+
**Available filters**
239+
240+
- ``search`` (search by device name)
241+
- ``status`` (monitoring status of the device; multiple statuses can be
242+
provided and will be treated as OR filters)
243+
244+
Here's a few examples:
245+
246+
.. code-block:: text
247+
248+
# search by device name
249+
GET /api/v1/monitoring/location/{pk}/device/?search=hall
250+
# status
251+
GET /api/v1/monitoring/location/{pk}/device/?status=ok
252+
# multiple statuses
253+
GET /api/v1/monitoring/location/{pk}/device/?status=ok&status=problem
254+
255+
List Device Indoor Coordinates in a Location
256+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
257+
258+
Returns a list of indoor coordinates of network devices deployed in the
259+
specified location, along with device details and available floors.
260+
261+
.. code-block:: text
262+
263+
GET /api/v1/monitoring/location/{pk}/indoor-coordinates/
264+
265+
**Available filters**
266+
267+
- ``floor`` (floor number of the floorplan)
268+
269+
Here's a few examples:
270+
271+
.. code-block:: text
272+
273+
GET /api/v1/monitoring/location/5/indoor-coordinates/?floor=2
274+
229275
List WiFi Session
230276
~~~~~~~~~~~~~~~~~
231277

openwisp_monitoring/device/api/filters.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.db.models import Q
12
from django.utils.translation import gettext_lazy as _
23
from django_filters import rest_framework as filters
34
from swapper import load_model
@@ -9,6 +10,7 @@
910

1011
Device = load_model("config", "Device")
1112
WifiSession = load_model("device_monitoring", "WifiSession")
13+
DeviceMonitoring = load_model("device_monitoring", "DeviceMonitoring")
1214

1315

1416
class WifiSessionFilter(FilterDjangoByOrgManaged):
@@ -23,6 +25,24 @@ class Meta:
2325
}
2426

2527

28+
class MonitoringLocationDeviceFilter(filters.FilterSet):
29+
search = filters.CharFilter(method="filter_search")
30+
status = filters.MultipleChoiceFilter(
31+
field_name="monitoring__status",
32+
choices=DeviceMonitoring.STATUS,
33+
)
34+
35+
def filter_search(self, queryset, name, value):
36+
value = value.strip()
37+
return queryset.filter(
38+
Q(name__icontains=value) | Q(mac_address__icontains=value)
39+
)
40+
41+
class Meta:
42+
model = Device
43+
fields = ["search", "status"]
44+
45+
2646
class MonitoringDeviceFilter(OrganizationManagedFilter):
2747
class Meta(OrganizationManagedFilter.Meta):
2848
model = Device

openwisp_monitoring/device/api/serializers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from openwisp_controller.config.api.serializers import DeviceListSerializer
55
from openwisp_controller.geo.api.serializers import (
66
GeoJsonLocationSerializer,
7+
IndoorCoordinatesSerializer,
78
LocationDeviceSerializer,
89
)
910
from openwisp_users.api.mixins import FilterSerializerByOrgManaged
@@ -12,6 +13,7 @@
1213
DeviceMonitoring = load_model("device_monitoring", "DeviceMonitoring")
1314
DeviceData = load_model("device_monitoring", "DeviceData")
1415
Device = load_model("config", "Device")
16+
DeviceLocation = load_model("geo", "DeviceLocation")
1517
WifiSession = load_model("device_monitoring", "WifiSession")
1618
WifiClient = load_model("device_monitoring", "WifiClient")
1719

@@ -46,6 +48,15 @@ class MonitoringLocationDeviceSerializer(LocationDeviceSerializer):
4648
monitoring = DeviceMonitoringLocationSerializer()
4749

4850

51+
class MonitoringIndoorCoordinatesSerializer(IndoorCoordinatesSerializer):
52+
monitoring = DeviceMonitoringLocationSerializer(
53+
source="content_object.monitoring", read_only=True
54+
)
55+
56+
class Meta(IndoorCoordinatesSerializer.Meta):
57+
fields = IndoorCoordinatesSerializer.Meta.fields + ["monitoring"]
58+
59+
4960
class MonitoringNearbyDeviceSerializer(
5061
FilterSerializerByOrgManaged, serializers.ModelSerializer
5162
):

openwisp_monitoring/device/api/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,9 @@
4040
views.wifi_session_detail,
4141
name="api_wifi_session_detail",
4242
),
43+
path(
44+
"api/v1/monitoring/location/<str:pk>/indoor-coordinates/",
45+
views.monitoring_indoor_coordinates_list,
46+
name="api_indoor_coordinates_list",
47+
),
4348
]

openwisp_monitoring/device/api/views.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from openwisp_controller.geo.api.views import (
2929
DevicePermission,
3030
GeoJsonLocationList,
31+
IndoorCoordinatesList,
3132
LocationDeviceList,
3233
ProtectedAPIMixin,
3334
)
@@ -40,13 +41,15 @@
4041
from ..tasks import write_device_metrics
4142
from .filters import (
4243
MonitoringDeviceFilter,
44+
MonitoringLocationDeviceFilter,
4345
MonitoringNearbyDeviceFilter,
4446
WifiSessionFilter,
4547
)
4648
from .serializers import (
4749
MonitoringDeviceDetailSerializer,
4850
MonitoringDeviceListSerializer,
4951
MonitoringGeoJsonLocationSerializer,
52+
MonitoringIndoorCoordinatesSerializer,
5053
MonitoringLocationDeviceSerializer,
5154
MonitoringNearbyDeviceSerializer,
5255
WifiSessionSerializer,
@@ -59,6 +62,7 @@
5962
Device = load_model("config", "Device")
6063
DeviceMonitoring = load_model("device_monitoring", "DeviceMonitoring")
6164
DeviceData = load_model("device_monitoring", "DeviceData")
65+
DeviceLocation = load_model("geo", "DeviceLocation")
6266
Location = load_model("geo", "Location")
6367
WifiSession = load_model("device_monitoring", "WifiSession")
6468

@@ -246,6 +250,8 @@ class MonitoringGeoJsonLocationList(GeoJsonLocationList):
246250

247251
class MonitoringLocationDeviceList(LocationDeviceList):
248252
serializer_class = MonitoringLocationDeviceSerializer
253+
filter_backends = [DjangoFilterBackend]
254+
filterset_class = MonitoringLocationDeviceFilter
249255

250256
def get_queryset(self):
251257
return super().get_queryset().select_related("monitoring").order_by("name")
@@ -254,6 +260,27 @@ def get_queryset(self):
254260
monitoring_location_device_list = MonitoringLocationDeviceList.as_view()
255261

256262

263+
class MonitoringIndoorCoordinatesList(IndoorCoordinatesList):
264+
queryset = (
265+
DeviceLocation.objects.filter(
266+
location__type="indoor",
267+
floorplan__isnull=False,
268+
)
269+
.select_related(
270+
"content_object",
271+
"content_object__monitoring",
272+
"content_object__organization",
273+
"location",
274+
"floorplan",
275+
)
276+
.order_by("floorplan__floor")
277+
)
278+
serializer_class = MonitoringIndoorCoordinatesSerializer
279+
280+
281+
monitoring_indoor_coordinates_list = MonitoringIndoorCoordinatesList.as_view()
282+
283+
257284
class MonitoringNearbyDeviceList(
258285
DeviceKeyAuthenticationMixin, FilterByOrganizationManaged, ListAPIView
259286
):

openwisp_monitoring/device/apps.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,17 @@ def register_dashboard_items(self):
375375
urlconf=MONITORING_API_URLCONF,
376376
args=["000"],
377377
)
378+
indoor_coordinates_list_url = reverse_lazy(
379+
"monitoring:api_indoor_coordinates_list",
380+
urlconf=MONITORING_API_URLCONF,
381+
args=["000"],
382+
)
378383
if MONITORING_API_BASEURL:
379384
device_list_url = urljoin(MONITORING_API_BASEURL, str(device_list_url))
380385
loc_geojson_url = urljoin(MONITORING_API_BASEURL, str(loc_geojson_url))
386+
indoor_coordinates_list_url = urljoin(
387+
MONITORING_API_BASEURL, str(indoor_coordinates_list_url)
388+
)
381389

382390
register_dashboard_template(
383391
position=0,
@@ -393,11 +401,14 @@ def register_dashboard_items(self):
393401
"monitoring/js/lib/netjsongraph.min.js",
394402
"monitoring/js/lib/leaflet.fullscreen.min.js",
395403
"monitoring/js/device-map.js",
404+
"monitoring/js/floorplan.js",
396405
),
397406
},
398407
extra_config={
399408
"monitoring_device_list_url": device_list_url,
400409
"monitoring_location_geojson_url": loc_geojson_url,
410+
"monitoring_indoor_coordinates_list": indoor_coordinates_list_url,
411+
"monitoring_labels": app_settings.HEALTH_STATUS_LABELS,
401412
},
402413
)
403414

openwisp_monitoring/device/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ def get_critical_device_metrics():
3737

3838
def get_health_status_labels():
3939
default_labels = {
40-
"unknown": "unknown",
4140
"ok": "ok",
4241
"problem": "problem",
4342
"critical": "critical",
43+
"unknown": "unknown",
4444
"deactivated": "deactivated",
4545
}
4646
labels = default_labels.copy()

0 commit comments

Comments
 (0)