Add XML support to RESTful binary sensor (#110062)
* Add XML support to RESTful binary sensor * Add test for binary sensor with XML input data * Address mypy validation results by handling None returns * Use proper incorrect XML instead of blank * Change failure condition to match the behavior of the library method * Change error handling for bad XML to expect ExpatError * Parametrize bad XML test to catch both empty and invalid XML * Move exception handling out of the shared method --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
4b95ea864f
commit
2cc38b426a
5 changed files with 107 additions and 18 deletions
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import ssl
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -149,24 +150,31 @@ class RestBinarySensor(ManualTriggerEntity, RestEntity, BinarySensorEntity):
|
|||
self._attr_is_on = False
|
||||
return
|
||||
|
||||
response = self.rest.data
|
||||
try:
|
||||
response = self.rest.data_without_xml()
|
||||
except ExpatError as err:
|
||||
self._attr_is_on = False
|
||||
_LOGGER.warning(
|
||||
"REST xml result could not be parsed and converted to JSON: %s", err
|
||||
)
|
||||
return
|
||||
|
||||
raw_value = response
|
||||
|
||||
if self._value_template is not None:
|
||||
if response is not None and self._value_template is not None:
|
||||
response = self._value_template.async_render_with_possible_json_value(
|
||||
self.rest.data, False
|
||||
response, False
|
||||
)
|
||||
|
||||
try:
|
||||
self._attr_is_on = bool(int(response))
|
||||
self._attr_is_on = bool(int(str(response)))
|
||||
except ValueError:
|
||||
self._attr_is_on = {
|
||||
"true": True,
|
||||
"on": True,
|
||||
"open": True,
|
||||
"yes": True,
|
||||
}.get(response.lower(), False)
|
||||
}.get(str(response).lower(), False)
|
||||
|
||||
self._process_manual_data(raw_value)
|
||||
self.async_write_ha_state()
|
||||
|
|
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import ssl
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
import httpx
|
||||
import xmltodict
|
||||
|
@ -79,14 +78,8 @@ class RestData:
|
|||
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", value)
|
||||
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:
|
||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
import ssl
|
||||
from typing import Any
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -159,7 +160,13 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
|
|||
|
||||
def _update_from_rest_data(self) -> None:
|
||||
"""Update state from the rest data."""
|
||||
value = self.rest.data_without_xml()
|
||||
try:
|
||||
value = self.rest.data_without_xml()
|
||||
except ExpatError as err:
|
||||
_LOGGER.warning(
|
||||
"REST xml result could not be parsed and converted to JSON: %s", err
|
||||
)
|
||||
value = self.rest.data
|
||||
|
||||
if self._json_attrs:
|
||||
self._attr_extra_state_attributes = parse_json_attributes(
|
||||
|
|
|
@ -362,6 +362,77 @@ async def test_setup_get_on(hass: HomeAssistant) -> None:
|
|||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_setup_get_xml(hass: HomeAssistant) -> None:
|
||||
"""Test setup with valid xml configuration."""
|
||||
respx.get("http://localhost").respond(
|
||||
status_code=HTTPStatus.OK,
|
||||
headers={"content-type": "text/xml"},
|
||||
content="<dog>1</dog>",
|
||||
)
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
{
|
||||
BINARY_SENSOR_DOMAIN: {
|
||||
"platform": DOMAIN,
|
||||
"resource": "http://localhost",
|
||||
"method": "GET",
|
||||
"value_template": "{{ value_json.dog }}",
|
||||
"name": "foo",
|
||||
"verify_ssl": "true",
|
||||
"timeout": 30,
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all(BINARY_SENSOR_DOMAIN)) == 1
|
||||
|
||||
state = hass.states.get("binary_sensor.foo")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.parametrize(
|
||||
("content"),
|
||||
[
|
||||
(""),
|
||||
("<open></close>"),
|
||||
],
|
||||
)
|
||||
async def test_setup_get_bad_xml(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, content: str
|
||||
) -> None:
|
||||
"""Test attributes get extracted from a XML result with bad xml."""
|
||||
|
||||
respx.get("http://localhost").respond(
|
||||
status_code=HTTPStatus.OK,
|
||||
headers={"content-type": "text/xml"},
|
||||
content=content,
|
||||
)
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
{
|
||||
BINARY_SENSOR_DOMAIN: {
|
||||
"platform": DOMAIN,
|
||||
"resource": "http://localhost",
|
||||
"method": "GET",
|
||||
"value_template": "{{ value_json.toplevel.master_value }}",
|
||||
"name": "foo",
|
||||
"verify_ssl": "true",
|
||||
"timeout": 30,
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all(BINARY_SENSOR_DOMAIN)) == 1
|
||||
state = hass.states.get("binary_sensor.foo")
|
||||
|
||||
assert state.state == STATE_OFF
|
||||
assert "REST xml result could not be parsed" in caplog.text
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_setup_with_exception(hass: HomeAssistant) -> None:
|
||||
"""Test setup with exception."""
|
||||
|
|
|
@ -868,15 +868,25 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp
|
|||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.parametrize(
|
||||
("content", "error_message"),
|
||||
[
|
||||
("", "Empty reply"),
|
||||
("<open></close>", "Erroneous JSON"),
|
||||
],
|
||||
)
|
||||
async def test_update_with_xml_convert_bad_xml(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
content: str,
|
||||
error_message: str,
|
||||
) -> None:
|
||||
"""Test attributes get extracted from a XML result with bad xml."""
|
||||
|
||||
respx.get("http://localhost").respond(
|
||||
status_code=HTTPStatus.OK,
|
||||
headers={"content-type": "text/xml"},
|
||||
content="",
|
||||
content=content,
|
||||
)
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
|
@ -901,7 +911,7 @@ async def test_update_with_xml_convert_bad_xml(
|
|||
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert "REST xml result could not be parsed" in caplog.text
|
||||
assert "Empty reply" in caplog.text
|
||||
assert error_message in caplog.text
|
||||
|
||||
|
||||
@respx.mock
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue