Align Windy resend with Stations API response handling

- Add WINDY_MAX_RETRIES constant and use it consistently when deciding to disable resending
- Refactor Windy response verification to rely on HTTP status codes per stations.windy.com API
- Improve error handling for missing password, duplicate payloads and rate limiting
- Enhance retry logging and disable Windy resend via persistent notification on repeated failures
ecowitt_support
SchiZzA 2026-03-01 13:51:17 +01:00
parent 95663fd78b
commit f06f8b31ae
No known key found for this signature in database
2 changed files with 63 additions and 26 deletions

View File

@ -25,6 +25,8 @@ SENSOR_TO_MIGRATE: Final = "sensor_to_migrate"
DEV_DBG: Final = "dev_debug_checkbox" DEV_DBG: Final = "dev_debug_checkbox"
WSLINK: Final = "wslink" WSLINK: Final = "wslink"
WINDY_MAX_RETRIES: Final = 3
__all__ = [ __all__ = [
"DOMAIN", "DOMAIN",
"DEFAULT_URL", "DEFAULT_URL",

View File

@ -2,12 +2,12 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
import re
from aiohttp.client import ClientResponse
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
from homeassistant.components import persistent_notification
from py_typecheck import checked from py_typecheck import checked
from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -17,6 +17,7 @@ from .const import (
WINDY_ENABLED, WINDY_ENABLED,
WINDY_INVALID_KEY, WINDY_INVALID_KEY,
WINDY_LOGGER_ENABLED, WINDY_LOGGER_ENABLED,
WINDY_MAX_RETRIES,
WINDY_NOT_INSERTED, WINDY_NOT_INSERTED,
WINDY_STATION_ID, WINDY_STATION_ID,
WINDY_STATION_PW, WINDY_STATION_PW,
@ -30,15 +31,36 @@ _LOGGER = logging.getLogger(__name__)
class WindyNotInserted(Exception): class WindyNotInserted(Exception):
"""NotInserted state.""" """NotInserted state.
Possible variants are:
- station password is invalid
- station password does not match the station
- payload failed validation
"""
class WindySuccess(Exception): class WindySuccess(Exception):
"""WindySucces state.""" """WindySucces state."""
class WindyApiKeyError(Exception): class WindyPasswordMissing(Exception):
"""Windy API Key error.""" """Windy password is missing in query or Authorization header.
This should not happend, while we are checking if we have password set and do exits early.
"""
class WindyDuplicatePayloadDetected(Exception):
"""Duplicate payload detected."""
class WindyRateLimitExceeded(Exception):
"""Rate limit exceeded. Minimum interval is 5 minutes.
This should not happend in runnig integration.
Might be seen, if restart of HomeAssistant occured and we are not aware of previous update.
"""
def timed(minutes: int): def timed(minutes: int):
@ -66,29 +88,32 @@ class WindyPush:
self.log: bool = self.config.options.get(WINDY_LOGGER_ENABLED, False) self.log: bool = self.config.options.get(WINDY_LOGGER_ENABLED, False)
# Lets chcek if Windy server is responding right. # Lets chcek if Windy server is responding right.
# Otherwise, try 3 times and then disable resending, as we might have bad credentials. # Otherwise, try 3 times and then disable resending.
self.invalid_response_count: int = 0 self.invalid_response_count: int = 0
def verify_windy_response( # Refactored responses verification.
self, #
response: str, # We now comply to API at https://stations.windy.com/api-reference
): def verify_windy_response(self, response: ClientResponse):
"""Verify answer form Windy.""" """Verify answer form Windy."""
if self.log: if self.log and response:
_LOGGER.info("Windy response raw response: %s", response) _LOGGER.info("Windy raw response: %s", response.text)
if "NOTICE" in response: if response.status == 200:
raise WindyNotInserted
if "SUCCESS" in response:
raise WindySuccess raise WindySuccess
if "Invalid API key" in response: if response.status == 400:
raise WindyApiKeyError raise WindyNotInserted
if "Unauthorized" in response: if response.status == 401:
raise WindyApiKeyError raise WindyPasswordMissing
if response.status == 409:
raise WindyDuplicatePayloadDetected
if response.status == 429:
raise WindyRateLimitExceeded
def _covert_wslink_to_pws(self, indata: dict[str, str]) -> dict[str, str]: def _covert_wslink_to_pws(self, indata: dict[str, str]) -> dict[str, str]:
"""Convert WSLink API data to Windy API data protocol.""" """Convert WSLink API data to Windy API data protocol."""
@ -190,19 +215,25 @@ class WindyPush:
async with session.get( async with session.get(
request_url, params=purged_data, headers=headers request_url, params=purged_data, headers=headers
) as resp: ) as resp:
status = await resp.text()
try: try:
self.verify_windy_response(status) self.verify_windy_response(response=resp)
except WindyNotInserted: except WindyNotInserted:
# log despite of settings # log despite of settings
_LOGGER.error(WINDY_NOT_INSERTED) _LOGGER.error(WINDY_NOT_INSERTED)
self.invalid_response_count += 1
except WindyApiKeyError: except WindyPasswordMissing:
# log despite of settings # log despite of settings
_LOGGER.critical(WINDY_INVALID_KEY) _LOGGER.critical(WINDY_INVALID_KEY)
await self._disable_windy( await self._disable_windy(
reason="Windy server refused your API key. Resending is disabled for now. Reconfigure your Windy settings." reason="Windy password is missing in payload or Authorization header. Resending is disabled for now. Reconfigure your Windy settings."
) )
except WindyDuplicatePayloadDetected:
_LOGGER.critical(
"Duplicate payload detected by Windy server. Will try again later. Max retries before disabling resend function: %s",
(WINDY_MAX_RETRIES - self.invalid_response_count),
)
self.invalid_response_count += 1
except WindySuccess: except WindySuccess:
if self.log: if self.log:
@ -212,9 +243,13 @@ class WindyPush:
_LOGGER.debug(WINDY_NOT_INSERTED) _LOGGER.debug(WINDY_NOT_INSERTED)
except ClientError as ex: except ClientError as ex:
_LOGGER.critical("Invalid response from Windy: %s", str(ex)) _LOGGER.critical(
"Invalid response from Windy: %s. Will try again later, max retries before disabling resend function: %s",
str(ex),
(WINDY_MAX_RETRIES - self.invalid_response_count),
)
self.invalid_response_count += 1 self.invalid_response_count += 1
if self.invalid_response_count > 3: if self.invalid_response_count >= WINDY_MAX_RETRIES:
_LOGGER.critical(WINDY_UNEXPECTED) _LOGGER.critical(WINDY_UNEXPECTED)
await self._disable_windy( await self._disable_windy(
reason="Invalid response from Windy 3 times. Disabling resending option." reason="Invalid response from Windy 3 times. Disabling resending option."