feat: add weather source entity and weather-reactive CSS source type
Some checks failed
Lint & Test / test (push) Failing after 34s
Some checks failed
Lint & Test / test (push) Failing after 34s
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.
This commit is contained in:
157
server/src/wled_controller/api/routes/weather_sources.py
Normal file
157
server/src/wled_controller/api/routes/weather_sources.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user