core/homeassistant/components/ssdp/descriptions.py

70 lines
2.4 KiB
Python

"""The SSDP integration."""
from __future__ import annotations
import asyncio
import logging
import aiohttp
from defusedxml import ElementTree
from homeassistant.core import HomeAssistant, callback
from .util import etree_to_dict
_LOGGER = logging.getLogger(__name__)
class DescriptionManager:
"""Class to cache and manage fetching descriptions."""
def __init__(self, hass: HomeAssistant) -> None:
"""Init the manager."""
self.hass = hass
self._description_cache: dict[str, None | dict[str, str]] = {}
async def fetch_description(
self, xml_location: str | None
) -> None | dict[str, str]:
"""Fetch the location or get it from the cache."""
if xml_location is None:
return None
if xml_location not in self._description_cache:
try:
self._description_cache[xml_location] = await self._fetch_description(
xml_location
)
except Exception: # pylint: disable=broad-except
# If it fails, cache the failure so we do not keep trying over and over
self._description_cache[xml_location] = None
_LOGGER.exception("Failed to fetch ssdp data from: %s", xml_location)
return self._description_cache[xml_location]
@callback
def async_cached_description(self, xml_location: str) -> None | dict[str, str]:
"""Fetch the description from the cache."""
return self._description_cache.get(xml_location)
async def _fetch_description(self, xml_location: str) -> None | dict[str, str]:
"""Fetch an XML description."""
session = self.hass.helpers.aiohttp_client.async_get_clientsession()
try:
for _ in range(2):
resp = await session.get(xml_location, timeout=5)
# Samsung Smart TV sometimes returns an empty document the
# first time. Retry once.
if xml := await resp.text(errors="replace"):
break
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.debug("Error fetching %s: %s", xml_location, err)
return None
try:
tree = ElementTree.fromstring(xml)
except ElementTree.ParseError as err:
_LOGGER.debug("Error parsing %s: %s", xml_location, err)
return None
root = etree_to_dict(tree).get("root") or {}
return root.get("device") or {}