beacon_type support

This commit is contained in:
Laura Klünder 2024-12-28 14:59:09 +01:00
parent a2b19dad4e
commit ca6252583c
4 changed files with 213 additions and 22 deletions

View file

@ -0,0 +1,138 @@
import hashlib
import requests
from django.conf import settings
from django.core.management.base import BaseCommand
from pydantic import BaseModel
from shapely import distance
from c3nav.mapdata.models import MapUpdate, Space, Level
from c3nav.mapdata.models.geometry.space import RangingBeacon
from c3nav.mapdata.models.report import Report
from c3nav.mapdata.utils.cache.changes import changed_geometries
from c3nav.mapdata.utils.geometry import unwrap_geom
from shapely.ops import nearest_points, unary_union
class NocImportItem(BaseModel):
"""
Something imported from the NOC
"""
lat: float | int
lng: float | int
layer: str
type: str = "unknown"
class Command(BaseCommand):
help = 'import APs from noc'
def handle(self, *args, **options):
r = requests.get(settings.NOC_BASE+"/api/markers/get")
r.raise_for_status()
items = {name: NocImportItem.model_validate(item)
for name, item in r.json()["markers"].items()
if not name.startswith("__polyline")}
with MapUpdate.lock():
changed_geometries.reset()
self.do_import(items)
MapUpdate.objects.create(type='importnoc')
def do_import(self, items: dict[str, NocImportItem]):
spaces_for_level = {}
levels = tuple(Level.objects.values_list("pk", flat=True))
lower_levels_for_level = {pk: levels[:i] for i, pk in enumerate(levels)}
for space in Space.objects.select_related('level').prefetch_related('holes'):
spaces_for_level.setdefault(space.level_id, []).append(space)
beacons_so_far: dict[str, RangingBeacon] = {
**{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="noc:")},
}
for name, item in items.items():
import_tag = f"noc:{name}"
if item.type != "AP":
continue
# determine geometry
converter = settings.NOC_LAYERS.get(item.layer, None)
if not converter:
print(f"ERROR: {name} has invalid layer: {item.layer}")
continue
new_geometry = converter.convert(item.lat, item.lng)
# determine space
possible_spaces = [space for space in spaces_for_level[converter.level_id]
if space.geometry.intersects(new_geometry)]
if not possible_spaces:
possible_spaces = [space for space in spaces_for_level[converter.level_id]
if distance(unwrap_geom(space.geometry), new_geometry) < 0.3]
if len(possible_spaces) == 1:
new_space = possible_spaces[0]
the_distance = distance(unwrap_geom(new_space.geometry), new_geometry)
print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}")
elif len(possible_spaces) == 2:
new_space = min(possible_spaces, key=lambda s: distance(unwrap_geom(s.geometry), new_geometry))
print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}...")
else:
print(f"ERROR: {name} is not within any space (NOC: {(item.lat, item.lng)}, NAV: {new_geometry}")
continue
# move point into space if needed
new_space_geometry = new_space.geometry.difference(
unary_union([unwrap_geom(hole.geometry) for hole in new_space.columns.all()])
)
if not new_space_geometry.intersects(new_geometry):
new_geometry = nearest_points(new_space_geometry.buffer(-0.05), new_geometry)[0]
elif len(possible_spaces) == 1:
new_space = possible_spaces[0]
print(f"SUCCESS: {name} is in {new_space.title}")
else:
print(f"WARNING: {name} could be in multiple spaces, picking one...")
new_space = possible_spaces[0]
lower_levels = lower_levels_for_level[new_space.level_id]
for lower_level in reversed(lower_levels):
# let's go through the lower levels
if not unary_union([unwrap_geom(h.geometry) for h in new_space.holes.all()]).intersects(new_geometry):
# current selected spacae is fine, that's it
break
print(f"NOTE: {name} is in a hole, looking lower...")
# find a lower space
possible_spaces = [space for space in spaces_for_level[lower_level]
if space.geometry.intersects(new_geometry)]
if possible_spaces:
new_space = possible_spaces[0]
print(f"NOTE: {name} moved to lower space {new_space}")
else:
print(f"WARNING: {name} couldn't find a lower space, still in a hole")
# find existing location
result = beacons_so_far.pop(import_tag, None)
# build resulting object
altitude_quest = True
if not result:
result = RangingBeacon(import_tag=import_tag)
else:
if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.03:
continue
if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.20:
altitude_quest = False
result.comment = name
result.space = new_space
result.geometry = new_geometry
result.altitude = 0
if altitude_quest:
result.altitude_quest = True
result.save()
for import_tag, location in beacons_so_far.items():
location.delete()
print(f"NOTE: {import_tag} was deleted")

