reverse geocode async

This commit is contained in:
2026-04-04 10:15:00 -04:00
parent c395492583
commit 5b04a6aac4
6 changed files with 505 additions and 102 deletions

View File

@@ -11,9 +11,7 @@ import random
import math
import socketio
from contextlib import asynccontextmanager, suppress
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
from typing import Optional, Dict
from dotenv import load_dotenv
with warnings.catch_warnings():
@@ -21,7 +19,7 @@ with warnings.catch_warnings():
warnings.simplefilter("ignore", category=UserWarning)
import fastapi
import uvicorn
from fastapi import FastAPI
from fastapi import FastAPI, APIRouter
from fastapi.encoders import jsonable_encoder
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
@@ -56,8 +54,14 @@ from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider
from pymobiledevice3.tunneld.server import TunneldCore, TunnelTask
from .icloud_monitor import FindMyMonitor
from .models import *
from .models import (
SimulationRequestData,
SimulationRequestResponseData,
iCloudLocationData,
LatLng
)
from .json_formatter import JsonFormatter, handler, root_logger, logger
from .geo_cache import AsyncReverseGeocoder
load_dotenv()
@@ -88,7 +92,8 @@ DVT_CONNECT_TIMEOUT_SECONDS = 20
class LocationSimulationState:
def __init__(self):
self.connected_clients: set[str] = set()
self.current_location: Optional[Dict[str, SimulationRequestResponseData]] = None
self.current_location: Optional[Dict[str,
SimulationRequestResponseData]] = None
self.device_name: Optional[str] = os.getenv("SELECTED_DEVICE_NAME")
self.fmf_location: Optional[iCloudLocationData] = None
self.fmf_queue: asyncio.Queue = asyncio.Queue()
@@ -115,8 +120,7 @@ class LocationSimulationState:
self.test_mode: bool = os.getenv("TEST_MODE", "True").lower() == "true"
self.tunnel: Optional[RemoteServiceDiscoveryService] = None
self.udid: Optional[str] = None
self.geolocator = Nominatim(user_agent="pymd3_location_sim")
self.reverse_geocode = RateLimiter(self.geolocator.reverse, min_delay_seconds=1)
self.reverse_geocode = AsyncReverseGeocoder()
class TunneldRunnerSio:
@@ -134,7 +138,17 @@ class TunneldRunnerSio:
usbmux_monitor: bool = True,
mobdev2_monitor: bool = True,
) -> None:
instance = cls(
# instance = cls(
# host,
# port,
# protocol=protocol,
# usb_monitor=usb_monitor,
# wifi_monitor=wifi_monitor,
# usbmux_monitor=usbmux_monitor,
# mobdev2_monitor=mobdev2_monitor,
# context=context,)
# asyncio.run(instance._run_app())
cls(
host,
port,
protocol=protocol,
@@ -142,8 +156,7 @@ class TunneldRunnerSio:
wifi_monitor=wifi_monitor,
usbmux_monitor=usbmux_monitor,
mobdev2_monitor=mobdev2_monitor,
context=context,)
asyncio.run(instance._run_app())
context=context)._run_app()
def __init__(
self,
@@ -176,8 +189,8 @@ class TunneldRunnerSio:
"127.0.0.1" if host in ("0.0.0.0", "::") else host,
port,
)
self._vue_app = FastAPI()
self._app = FastAPI(
self._app = APIRouter()
self._vue_app = FastAPI(
title="iOS Device Management API",
lifespan=lifespan,
cors_allowed_origins="*",
@@ -194,6 +207,7 @@ class TunneldRunnerSio:
usbmux_monitor=usbmux_monitor,
mobdev2_monitor=mobdev2_monitor,
)
async def get_tun(
udid: Optional[str] = None, max_retries: int = 10, retry_delay: float = 0.5
) -> RemoteServiceDiscoveryService:
@@ -231,7 +245,8 @@ class TunneldRunnerSio:
)
try:
rsd = RemoteServiceDiscoveryService((tunnel.address, tunnel.port))
rsd = RemoteServiceDiscoveryService(
(tunnel.address, tunnel.port))
await rsd.connect()
logger.info(
"Connected to local tunnel at %s:%s after %s retries",
@@ -257,8 +272,6 @@ class TunneldRunnerSio:
raise TunneldConnectionError()
async def get_device_name():
if self.context.tunnel is None:
await get_tun()
@@ -281,31 +294,29 @@ class TunneldRunnerSio:
return active
async def handle_tunnel_drop(disconnected: list[dict]) -> None:
disconnected_udids = {d.get("udid") for d in disconnected if d.get("udid")}
disconnected_udids = {d.get("udid")
for d in disconnected if d.get("udid")}
if not disconnected_udids:
return
current_active = collect_active_tunnels()
active_udids = {d.get("udid") for d in current_active.values() if d.get("udid")}
active_udids = {d.get("udid")
for d in current_active.values() if d.get("udid")}
primary_disconnected = self.context.udid in disconnected_udids if self.context.udid else False
no_tunnels_left = len(current_active) == 0
if not primary_disconnected and not no_tunnels_left:
return
logger.warning(
"Tunnel drop detected. primary_udid=%s disconnected_udids=%s active_udids=%s",
self.context.udid,
sorted(disconnected_udids),
sorted(active_udids),
)
if self.context.tunnel is not None:
with suppress(Exception):
await self.context.tunnel.close()
self.context.tunnel = None
if no_tunnels_left:
self.context.udid = None
if self.context.simulation_active:
await pause_simulation_queue()
await self.context.sio.emit(
@@ -337,10 +348,8 @@ class TunneldRunnerSio:
current = collect_active_tunnels()
added_keys = set(current.keys()) - set(previous.keys())
removed_keys = set(previous.keys()) - set(current.keys())
added = [current[k] for k in sorted(added_keys)]
removed = [previous[k] for k in sorted(removed_keys)]
for item in added:
logger.info(
"Tunnel discovered interface=%s udid=%s address=%s port=%s transport=%s",
@@ -351,7 +360,8 @@ class TunneldRunnerSio:
item.get("transport"),
)
await safe_sio_emit("tunnel_device_connected", item)
logger.info(f"Current udid: %s, new udid: %s", self.context.udid, item.get("udid"))
logger.info(f"Current udid: %s, new udid: %s",
self.context.udid, item.get("udid"))
if self.context.udid is None:
self.context.udid = item.get("udid")
device_name = await get_device_name()
@@ -438,7 +448,8 @@ class TunneldRunnerSio:
)
data = {"status": "started", "message": "Simulation started"}
else:
data = {"status": "error", "message": "Simulation already running"}
data = {"status": "error",
"message": "Simulation already running"}
return data
async def start_queue_worker():
@@ -568,7 +579,7 @@ class TunneldRunnerSio:
try:
delay = parse_delay_seconds(delay)
except ValueError as e:
return {"status": "error","command": "add", "message": str(e) }
return {"status": "error", "command": "add", "message": str(e)}
if latitude is not None and longitude is not None:
logger.info(
"Adding location %s (%s, %s) with %s delay to the queue",
@@ -579,22 +590,24 @@ class TunneldRunnerSio:
)
accrued_delay = 0
if self.context.simulation_queue_data and len(self.context.simulation_queue_order) > 1:
current_index = get_item_index(self.context.loc_id) if self.context.loc_id else 0
current_index = get_item_index(
self.context.loc_id) if self.context.loc_id else 0
remaining_items = self.context.simulation_queue_order[current_index + 1:]
accrued_delay = sum(parse_delay_seconds(self.context.simulation_queue_data[loc_id]['delay']) for loc_id in remaining_items if loc_id in self.context.simulation_queue_data)
accrued_delay = accrued_delay + parse_delay_seconds(self.context.next_move)
accrued_delay = sum(parse_delay_seconds(
self.context.simulation_queue_data[loc_id]['delay']) for loc_id in remaining_items if loc_id in self.context.simulation_queue_data)
accrued_delay = accrued_delay + \
parse_delay_seconds(self.context.next_move)
now_time = datetime.now(timezone.utc)
new_time = (
now_time
+ timedelta(seconds=accrued_delay)
+ timedelta(seconds=delay)
now_time
+ timedelta(seconds=accrued_delay)
+ timedelta(seconds=delay)
)
start_time = new_time.isoformat()
coords = f"{latitude}, {longitude}"
rev_geocode = self.context.reverse_geocode(coords)
rev_geocode = await self.context.reverse_geocode.get_address(latitude, longitude)
if rev_geocode:
address = rev_geocode.address
address = rev_geocode
else:
address = f"{latitude}, {longitude}"
location_item = {
@@ -616,7 +629,8 @@ class TunneldRunnerSio:
logger.info("Location %s added to the queue", loc_id)
else:
logger.error("Invalid location data: %s", data)
resp = {"status": "error", "message": "Invalid location data", "data": data}
resp = {"status": "error",
"message": "Invalid location data", "data": data}
return resp
async def start_icloud_monitor():
@@ -730,9 +744,11 @@ class TunneldRunnerSio:
new_delay = self.context.next_move or 0
now_time = datetime.now(timezone.utc)
for item in remaining_items:
item_delay = parse_delay_seconds(self.context.simulation_queue_data[item].get("delay")) or 0
item_delay = parse_delay_seconds(
self.context.simulation_queue_data[item].get("delay")) or 0
new_delay += item_delay
new_time = (now_time + timedelta(seconds=new_delay)).isoformat()
new_time = (
now_time + timedelta(seconds=new_delay)).isoformat()
self.context.simulation_queue_data[item].start = new_time
update_queue_data()
@@ -746,8 +762,8 @@ class TunneldRunnerSio:
"worker_task": self.context.simulation_task.get_name() if self.context.simulation_task else None,
}
}
self.context.sio.emit("queue_data_update", { "data": data }, namespace="/" )
self.context.sio.emit("queue_data_update", {
"data": data}, namespace="/")
async def empty_simulation_queue():
"""Empties all items from an asyncio.Queue."""
@@ -763,14 +779,13 @@ class TunneldRunnerSio:
break
clear_items()
def add_item(item_id, payload):
self.context.simulation_queue_data[item_id] = payload
self.context.simulation_queue_order.append(item_id)
def remove_item(item_id):
if item_id in self.context.simulation_queue_order:
# self.context.simulation_queue_order.remove(item_id)
# self.context.simulation_queue_order.remove(item_id)
self.context.simulation_queue_data[item_id]["status"] = "deleted"
def clear_item(item_id):
@@ -843,7 +858,8 @@ class TunneldRunnerSio:
try:
await asyncio.wait_for(self.context.simulation_task, timeout=5)
except TimeoutError:
logger.warning("Simulation worker did not stop in time; canceling task")
logger.warning(
"Simulation worker did not stop in time; canceling task")
self.context.simulation_task.cancel()
with suppress(asyncio.CancelledError):
await self.context.simulation_task
@@ -873,10 +889,11 @@ class TunneldRunnerSio:
def toggle_test_mode() -> dict:
self.context.test_mode = not self.context.test_mode
return {"test_mode": self.context.test_mode }
return {"test_mode": self.context.test_mode}
def get_status() -> dict:
current_item = self.context.simulation_queue_data.get(self.context.loc_id) if self.context.loc_id else None
current_item = self.context.simulation_queue_data.get(
self.context.loc_id) if self.context.loc_id else None
current_start = (
current_item.get("start")
if isinstance(current_item, dict)
@@ -893,11 +910,11 @@ class TunneldRunnerSio:
"device_name": self.context.device_name,
"fmf_location": self.context.fmf_location,
"icloud": {
"consumer_queue": self.context.fmf_queue.qsize() if self.context.fmf_queue else 0,
"consumer_task": self.context.icloud_consumer_task.done() if self.context.icloud_consumer_task else False,
"monitor_task": self.context.icloud_monitor_task.done() if self.context.icloud_monitor_task else False,
"monitor_enabled": self.context.icloud_monitor_enabled,
"monitor_running": is_icloud_monitor_running(),
"consumer_queue": self.context.fmf_queue.qsize() if self.context.fmf_queue else 0,
"consumer_task": self.context.icloud_consumer_task.done() if self.context.icloud_consumer_task else False,
"monitor_task": self.context.icloud_monitor_task.done() if self.context.icloud_monitor_task else False,
"monitor_enabled": self.context.icloud_monitor_enabled,
"monitor_running": is_icloud_monitor_running(),
},
"tunnel_watcher_running": True if self.context.tunnel_watcher_task and not self.context.tunnel_watcher_task.done() else False,
"next_move": self.context.next_move,
@@ -916,6 +933,7 @@ class TunneldRunnerSio:
return data
import numpy as np
def add_gps_noise(lat, lon, std_dev_meters=5):
"""
Simulates GPS noise by adding Gaussian noise to coordinates.
@@ -927,7 +945,8 @@ class TunneldRunnerSio:
# Convert meters to degrees
lat_diff = (std_dev_meters / earth_radius) * (180 / np.pi)
lon_diff = (std_dev_meters / (earth_radius * np.cos(np.radians(lat)))) * (180 / np.pi)
lon_diff = (std_dev_meters / (earth_radius *
np.cos(np.radians(lat)))) * (180 / np.pi)
# Generate Gaussian noise
noised_lat = lat + np.random.normal(0, lat_diff)
@@ -935,7 +954,6 @@ class TunneldRunnerSio:
return noised_lat, noised_lon
""" FastAPI HTTP Functions"""
def generate_http_response(
@@ -947,7 +965,6 @@ class TunneldRunnerSio:
content=json.dumps(jsonable_encoder(data)),
)
""" Device Functions """
@self._app.get("/device/name")
@@ -956,7 +973,6 @@ class TunneldRunnerSio:
device_name = await get_device_name()
return generate_http_response(device_name)
@self._app.get("/device/info")
async def device_info():
"""Get device information"""
@@ -970,10 +986,12 @@ class TunneldRunnerSio:
lockdown = await create_using_usbmux(
serial=active_tunnel.udid, autopair=False
)
tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values)
tunnels[active_tunnel.udid] = iterate_multidim(
lockdown.all_values)
except Exception as e:
logger.error(
f"Failed to create lockdown session for device {active_tunnel.udid}: {e}"
f"Failed to create lockdown session for device {
active_tunnel.udid}: {e}"
)
continue
return tunnels
@@ -1001,7 +1019,8 @@ class TunneldRunnerSio:
async def shutdown() -> fastapi.Response:
"""Shutdown Tunneld"""
os.kill(os.getpid(), signal.SIGINT)
data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."}
data = {"operation": "shutdown", "data": True,
"message": "Server shutting down..."}
return generate_http_response(data)
@self._app.get("/clear-tunnels")
@@ -1027,7 +1046,6 @@ class TunneldRunnerSio:
}
return generate_http_response(data)
@self._app.get("/hello")
async def hello() -> fastapi.Response:
data = {"message": "Hello, I'm alive"}
@@ -1157,11 +1175,8 @@ class TunneldRunnerSio:
}
return generate_http_response(data)
""" iCloud Monitor"""
@self._app.get("/icloud-monitor/start")
async def app_start_icloud_monitor() -> fastapi.Response:
await start_icloud_monitor()
@@ -1196,7 +1211,6 @@ class TunneldRunnerSio:
data = await refresh_icloud_location()
return generate_http_response(data)
"""Simulation Functions"""
""" start, add, clear, pause, resume, end, status """
@@ -1205,7 +1219,6 @@ class TunneldRunnerSio:
data = await start_simulation_queue()
return generate_http_response(data)
@self._app.post("/simulation/add")
async def app_add_location(data: SimulationRequestData) -> fastapi.Response:
"""Add a location to the simulation queue"""
@@ -1247,14 +1260,14 @@ class TunneldRunnerSio:
before_toggle = self.context.test_mode
data = toggle_test_mode()
data['status'] = "OK"
data['message'] = f"Test mode toggled from {before_toggle} to {self.context.test_mode}"
data['message'] = f"Test mode toggled from {
before_toggle} to {self.context.test_mode}"
return generate_http_response(data)
"""Status Functions"""
@self._app.get("/status/rsd")
async def rsd_info():
async def app_rsd_info():
"""Get rsd information"""
rsd_info = {}
if self.context.tunnel is None:
@@ -1263,15 +1276,29 @@ class TunneldRunnerSio:
rsd_info = self.context.tunnel.peer_info
return generate_http_response(rsd_info)
@self._app.get("/status/context")
async def app_context_status() -> fastapi.Response:
data = get_status()
return generate_http_response(data)
@self._app.post("/rev_geocode")
async def app_proxy_osm(data: LatLng):
logger.info("OSM Proxy Request, data: %s", data)
latitude = (
data.get("latitude")
if isinstance(data, dict)
else getattr(data, "latitude", None)
)
longitude = (
data.get("longitude")
if isinstance(data, dict)
else getattr(data, "longitude", None)
)
if latitude and longitude:
coords = f"{latitude}, {longitude}"
rev_geocode = await self.context.reverse_geocode.get_address(latitude, longitude)
logger.info("Reverse Geocoded %s to %s", coords, rev_geocode)
return generate_http_response(rev_geocode)
""" Socket.IO Functions"""
@@ -1302,7 +1329,8 @@ class TunneldRunnerSio:
@self.context.sio.event
async def request_update(sid):
status_update = jsonable_encoder(get_status())
logger.info("Update request from %s sending %s", sid, status_update)
logger.info("Update request from %s sending %s",
sid, status_update)
return status_update
@self.context.sio.event
@@ -1413,12 +1441,14 @@ class TunneldRunnerSio:
}
case "end":
""" End the simulation queue"""
logger.info("End location simulation request from %s", sid)
logger.info(
"End location simulation request from %s", sid)
data = await end_simulation_queue()
return data
case "start":
""" Start the simulation queue"""
logger.info("Start location simulation request from %s", sid)
logger.info(
"Start location simulation request from %s", sid)
data = await start_simulation_queue()
return data
case _:
@@ -1486,7 +1516,8 @@ class TunneldRunnerSio:
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info("Tunneld Control command: %s requested from %s", command, sid)
logger.info(
"Tunneld Control command: %s requested from %s", command, sid)
match command:
case "start":
"""Start Tunneld"""
@@ -1508,7 +1539,8 @@ class TunneldRunnerSio:
case "start-watcher":
""" Start Tunneld Watcher """
logger.info("Start tunneld watcher request from %s: %s", sid, data)
logger.info(
"Start tunneld watcher request from %s: %s", sid, data)
await start_tunnel_watcher()
return {
"status": "running",
@@ -1517,7 +1549,8 @@ class TunneldRunnerSio:
case "end-watcher":
""" End Tunneld Watcher """
logger.info("End tunneld watcher request from %s: %s", sid, data)
logger.info(
"End tunneld watcher request from %s: %s", sid, data)
await end_tunnel_watcher()
return {
"status": "stopped",
@@ -1526,7 +1559,8 @@ class TunneldRunnerSio:
case "shutdown":
"""Shutdown Tunneld"""
logger.info("Shutdown tunneld request from %s: %s", sid, data)
logger.info(
"Shutdown tunneld request from %s: %s", sid, data)
try:
os.kill(os.getpid(), signal.SIGINT)
return {
@@ -1562,7 +1596,8 @@ class TunneldRunnerSio:
case "cancel":
"""Cancel a tunnel"""
logger.info("Canceling tunnel request from %s: %s", sid, data)
logger.info(
"Canceling tunnel request from %s: %s", sid, data)
try:
udid = (
data.get("udid")
@@ -1593,21 +1628,22 @@ class TunneldRunnerSio:
"message": f"Unknown operation: {command}",
}
self._vue_app.include_router(self._app, prefix="/api")
self._vue_app.mount(
"/assets", StaticFiles(directory="../../front-end/dist/spa/assets", html=True), name="vue")
self._vue_app.mount("/assets", StaticFiles(directory="../../front-end/dist/spa/assets", html=True), name="vue")
@self._vue_app.get("/{full_path:path}")
async def serve_vue_app(full_path: str):
async def serve_vue_app(full_path: str):
return FileResponse("../../front-end/dist/spa/index.html")
def _run_app(self) -> None:
# api = uvicorn.Server(uvicorn.Config(self._app, host=self.host, port=49151, log_level="info"))
# vue = uvicorn.Server(uvicorn.Config(self._asgi_app, host=self.host, port=8087, log_level="info"))
# await asyncio.gather(api.serve(), vue.serve())
async def _run_app(self) -> None:
api = uvicorn.Server(uvicorn.Config(self._app, host=self.host, port=49151, log_level="info"))
vue = uvicorn.Server(uvicorn.Config(self._asgi_app, host=self.host, port=8087, log_level="info"))
await asyncio.gather(api.serve(), vue.serve())
# uvicorn.run(
# self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1
# )
uvicorn.run(
self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1
)
class LocationSimulationQueue(LocationSimulation):
@@ -1625,7 +1661,8 @@ class LocationSimulationQueue(LocationSimulation):
cos_lat = math.cos(math.radians(lat))
if abs(cos_lat) < 1e-6:
cos_lat = 1e-6
lon_sigma_deg = (std_dev_meters / (earth_radius * cos_lat)) * (180.0 / math.pi)
lon_sigma_deg = (std_dev_meters / (earth_radius *
cos_lat)) * (180.0 / math.pi)
noised_lat = lat + random.gauss(0.0, lat_sigma_deg)
noised_lon = lon + random.gauss(0.0, lon_sigma_deg)
return noised_lat, noised_lon
@@ -1699,7 +1736,8 @@ class LocationSimulationQueue(LocationSimulation):
new_delay = 0 if new_delay is None else new_delay
new_start = location_item.get("start")
current_location_item = self.context.simulation_queue_data.get(self.context.loc_id)
current_location_item = self.context.simulation_queue_data.get(
self.context.loc_id)
current_latitude = (
current_location_item.get("latitude")
if isinstance(current_location_item, dict)
@@ -1750,9 +1788,11 @@ class LocationSimulationQueue(LocationSimulation):
if self.context.simulation_queue_state == "SHUTDOWN":
self.context.simulation_queue.task_done()
break
self.context.simulation_queue_data[loc_id]["start"] = datetime.now(timezone.utc).isoformat()
self.context.simulation_queue_data[loc_id]["start"] = datetime.now(
timezone.utc).isoformat()
if self.context.loc_id is not None:
self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat()
self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now(
timezone.utc).isoformat()
update_queue_data()
await self._stop_noise_task()
@@ -1761,7 +1801,8 @@ class LocationSimulationQueue(LocationSimulation):
self.context.latitude = new_latitude
self.context.longitude = new_longitude
if self.context.simulation_noise:
self._start_noise_task(loc_id, new_latitude, new_longitude)
self._start_noise_task(
loc_id, new_latitude, new_longitude)
await self.context.sio.emit(
"simulation_status",
{
@@ -1850,7 +1891,8 @@ class LocationSimulationTestQueue(LocationSimulationBase):
new_delay = 0 if new_delay is None else new_delay
new_start = location_item.get("start")
current_location_item = self.context.simulation_queue_data.get(self.context.loc_id)
current_location_item = self.context.simulation_queue_data.get(
self.context.loc_id)
current_latitude = (
current_location_item.get("latitude")
if isinstance(current_location_item, dict)
@@ -1871,7 +1913,7 @@ class LocationSimulationTestQueue(LocationSimulationBase):
if new_delay > 0 and not disable_sleep:
countdown_delay = int(round(float(new_delay)))
if timing_randomness_range > 0:
delay = new_delay + random.uniform(
new_delay = new_delay + random.uniform(
-timing_randomness_range, timing_randomness_range
)
countdown_delay = int(round(float(new_delay)))
@@ -1901,9 +1943,11 @@ class LocationSimulationTestQueue(LocationSimulationBase):
if self.context.simulation_queue_state == "SHUTDOWN":
self.context.simulation_queue.task_done()
break
self.context.simulation_queue_data[loc_id]["start"] = datetime.now(timezone.utc).isoformat()
self.context.simulation_queue_data[loc_id]["start"] = datetime.now(
timezone.utc).isoformat()
if self.context.loc_id is not None:
self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat()
self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now(
timezone.utc).isoformat()
await self.set(new_latitude, new_longitude)
self.context.loc_id = loc_id
self.context.loc_id = loc_id