Skip to content

Commit 672ffa5

Browse files
bdracofrenck
authored andcommitted
Restore httpx compatibility for non-primitive REST query parameters (home-assistant#148286)
1 parent 3d3f252 commit 672ffa5

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

homeassistant/components/rest/data.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ async def async_update(self, log_errors: bool = True) -> None:
115115
for key, value in rendered_params.items():
116116
if isinstance(value, bool):
117117
rendered_params[key] = str(value).lower()
118+
elif not isinstance(value, (str, int, float, type(None))):
119+
# For backward compatibility with httpx behavior, convert non-primitive
120+
# types to strings. This maintains compatibility after switching from
121+
# httpx to aiohttp. See https://github.com/home-assistant/core/issues/148153
122+
_LOGGER.debug(
123+
"REST query parameter '%s' has type %s, converting to string",
124+
key,
125+
type(value).__name__,
126+
)
127+
rendered_params[key] = str(value)
118128

119129
_LOGGER.debug("Updating from %s", self._resource)
120130
# Create request kwargs

tests/components/rest/test_sensor.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""The tests for the REST sensor platform."""
22

33
from http import HTTPStatus
4+
import logging
45
import ssl
56
from unittest.mock import patch
67

@@ -19,6 +20,14 @@
1920
ATTR_DEVICE_CLASS,
2021
ATTR_ENTITY_ID,
2122
ATTR_UNIT_OF_MEASUREMENT,
23+
CONF_DEVICE_CLASS,
24+
CONF_FORCE_UPDATE,
25+
CONF_METHOD,
26+
CONF_NAME,
27+
CONF_PARAMS,
28+
CONF_RESOURCE,
29+
CONF_UNIT_OF_MEASUREMENT,
30+
CONF_VALUE_TEMPLATE,
2231
CONTENT_TYPE_JSON,
2332
SERVICE_RELOAD,
2433
STATE_UNAVAILABLE,
@@ -1066,6 +1075,124 @@ async def test_update_with_failed_get(
10661075
assert "Empty reply" in caplog.text
10671076

10681077

1078+
async def test_query_param_dict_value(
1079+
hass: HomeAssistant,
1080+
caplog: pytest.LogCaptureFixture,
1081+
aioclient_mock: AiohttpClientMocker,
1082+
) -> None:
1083+
"""Test dict values in query params are handled for backward compatibility."""
1084+
# Mock response
1085+
aioclient_mock.post(
1086+
"https://www.envertecportal.com/ApiInverters/QueryTerminalReal",
1087+
status=HTTPStatus.OK,
1088+
json={"Data": {"QueryResults": [{"POWER": 1500}]}},
1089+
)
1090+
1091+
# This test checks that when template_complex processes a string that looks like
1092+
# a dict/list, it converts it to an actual dict/list, which then needs to be
1093+
# handled by our backward compatibility code
1094+
with caplog.at_level(logging.DEBUG, logger="homeassistant.components.rest.data"):
1095+
assert await async_setup_component(
1096+
hass,
1097+
DOMAIN,
1098+
{
1099+
DOMAIN: [
1100+
{
1101+
CONF_RESOURCE: (
1102+
"https://www.envertecportal.com/ApiInverters/"
1103+
"QueryTerminalReal"
1104+
),
1105+
CONF_METHOD: "POST",
1106+
CONF_PARAMS: {
1107+
"page": "1",
1108+
"perPage": "20",
1109+
"orderBy": "SN",
1110+
# When processed by template.render_complex, certain
1111+
# strings might be converted to dicts/lists if they
1112+
# look like JSON
1113+
"whereCondition": (
1114+
"{{ {'STATIONID': 'A6327A17797C1234'} }}"
1115+
), # Template that evaluates to dict
1116+
},
1117+
"sensor": [
1118+
{
1119+
CONF_NAME: "Solar MPPT1 Power",
1120+
CONF_VALUE_TEMPLATE: (
1121+
"{{ value_json.Data.QueryResults[0].POWER }}"
1122+
),
1123+
CONF_DEVICE_CLASS: "power",
1124+
CONF_UNIT_OF_MEASUREMENT: "W",
1125+
CONF_FORCE_UPDATE: True,
1126+
"state_class": "measurement",
1127+
}
1128+
],
1129+
}
1130+
]
1131+
},
1132+
)
1133+
await hass.async_block_till_done()
1134+
1135+
# The sensor should be created successfully with backward compatibility
1136+
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
1137+
state = hass.states.get("sensor.solar_mppt1_power")
1138+
assert state is not None
1139+
assert state.state == "1500"
1140+
1141+
# Check that a debug message was logged about the parameter conversion
1142+
assert "REST query parameter 'whereCondition' has type" in caplog.text
1143+
assert "converting to string" in caplog.text
1144+
1145+
1146+
async def test_query_param_json_string_preserved(
1147+
hass: HomeAssistant,
1148+
aioclient_mock: AiohttpClientMocker,
1149+
) -> None:
1150+
"""Test that JSON strings in query params are preserved and not converted to dicts."""
1151+
# Mock response
1152+
aioclient_mock.get(
1153+
"https://api.example.com/data",
1154+
status=HTTPStatus.OK,
1155+
json={"value": 42},
1156+
)
1157+
1158+
# Config with JSON string (quoted) - should remain a string
1159+
assert await async_setup_component(
1160+
hass,
1161+
DOMAIN,
1162+
{
1163+
DOMAIN: [
1164+
{
1165+
CONF_RESOURCE: "https://api.example.com/data",
1166+
CONF_METHOD: "GET",
1167+
CONF_PARAMS: {
1168+
"filter": '{"type": "sensor", "id": 123}', # JSON string
1169+
"normal": "value",
1170+
},
1171+
"sensor": [
1172+
{
1173+
CONF_NAME: "Test Sensor",
1174+
CONF_VALUE_TEMPLATE: "{{ value_json.value }}",
1175+
}
1176+
],
1177+
}
1178+
]
1179+
},
1180+
)
1181+
await hass.async_block_till_done()
1182+
1183+
# Check the sensor was created
1184+
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
1185+
state = hass.states.get("sensor.test_sensor")
1186+
assert state is not None
1187+
assert state.state == "42"
1188+
1189+
# Verify the request was made with the JSON string intact
1190+
assert len(aioclient_mock.mock_calls) == 1
1191+
method, url, data, headers = aioclient_mock.mock_calls[0]
1192+
assert url.query["filter"] == '{"type": "sensor", "id": 123}'
1193+
assert url.query["normal"] == "value"
1194+
1195+
10691196
async def test_reload(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None:
10701197
"""Verify we can reload reset sensors."""
10711198

0 commit comments

Comments
 (0)