from __future__ import annotations import logging from datetime import datetime from threading import Event, Thread from typing import Any import pytz import requests from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout from core.constants import HTTP_HEADERS from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider logger = logging.getLogger(__name__) class HTTPSolarConditionsProvider(SolarConditionsProvider): """Generic solar conditions provider for providers that request data via HTTP(S). Subclasses implement _http_response_to_solar_conditions() to parse the specific API response format.""" def __init__(self, name: str, provider_config: dict[str, Any], url: str, poll_interval: float) -> None: super().__init__(name, provider_config) self._url: str = url self._poll_interval: float = poll_interval self._thread: Thread | None = None self._stop_event: Event = Event() def start(self) -> None: logger.info(f"Set up query of {self.name} solar conditions API every {self._poll_interval!s} seconds.") self._thread = Thread(target=self._run, name=f"HTTPSolarConditionsProvider-{self.name}", daemon=True) self._thread.start() def stop(self) -> None: self._stop_event.set() if self._thread: self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning(f"{self.name} solar conditions worker thread did not exit on time and will be killed.") def _run(self) -> None: while True: self._poll() if self._stop_event.wait(timeout=self._poll_interval): break def _poll(self) -> None: try: logger.debug(f"Polling {self.name} solar conditions API...") http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30)) # Check response code was good if http_response.ok: new_data = self._http_response_to_solar_conditions(http_response) self.update_data(new_data) self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) logger.debug(f"Received data from {self.name} solar conditions API.") else: self.status = "Error" logger.warning(f"HTTP {http_response.status_code} when calling {self.name} solar conditions API.") except ConnectionError: logger.warning(f"Connection error when accessing {self.name} solar conditions API.") except (ConnectTimeout, ReadTimeout): logger.warning(f"Timeout when accessing {self.name} solar conditions API.") except Exception: self.status = "Error" logger.exception(f"Exception in HTTP Solar Conditions Provider ({self.name})") self._stop_event.wait(timeout=1) def _http_response_to_solar_conditions(self, http_response: requests.Response) -> dict[str, Any] | None: """Convert an HTTP response into solar conditions data. Returns a dict mapping SolarConditions field names to their new values, or None if the response could not be parsed. Only the fields returned will be updated on the shared SolarConditions object; any fields not included will be left unchanged.""" raise NotImplementedError("Subclasses must implement this method")