Initial Commit
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
API_URL_MAP_LOCATIONS,
|
||||
API_URL_SEARCH,
|
||||
BASE_URL,
|
||||
DEFAULT_USER_AGENT,
|
||||
DOMAIN,
|
||||
)
|
||||
from .parser import parse_location_page, validate_location_path
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MAP_LOCATIONS_CACHE = "map_locations_cache"
|
||||
|
||||
|
||||
def parse_search_results(data: Any) -> list[dict[str, str]]:
|
||||
"""Parse search endpoint results into normalized location dictionaries."""
|
||||
if not isinstance(data, dict):
|
||||
return []
|
||||
|
||||
results: list[dict[str, str]] = []
|
||||
for item in data.get("results", []):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
path = item.get("path")
|
||||
name = item.get("name")
|
||||
if not isinstance(path, str) or not isinstance(name, str):
|
||||
continue
|
||||
|
||||
try:
|
||||
normalized_path = validate_location_path(path)
|
||||
except ValueError:
|
||||
_LOGGER.debug("Ignoring search result with invalid path: %s", path)
|
||||
continue
|
||||
|
||||
results.append(
|
||||
{
|
||||
"name": name,
|
||||
"country": item.get("country", "")
|
||||
if isinstance(item.get("country"), str)
|
||||
else "",
|
||||
"region": item.get("region", "")
|
||||
if isinstance(item.get("region"), str)
|
||||
else "",
|
||||
"area": item.get("area", "")
|
||||
if isinstance(item.get("area"), str)
|
||||
else "",
|
||||
"path": normalized_path,
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_map_locations(data: Any) -> dict[str, dict[str, str]]:
|
||||
"""Parse map-locations payload into a place_id -> location mapping."""
|
||||
if isinstance(data, dict):
|
||||
raw_locations = data.get("locations", [])
|
||||
else:
|
||||
raw_locations = data
|
||||
|
||||
if not isinstance(raw_locations, list):
|
||||
return {}
|
||||
|
||||
mapping: dict[str, dict[str, str]] = {}
|
||||
for item in raw_locations:
|
||||
if not isinstance(item, list) or len(item) < 5:
|
||||
continue
|
||||
|
||||
place_id, name, country, area, path = item[:5]
|
||||
if not isinstance(place_id, str) or not isinstance(name, str):
|
||||
continue
|
||||
if not isinstance(path, str):
|
||||
continue
|
||||
|
||||
try:
|
||||
normalized_path = validate_location_path(path)
|
||||
except ValueError:
|
||||
_LOGGER.debug("Ignoring map location with invalid path: %s", path)
|
||||
continue
|
||||
|
||||
mapping[place_id] = {
|
||||
"name": name,
|
||||
"country": country if isinstance(country, str) else "",
|
||||
"area": area if isinstance(area, str) else "",
|
||||
"path": normalized_path,
|
||||
}
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
class SeaTemperatureAPI:
|
||||
"""API to fetch sea temperature data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the API."""
|
||||
self.hass = hass
|
||||
self._headers = {"User-Agent": DEFAULT_USER_AGENT}
|
||||
|
||||
async def search_locations(self, query: str) -> list[dict[str, str]] | None:
|
||||
"""Search SeaTemperatures locations by query string."""
|
||||
if not query.strip():
|
||||
return []
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
try:
|
||||
async with session.get(
|
||||
API_URL_SEARCH,
|
||||
params={"q": query.strip()},
|
||||
headers=self._headers,
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
return parse_search_results(await response.json())
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
_LOGGER.error("Error searching SeaTemperatures locations for %s: %s", query, err)
|
||||
return None
|
||||
|
||||
async def get_location_by_place_id(self, place_id: str) -> dict[str, str] | None:
|
||||
"""Resolve a legacy place ID to a current path-based location."""
|
||||
if not place_id:
|
||||
return None
|
||||
|
||||
cache = await self._get_map_locations()
|
||||
if cache is None:
|
||||
return None
|
||||
|
||||
location = cache.get(place_id)
|
||||
if location is None and not place_id.startswith("sea-"):
|
||||
location = cache.get(f"sea-{place_id}")
|
||||
return location
|
||||
|
||||
async def get_temperatures(self, location_path: str) -> dict[str, Any] | None:
|
||||
"""Fetch temperature data for a specific location path."""
|
||||
try:
|
||||
normalized_path = validate_location_path(location_path)
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Invalid SeaTemperatures path %s: %s", location_path, err)
|
||||
return None
|
||||
|
||||
url = f"{BASE_URL}{normalized_path}"
|
||||
session = async_get_clientsession(self.hass)
|
||||
try:
|
||||
async with session.get(url, headers=self._headers) as response:
|
||||
response.raise_for_status()
|
||||
html = await response.text()
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
_LOGGER.error(
|
||||
"Error fetching temperature data for path %s: %s",
|
||||
normalized_path,
|
||||
err,
|
||||
)
|
||||
return None
|
||||
|
||||
return parse_location_page(html).as_legacy_payload()
|
||||
|
||||
async def _get_map_locations(self) -> dict[str, dict[str, str]] | None:
|
||||
"""Fetch and cache the map-locations payload for legacy migration."""
|
||||
domain_data = self.hass.data.setdefault(DOMAIN, {})
|
||||
if _MAP_LOCATIONS_CACHE in domain_data:
|
||||
return domain_data[_MAP_LOCATIONS_CACHE]
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
try:
|
||||
async with session.get(API_URL_MAP_LOCATIONS, headers=self._headers) as response:
|
||||
response.raise_for_status()
|
||||
mapping = parse_map_locations(await response.json())
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
_LOGGER.error("Error fetching SeaTemperatures map locations: %s", err)
|
||||
return None
|
||||
|
||||
domain_data[_MAP_LOCATIONS_CACHE] = mapping
|
||||
return mapping
|
||||
Reference in New Issue
Block a user