Skip to content

Commit

Permalink
[feature] Added API endpoint to return nearby devices #541
Browse files Browse the repository at this point in the history
Closes #541
  • Loading branch information
pandafy authored Oct 21, 2023
1 parent 3795e61 commit b29cce3
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
jobs:
build:
name: Python==${{ matrix.python-version }} | ${{ matrix.django-version }}
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04

services:
redis:
Expand Down
36 changes: 33 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2597,11 +2597,41 @@ and sending of data by the OpenWISP Monitoring Agent
<https://github.com/openwisp/openwrt-openwisp-monitoring#collecting-vs-sending>`_,
this feature allows sending data collected while the device is offline.

List nearby devices
###################

.. code-block:: text
GET /api/v1/monitoring/device/{pk}/nearby-devices/
Returns list of nearby devices along with respective distance (in metres) and
monitoring status.

**Available filters**

The list of nearby devices provides the following filters:

- ``organization`` (Organization ID of the device)
- ``organization__slug`` (Organization slug of the device)
- ``monitoring__status`` (Monitoring status (``unknown``, ``ok``, ``problem``, or ``critical``))
- ``model`` (Pipe `|` separated list of device models)
- ``distance__lte`` (Distance in metres)

Here's a few examples:

.. code-block:: text
GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization={organization_id}
GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization__slug={organization_slug}
GET /api/v1/monitoring/device/{pk}/nearby-devices/?monitoring__status={monitoring_status}
GET /api/v1/monitoring/device/{pk}/nearby-devices/?model={model1,model2}
GET /api/v1/monitoring/device/{pk}/nearby-devices/?distance__lte={distance}
List wifi session
#################

.. code-block:: text
GET /api/v1/monitoring/wifi-session/
**Available filters**
Expand Down Expand Up @@ -2640,7 +2670,7 @@ Get wifi session
################

.. code-block:: text
GET /api/v1/monitoring/wifi-session/{id}/
Pagination
Expand All @@ -2650,7 +2680,7 @@ Wifi session endpoint support the ``page_size`` parameter
that allows paginating the results in conjunction with the page parameter.

.. code-block:: text
GET /api/v1/monitoring/wifi-session/?page_size=10
GET /api/v1/monitoring/wifi-session/?page_size=10&page=1
Expand Down
22 changes: 22 additions & 0 deletions openwisp_monitoring/device/api/filters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as filters
from swapper import load_model

from openwisp_users.api.filters import (
Expand Down Expand Up @@ -25,3 +27,23 @@ class MonitoringDeviceFilter(OrganizationManagedFilter):
class Meta(OrganizationManagedFilter.Meta):
model = Device
fields = OrganizationManagedFilter.Meta.fields + ['monitoring__status']


class MonitoringNearbyDeviceFilter(OrganizationManagedFilter):
distance__lte = filters.NumberFilter(
label=_('Distance is less than or equal to'),
field_name='distance',
lookup_expr='lte',
)
model = filters.CharFilter(method='filter_model')

class Meta(OrganizationManagedFilter.Meta):
model = Device
fields = OrganizationManagedFilter.Meta.fields + [
'monitoring__status',
'model',
]

def filter_model(self, queryset, name, value):
values = value.split('|')
return queryset.filter(**{f"{name}__in": values})
34 changes: 34 additions & 0 deletions openwisp_monitoring/device/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
GeoJsonLocationSerializer,
LocationDeviceSerializer,
)
from openwisp_users.api.mixins import FilterSerializerByOrgManaged

Device = load_model('config', 'Device')
DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring')
DeviceData = load_model('device_monitoring', 'DeviceData')
Device = load_model('config', 'Device')
WifiSession = load_model('device_monitoring', 'WifiSession')
WifiClient = load_model('device_monitoring', 'WifiClient')
Expand Down Expand Up @@ -44,6 +46,38 @@ class MonitoringLocationDeviceSerializer(LocationDeviceSerializer):
monitoring = DeviceMonitoringLocationSerializer()


class MonitoringNearbyDeviceSerializer(
FilterSerializerByOrgManaged, serializers.ModelSerializer
):
monitoring_status = serializers.CharField(source='monitoring.status')
distance = serializers.SerializerMethodField('get_distance')
monitoring_data = serializers.SerializerMethodField('get_monitoring_data')

class Meta(DeviceListSerializer.Meta):
model = Device
fields = [
'id',
'name',
'organization',
'group',
'mac_address',
'management_ip',
'model',
'os',
'system',
'notes',
'distance',
'monitoring_status',
'monitoring_data',
]

def get_distance(self, obj):
return obj.distance.m

def get_monitoring_data(self, obj):
return DeviceData.objects.only('id').get(id=obj.id).data


