Refactor XML parsing in rest (#94268)

* Refactor XML parsing in rest

* Adjust caplog check

* Adjust

* Rename

* Simplify
This commit is contained in:
epenet 2023-06-15 09:15:25 +02:00 committed by GitHub
parent 61d260e5fe
commit 580b09d0f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 35 additions and 25 deletions

View File

@ -26,3 +26,10 @@ REST = "rest"
REST_DATA = "rest_data" REST_DATA = "rest_data"
METHODS = ["POST", "GET"] METHODS = ["POST", "GET"]
XML_MIME_TYPES = (
"application/rss+xml",
"application/xhtml+xml",
"application/xml",
"text/xml",
)

View File

@ -3,14 +3,19 @@ from __future__ import annotations
import logging import logging
import ssl import ssl
from xml.parsers.expat import ExpatError
import httpx import httpx
import xmltodict
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.httpx_client import create_async_httpx_client from homeassistant.helpers.httpx_client import create_async_httpx_client
from homeassistant.helpers.json import json_dumps
from homeassistant.util.ssl import SSLCipherList from homeassistant.util.ssl import SSLCipherList
from .const import XML_MIME_TYPES
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -59,6 +64,26 @@ class RestData:
"""Set url.""" """Set url."""
self._resource = 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)
):
try:
value = json_dumps(xmltodict.parse(value))
except ExpatError:
_LOGGER.warning(
"REST xml result could not be parsed and converted to JSON"
)
else:
_LOGGER.debug("JSON converted from XML: %s", self.data)
return value
async def async_update(self, log_errors: bool = True) -> None: async def async_update(self, log_errors: bool = True) -> None:
"""Get the latest data from REST service with provided method.""" """Get the latest data from REST service with provided method."""
if not self._async_client: if not self._async_client:

View File

@ -3,11 +3,9 @@ from __future__ import annotations
import logging import logging
import ssl import ssl
from xml.parsers.expat import ExpatError
from jsonpath import jsonpath from jsonpath import jsonpath
import voluptuous as vol import voluptuous as vol
import xmltodict
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN, DOMAIN as SENSOR_DOMAIN,
@ -26,7 +24,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.template_entity import TemplateSensor from homeassistant.helpers.template_entity import TemplateSensor
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@ -127,26 +124,7 @@ class RestSensor(RestEntity, TemplateSensor):
def _update_from_rest_data(self) -> None: def _update_from_rest_data(self) -> None:
"""Update state from the rest data.""" """Update state from the rest data."""
value = self.rest.data value = self.rest.data_without_xml()
_LOGGER.debug("Data fetched from resource: %s", value)
if self.rest.headers is not None:
# If the http request failed, headers will be None
content_type = self.rest.headers.get("content-type")
if content_type and (
content_type.startswith("text/xml")
or content_type.startswith("application/xml")
or content_type.startswith("application/xhtml+xml")
or content_type.startswith("application/rss+xml")
):
try:
value = json_dumps(xmltodict.parse(value))
_LOGGER.debug("JSON converted from XML: %s", value)
except ExpatError:
_LOGGER.warning(
"REST xml result could not be parsed and converted to JSON"
)
_LOGGER.debug("Erroneous XML: %s", value)
if self._json_attrs: if self._json_attrs:
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}

View File

@ -899,7 +899,7 @@ async def test_update_with_xml_convert_bad_xml(
state = hass.states.get("sensor.foo") state = hass.states.get("sensor.foo")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
assert "Erroneous XML" in caplog.text assert "REST xml result could not be parsed" in caplog.text
assert "Empty reply" in caplog.text assert "Empty reply" in caplog.text
@ -936,7 +936,7 @@ async def test_update_with_failed_get(
state = hass.states.get("sensor.foo") state = hass.states.get("sensor.foo")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
assert "Erroneous XML" in caplog.text assert "REST xml result could not be parsed" in caplog.text
assert "Empty reply" in caplog.text assert "Empty reply" in caplog.text