Compare commits
2 commits
main
...
thermokars
Author | SHA1 | Date | |
---|---|---|---|
5d4937e57a | |||
fc964d3ac4 |
4 changed files with 133 additions and 20 deletions
35
README.md
35
README.md
|
@ -0,0 +1,35 @@
|
|||
# TRMNL Weather & Pollen Report
|
||||
|
||||
A custom TRMNL plugin that fetches and displays weather and pollen data.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Set up a virtual environment and install dependencies:
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
uv sync
|
||||
```
|
||||
|
||||
2. Set required environment variables:
|
||||
```bash
|
||||
export WEATHER_API_KEY="your_openweathermap_api_key"
|
||||
export AUTH_TOKEN="your_chosen_auth_token"
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
```bash
|
||||
fastapi run main.py --port 8887
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Build and run with Docker:
|
||||
```bash
|
||||
docker build -t trmnl-report .
|
||||
docker run -p 8887:8887 -e WEATHER_API_KEY=your_key -e AUTH_TOKEN=your_token trmnl-report
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Access the API at `http://localhost:8887/?token=your_auth_token`
|
106
main.py
106
main.py
|
@ -7,6 +7,7 @@ import pprint
|
|||
import zoneinfo
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from cachetools import TTLCache
|
||||
import httpx
|
||||
|
||||
|
||||
|
@ -16,6 +17,9 @@ eastern = zoneinfo.ZoneInfo("America/New_York")
|
|||
|
||||
app = FastAPI()
|
||||
|
||||
# Initialize caches for 15 minutes
|
||||
weather_cache = TTLCache(maxsize=100, ttl=900) # 15 minutes
|
||||
pollen_cache = TTLCache(maxsize=100, ttl=900) # 15 minutes
|
||||
|
||||
CONFIG = {
|
||||
# salem, ma
|
||||
|
@ -42,19 +46,6 @@ def format_datetime(dt):
|
|||
return f"{format_date(dt)} {format_time(dt)}"
|
||||
|
||||
|
||||
def pollen_desc(index):
|
||||
if index < 2.5:
|
||||
return f"{index} l"
|
||||
elif index < 4.9:
|
||||
return f"{index} l-m"
|
||||
elif index < 7.3:
|
||||
return f"{index} m"
|
||||
elif index < 9.7:
|
||||
return f"{index} m-h"
|
||||
else:
|
||||
return f"{index} h"
|
||||
|
||||
|
||||
def relative_day_to_date(rel_dt):
|
||||
dt = datetime.now()
|
||||
day = timedelta(days=1)
|
||||
|
@ -69,6 +60,24 @@ def relative_day_to_date(rel_dt):
|
|||
return passthrough
|
||||
|
||||
|
||||
def build_daily_data(date, pollen_periods, weather_periods):
|
||||
daily_data = {}
|
||||
if date in pollen_periods:
|
||||
daily_data["pollen"] = pollen_periods[date]["index"]
|
||||
if date in weather_periods:
|
||||
weather_data = weather_periods[date]
|
||||
daily_data.update({
|
||||
"low": weather_data["low"],
|
||||
"high": weather_data["high"],
|
||||
"desc": weather_data["desc"],
|
||||
"humidity": weather_data["humidity"],
|
||||
"sunrise": weather_data["sunrise"],
|
||||
"sunset": weather_data["sunset"],
|
||||
"pressure": weather_data["pressure"],
|
||||
})
|
||||
return daily_data
|
||||
|
||||
|
||||
def fallback_handler(func):
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
|
@ -84,6 +93,10 @@ def fallback_handler(func):
|
|||
|
||||
@fallback_handler
|
||||
async def fetch_pollen(zipcode):
|
||||
# Check the cache first
|
||||
if zipcode in pollen_cache:
|
||||
return pollen_cache[zipcode]
|
||||
|
||||
url = f"https://www.pollen.com/api/forecast/current/pollen/{zipcode}"
|
||||
headers = {
|
||||
"User-Agent": (
|
||||
|
@ -99,31 +112,40 @@ async def fetch_pollen(zipcode):
|
|||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return [
|
||||
result = [
|
||||
{
|
||||
"forecast_date": format_datetime(
|
||||
datetime.fromisoformat(data["ForecastDate"])
|
||||
),
|
||||
"periods": [
|
||||
{
|
||||
"index": pollen_desc(d["Index"]),
|
||||
"index": int(d["Index"] / 12. * 100),
|
||||
"period": relative_day_to_date(d["Type"]),
|
||||
}
|
||||
for d in data["Location"]["periods"]
|
||||
if d["Type"].lower().strip() in ["today", "tomorrow"]
|
||||
],
|
||||
}
|
||||
]
|
||||
# Cache the result
|
||||
pollen_cache[zipcode] = result
|
||||
return result
|
||||
|
||||
|
||||
@fallback_handler
|
||||
async def fetch_weather(lat, lon, weather_api_key):
|
||||
# Check the cache first
|
||||
cache_key = (lat, lon)
|
||||
if cache_key in weather_cache:
|
||||
return weather_cache[cache_key]
|
||||
|
||||
url = f"https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&appid={weather_api_key}&units=imperial"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
current, periods = data["current"], data["daily"][:3]
|
||||
return [
|
||||
current, periods = data["current"], data["daily"][:2]
|
||||
result = [
|
||||
{
|
||||
"forecast_date": format_datetime(datetime.fromtimestamp(current["dt"])),
|
||||
"current_temp": int(round(current["temp"])),
|
||||
|
@ -148,6 +170,9 @@ async def fetch_weather(lat, lon, weather_api_key):
|
|||
],
|
||||
}
|
||||
]
|
||||
# Cache the result
|
||||
weather_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
@ -163,8 +188,49 @@ async def read_root(token: str):
|
|||
fetch_weather(CONFIG["lat"], CONFIG["lon"], CONFIG["weather_api_key"]),
|
||||
)
|
||||
|
||||
return {
|
||||
pollen_periods = {p["period"]: p for p in pollen[0]["periods"]} if pollen and pollen[0]["periods"] else {}
|
||||
weather_periods = {p["period"]: p for p in weather[0]["periods"]} if weather and weather[0]["periods"] else {}
|
||||
|
||||
# Add current weather data as a "current" period
|
||||
if weather and weather[0]:
|
||||
data = weather[0]
|
||||
weather_periods["current"] = {
|
||||
"period": "current",
|
||||
"temp": data["current_temp"],
|
||||
"feels_like": data["current_feels_like"],
|
||||
"humidity": data["current_humidity"],
|
||||
"pressure": data["current_pressure"],
|
||||
"desc": data["current_desc"],
|
||||
"sunrise": data["sunrise"],
|
||||
"sunset": data["sunset"]
|
||||
}
|
||||
|
||||
today_date = format_date(datetime.now())
|
||||
tomorrow_date = format_date(datetime.now() + timedelta(days=1))
|
||||
|
||||
result = {
|
||||
"fetched_at": format_datetime(datetime.now()),
|
||||
"pollen": pollen,
|
||||
"weather": weather,
|
||||
"current": {},
|
||||
"today": {},
|
||||
"tomorrow": {},
|
||||
}
|
||||
|
||||
if "current" in weather_periods:
|
||||
weather_data = weather_periods["current"]
|
||||
result["current"] = {
|
||||
"temp": weather_data["temp"],
|
||||
"feels_like": weather_data["feels_like"],
|
||||
"desc": weather_data["desc"],
|
||||
"humidity": weather_data["humidity"],
|
||||
"pressure": weather_data["pressure"]
|
||||
}
|
||||
|
||||
today_data = build_daily_data(today_date, pollen_periods, weather_periods)
|
||||
if today_data:
|
||||
result["today"] = today_data
|
||||
|
||||
tomorrow_data = build_daily_data(tomorrow_date, pollen_periods, weather_periods)
|
||||
if tomorrow_data:
|
||||
result["tomorrow"] = tomorrow_data
|
||||
|
||||
return result
|
||||
|
|
|
@ -7,6 +7,7 @@ requires-python = ">=3.13"
|
|||
dependencies = [
|
||||
"fastapi[standard]>=0.115.12",
|
||||
"httpx>=0.28.1",
|
||||
"cachetools>=5.0.0",
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
|
|
11
uv.lock
generated
11
uv.lock
generated
|
@ -24,6 +24,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "6.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
|
@ -390,12 +399,14 @@ name = "trmnl-report"
|
|||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cachetools" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "httpx" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "cachetools", specifier = ">=5.0.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue