Implement

This commit is contained in:
2023-05-18 02:46:08 +02:00
commit d1b8839dba
8 changed files with 1224 additions and 0 deletions

110
sat_kalender/__main__.py Normal file
View File

@@ -0,0 +1,110 @@
import asyncio
from dataclasses import dataclass
from datetime import datetime, timezone
from uuid import uuid4
import os
from aiocaldav import DAVClient, Calendar
from .n2yo import n2yo_api, Compass
from .satellites import read_satellites, Downlink
from .settings import settings
@dataclass
class Pass:
name: str
norad: int
start_utc: int
start_az: float
start_compass: Compass
end_utc: int
end_az: float
end_compass: Compass
max_el: float
downlinks: list[Downlink]
def as_ical(self) -> str:
now = datetime.now().astimezone(timezone.utc)
start = datetime.fromtimestamp(self.start_utc, timezone.utc)
end = datetime.fromtimestamp(self.end_utc, timezone.utc)
summary = f"{self.name} @ {self.max_el:.0f}deg"
if self.max_el > settings.observer.good_elevation:
summary += " [!]"
description = []
description.append(f"Azimuth: {self.start_az} ({self.start_compass}) to {self.end_az} ({self.end_compass})")
description.append("")
description.append("Downlinks:")
for downlink in self.downlinks:
description.append(f"- {downlink.proto} @ {downlink.freq} MHz")
description = "\\n".join(description)
return (
"BEGIN:VCALENDAR\n"
"VERSION:2.0\n"
"PRODID:-//leafbla.de//sat-kalender.py//EN\n"
"BEGIN:VEVENT\n"
f"UID:sat-kalender-{uuid4()}\n"
f"DTSTAMP:{now.year:04}{now.month:02}{now.day:02}T{now.hour:02}{now.minute:02}{now.second:02}Z\n"
f"DTSTART:{start.year:04}{start.month:02}{start.day:02}T{start.hour:02}{start.minute:02}{start.second:02}Z\n"
f"DTEND:{end.year:04}{end.month:02}{end.day:02}T{end.hour:02}{end.minute:02}{end.second:02}Z\n"
f"SUMMARY:{summary}\n"
f"DESCRIPTION:{description}\n"
"END:VEVENT\n"
"END:VCALENDAR\n"
)
async def main():
satellites = read_satellites()
passes = list()
print(f"Fetching data for {', '.join(v.name for v in satellites.values())}")
async with n2yo_api(settings.n2yo_api_key) as api:
for norad, info in satellites.items():
radio_passes = await api.get_radio_passes(
norad,
settings.observer.latitude,
settings.observer.longitude,
settings.observer.altitude,
1,
settings.observer.min_elevation,
)
print(f"{info.name}: {len(radio_passes.passes)} passes")
for radio_pass in radio_passes.passes:
passes.append(
Pass(
info.name,
norad,
radio_pass.start_utc,
radio_pass.start_az,
radio_pass.start_az_compass,
radio_pass.end_utc,
radio_pass.end_az,
radio_pass.end_az_compass,
radio_pass.max_el,
info.downlink,
)
)
print("Adding events to calendar")
dav_client = DAVClient(
settings.caldav.uri,
username=settings.caldav.username,
password=settings.caldav.password,
)
cal = Calendar(client=dav_client, url=settings.caldav.uri)
await asyncio.gather(*[cal.add_event(p.as_ical()) for p in passes])
print(f"Done :3")
if __name__ == "__main__":
asyncio.run(main())

95
sat_kalender/n2yo.py Normal file
View File

@@ -0,0 +1,95 @@
from contextlib import asynccontextmanager
from typing import Literal
import aiohttp
from pydantic import BaseModel, Field
API_URL = "https://api.n2yo.com/rest/v1/satellite"
Compass = Literal[
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
]
class PassInfo(BaseModel):
sat_id: int = Field(alias="satid")
sat_name: str = Field(alias="satname")
transaction_count: int = Field(alias="transactionscount")
pass_count: int = Field(alias="passescount")
class RadioPass(BaseModel):
start_az: float = Field(alias="startAz")
start_az_compass: Compass = Field(alias="startAzCompass")
start_utc: int = Field(alias="startUTC")
max_az: float = Field(alias="maxAz")
max_az_compass: Compass = Field(alias="maxAzCompass")
max_el: float = Field(alias="maxEl")
max_utc: int = Field(alias="maxUTC")
end_az: float = Field(alias="endAz")
end_az_compass: Compass = Field(alias="endAzCompass")
end_utc: int = Field(alias="endUTC")
class RadioPasses(BaseModel):
info: PassInfo
passes: list[RadioPass] | None
class N2YO:
def __init__(self, api_key: str):
self.api_key = api_key
self.client = aiohttp.ClientSession()
async def get_radio_passes(
self,
norad: int,
latitude: float,
longitude: float,
altitude: float,
days: int,
min_elevation: int,
):
url = "/".join(
str(s)
for s in [
API_URL,
"radiopasses",
norad,
latitude,
longitude,
altitude,
days,
min_elevation,
]
)
async with self.client.get(url, params={"apiKey": self.api_key}) as request:
response = await request.json()
return RadioPasses.parse_obj(response)
@asynccontextmanager
async def n2yo_api(key: str):
api = N2YO(key)
try:
yield api
finally:
await api.client.close()

View File

@@ -0,0 +1,23 @@
import tomllib
from pydantic import BaseModel
from .settings import settings
class Downlink(BaseModel):
proto: str
freq: float
class Satellite(BaseModel):
name: str
downlink: list[Downlink]
def read_satellites() -> dict[int, Satellite]:
with open(settings.satellites_file, "rb") as f:
data = tomllib.load(f)
return {
int(k): Satellite.parse_obj(v)
for k, v in data.items()
}

25
sat_kalender/settings.py Normal file
View File

@@ -0,0 +1,25 @@
from pydantic import BaseSettings, BaseModel
class Observer(BaseModel):
latitude: float
longitude: float
altitude: float
min_elevation: int = 15
good_elevation: int = 50
class CalDav(BaseModel):
uri: str
username: str
password: str
class Settings(BaseSettings):
n2yo_api_key: str
satellites_file: str = "satellites.toml"
observer: Observer
caldav: CalDav
class Config:
env_file = ".env"
env_nested_delimiter = '_'
settings = Settings()