Files
spothole/providers/solarconditions/http_solar_conditions_provider.py
T

76 lines
3.3 KiB
Python

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")