Files
core/homeassistant/components/rest/data.py
2025-06-28 10:32:53 +02:00

146 lines
4.8 KiB
Python

"""Support for RESTful API."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
from multidict import CIMultiDictProxy
import xmltodict
from homeassistant.core import HomeAssistant
from homeassistant.helpers import template
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.json import json_dumps
from homeassistant.util.ssl import SSLCipherList
from .const import XML_MIME_TYPES
DEFAULT_TIMEOUT = 10
_LOGGER = logging.getLogger(__name__)
class RestData:
"""Class for handling the data retrieval."""
def __init__(
self,
hass: HomeAssistant,
method: str,
resource: str,
encoding: str,
auth: aiohttp.DigestAuthMiddleware | aiohttp.BasicAuth | tuple[str, str] | None,
headers: dict[str, str] | None,
params: dict[str, str] | None,
data: str | None,
verify_ssl: bool,
ssl_cipher_list: str,
timeout: int = DEFAULT_TIMEOUT,
) -> None:
"""Initialize the data object."""
self._hass = hass
self._method = method
self._resource = resource
self._encoding = encoding
# Convert auth tuple to aiohttp.BasicAuth if needed
if isinstance(auth, tuple) and len(auth) == 2:
self._auth: aiohttp.BasicAuth | aiohttp.DigestAuthMiddleware | None = (
aiohttp.BasicAuth(auth[0], auth[1])
)
else:
self._auth = auth
self._headers = headers
self._params = params
self._request_data = data
self._timeout = aiohttp.ClientTimeout(total=timeout)
self._verify_ssl = verify_ssl
self._ssl_cipher_list = SSLCipherList(ssl_cipher_list)
self._session: aiohttp.ClientSession | None = None
self.data: str | None = None
self.last_exception: Exception | None = None
self.headers: CIMultiDictProxy[str] | None = None
def set_payload(self, payload: str) -> None:
"""Set request data."""
self._request_data = payload
@property
def url(self) -> str:
"""Get url."""
return self._resource
def set_url(self, url: str) -> None:
"""Set url."""
self._resource = url
def data_without_xml(self) -> str | None:
"""If the data is an XML string, convert it to a JSON string."""
_LOGGER.debug("Data fetched from resource: %s", self.data)
if (
(value := self.data) is not None
# If the http request failed, headers will be None
and (headers := self.headers) is not None
and (content_type := headers.get("content-type"))
and content_type.startswith(XML_MIME_TYPES)
):
value = json_dumps(xmltodict.parse(value))
_LOGGER.debug("JSON converted from XML: %s", value)
return value
async def async_update(self, log_errors: bool = True) -> None:
"""Get the latest data from REST service with provided method."""
if not self._session:
self._session = async_create_clientsession(
self._hass,
verify_ssl=self._verify_ssl,
ssl_cipher=self._ssl_cipher_list,
)
rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params)
_LOGGER.debug("Updating from %s", self._resource)
# Create request kwargs
request_kwargs: dict[str, Any] = {
"headers": rendered_headers,
"params": rendered_params,
"timeout": self._timeout,
}
# Handle authentication
if isinstance(self._auth, aiohttp.BasicAuth):
request_kwargs["auth"] = self._auth
elif isinstance(self._auth, aiohttp.DigestAuthMiddleware):
request_kwargs["middlewares"] = (self._auth,)
# Handle data/content
if self._request_data:
request_kwargs["data"] = self._request_data
try:
# Make the request
async with self._session.request(
self._method, self._resource, **request_kwargs
) as response:
# Read the response
self.data = await response.text(encoding=self._encoding)
self.headers = response.headers
except TimeoutError as ex:
if log_errors:
_LOGGER.error("Timeout while fetching data: %s", self._resource)
self.last_exception = ex
self.data = None
self.headers = None
except aiohttp.ClientError as ex:
if log_errors:
_LOGGER.error(
"Error fetching data: %s failed with %s", self._resource, ex
)
self.last_exception = ex
self.data = None
self.headers = None