From 182c42de886ec22c4cfe0d4e584ddec4181e7465 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Fri, 15 Aug 2025 18:44:37 +0000 Subject: [PATCH] feat: Implement `search` API for `owntracks` geo-fence time ranges Co-authored-by: aider (gemini/gemini-2.5-pro) --- main.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 72eb327..90e006b 100644 --- a/main.py +++ b/main.py @@ -529,19 +529,89 @@ async def history(request): async def search(request): + api_key = request.rel_url.query.get('api_key', '') + authed = api_key == settings.SENSORS_API_KEY + measurement = request.match_info.get('measurement') name = request.match_info.get('name') - params = request.rel_url.query + + if not authed: + return web.json_response([]) + + if name not in [x.name for x in sensors.sensors]: + raise + + if measurement != 'owntracks': + return web.json_response({'error': 'not implemented for this measurement'}, status=400) try: post_data = await request.json() except json.JSONDecodeError: - post_data = await request.post() + return web.json_response({'error': 'invalid json'}, status=400) + params = request.rel_url.query logging.info('Search request: meas=%s, name=%s, params=%s, data=%s', measurement, name, params, post_data) - return web.json_response({}) + areas = post_data.get('areas') + if not areas or not isinstance(areas, list): + return web.json_response({'error': 'invalid areas format'}, status=400) + + try: + for area in areas: + _ = area['southWest']['lat'] + _ = area['southWest']['lng'] + _ = area['northEast']['lat'] + _ = area['northEast']['lng'] + except (KeyError, TypeError): + return web.json_response({'error': 'invalid area format in areas list'}, status=400) + + client = sensors_client + q = 'select "lat", "lon" from owntracks where "acc" < 100 and "name" = \'{}\' order by time asc'.format(name) + points = list(client.query(q).get_points()) + + ranges = [] + current_range = None + + for point in points: + if point.get('lat') is None or point.get('lon') is None: + continue + + is_inside = False + for area in areas: + sw = area['southWest'] + ne = area['northEast'] + if sw['lat'] <= point['lat'] <= ne['lat'] and sw['lng'] <= point['lon'] <= ne['lng']: + is_inside = True + break + + # InfluxDB time format can vary. It's UTC (Z suffix). + point_time_str = point['time'] + if '.' in point_time_str: + point_dt = datetime.strptime(point_time_str, '%Y-%m-%dT%H:%M:%S.%fZ') + else: + point_dt = datetime.strptime(point_time_str, '%Y-%m-%dT%H:%M:%SZ') + + if is_inside: + if current_range is None: + current_range = {'start': point_dt, 'end': point_dt} + else: + current_range['end'] = point_dt + else: + if current_range is not None: + ranges.append({ + 'start': int(current_range['start'].timestamp()), + 'end': int(current_range['end'].timestamp()) + }) + current_range = None + + if current_range is not None: + ranges.append({ + 'start': int(current_range['start'].timestamp()), + 'end': int(current_range['end'].timestamp()) + }) + + return web.json_response(ranges) async def options_handler(request):