Files

155 lines
5.5 KiB
Python

from typing import Protocol
class WeatherProvider(Protocol):
def get_current(self) -> dict: ...
def get_forecast(self) -> list[dict]: ...
class MockWeatherProvider:
def get_current(self):
return {
"location": "上海",
"temperature": "--",
"summary": "天气演示数据 · 尚未连接实时天气源",
"mock": True,
}
def get_forecast(self):
return []
class ConfigurableWeatherProvider:
"""On-demand public weather with a ten-minute cache and bounded error backoff."""
def __init__(self, db, transport=None):
import threading
import requests
self.db = db
self.transport = transport or requests.get
self.lock = threading.Lock()
self.cache = None
self.next_fetch = 0.0
self.cached_config = None
def config(self):
import json
from pigway_frame_flow.models import WeatherConfig
row = self.db.one("SELECT payload FROM app_config WHERE key='weather'")
return WeatherConfig.model_validate(json.loads(row["payload"]) if row else {})
def configure(self, config):
from pigway_frame_flow.storage.database import encode
with self.lock:
self.db.execute(
"INSERT OR REPLACE INTO app_config VALUES('weather',?)",
(encode(config.model_dump()),),
)
self.cache = None
self.next_fetch = 0
self.cached_config = None
return config
@staticmethod
def summary(code):
if code == 0:
return "晴 / Clear"
if code in (1, 2, 3):
return "多云 / Cloudy"
if code in (45, 48):
return "雾 / Fog"
if code in (71, 73, 75, 77, 85, 86):
return "雪 / Snow"
if code in (95, 96, 99):
return "雷雨 / Thunderstorm"
if code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82):
return "雨 / Rain"
return "未知 / Unknown"
def fetch(self):
import copy
import time
config = self.config()
if config.provider == "mock":
current = MockWeatherProvider().get_current()
current["location"] = config.location
return {"current": current, "forecast": []}
with self.lock:
if self.cached_config != config:
self.cache = None
self.next_fetch = 0
self.cached_config = config
if time.monotonic() < self.next_fetch:
return copy.deepcopy(self.cache)
try:
response = self.transport(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": config.latitude,
"longitude": config.longitude,
"current": "temperature_2m,weather_code,relative_humidity_2m,wind_speed_10m",
"daily": "weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max",
"timezone": "Asia/Shanghai",
"forecast_days": 3,
},
timeout=8,
allow_redirects=False,
)
response.raise_for_status()
raw = response.json()
cur, daily = raw["current"], raw["daily"]
current = {
"location": config.location,
"temperature": f"{cur['temperature_2m']}°C",
"summary": self.summary(cur["weather_code"]),
"humidity": cur.get("relative_humidity_2m"),
"wind_kmh": cur.get("wind_speed_10m"),
"observed_at": cur["time"] + "+08:00",
"mock": False,
"stale": False,
"available": True,
"attribution": "Open-Meteo / CC BY 4.0",
}
forecast = [
{
"date": day,
"summary": self.summary(daily["weather_code"][i]),
"minimum_c": daily["temperature_2m_min"][i],
"maximum_c": daily["temperature_2m_max"][i],
"rain_probability": daily["precipitation_probability_max"][i],
}
for i, day in enumerate(daily["time"])
]
self.cache = {"current": current, "forecast": forecast}
self.next_fetch = time.monotonic() + 600
except Exception:
if self.cache:
self.cache["current"]["stale"] = True
else:
self.cache = {
"current": {
"location": config.location,
"temperature": "--",
"summary": "天气暂不可用 / Weather unavailable",
"mock": False,
"stale": True,
"available": False,
"attribution": "Open-Meteo / CC BY 4.0",
},
"forecast": [],
}
self.next_fetch = time.monotonic() + 120
return copy.deepcopy(self.cache)
def get_current(self):
return self.fetch()["current"]
def get_forecast(self):
return self.fetch()["forecast"]