Skip to content

Commit d5455c9

Browse files
committed
tweaks & tidy up (json=, not data=), add snake_case framework
1 parent 4cced87 commit d5455c9

File tree

12 files changed

+87
-45
lines changed

12 files changed

+87
-45
lines changed

src/evohomeasync/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ async def _set_system_mode(
331331
data |= {SZ_QUICK_ACTION_NEXT_TIME: until.strftime("%Y-%m-%dT%H:%M:%SZ")}
332332

333333
url = f"evoTouchSystems?locationId={self.location_id}"
334-
await self.broker.make_request(HTTPMethod.PUT, url, data=data)
334+
await self.broker.make_request(HTTPMethod.PUT, url, json=data)
335335

336336
async def set_mode_auto(self) -> None:
337337
"""Set the system to normal operation."""
@@ -416,7 +416,7 @@ async def _set_heat_setpoint(
416416
}
417417

418418
url = f"devices/{zone_id}/thermostat/changeableValues/heatSetpoint"
419-
await self.broker.make_request(HTTPMethod.PUT, url, data=data)
419+
await self.broker.make_request(HTTPMethod.PUT, url, json=data)
420420

421421
async def set_temperature(
422422
self, zone: _ZoneIdT | _ZoneNameT, temperature: float, until: dt | None = None
@@ -474,7 +474,7 @@ async def _set_dhw(
474474
data |= {SZ_NEXT_TIME: next_time.strftime("%Y-%m-%dT%H:%M:%SZ")}
475475

476476
url = f"devices/{dhw_id}/thermostat/changeableValues"
477-
await self.broker.make_request(HTTPMethod.PUT, url, data=data)
477+
await self.broker.make_request(HTTPMethod.PUT, url, json=data)
478478

479479
async def set_dhw_on(self, until: dt | None = None) -> None:
480480
"""Set DHW to On, either indefinitely, or until a specified time.

src/evohomeasync/broker.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ async def _populate_user_data(self) -> tuple[_UserDataT, aiohttp.ClientResponse]
8888
"""Return the latest user data as retrieved from the web."""
8989

9090
url = "session"
91-
response = await self.make_request(HTTPMethod.POST, url, data=self._POST_DATA)
91+
response = await self.make_request(HTTPMethod.POST, url, json=self._POST_DATA)
9292

9393
self._user_data: _UserDataT = await response.json()
9494

@@ -109,7 +109,7 @@ async def populate_full_data(self) -> list[_LocnDataT]:
109109
await self.populate_user_data()
110110

111111
url = f"locations?userId={self._user_id}&allData=True"
112-
response = await self.make_request(HTTPMethod.GET, url, data=self._POST_DATA)
112+
response = await self.make_request(HTTPMethod.GET, url, json=self._POST_DATA)
113113

114114
self._full_data: list[_LocnDataT] = await response.json()
115115

@@ -122,7 +122,7 @@ async def _make_request(
122122
url: str,
123123
/,
124124
*,
125-
data: dict[str, Any] | None = None,
125+
json: dict[str, Any] | None = None,
126126
_dont_reauthenticate: bool = False, # used only with recursive call
127127
) -> aiohttp.ClientResponse:
128128
"""Perform an HTTP request, with an optional retry if re-authenticated."""
@@ -138,7 +138,7 @@ async def _make_request(
138138

139139
url_ = self.hostname + "/WebAPI/api/" + url
140140

141-
async with func(url_, json=data, headers=self._headers) as r:
141+
async with func(url_, json=json, headers=self._headers) as r:
142142
response_text = await r.text() # why cant I move this below the if?
143143

144144
# if 401/unauthorized, may need to refresh sessionId (expires in 15 mins?)
@@ -170,7 +170,7 @@ async def _make_request(
170170

171171
# NOTE: this is a recursive call, used only after (success) re-authenticating
172172
return await self._make_request(
173-
method, url, data=data, _dont_reauthenticate=True
173+
method, url, json=json, _dont_reauthenticate=True
174174
)
175175

176176
async def make_request(
@@ -179,12 +179,12 @@ async def make_request(
179179
url: str,
180180
/,
181181
*,
182-
data: dict[str, Any] | None = None,
182+
json: dict[str, Any] | None = None,
183183
) -> aiohttp.ClientResponse:
184184
"""Perform an HTTP request, will authenticate if required."""
185185

186186
try:
187-
response = await self._make_request(method, url, data=data) # ? ClientError
187+
response = await self._make_request(method, url, json=json) # ? ClientError
188188
response.raise_for_status() # ? ClientResponseError
189189

190190
# response.method, response.url, response.status, response._body

src/evohomeasync2/base.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
from typing import TYPE_CHECKING, Any, Final, NoReturn
99

1010
from . import exceptions as exc
11-
from .broker import AbstractTokenManager, Broker
11+
from .broker import AbstractTokenManager, Broker, convert_json
1212
from .location import Location
1313
from .schema import SCH_FULL_CONFIG, SCH_USER_ACCOUNT
14+
from .schema.const import SZ_USER_ID
1415

1516
if TYPE_CHECKING:
1617
from datetime import datetime as dt
@@ -191,9 +192,8 @@ async def user_account(self, *, force_update: bool = False) -> _EvoDictT:
191192
if self._user_account and not force_update:
192193
return self._user_account
193194

194-
self._user_account: _EvoDictT = await self.broker.get(
195-
"userAccount", schema=SCH_USER_ACCOUNT
196-
) # type: ignore[assignment]
195+
result = await self.broker.get("userAccount", schema=SCH_USER_ACCOUNT)
196+
self._user_account: _EvoDictT = convert_json(result) # type: ignore[assignment]
197197

198198
return self._user_account # type: ignore[return-value]
199199

@@ -228,10 +228,11 @@ async def _installation(self, *, refresh_status: bool = True) -> _EvoListT:
228228
# FIXME: shouldn't really be starting again with new objects?
229229
self.locations = [] # for now, need to clear this before GET
230230

231-
url = f"location/installationInfo?userId={self.account_info['userId']}"
231+
url = f"location/installationInfo?userId={self.account_info[SZ_USER_ID]}"
232232
url += "&includeTemperatureControlSystems=True"
233233

234-
self._full_config = await self.broker.get(url, schema=SCH_FULL_CONFIG) # type: ignore[assignment]
234+
result = await self.broker.get(url, schema=SCH_FULL_CONFIG)
235+
self._full_config: _EvoDictT = convert_json(result) # type: ignore[assignment]
235236

236237
# populate each freshly instantiated location with its initial status
237238
loc_config: _EvoDictT

src/evohomeasync2/broker.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
from __future__ import annotations
55

66
import logging
7+
import re
78
from abc import ABC, abstractmethod
89
from datetime import datetime as dt, timedelta as td
910
from http import HTTPMethod, HTTPStatus
10-
from typing import TYPE_CHECKING, Any, Final, TypedDict
11+
from typing import TYPE_CHECKING, Any, Final, TypedDict, TypeVar
1112

1213
import aiohttp
1314
import voluptuous as vol
@@ -51,7 +52,7 @@
5152
}
5253

5354
_ERR_MSG_LOOKUP_BASE: dict[int, str] = _ERR_MSG_LOOKUP_BOTH | { # GET/PUT URL_BASE
54-
HTTPStatus.BAD_REQUEST: "Bad request (invalid data/json?)",
55+
HTTPStatus.BAD_REQUEST: "Bad request (invalid json?)",
5556
HTTPStatus.NOT_FOUND: "Not Found (invalid entity type?)",
5657
HTTPStatus.UNAUTHORIZED: "Unauthorized (expired access token/unknown entity id?)",
5758
}
@@ -188,7 +189,7 @@ async def _obtain_access_token(self, credentials: dict[str, str]) -> None:
188189

189190
token_data = await self._post_access_token_request(
190191
AUTH_URL,
191-
data=AUTH_PAYLOAD | credentials,
192+
data=AUTH_PAYLOAD | credentials, # NOTE: here, must be data=, not json=
192193
headers=AUTH_HEADER,
193194
)
194195

@@ -344,3 +345,32 @@ async def put(
344345
)
345346

346347
return content
348+
349+
350+
T = TypeVar("T", dict[str, Any], list[Any], dict[str, Any] | list[Any])
351+
352+
353+
def convert_json(node: T) -> T:
354+
"""Convert all the strings in a JSON object from camelCase to snake_case."""
355+
356+
return node
357+
358+
def camel_to_snake(value: str, /) -> str:
359+
"""Convert a camelCase / PascalCase string to snake_case."""
360+
361+
s = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", value)
362+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s).lower()
363+
364+
if isinstance(node, str):
365+
return camel_to_snake(node)
366+
367+
if isinstance(node, list):
368+
return [convert_json(i) for i in node]
369+
370+
if not isinstance(node, dict):
371+
return node
372+
373+
return {
374+
camel_to_snake(k) if isinstance(k, str) else k: convert_json(v)
375+
for k, v in node.items()
376+
}

src/evohomeasync2/hotwater.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def _next_setpoint(self) -> tuple[dt, str] | None: # WIP: for convenience (new)
113113

114114
async def _set_mode(self, mode: dict[str, str | None]) -> None:
115115
"""Set the DHW mode (state)."""
116-
_ = await self._broker.put(f"{self.TYPE}/{self.id}/state", json=mode)
116+
await self._broker.put(f"{self.TYPE}/{self.id}/state", json=mode)
117117

118118
async def reset_mode(self) -> None:
119119
"""Cancel any override and allow the DHW to follow its schedule."""

src/evohomeasync2/location.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import TYPE_CHECKING, Any, Final, NoReturn
88

99
from . import exceptions as exc
10+
from .broker import convert_json
1011
from .gateway import Gateway
1112
from .schema import SCH_LOCN_STATUS
1213
from .schema.const import (
@@ -110,10 +111,10 @@ def use_daylight_save_switching(self) -> bool:
110111
async def refresh_status(self) -> _EvoDictT:
111112
"""Update the entire Location with its latest status (returns the status)."""
112113

113-
status: _EvoDictT = await self._broker.get(
114-
f"{self.TYPE}/{self.id}/status?includeTemperatureControlSystems=True",
115-
schema=self.STATUS_SCHEMA,
116-
) # type: ignore[assignment]
114+
url = f"{self.TYPE}/{self.id}/status?includeTemperatureControlSystems=True"
115+
116+
result = await self._broker.get(url, schema=self.STATUS_SCHEMA)
117+
status: _EvoDictT = convert_json(result) # type: ignore[arg-type]
117118

118119
self._update_status(status)
119120
return status

src/evohomeasync2/system.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def zone_by_name(self, name: str) -> Zone | None:
217217

218218
async def _set_mode(self, mode: dict[str, str | bool]) -> None:
219219
"""Set the TCS mode.""" # {'mode': 'Auto', 'isPermanent': True}
220-
_ = await self._broker.put(f"{self.TYPE}/{self.id}/mode", json=mode)
220+
await self._broker.put(f"{self.TYPE}/{self.id}/mode", json=mode)
221221

222222
async def reset_mode(self) -> None:
223223
"""Set the TCS to auto mode (and DHW/all zones to FollowSchedule mode)."""

src/evohomeasync2/zone.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import voluptuous as vol
1515

1616
from . import exceptions as exc
17+
from .broker import convert_json
1718
from .const import API_STRFTIME, ZoneMode
1819
from .schema import (
1920
SCH_GET_SCHEDULE_ZONE,
@@ -154,9 +155,10 @@ async def _refresh_status(self) -> _EvoDictT:
154155
with a single GET.
155156
"""
156157

157-
status: _EvoDictT = await self._broker.get(
158-
f"{self.TYPE}/{self.id}/status", schema=self.STATUS_SCHEMA
159-
) # type: ignore[assignment]
158+
url = f"{self.TYPE}/{self.id}/status"
159+
160+
result = await self._broker.get(url, schema=self.STATUS_SCHEMA) # XXX
161+
status: _EvoDictT = convert_json(result) # type: ignore[arg-type]
160162

161163
self._update_status(status)
162164
return status
@@ -182,10 +184,10 @@ async def get_schedule(self) -> _EvoDictT:
182184

183185
self._logger.debug(f"{self}: Getting schedule...")
184186

187+
url = f"{self.TYPE}/{self.id}/schedule"
188+
185189
try:
186-
schedule: _EvoDictT = await self._broker.get(
187-
f"{self.TYPE}/{self.id}/schedule", schema=self.SCH_SCHEDULE_GET
188-
) # type: ignore[assignment]
190+
result = await self._broker.get(url, schema=self.SCH_SCHEDULE_GET) # XXX
189191

190192
except exc.RequestFailedError as err:
191193
if err.status == HTTPStatus.BAD_REQUEST:
@@ -199,7 +201,8 @@ async def get_schedule(self) -> _EvoDictT:
199201
f"{self}: No Schedule / Schedule is invalid"
200202
) from err
201203

202-
self._schedule = convert_to_put_schedule(schedule)
204+
# TODO: convert_json()
205+
self._schedule = convert_to_put_schedule(result) # type: ignore[arg-type]
203206
return self._schedule
204207

205208
async def set_schedule(self, schedule: _EvoDictT | str) -> None:
@@ -228,7 +231,7 @@ async def set_schedule(self, schedule: _EvoDictT | str) -> None:
228231
f"{self}: Invalid schedule type: {type(schedule)}"
229232
)
230233

231-
_ = await self._broker.put(
234+
await self._broker.put(
232235
f"{self.TYPE}/{self.id}/schedule",
233236
json=schedule,
234237
schema=self.SCH_SCHEDULE_PUT,
@@ -347,9 +350,8 @@ def target_heat_temperature(self) -> float | None:
347350

348351
# TODO: no provision for cooling
349352
async def _set_mode(self, mode: dict[str, str | float]) -> None:
350-
"""Set the zone mode (heat_setpoint, cooling is TBD)."""
351-
# TODO: also coolSetpoint
352-
_ = await self._broker.put(f"{self.TYPE}/{self.id}/heatSetpoint", json=mode)
353+
"""Set the zone mode (heat_setpoint, cooling is TBD).""" # TODO: coolSetpoint
354+
await self._broker.put(f"{self.TYPE}/{self.id}/heatSetpoint", json=mode)
353355

354356
async def reset_mode(self) -> None:
355357
"""Cancel any override and allow the zone to follow its schedule"""

tests/tests/helpers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@ def fixture_file(folder: Path, file_name: str, /) -> dict:
6767
pytest.skip(f"Fixture {file_name} not found in {folder.name}")
6868

6969
with (folder / file_name).open() as f:
70-
data: dict = json.load(f)
71-
72-
return data
70+
return json.load(f) # type: ignore[no-any-return]
7371

7472

7573
def refresh_config_with_status(config: dict, status: dict) -> None:

tests/tests/test_helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import json
77

8+
import pytest
9+
10+
from evohomeasync2.broker import convert_json
811
from evohomeasync2.schema.helpers import camel_case, pascal_case
912
from evohomeasync2.schema.schedule import (
1013
SCH_GET_SCHEDULE_DHW,
@@ -48,7 +51,7 @@ def test_get_schedule_dhw() -> None:
4851
assert get_schedule == convert_to_get_schedule(put_schedule)
4952

5053

51-
def test_helper_function() -> None:
54+
def test_case_converters() -> None:
5255
"""Test helper functions."""
5356

5457
camel_case_str = "testString"
@@ -61,3 +64,11 @@ def test_helper_function() -> None:
6164

6265
assert camel_case(pascal_case(camel_case_str)) == camel_case_str
6366
assert pascal_case(camel_case(pascal_case_str)) == pascal_case_str
67+
68+
69+
@pytest.mark.skip("This is a WIP")
70+
def test_json_snakator() -> None:
71+
"""Confirm the recursive snake_case converter works as expected."""
72+
73+
assert convert_json({"UpperCase": "lowerCase"}) == {"upper_case": "lower_case"}
74+
assert convert_json({"UpperCase": ["lowerCase"]}) == {"upper_case": ["lower_case"]}

tests/tests_rf/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async def should_work_v1( # noqa: PLR0913
6464
response: aiohttp.ClientResponse
6565

6666
# unlike _make_request(), make_request() incl. raise_for_status()
67-
response = await evo.broker._make_request(method, url, data=json)
67+
response = await evo.broker._make_request(method, url, json=json)
6868
response.raise_for_status()
6969

7070
# TODO: perform this transform in the broker
@@ -97,7 +97,7 @@ async def should_fail_v1( # noqa: PLR0913
9797

9898
try:
9999
# unlike _make_request(), make_request() incl. raise_for_status()
100-
response = await evo.broker._make_request(method, url, data=json)
100+
response = await evo.broker._make_request(method, url, json=json)
101101
response.raise_for_status()
102102

103103
except aiohttp.ClientResponseError as err:

tests/tests_rf/test_v2_apis.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,8 @@ async def test_basics(
181181
) -> None:
182182
"""Test authentication, `user_account()` and `installation()`."""
183183

184-
await _test_basics_apis(
185-
await instantiate_client_v2(user_credentials, session, dont_login=True)
186-
)
184+
evo2 = await instantiate_client_v2(user_credentials, session, dont_login=True)
185+
await _test_basics_apis(evo2)
187186

188187

189188
async def test_sched_(evo2: Awaitable[ev2.EvohomeClient]) -> None:

0 commit comments

Comments
 (0)