import logging from datetime import datetime from threading import Event, Thread 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, provider_config, url, poll_interval): super().__init__(name, provider_config) self._url = url self._poll_interval = poll_interval self._thread = None self._stop_event = Event() def start(self): 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): self._stop_event.set() if self._thread: self._thread.join(timeout=35) 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): while True: self._poll() if self._stop_event.wait(timeout=self._poll_interval): break def _poll(self): 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): """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")