Restore httpx compatibility for non-primitive REST query parameters (#148286)

This commit is contained in:
J. Nick Koston 2025-07-07 08:01:48 -05:00 committed by GitHub
parent b71bcb002b
commit 03e295ace0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 137 additions and 0 deletions

View File

@ -115,6 +115,16 @@ class RestData:
for key, value in rendered_params.items(): for key, value in rendered_params.items():
if isinstance(value, bool): if isinstance(value, bool):
rendered_params[key] = str(value).lower() rendered_params[key] = str(value).lower()
elif not isinstance(value, (str, int, float, type(None))):
# For backward compatibility with httpx behavior, convert non-primitive
# types to strings. This maintains compatibility after switching from
# httpx to aiohttp. See https://github.com/home-assistant/core/issues/148153
_LOGGER.debug(
"REST query parameter '%s' has type %s, converting to string",
key,
type(value).__name__,
)
rendered_params[key] = str(value)
_LOGGER.debug("Updating from %s", self._resource) _LOGGER.debug("Updating from %s", self._resource)
# Create request kwargs # Create request kwargs

View File

@ -1,6 +1,7 @@
"""The tests for the REST sensor platform.""" """The tests for the REST sensor platform."""
from http import HTTPStatus from http import HTTPStatus
import logging
import ssl import ssl
from unittest.mock import patch from unittest.mock import patch
@ -19,6 +20,14 @@ from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
CONF_METHOD,
CONF_NAME,
CONF_PARAMS,
CONF_RESOURCE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
CONTENT_TYPE_JSON, CONTENT_TYPE_JSON,
SERVICE_RELOAD, SERVICE_RELOAD,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
@ -978,6 +987,124 @@ async def test_update_with_failed_get(
assert "Empty reply" in caplog.text assert "Empty reply" in caplog.text
async def test_query_param_dict_value(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test dict values in query params are handled for backward compatibility."""
# Mock response
aioclient_mock.post(
"https://www.envertecportal.com/ApiInverters/QueryTerminalReal",
status=HTTPStatus.OK,
json={"Data": {"QueryResults": [{"POWER": 1500}]}},
)
# This test checks that when template_complex processes a string that looks like
# a dict/list, it converts it to an actual dict/list, which then needs to be
# handled by our backward compatibility code
with caplog.at_level(logging.DEBUG, logger="homeassistant.components.rest.data"):
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
CONF_RESOURCE: (
"https://www.envertecportal.com/ApiInverters/"
"QueryTerminalReal"
),
CONF_METHOD: "POST",
CONF_PARAMS: {
"page": "1",
"perPage": "20",
"orderBy": "SN",
# When processed by template.render_complex, certain
# strings might be converted to dicts/lists if they
# look like JSON
"whereCondition": (
"{{ {'STATIONID': 'A6327A17797C1234'} }}"
), # Template that evaluates to dict
},
"sensor": [
{
CONF_NAME: "Solar MPPT1 Power",
CONF_VALUE_TEMPLATE: (
"{{ value_json.Data.QueryResults[0].POWER }}"
),
CONF_DEVICE_CLASS: "power",
CONF_UNIT_OF_MEASUREMENT: "W",
CONF_FORCE_UPDATE: True,
"state_class": "measurement",
}
],
}
]
},
)
await hass.async_block_till_done()
# The sensor should be created successfully with backward compatibility
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
state = hass.states.get("sensor.solar_mppt1_power")
assert state is not None
assert state.state == "1500"
# Check that a debug message was logged about the parameter conversion
assert "REST query parameter 'whereCondition' has type" in caplog.text
assert "converting to string" in caplog.text
async def test_query_param_json_string_preserved(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test that JSON strings in query params are preserved and not converted to dicts."""
# Mock response
aioclient_mock.get(
"https://api.example.com/data",
status=HTTPStatus.OK,
json={"value": 42},
)
# Config with JSON string (quoted) - should remain a string
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
CONF_RESOURCE: "https://api.example.com/data",
CONF_METHOD: "GET",
CONF_PARAMS: {
"filter": '{"type": "sensor", "id": 123}', # JSON string
"normal": "value",
},
"sensor": [
{
CONF_NAME: "Test Sensor",
CONF_VALUE_TEMPLATE: "{{ value_json.value }}",
}
],
}
]
},
)
await hass.async_block_till_done()
# Check the sensor was created
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
state = hass.states.get("sensor.test_sensor")
assert state is not None
assert state.state == "42"
# Verify the request was made with the JSON string intact
assert len(aioclient_mock.mock_calls) == 1
method, url, data, headers = aioclient_mock.mock_calls[0]
assert url.query["filter"] == '{"type": "sensor", "id": 123}'
assert url.query["normal"] == "value"
async def test_reload(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: async def test_reload(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None:
"""Verify we can reload reset sensors.""" """Verify we can reload reset sensors."""