Add weather sync job, API endpoints and settings UI
Fetches daily ERA5 weather from Open-Meteo (no API key). Configurable location, timezone and sync time via Settings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fc15bc5d9a
commit
5fbf155f62
4 changed files with 671 additions and 1 deletions
|
|
@ -1145,6 +1145,79 @@ async def delete_key(
|
|||
# AI INSIGHTS SETTINGS
|
||||
# ============================================
|
||||
|
||||
class WeatherSettingsResponse(BaseModel):
|
||||
enabled: bool = True
|
||||
location_name: str = "Stow on the Wold, GL54 1JX"
|
||||
latitude: float = 51.9253
|
||||
longitude: float = -1.7272
|
||||
timezone: str = "Europe/London"
|
||||
sync_time: str = "05:15"
|
||||
|
||||
|
||||
class WeatherSettingsUpdate(BaseModel):
|
||||
enabled: Optional[bool] = None
|
||||
location_name: Optional[str] = None
|
||||
latitude: Optional[float] = None
|
||||
longitude: Optional[float] = None
|
||||
timezone: Optional[str] = None
|
||||
sync_time: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/settings/weather", response_model=WeatherSettingsResponse)
|
||||
async def get_weather_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get weather sync configuration."""
|
||||
result = await db.execute(
|
||||
text("SELECT config_key, config_value FROM system_config WHERE config_key LIKE 'weather_%'")
|
||||
)
|
||||
cfg = {row.config_key: row.config_value for row in result.fetchall()}
|
||||
return WeatherSettingsResponse(
|
||||
enabled=cfg.get("weather_sync_enabled", "true").lower() in ("true", "1", "yes"),
|
||||
location_name=cfg.get("weather_location_name", "Stow on the Wold, GL54 1JX"),
|
||||
latitude=float(cfg.get("weather_latitude", "51.9253")),
|
||||
longitude=float(cfg.get("weather_longitude", "-1.7272")),
|
||||
timezone=cfg.get("weather_timezone", "Europe/London"),
|
||||
sync_time=cfg.get("weather_sync_time", "05:15"),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/settings/weather")
|
||||
async def update_weather_settings(
|
||||
settings: WeatherSettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Update weather sync configuration."""
|
||||
updates = {}
|
||||
if settings.enabled is not None:
|
||||
updates["weather_sync_enabled"] = "true" if settings.enabled else "false"
|
||||
if settings.location_name is not None:
|
||||
updates["weather_location_name"] = settings.location_name
|
||||
if settings.latitude is not None:
|
||||
updates["weather_latitude"] = str(settings.latitude)
|
||||
if settings.longitude is not None:
|
||||
updates["weather_longitude"] = str(settings.longitude)
|
||||
if settings.timezone is not None:
|
||||
updates["weather_timezone"] = settings.timezone
|
||||
if settings.sync_time is not None:
|
||||
updates["weather_sync_time"] = settings.sync_time
|
||||
|
||||
for key, value in updates.items():
|
||||
await db.execute(
|
||||
text("""
|
||||
INSERT INTO system_config (config_key, config_value, updated_at)
|
||||
VALUES (:key, :value, NOW())
|
||||
ON CONFLICT (config_key) DO UPDATE
|
||||
SET config_value = :value, updated_at = NOW()
|
||||
"""),
|
||||
{"key": key, "value": value}
|
||||
)
|
||||
await db.commit()
|
||||
return {"status": "saved", "keys_updated": list(updates.keys())}
|
||||
|
||||
|
||||
class AIInsightsSettingsResponse(BaseModel):
|
||||
enabled: bool = False
|
||||
api_key_set: bool = False
|
||||
|
|
|
|||
|
|
@ -886,3 +886,110 @@ async def run_backfill_job(
|
|||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# WEATHER SYNC ENDPOINTS
|
||||
# ============================================================
|
||||
|
||||
@router.post("/weather")
|
||||
async def trigger_weather_sync(
|
||||
background_tasks: BackgroundTasks,
|
||||
from_date: Optional[date] = Query(None, description="Start date (default: 7 days ago)"),
|
||||
to_date: Optional[date] = Query(None, description="End date (default: today)"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Trigger a manual weather data sync from Open-Meteo.
|
||||
Defaults to last 7 days. Pass from_date/to_date for a custom range.
|
||||
"""
|
||||
from jobs.weather_sync import sync_weather_data
|
||||
|
||||
if from_date is None:
|
||||
from_date = date.today() - timedelta(days=7)
|
||||
if to_date is None:
|
||||
to_date = date.today()
|
||||
|
||||
background_tasks.add_task(
|
||||
sync_weather_data,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
triggered_by=f"user:{current_user['username']}"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "started",
|
||||
"source": "open_meteo",
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"message": f"Weather sync {from_date} to {to_date} started in background"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/weather/backfill")
|
||||
async def trigger_weather_backfill(
|
||||
background_tasks: BackgroundTasks,
|
||||
from_date: date = Query(..., description="Start date for backfill"),
|
||||
to_date: date = Query(..., description="End date for backfill"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Backfill weather data over an arbitrary date range.
|
||||
Runs in 6-month chunks so progress is logged incrementally.
|
||||
Open-Meteo has data from 1940-01-01 to present.
|
||||
"""
|
||||
from jobs.weather_sync import run_weather_backfill
|
||||
|
||||
background_tasks.add_task(
|
||||
run_weather_backfill,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
triggered_by=f"user:{current_user['username']}"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "started",
|
||||
"source": "open_meteo",
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"message": f"Weather backfill {from_date} to {to_date} started in background"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/weather/status")
|
||||
async def get_weather_status(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Return weather data coverage and last sync status."""
|
||||
coverage = await db.execute(text("""
|
||||
SELECT
|
||||
MIN(date) as first_date,
|
||||
MAX(date) as last_date,
|
||||
COUNT(*) as total_days
|
||||
FROM weather_data
|
||||
"""))
|
||||
cov = coverage.fetchone()
|
||||
|
||||
last_sync = await db.execute(text("""
|
||||
SELECT completed_at, status, records_fetched, records_created, date_from, date_to
|
||||
FROM sync_log
|
||||
WHERE source = 'open_meteo'
|
||||
ORDER BY started_at DESC LIMIT 1
|
||||
"""))
|
||||
sync_row = last_sync.fetchone()
|
||||
|
||||
return {
|
||||
"coverage": {
|
||||
"first_date": cov.first_date,
|
||||
"last_date": cov.last_date,
|
||||
"total_days": cov.total_days,
|
||||
},
|
||||
"last_sync": {
|
||||
"completed_at": sync_row.completed_at if sync_row else None,
|
||||
"status": sync_row.status if sync_row else None,
|
||||
"records_fetched": sync_row.records_fetched if sync_row else None,
|
||||
"date_from": sync_row.date_from if sync_row else None,
|
||||
"date_to": sync_row.date_to if sync_row else None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue