Compare commits
2 commits
5d4937e57a
...
c08cfffc65
Author | SHA1 | Date | |
---|---|---|---|
c08cfffc65 | |||
88b932d2e5 |
5 changed files with 135 additions and 90 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`
|
23
main.py
23
main.py
|
@ -7,6 +7,7 @@ import pprint
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from cachetools import TTLCache
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,6 +17,9 @@ eastern = zoneinfo.ZoneInfo("America/New_York")
|
||||||
|
|
||||||
app = FastAPI()
|
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 = {
|
CONFIG = {
|
||||||
# salem, ma
|
# salem, ma
|
||||||
|
@ -89,6 +93,10 @@ def fallback_handler(func):
|
||||||
|
|
||||||
@fallback_handler
|
@fallback_handler
|
||||||
async def fetch_pollen(zipcode):
|
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}"
|
url = f"https://www.pollen.com/api/forecast/current/pollen/{zipcode}"
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": (
|
"User-Agent": (
|
||||||
|
@ -104,7 +112,7 @@ async def fetch_pollen(zipcode):
|
||||||
response = await client.get(url, headers=headers)
|
response = await client.get(url, headers=headers)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return [
|
result = [
|
||||||
{
|
{
|
||||||
"forecast_date": format_datetime(
|
"forecast_date": format_datetime(
|
||||||
datetime.fromisoformat(data["ForecastDate"])
|
datetime.fromisoformat(data["ForecastDate"])
|
||||||
|
@ -119,17 +127,25 @@ async def fetch_pollen(zipcode):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
# Cache the result
|
||||||
|
pollen_cache[zipcode] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@fallback_handler
|
@fallback_handler
|
||||||
async def fetch_weather(lat, lon, weather_api_key):
|
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"
|
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:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.get(url)
|
response = await client.get(url)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
current, periods = data["current"], data["daily"][:2]
|
current, periods = data["current"], data["daily"][:2]
|
||||||
return [
|
result = [
|
||||||
{
|
{
|
||||||
"forecast_date": format_datetime(datetime.fromtimestamp(current["dt"])),
|
"forecast_date": format_datetime(datetime.fromtimestamp(current["dt"])),
|
||||||
"current_temp": int(round(current["temp"])),
|
"current_temp": int(round(current["temp"])),
|
||||||
|
@ -154,6 +170,9 @@ async def fetch_weather(lat, lon, weather_api_key):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
# Cache the result
|
||||||
|
weather_cache[cache_key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|
|
@ -7,6 +7,7 @@ requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.115.12",
|
"fastapi[standard]>=0.115.12",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
|
"cachetools>=5.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
|
|
155
template.liquid
155
template.liquid
|
@ -1,94 +1,73 @@
|
||||||
<div class="columns">
|
<div>
|
||||||
<div class="column">
|
<div class="flex flex--row">
|
||||||
<span class="title title--small text--gray-4 text--center">fetched: {{ fetched_at }}</span>
|
<div>
|
||||||
{% for pollen in pollen %}
|
<span class="value">{{ current.temp }}°F</span>
|
||||||
<span class="title title--small bg--black text--white text--center">pollen index</span>
|
<span class="label text--gray-4">feels like {{ current.feels_like }}°F</span>
|
||||||
<div class="richtext gap--xxsmall">
|
|
||||||
<p class="content content--small">forecast at {{pollen.forecast_date}}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<table class="table table--condensed">
|
<div class="stretch text--right">
|
||||||
<thead>
|
<span class="value">{{ current.desc }}</span>
|
||||||
<tr>
|
<span class="label text--gray-4">conditions</span>
|
||||||
{% for period in pollen.periods %}
|
</div>
|
||||||
<th><span class="label label--small text--center">{{period.period}}</span></th>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
{% for period in pollen.periods %}
|
|
||||||
<td><span class="label label--small text--center">{{period.index}}</span></td>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for weather in weather %}
|
<div class="flex flex--row">
|
||||||
<span class="title title--small bg--black text--white text--center">weather forecast</span>
|
<div>
|
||||||
<div class="richtext gap--xxsmall">
|
<span class="value">{{ current.humidity }}%</span>
|
||||||
<p class="content content--small">forecast at {{weather.forecast_date}}</p>
|
<span class="label text--gray-4">humidity</span>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table--condensed">
|
<div class="stretch text--center">
|
||||||
<thead>
|
<span class="value">{{ current.pressure }}</span>
|
||||||
<tr>
|
<span class="label text--gray-4">pressure</span>
|
||||||
<th><span class="label label--small w--16"></span></th>
|
</div>
|
||||||
{% for period in weather.periods %}
|
<div class="text--right">
|
||||||
<th><span class="label label--small w--16 text--center">{{period.period}}</span></th>
|
<span class="value">{{ fetched_at }}</span>
|
||||||
{% endfor %}
|
<span class="label text--gray-4">date</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</div>
|
||||||
<tr>
|
|
||||||
<td><span class="label label--small text--right">low</span></td>
|
<div class="border--h-1 pt--4"></div>
|
||||||
{% for period in weather.periods %}
|
|
||||||
<td><span class="label label--small text--center">{{period.low}} F</span></td>
|
<div class="pt--4">
|
||||||
{% endfor %}
|
<div class="flex flex--row">
|
||||||
</tr>
|
{% assign periods = "today,tomorrow" | split: "," %}
|
||||||
<tr>
|
{% for period in periods %}
|
||||||
<td><span class="label label--small text--right">high</span></td>
|
{% assign data = [period] %}
|
||||||
{% for period in weather.periods %}
|
<div class="stretch ">
|
||||||
<td><span class="label label--small text--center">{{period.high}} F</span></td>
|
<div>
|
||||||
{% endfor %}
|
<div class="label label--inverted text--center">{{ period }}</div>
|
||||||
</tr>
|
|
||||||
<tr>
|
<div class="pt--2 text--center">
|
||||||
<td><span class="label label--small text--right">humidity</span></td>
|
<span class="value">
|
||||||
{% for period in weather.periods %}
|
{{ data.low }}°F / {{ data.high }}°F
|
||||||
<td><span class="label label--small text--center">{{period.humidity}}%</span></td>
|
</span>
|
||||||
{% endfor %}
|
</div>
|
||||||
</tr>
|
|
||||||
<tr>
|
<div class="text--center">
|
||||||
<td><span class="label label--small text--right">pressure</span></td>
|
<span class="value">{{ data.desc }}</span>
|
||||||
{% for period in weather.periods %}
|
</div>
|
||||||
<td><span class="label label--small text--center">{{period.pressure}}</span></td>
|
|
||||||
{% endfor %}
|
<div class="text--center">
|
||||||
</tr>
|
<span class="value">{{ data.sunrise }} - {{ data.sunset }}</span>
|
||||||
<tr>
|
</div>
|
||||||
<td><span class="label label--small text--right">sunrise</span></td>
|
|
||||||
{% for period in weather.periods %}
|
<div class="flex flex--row">
|
||||||
<td><span class="label label--small text--center">{{period.sunrise}}</span></td>
|
<div class="text--center">
|
||||||
{% endfor %}
|
<span class="value">{{ data.humidity }}%</span>
|
||||||
</tr>
|
<span class="label text--gray-4">humidity</span>
|
||||||
<tr>
|
</div>
|
||||||
<td><span class="label label--small text--right">sunset</span></td>
|
<div class="stretch text--center">
|
||||||
{% for period in weather.periods %}
|
<span class="value">{{ data.pressure }}</span>
|
||||||
<td><span class="label label--small text--center">{{period.sunset}}</span></td>
|
<span class="label text--gray-4">pressure</span>
|
||||||
{% endfor %}
|
</div>
|
||||||
</tr>
|
<div class="text--center">
|
||||||
<tr>
|
<span class="value">{{ data.pollen }}%</span>
|
||||||
<td><span class="label label--small text--right">desc</span></td>
|
<span class="label text--gray-4">pollen</span>
|
||||||
{% for period in weather.periods %}
|
</div>
|
||||||
<td><span class="label label--small h--10 clamp--none text--center">{{period.desc}}</span></td>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
|
||||||
{{ Content for column 2 }}
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
{{ Content for column 3 }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2025.4.26"
|
version = "2025.4.26"
|
||||||
|
@ -390,12 +399,14 @@ name = "trmnl-report"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "cachetools" },
|
||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "cachetools", specifier = ">=5.0.0" },
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue