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():
if isinstance(value, bool):
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)
# Create request kwargs

View File

@ -1,6 +1,7 @@
"""The tests for the REST sensor platform."""
from http import HTTPStatus
import logging
import ssl
from unittest.mock import patch
@ -19,6 +20,14 @@ from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
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,
SERVICE_RELOAD,
STATE_UNAVAILABLE,
@ -978,6 +987,124 @@ async def test_update_with_failed_get(
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:
"""Verify we can reload reset sensors."""