Files
wled-screen-controller-mixed/server/src/wled_controller/api/routes/weather_sources.py
alexei.dolgolyov ef33935188
Some checks failed
Lint & Test / test (push) Failing after 34s
feat: add weather source entity and weather-reactive CSS source type
New standalone WeatherSource entity with pluggable provider architecture
(Open-Meteo v1, free, no API key). Full CRUD, test endpoint, browser
geolocation, IconSelect provider picker, CardSection with test/clone/edit.

WeatherColorStripStream maps WMO weather codes to ambient color palettes
with temperature hue shifting and thunderstorm flash effects. Ref-counted
WeatherManager polls API and caches data per source.

CSS editor integration: weather type with EntitySelect source picker,
speed and temperature influence sliders. Backup/restore support.

i18n for en/ru/zh.
2026-03-24 18:52:46 +03:00

158 lines
5.5 KiB
Python

"""Weather source routes: CRUD + test endpoint."""
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
fire_entity_event,
get_weather_manager,
get_weather_source_store,
)
from wled_controller.api.schemas.weather_sources import (
WeatherSourceCreate,
WeatherSourceListResponse,
WeatherSourceResponse,
WeatherSourceUpdate,
WeatherTestResponse,
)
from wled_controller.core.weather.weather_manager import WeatherManager
from wled_controller.core.weather.weather_provider import WMO_CONDITION_NAMES
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.storage.weather_source import WeatherSource
from wled_controller.storage.weather_source_store import WeatherSourceStore
from wled_controller.utils import get_logger
logger = get_logger(__name__)
router = APIRouter()
def _to_response(source: WeatherSource) -> WeatherSourceResponse:
d = source.to_dict()
return WeatherSourceResponse(
id=d["id"],
name=d["name"],
provider=d["provider"],
provider_config=d.get("provider_config", {}),
latitude=d["latitude"],
longitude=d["longitude"],
update_interval=d["update_interval"],
description=d.get("description"),
tags=d.get("tags", []),
created_at=source.created_at,
updated_at=source.updated_at,
)
@router.get("/api/v1/weather-sources", response_model=WeatherSourceListResponse, tags=["Weather Sources"])
async def list_weather_sources(
_auth: AuthRequired,
store: WeatherSourceStore = Depends(get_weather_source_store),
):
sources = store.get_all_sources()
return WeatherSourceListResponse(
sources=[_to_response(s) for s in sources],
count=len(sources),
)
@router.post("/api/v1/weather-sources", response_model=WeatherSourceResponse, status_code=201, tags=["Weather Sources"])
async def create_weather_source(
data: WeatherSourceCreate,
_auth: AuthRequired,
store: WeatherSourceStore = Depends(get_weather_source_store),
):
try:
source = store.create_source(
name=data.name,
provider=data.provider,
provider_config=data.provider_config,
latitude=data.latitude,
longitude=data.longitude,
update_interval=data.update_interval,
description=data.description,
tags=data.tags,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
fire_entity_event("weather_source", "created", source.id)
return _to_response(source)
@router.get("/api/v1/weather-sources/{source_id}", response_model=WeatherSourceResponse, tags=["Weather Sources"])
async def get_weather_source(
source_id: str,
_auth: AuthRequired,
store: WeatherSourceStore = Depends(get_weather_source_store),
):
try:
return _to_response(store.get_source(source_id))
except EntityNotFoundError:
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
@router.put("/api/v1/weather-sources/{source_id}", response_model=WeatherSourceResponse, tags=["Weather Sources"])
async def update_weather_source(
source_id: str,
data: WeatherSourceUpdate,
_auth: AuthRequired,
store: WeatherSourceStore = Depends(get_weather_source_store),
manager: WeatherManager = Depends(get_weather_manager),
):
try:
source = store.update_source(
source_id,
name=data.name,
provider=data.provider,
provider_config=data.provider_config,
latitude=data.latitude,
longitude=data.longitude,
update_interval=data.update_interval,
description=data.description,
tags=data.tags,
)
except EntityNotFoundError:
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
manager.update_source(source_id)
fire_entity_event("weather_source", "updated", source.id)
return _to_response(source)
@router.delete("/api/v1/weather-sources/{source_id}", status_code=204, tags=["Weather Sources"])
async def delete_weather_source(
source_id: str,
_auth: AuthRequired,
store: WeatherSourceStore = Depends(get_weather_source_store),
):
try:
store.delete_source(source_id)
except EntityNotFoundError:
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
fire_entity_event("weather_source", "deleted", source_id)
@router.post("/api/v1/weather-sources/{source_id}/test", response_model=WeatherTestResponse, tags=["Weather Sources"])
async def test_weather_source(
source_id: str,
_auth: AuthRequired,
store: WeatherSourceStore = Depends(get_weather_source_store),
manager: WeatherManager = Depends(get_weather_manager),
):
"""Force-fetch current weather and return the result."""
try:
store.get_source(source_id) # validate exists
except EntityNotFoundError:
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
data = manager.fetch_now(source_id)
condition = WMO_CONDITION_NAMES.get(data.code, f"Unknown ({data.code})")
return WeatherTestResponse(
code=data.code,
condition=condition,
temperature=data.temperature,
wind_speed=data.wind_speed,
cloud_cover=data.cloud_cover,
)