View file

@ -0,0 +1,24 @@
# Generated by Django 5.0.8 on 2024-12-28 13:26
from django.db import migrations, models
def add_beacon_type(apps, schema_editor):
RangingBeacon = apps.get_model('mapdata', 'rangingbeacon')
RangingBeacon.objects.filter(import_tag__startswith='noc:').update(beacon_type="event_wifi")
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0134_rangingbeacon_ap_name'),
]
operations = [
migrations.AddField(
model_name='rangingbeacon',
name='beacon_type',
field=models.CharField(blank=True, choices=[('event_wifi', 'Event WiFi AP'), ('dect', 'DECT antenna')], max_length=16, null=True, verbose_name='beacon type'),
),
migrations.RunPython(add_beacon_type, migrations.RunPython.noop),
]

View file

@ -484,8 +484,15 @@ class RangingBeacon(SpaceGeometryMixin, models.Model):
"""
A ranging beacon
"""
class BeaconType(models.TextChoices):
EVENT_WIFI = "event_wifi", _("Event WiFi AP")
DECT = "dect", _("DECT antenna")
geometry = GeometryField('point')
beacon_type = models.CharField(_('beacon type'), choices=BeaconType.choices,
null=True, blank=True, max_length=16)
node_number = models.PositiveSmallIntegerField(_('Node Number'), unique=True, null=True, blank=True)
wifi_bssids: list[MacAddress] = SchemaField(list[MacAddress], verbose_name=_('WiFi BSSIDs'), default=list,
@ -535,11 +542,19 @@ class RangingBeacon(SpaceGeometryMixin, models.Model):
@property
def title(self):
if self.node_number is not None or self.wifi_bssids or self.ap_name:
if self.comment:
return (f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])} {self.ap_name or ''} '
f' ({self.comment})').strip()
else:
return f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])} {self.ap_name or ''}'.strip()
segments = []
if self.node_number is not None:
segments.append(self.node_number)
if self.ap_name is not None:
segments.append(f'"{self.ap_name}"')
if segments:
title = ' - '.join(segments).strip()
else:
return self.comment
title = f'#{self.pk}'
if self.wifi_bssids:
ssids = self.wifi_bssids[0] + (', …' if len(self.wifi_bssids) > 1 else '')
title += f' ({ssids})'
if self.comment:
title += f' ({self.comment})'
return f'{self.get_beacon_type_display() if self.beacon_type else self._meta.verbose_name} {title}'

View file

@ -1,9 +1,9 @@
import operator
import pickle
from dataclasses import dataclass, field
from enum import StrEnum
from functools import cached_property, reduce
from pprint import pprint
from typing import Annotated
from typing import Annotated, NamedTuple, Union
from typing import Optional, Self, Sequence, TypeAlias
from uuid import UUID
@ -24,13 +24,25 @@ try:
except ImportError:
from threading import local as LocalContext
LocatorPeerIdentifier: TypeAlias = MacAddress | str | tuple[
UUID, Annotated[NonNegativeInt, Lt(2 ** 16)], Annotated[NonNegativeInt, Lt(2 ** 16)]]
class PeerType(StrEnum):
WIFI = "wifi"
DECT = "dect"
IBEACON = "ibeacon"
class TypedIdentifier(NamedTuple):
peer_type: PeerType
identifier: Union[
MacAddress,
str,
tuple[UUID, Annotated[NonNegativeInt, Lt(2 ** 16)], Annotated[NonNegativeInt, Lt(2 ** 16)]]
]
@dataclass
class LocatorPeer:
identifier: LocatorPeerIdentifier
identifier: TypedIdentifier
frequencies: set[int] = field(default_factory=set)
xyz: Optional[tuple[int, int, int]] = None
space_id: Optional[int] = None
@ -67,7 +79,7 @@ class LocatorPoint:
@dataclass
class Locator:
peers: list[LocatorPeer] = field(default_factory=list)
peer_lookup: dict[LocatorPeerIdentifier, int] = field(default_factory=dict)
peer_lookup: dict[TypedIdentifier, int] = field(default_factory=dict)
xyz: np.array = field(default_factory=(lambda: np.empty((0,))))
spaces: dict[int, "LocatorSpace"] = field(default_factory=dict)
@ -83,9 +95,11 @@ class Locator:
for beacon in calculated.beacons.values():
identifiers = []
for bssid in beacon.wifi_bssids:
identifiers.append(bssid)
identifiers.append(TypedIdentifier(PeerType.WIFI, bssid))
if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None:
identifiers.append((beacon.ibeacon_uuid, beacon.ibeacon_major, beacon.ibeacon_minor))
identifiers.append(
TypedIdentifier(PeerType.IBEACON, (beacon.ibeacon_uuid, beacon.ibeacon_major, beacon.ibeacon_minor))
)
for identifier in identifiers:
peer_id = self.get_peer_id(identifier, create=True)
self.peers[peer_id].xyz = (
@ -111,7 +125,7 @@ class Locator:
if new_space.points:
self.spaces[space.pk] = new_space
def get_peer_id(self, identifier: LocatorPeerIdentifier, create=False) -> Optional[int]:
def get_peer_id(self, identifier: TypedIdentifier, create=False) -> Optional[int]:
peer_id = self.peer_lookup.get(identifier, None)
if peer_id is None and create:
peer = LocatorPeer(identifier=identifier)
@ -126,9 +140,9 @@ class Locator:
if settings.WIFI_SSIDS and scan_value.ssid not in settings.WIFI_SSIDS:
continue
peer_ids = {
self.get_peer_id(scan_value.bssid, create=create_peers),
self.get_peer_id(scan_value.ap_name, create=create_peers),
} - {None, ""}
self.get_peer_id(TypedIdentifier(PeerType.WIFI, scan_value.bssid), create=create_peers),
self.get_peer_id(TypedIdentifier(PeerType.WIFI, scan_value.ap_name), create=create_peers),
} - {None, ""}
for peer_id in peer_ids:
result[peer_id] = ScanDataValue(rssi=scan_value.rssi, distance=scan_value.distance)
return result
@ -137,7 +151,7 @@ class Locator:
result = {}
for scan_value in scan_data:
peer_id = self.get_peer_id(
(scan_value.uuid, scan_value.major, scan_value.minor),
TypedIdentifier(PeerType.IBEACON, (scan_value.uuid, scan_value.major, scan_value.minor)),
create=create_peers
)
if peer_id is not None:
@ -185,13 +199,13 @@ class Locator:
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:
def get_xyz(self, identifier: TypedIdentifier) -> tuple[int, int, int] | None:
i = self.get_peer_id(identifier)
if i is None:
return None
return self.peers[i].xyz
def get_all_nodes_xyz(self) -> dict[LocatorPeerIdentifier, tuple[float, float, float]]:
def get_all_nodes_xyz(self) -> dict[TypedIdentifier, tuple[float, float, float]]:
return {
peer.identifier: peer.xyz for peer in self.peers[:len(self.xyz)]
if isinstance(peer.identifier, MacAddress)