use shcmea for beaconmeasurement data and fix some related things

This commit is contained in:
Laura Klünder 2024-12-23 16:26:15 +01:00
parent b45fd961c0
commit 8feac6bf43
6 changed files with 67 additions and 44 deletions

View file

@ -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):

View file

@ -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'),
),
]

View file

@ -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')

View file

@ -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",
)

View file

@ -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:

View file

@ -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]] = []