From c83bbe2c24532678d1841f3a74c6d01c02d4af9f Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 15 Mar 2026 11:47:27 -0400 Subject: [PATCH] added more data to van weatere and moved retry logic to tenacity --- python/van_weather/main.py | 81 ++++++++++++++++++++++++++---------- python/van_weather/models.py | 13 +++++- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/python/van_weather/main.py b/python/van_weather/main.py index fed60b3..4177df9 100644 --- a/python/van_weather/main.py +++ b/python/van_weather/main.py @@ -1,13 +1,13 @@ """Van weather service - fetches weather with masked GPS for privacy.""" import logging -import time from datetime import UTC, datetime from typing import Annotated, Any import requests import typer from apscheduler.schedulers.blocking import BlockingScheduler +from tenacity import before_sleep_log, retry, stop_after_attempt, wait_fixed from python.common import configure_logger from python.van_weather.models import Config, DailyForecast, HourlyForecast, Weather @@ -29,15 +29,25 @@ CONDITION_MAP = { logger = logging.getLogger(__name__) +@retry( + stop=stop_after_attempt(3), + wait=wait_fixed(5), + before_sleep=before_sleep_log(logger, logging.WARNING), + reraise=True, +) def get_ha_state(url: str, token: str, entity_id: str) -> float: - """Get numeric state from Home Assistant entity.""" + """Get numeric state from Home Asasistant entity.""" response = requests.get( f"{url}/api/states/{entity_id}", headers={"Authorization": f"Bearer {token}"}, timeout=30, ) response.raise_for_status() - return float(response.json()["state"]) + state = response.json()["state"] + if state in ("unavailable", "unknown"): + error = f"{entity_id} is {state}" + raise ValueError(error) + return float(state) def parse_daily_forecast(data: dict[str, dict[str, Any]]) -> list[DailyForecast]: @@ -55,6 +65,9 @@ def parse_daily_forecast(data: dict[str, dict[str, Any]]) -> list[DailyForecast] temperature=day.get("temperatureHigh"), templow=day.get("temperatureLow"), precipitation_probability=day.get("precipProbability"), + moon_phase=day.get("moonPhase"), + wind_gust=day.get("windGust"), + cloud_cover=day.get("cloudCover"), ) ) @@ -80,6 +93,12 @@ def parse_hourly_forecast(data: dict[str, dict[str, Any]]) -> list[HourlyForecas return hourly_forecasts +@retry( + stop=stop_after_attempt(3), + wait=wait_fixed(5), + before_sleep=before_sleep_log(logger, logging.WARNING), + reraise=True, +) def fetch_weather(api_key: str, lat: float, lon: float) -> Weather: """Fetch weather from Pirate Weather API.""" url = f"https://api.pirateweather.net/forecast/{api_key}/{lat},{lon}" @@ -102,29 +121,25 @@ def fetch_weather(api_key: str, lat: float, lon: float) -> Weather: summary=current.get("summary"), pressure=current.get("pressure"), visibility=current.get("visibility"), + uv_index=current.get("uvIndex"), + ozone=current.get("ozone"), + nearest_storm_distance=current.get("nearestStormDistance"), + nearest_storm_bearing=current.get("nearestStormBearing"), + precip_probability=current.get("precipProbability"), + cloud_cover=current.get("cloudCover"), daily_forecasts=daily_forecasts, hourly_forecasts=hourly_forecasts, ) +@retry( + stop=stop_after_attempt(3), + wait=wait_fixed(5), + before_sleep=before_sleep_log(logger, logging.WARNING), + reraise=True, +) def post_to_ha(url: str, token: str, weather: Weather) -> None: """Post weather data to Home Assistant as sensor entities.""" - max_retries = 6 - retry_delay = 10 - - for attempt in range(1, max_retries + 1): - try: - _post_weather_data(url, token, weather) - except requests.RequestException: - if attempt == max_retries: - logger.exception(f"Failed to post weather to HA after {max_retries} attempts") - return - logger.warning(f"Post to HA failed (attempt {attempt}/{max_retries}), retrying in {retry_delay}s") - time.sleep(retry_delay) - - -def _post_weather_data(url: str, token: str, weather: Weather) -> None: - """Post all weather data to Home Assistant. Raises on failure.""" headers = {"Authorization": f"Bearer {token}"} # Post current weather as individual sensors @@ -161,6 +176,30 @@ def _post_weather_data(url: str, token: str, weather: Weather) -> None: "state": weather.visibility, "attributes": {"unit_of_measurement": "mi"}, }, + "sensor.van_weather_uv_index": { + "state": weather.uv_index, + "attributes": {"friendly_name": "Van Weather UV Index", "icon": "mdi:sun-wireless"}, + }, + "sensor.van_weather_ozone": { + "state": weather.ozone, + "attributes": {"unit_of_measurement": "DU", "icon": "mdi:earth"}, + }, + "sensor.van_weather_nearest_storm_distance": { + "state": weather.nearest_storm_distance, + "attributes": {"unit_of_measurement": "mi", "icon": "mdi:weather-lightning"}, + }, + "sensor.van_weather_nearest_storm_bearing": { + "state": weather.nearest_storm_bearing, + "attributes": {"unit_of_measurement": "°", "icon": "mdi:weather-lightning"}, + }, + "sensor.van_weather_precip_probability": { + "state": int((weather.precip_probability or 0) * 100), + "attributes": {"unit_of_measurement": "%", "icon": "mdi:weather-rainy"}, + }, + "sensor.van_weather_cloud_cover": { + "state": int((weather.cloud_cover or 0) * 100), + "attributes": {"unit_of_measurement": "%", "icon": "mdi:weather-cloudy"}, + }, } for entity_id, data in sensors.items(): @@ -209,7 +248,7 @@ def _post_weather_data(url: str, token: str, weather: Weather) -> None: def update_weather(config: Config) -> None: - """Fetch GPS, mask it, get weather, post to HA.""" + """Fetch weather using last-known location, post to HA.""" lat = get_ha_state(config.ha_url, config.ha_token, config.lat_entity) lon = get_ha_state(config.ha_url, config.ha_token, config.lon_entity) @@ -218,7 +257,7 @@ def update_weather(config: Config) -> None: logger.info(f"Masked location: {masked_lat}, {masked_lon}") - weather = fetch_weather(config.pirate_weather_api_key, masked_lat, masked_lon) + weather = fetch_weather(config.pirate_weather_api_key, lat, lon) logger.info(f"Weather: {weather.temperature}°F, {weather.condition}") post_to_ha(config.ha_url, config.ha_token, weather) diff --git a/python/van_weather/models.py b/python/van_weather/models.py index b44044e..5ee880a 100644 --- a/python/van_weather/models.py +++ b/python/van_weather/models.py @@ -11,8 +11,8 @@ class Config(BaseModel): ha_url: str ha_token: str pirate_weather_api_key: str - lat_entity: str = "sensor.gps_latitude" - lon_entity: str = "sensor.gps_longitude" + lat_entity: str = "sensor.van_last_known_latitude" + lon_entity: str = "sensor.van_last_known_longitude" mask_decimals: int = 1 # ~11km accuracy @@ -24,6 +24,9 @@ class DailyForecast(BaseModel): temperature: float | None = None # High templow: float | None = None # Low precipitation_probability: float | None = None + moon_phase: float | None = None + wind_gust: float | None = None + cloud_cover: float | None = None @field_serializer("date_time") def serialize_date_time(self, date_time: datetime) -> str: @@ -57,5 +60,11 @@ class Weather(BaseModel): summary: str | None = None pressure: float | None = None visibility: float | None = None + uv_index: float | None = None + ozone: float | None = None + nearest_storm_distance: float | None = None + nearest_storm_bearing: float | None = None + precip_probability: float | None = None + cloud_cover: float | None = None daily_forecasts: list[DailyForecast] = [] hourly_forecasts: list[HourlyForecast] = []