class MonitoringDeviceListSerializer(DeviceListSerializer):
monitoring = BaseDeviceMonitoringSerializer(read_only=True)

Expand Down
5 changes: 5 additions & 0 deletions openwisp_monitoring/device/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
views.device_metric,
name='api_device_metric',
),
re_path(
r'^api/v1/monitoring/device/(?P<pk>[^/]+)/nearby-devices/$',
views.monitoring_nearby_device_list,
name='api_monitoring_nearby_device_list',
),
path(
'api/v1/monitoring/geojson/',
views.monitoring_geojson_location_list,
Expand Down
81 changes: 66 additions & 15 deletions openwisp_monitoring/device/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@

from cache_memoize import cache_memoize
from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.db.models.functions import Distance
from django.core.exceptions import ValidationError
from django.db.models import Count, Q
from django.db.models.functions import Round
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend
from pytz import UTC
from rest_framework import pagination, serializers, status
from rest_framework.generics import GenericAPIView, ListAPIView, RetrieveAPIView
from rest_framework.generics import (
GenericAPIView,
ListAPIView,
RetrieveAPIView,
get_object_or_404,
)
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from swapper import load_model
Expand All @@ -30,12 +37,17 @@
from ..schema import schema
from ..signals import device_metrics_received
from ..tasks import write_device_metrics
from .filters import MonitoringDeviceFilter, WifiSessionFilter
from .filters import (
MonitoringDeviceFilter,
MonitoringNearbyDeviceFilter,
WifiSessionFilter,
)
from .serializers import (
MonitoringDeviceDetailSerializer,
MonitoringDeviceListSerializer,
MonitoringGeoJsonLocationSerializer,
MonitoringLocationDeviceSerializer,
MonitoringNearbyDeviceSerializer,
WifiSessionSerializer,
)

Expand Down Expand Up @@ -71,7 +83,23 @@ def get_charts_args_rewrite(view, request, pk):
return (pk,)


class DeviceMetricView(MonitoringApiViewMixin, GenericAPIView):
class DeviceKeyAuthenticationMixin(object):
def get_permissions(self):
if self.request.method in SAFE_METHODS and not self.request.query_params.get(
'key'
):
self.permission_classes = ProtectedAPIMixin.permission_classes
return super().get_permissions()

def get_authenticators(self):
if self.request.method in SAFE_METHODS and not self.request.GET.get('key'):
self.authentication_classes = ProtectedAPIMixin.authentication_classes
return super().get_authenticators()


class DeviceMetricView(
DeviceKeyAuthenticationMixin, MonitoringApiViewMixin, GenericAPIView
):
"""
Retrieve device information, monitoring status (health status),
a list of metrics, chart data and
Expand Down Expand Up @@ -110,18 +138,6 @@ def invalidate_get_charts_cache(cls, instance, *args, **kwargs):
pk = instance.metric.object_id
cls._get_charts.invalidate(None, None, pk)

def get_permissions(self):
if self.request.method in SAFE_METHODS and not self.request.query_params.get(
'key'
):
self.permission_classes = ProtectedAPIMixin.permission_classes
return super().get_permissions()

def get_authenticators(self):
if self.request.method in SAFE_METHODS and not self.request.GET.get('key'):
self.authentication_classes = ProtectedAPIMixin.authentication_classes
return super().get_authenticators()

def get(self, request, pk):
# ensure valid UUID
try:
Expand Down Expand Up @@ -230,6 +246,41 @@ def get_queryset(self):
monitoring_location_device_list = MonitoringLocationDeviceList.as_view()


class MonitoringNearbyDeviceList(
DeviceKeyAuthenticationMixin, FilterByOrganizationManaged, ListAPIView
):
serializer_class = MonitoringNearbyDeviceSerializer
pagination_class = ListViewPagination
filter_backends = [DjangoFilterBackend]
filterset_class = MonitoringNearbyDeviceFilter
permission_classes = []

def get_queryset(self):
qs = Device.objects.select_related('monitoring')
location_lookup = Q(devicelocation__content_object_id=self.kwargs['pk'])
device_key = self.request.query_params.get('key')
if device_key:
location_lookup &= Q(devicelocation__content_object__key=device_key)
if not self.request.user.is_superuser and not device_key:
qs = self.get_organization_queryset(qs)
location = get_object_or_404(Location.objects, location_lookup)
return (
qs.exclude(id=self.kwargs['pk'])
.filter(
devicelocation__isnull=False,
)
.annotate(
distance=Round(
Distance('devicelocation__location__geometry', location.geometry)
)
)
.order_by('distance')
)


monitoring_nearby_device_list = MonitoringNearbyDeviceList.as_view()


class MonitoringDeviceList(DeviceListCreateView):
"""
Lists devices and their monitoring status (health status).
Expand Down
Loading

0 comments on commit b29cce3

Please sign in to comment.