From b21b509c8425749a53af35054b6fc5f08c813347 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Tue, 27 Aug 2024 16:34:04 +0200 Subject: [PATCH] Add location recorder --- .gitignore | 3 +- requirements.txt | 2 ++ src/main.py | 8 ++++- src/util/homeassistant.py | 23 +++++++++++--- src/util/location_recorder.py | 60 +++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 src/util/location_recorder.py diff --git a/.gitignore b/.gitignore index 6db97bd..8a375dd 100644 --- a/.gitignore +++ b/.gitignore @@ -149,4 +149,5 @@ dmypy.json # Cython debug symbols cython_debug/ -temp_data/ \ No newline at end of file +temp_data/ +*.db \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 10ddf12..c93f517 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ fastapi==0.112.1 fastapi-cli==0.0.5 fastapi-mqtt==2.2.0 gmqtt==0.6.16 +greenlet==3.0.3 h11==0.14.0 httpcore==1.0.5 httptools==0.6.1 @@ -34,6 +35,7 @@ requests==2.32.3 rich==13.7.1 shellingham==1.5.4 sniffio==1.3.1 +SQLAlchemy==2.0.32 starlette==0.38.2 typer==0.12.4 typing_extensions==4.12.2 diff --git a/src/main.py b/src/main.py index 257a8f8..f4d3ce5 100644 --- a/src/main.py +++ b/src/main.py @@ -1,26 +1,32 @@ from contextlib import asynccontextmanager +from pathlib import Path from fastapi import FastAPI from src.components.poo_recorder import PooRecorder from src.config import Config from src.util.homeassistant import HomeAssistant +from src.util.location_recorder import LocationRecorder from src.util.mqtt import MQTT from src.util.notion import NotionAsync from src.util.ticktick import TickTick Config.init() +location_recorder_db = str(Path(__file__).resolve().parent / ".." / "location_recorder.db") + ticktick = TickTick() notion = NotionAsync(token=Config.get_env(key="NOTION_TOKEN")) mqtt = MQTT() -homeassistant = HomeAssistant(ticktick=ticktick) +location_recorder = LocationRecorder(db_path=location_recorder_db) +homeassistant = HomeAssistant(ticktick=ticktick, location_recorder=location_recorder) poo_recorder = PooRecorder(mqtt=mqtt, notion=notion, homeassistant=homeassistant) @asynccontextmanager async def _lifespan(_app: FastAPI): # noqa: ANN202 await mqtt.start() + await location_recorder.create_db_engine() yield await mqtt.stop() diff --git a/src/util/homeassistant.py b/src/util/homeassistant.py index 968ee2b..d43b54a 100644 --- a/src/util/homeassistant.py +++ b/src/util/homeassistant.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import ast from datetime import datetime, timedelta, timezone @@ -7,6 +5,7 @@ import httpx from pydantic import BaseModel from src.config import Config +from src.util.location_recorder import LocationData, LocationRecorder from src.util.ticktick import TickTick @@ -16,13 +15,15 @@ class HomeAssistant: action: str content: str - def __init__(self, ticktick: TickTick) -> None: + def __init__(self, ticktick: TickTick, location_recorder: LocationRecorder) -> None: self._ticktick = ticktick + self._location_recorder = location_recorder async def process_message(self, message: Message) -> dict[str, str]: if message.target == "ticktick": return await self._process_ticktick_message(message=message) - + if message.target == "location_recorder": + return await self._process_location(message=message) return {"Status": "Unknown target"} async def trigger_webhook(self, payload: dict[str, str], webhook_id: str) -> None: @@ -39,6 +40,20 @@ class HomeAssistant: return {"Status": "Unknown action"} + async def _process_location(self, message: Message) -> dict[str, str]: + if message.action == "record": + location: dict[str, str] = ast.literal_eval(message.content) + await self._location_recorder.insert_location_now( + people=location["person"], + location=LocationData( + latitude=float(location["latitude"]), + longitude=float(location["longitude"]), + altitude=float(location["altitude"]), + ), + ) + return {"Status": "Location recorded"} + return {"Status": "Unknown action"} + async def _create_shopping_list(self, content: str) -> dict[str, str]: project_id = Config.get_env("TICKTICK_SHOPPING_LIST") item: dict[str, str] = ast.literal_eval(content) diff --git a/src/util/location_recorder.py b/src/util/location_recorder.py new file mode 100644 index 0000000..a1591d1 --- /dev/null +++ b/src/util/location_recorder.py @@ -0,0 +1,60 @@ +import datetime +from dataclasses import dataclass + +from sqlalchemy import INTEGER, REAL, TEXT, insert +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(DeclarativeBase): + pass + + +class Version(Base): + __tablename__ = "version" + version_type: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) + version: Mapped[int] = mapped_column(type_=INTEGER) + + +class Location(Base): + __tablename__ = "location" + people: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) + datetime: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) + latitude: Mapped[float] = mapped_column(type_=REAL) + longitude: Mapped[float] = mapped_column(type_=REAL) + altitude: Mapped[float] = mapped_column(type_=REAL) + + +@dataclass +class LocationData: + latitude: float + longitude: float + altitude: float + + +class LocationRecorder: + def __init__(self, db_path: str) -> None: + self._db_path = "sqlite+aiosqlite:///" + db_path + + async def create_db_engine(self) -> None: + self._engine = create_async_engine(self._db_path) + async with self._engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + async def insert_location(self, people: str, datetime: str, location: LocationData) -> None: + async with self._engine.connect() as conn: + await conn.execute( + insert(Location).values( + people=people, + datetime=datetime, + latitude=location.latitude, + longitude=location.longitude, + altitude=location.altitude, + ), + ) + await conn.commit() + + async def insert_location_now(self, people: str, location: LocationData) -> None: + now = datetime.datetime.now(tz=datetime.UTC) + now_str = now.strftime("%Y-%m-%dT%H:%M:%S%z") + await self.insert_location(people, now_str, location)