From 8feac6bf435522a417b43ce028b57c64653b0375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Mon, 23 Dec 2024 16:26:15 +0100 Subject: [PATCH] use shcmea for beaconmeasurement data and fix some related things --- src/c3nav/editor/forms.py | 30 +++------------- .../0124_beaconmeasurement_data_schema.py | 34 +++++++++++++++++++ src/c3nav/mapdata/models/geometry/space.py | 5 ++- src/c3nav/routing/api/positioning.py | 6 ++-- src/c3nav/routing/locator.py | 26 +++++++------- src/c3nav/routing/schemas.py | 10 ++++-- 6 files changed, 67 insertions(+), 44 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0124_beaconmeasurement_data_schema.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 83c5c9e1..d23bc09e 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -25,9 +25,9 @@ from c3nav.mapdata.forms import I18nModelFormMixin from c3nav.mapdata.models import GraphEdge, LocationGroup, Source, LocationGroupCategory, GraphNode, Space, \ LocationSlug, WayType from c3nav.mapdata.models.access import AccessPermission, AccessRestrictionGroup, AccessRestriction -from c3nav.mapdata.models.geometry.space import ObstacleGroup +from c3nav.mapdata.models.geometry.space import ObstacleGroup, BeaconMeasurement from c3nav.mapdata.models.theme import ThemeLocationGroupBackgroundColor, ThemeObstacleGroupBackgroundColor -from c3nav.routing.schemas import LocateRequestWifiPeerSchema +from c3nav.routing.schemas import LocateWifiPeerSchema, BeaconMeasurementDataSchema class EditorFormBase(I18nModelFormMixin, ModelForm): @@ -312,30 +312,10 @@ class EditorFormBase(I18nModelFormMixin, ModelForm): ) def clean_data(self): - if 'wifi' not in self.cleaned_data['data']: + data = self.cleaned_data['data'] + if not data.wifi: raise ValidationError(_('WiFi scan data is missing.')) - if not isinstance(self.cleaned_data['data']["wifi"], list): - raise ValidationError(_('WiFi scan data is not a list.')) - - data = list() - for scan in self.cleaned_data['data']["wifi"]: - scan: list[dict] - scan_data = list() - for item in scan: - # The app might return results without an SSID, we ignore those - if not item.get('ssid', ''): - continue - # App version < 4.2.4 use level instead fo rssi - if 'level' in item: - item['rssi'] = item['level'] - del item['level'] - try: - LocateRequestWifiPeerSchema.model_validate(item) - except PydanticValidationError as e: - raise ValidationError(str(e)) - scan_data.append(item) - data.append(scan_data) - + data.wifi = [[item for item in scan if item.ssid] for scan in data.wifi] return data def clean(self): diff --git a/src/c3nav/mapdata/migrations/0124_beaconmeasurement_data_schema.py b/src/c3nav/mapdata/migrations/0124_beaconmeasurement_data_schema.py new file mode 100644 index 00000000..4875a7d9 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0124_beaconmeasurement_data_schema.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.8 on 2024-12-23 15:24 + +import c3nav.routing.schemas +import django.core.serializers.json +import django.db.models.deletion +import django_pydantic_field.compat.django +import django_pydantic_field.fields +import types +import typing +from django.db import migrations, models + + +def forwards_func(apps, schema_editor): + BeaconMeasurement = apps.get_model('mapdata', 'BeaconMeasurement') + for measurement in BeaconMeasurement.objects.all(): + if isinstance(measurement.data, list): + measurement.data = {"wifi": measurement.data} + measurement.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0123_door_name_door_todo'), + ] + + operations = [ + migrations.RunPython(forwards_func, migrations.RunPython.noop), + migrations.AlterField( + model_name='beaconmeasurement', + name='data', + field=django_pydantic_field.fields.PydanticSchemaField(config=None, encoder=django.core.serializers.json.DjangoJSONEncoder, schema=c3nav.routing.schemas.BeaconMeasurementDataSchema, verbose_name='Measurement list'), + ), + ] diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index 3e44aaa5..8fc73ec0 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -9,6 +9,7 @@ from django.urls import reverse from django.utils.functional import cached_property from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ +from django_pydantic_field.fields import SchemaField from shapely.geometry import CAP_STYLE, JOIN_STYLE, mapping from c3nav.mapdata.fields import GeometryField, I18nField @@ -21,6 +22,7 @@ from c3nav.mapdata.models.locations import SpecificLocation from c3nav.mapdata.utils.cache.changes import changed_geometries from c3nav.mapdata.utils.geometry import unwrap_geom from c3nav.mapdata.utils.json import format_geojson +from c3nav.routing.schemas import BeaconMeasurementDataSchema if typing.TYPE_CHECKING: from c3nav.mapdata.render.theme import ThemeColorManager @@ -430,7 +432,8 @@ class BeaconMeasurement(SpaceGeometryMixin, models.Model): author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('author')) comment = models.TextField(null=True, blank=True, verbose_name=_('comment')) - data = models.JSONField(_('Measurement list'), default=dict) # todo: would be nice if this used pydantic + data: BeaconMeasurementDataSchema = SchemaField(BeaconMeasurementDataSchema, + verbose_name=_('Measurement list')) class Meta: verbose_name = _('Beacon Measurement') diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index 6f5f1449..8d9ad3e8 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -12,16 +12,16 @@ from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.schemas.models import CustomLocationSchema from c3nav.mapdata.utils.cache.stats import increment_cache_key from c3nav.routing.locator import Locator -from c3nav.routing.schemas import LocateRequestWifiPeerSchema, LocateRequestIBeaconPeerSchema +from c3nav.routing.schemas import LocateWifiPeerSchema, LocateIBeaconPeerSchema positioning_api_router = APIRouter(tags=["positioning"]) class LocateRequestSchema(BaseSchema): - wifi_peers: list[LocateRequestWifiPeerSchema] = APIField( + wifi_peers: list[LocateWifiPeerSchema] = APIField( title="list of visible/measured wifi location beacons", ) - ibeacon_peers: list[LocateRequestIBeaconPeerSchema] = APIField( + ibeacon_peers: list[LocateIBeaconPeerSchema] = APIField( title="list of visible/measured location iBeacons", ) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 2c76082c..1dd59efa 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -17,7 +17,7 @@ from c3nav.mapdata.models import MapUpdate, Space from c3nav.mapdata.utils.locations import CustomLocation from c3nav.mesh.utils import get_nodes_and_ranging_beacons from c3nav.routing.router import Router -from c3nav.routing.schemas import LocateRequestWifiPeerSchema +from c3nav.routing.schemas import LocateWifiPeerSchema, BeaconMeasurementDataSchema, LocateIBeaconPeerSchema try: from asgiref.local import Local as LocalContext @@ -117,33 +117,33 @@ class Locator: self.peers.append(peer) return peer_id - def convert_wifi_scan(self, scan_data, create_peers=False) -> ScanData: + def convert_wifi_scan(self, scan_data: list[LocateWifiPeerSchema], create_peers=False) -> ScanData: result = {} for scan_value in scan_data: - if settings.WIFI_SSIDS and scan_value['ssid'] not in settings.WIFI_SSIDS: + if settings.WIFI_SSIDS and scan_value.ssid not in settings.WIFI_SSIDS: continue - peer_id = self.get_peer_id(scan_value['bssid'], create=create_peers) + peer_id = self.get_peer_id(scan_value.bssid, create=create_peers) if peer_id is not None: - result[peer_id] = ScanDataValue(rssi=scan_value["rssi"], distance=scan_value.get("distance", None)) + result[peer_id] = ScanDataValue(rssi=scan_value.rssi, distance=scan_value.get("distance", None)) return result - def convert_ibeacon_scan(self, scan_data, create_peers=False) -> ScanData: + def convert_ibeacon_scan(self, scan_data: list[LocateIBeaconPeerSchema], create_peers=False) -> ScanData: result = {} for scan_value in scan_data: peer_id = self.get_peer_id( - (scan_value['uuid'], scan_value['major'], scan_value['minor']), + (scan_value.uuid, scan_value.major, scan_value.minor), create=create_peers ) if peer_id is not None: - result[peer_id] = ScanDataValue(ibeacon_range=scan_value["distance"]) + result[peer_id] = ScanDataValue(ibeacon_range=scan_value.distance) return result - def convert_scans(self, scans_data, create_peers=False) -> ScanData: + def convert_scans(self, scans_data: BeaconMeasurementDataSchema, create_peers=False) -> ScanData: converted = [] - for scan in scans_data.get("wifi", []): + for scan in scans_data.wifi: converted.append(self.convert_wifi_scan(scan, create_peers=create_peers)) - for scan in scans_data.get("ibeacon", []): + for scan in scans_data.ibeacon: converted.append(self.convert_ibeacon_scan(scan, create_peers=create_peers)) peer_ids = reduce(operator.or_, (frozenset(values.keys()) for values in converted), frozenset()) @@ -176,7 +176,7 @@ class Locator: cls.cached.data = cls.load_nocache(update) return cls.cached.data - def convert_raw_scan_data(self, raw_scan_data: list[LocateRequestWifiPeerSchema]) -> ScanData: + def convert_raw_scan_data(self, raw_scan_data: list[LocateWifiPeerSchema]) -> ScanData: return self.convert_wifi_scan(raw_scan_data, create_peers=False) def get_xyz(self, identifier: LocatorPeerIdentifier) -> tuple[int, int, int] | None: @@ -191,7 +191,7 @@ class Locator: if isinstance(peer.identifier, MacAddress) } - def locate(self, raw_scan_data: list[LocateRequestWifiPeerSchema], permissions=None): + def locate(self, raw_scan_data: list[LocateWifiPeerSchema], permissions=None): # todo: support for ibeacons scan_data = self.convert_raw_scan_data(raw_scan_data) if not scan_data: diff --git a/src/c3nav/routing/schemas.py b/src/c3nav/routing/schemas.py index d5a1afdc..b76d035f 100644 --- a/src/c3nav/routing/schemas.py +++ b/src/c3nav/routing/schemas.py @@ -10,7 +10,7 @@ from pydantic_extra_types.mac_address import MacAddress from c3nav.api.schema import BaseSchema -class LocateRequestWifiPeerSchema(BaseSchema): +class LocateWifiPeerSchema(BaseSchema): bssid: MacAddress = APIField( title="BSSID", description="BSSID of the peer", @@ -25,6 +25,7 @@ class LocateRequestWifiPeerSchema(BaseSchema): title="RSSI", description="RSSI in dBm", example=-42, + validation_alias="level", # App version < 4.2.4 use level instead fo rssi ) frequency: Union[ PositiveInt, @@ -64,7 +65,7 @@ class LocateRequestWifiPeerSchema(BaseSchema): ) -class LocateRequestIBeaconPeerSchema(BaseSchema): +class LocateIBeaconPeerSchema(BaseSchema): uuid: UUID = APIField( title="UUID", description="UUID of the iBeacon", @@ -82,3 +83,8 @@ class LocateRequestIBeaconPeerSchema(BaseSchema): last_seen_ago: NonNegativeInt = APIField( title="how many milliseconds ago this beacon was last seen" ) + + +class BeaconMeasurementDataSchema(BaseSchema): + wifi: list[list[LocateWifiPeerSchema]] = [] + ibeacon: list[list[LocateIBeaconPeerSchema]] = [] \ No newline at end of file