Skip to content

Commit 5e2b39a

Browse files
iMicknlCopilot
andauthored
Add action groups to diagnostics (#2016)
## Summary - `get_diagnostic_data()` now fetches both `/setup` and `/actionGroups` endpoints - Returns a structured dict with named sections instead of a flat setup dump - `obfuscate_sensitive_data()` extended to accept lists, so action groups are properly obfuscated ## Breaking changes - `get_diagnostic_data()` return shape changed from flat setup dict to `{"setup": ..., "action_groups": ...}` - Callers accessing `data["gateways"]` must update to `data["setup"]["gateways"]` - Return type narrowed from `JSON` to `dict[str, Any]` ## Test plan - [x] New test for list obfuscation in `test_obfuscate.py` - [x] New test for structured return shape in `test_client.py` - [x] 3 existing diagnostic tests updated for new shape - [x] Full suite passes (329/329) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: iMicknl <1424596+iMicknl@users.noreply.github.com>
1 parent 2298416 commit 5e2b39a

5 files changed

Lines changed: 161 additions & 21 deletions

File tree

docs/migration-v2.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,30 @@ The command execution API has been consolidated into a single method.
112112

113113
v2 also supports sending actions to **multiple devices** in a single call and choosing an `ExecutionMode` (`HIGH_PRIORITY`, `GEOLOCATED`, `INTERNAL`).
114114

115+
## Diagnostics
116+
117+
`get_diagnostic_data()` now returns a structured dict with named sections instead of a flat setup dump.
118+
119+
| v1 | v2 |
120+
|----|-----|
121+
| `data["gateways"]` | `data["setup"]["gateways"]` |
122+
| (not available) | `data["action_groups"]` |
123+
124+
=== "v1"
125+
126+
```python
127+
diagnostics = await client.get_diagnostic_data()
128+
gateways = diagnostics["gateways"]
129+
```
130+
131+
=== "v2"
132+
133+
```python
134+
diagnostics = await client.get_diagnostic_data()
135+
gateways = diagnostics["setup"]["gateways"]
136+
action_groups = diagnostics["action_groups"]
137+
```
138+
115139
## Scenarios → Action groups
116140

117141
| v1 | v2 |

pyoverkiz/client.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import logging
67
import ssl
78
import urllib.parse
@@ -329,23 +330,33 @@ async def get_setup(self, refresh: bool = False) -> Setup:
329330
return setup
330331

331332
@retry_on_auth_error
332-
async def get_diagnostic_data(self, mask_sensitive_data: bool = True) -> JSON:
333-
"""Get all data about the connected user setup.
333+
async def get_diagnostic_data(
334+
self, mask_sensitive_data: bool = True
335+
) -> dict[str, Any]:
336+
"""Get diagnostic data for the connected user setup.
334337
335338
-> gateways data (serial number, activation state, ...): <gateways/gateway>
336339
-> setup location: <location>
337340
-> house places (rooms and floors): <place>
338-
-> setup devices: <devices>.
341+
-> setup devices: <devices>
342+
-> action groups: <actionGroups>
339343
340344
By default, this data is masked to not return confidential or PII data.
341-
Set `mask_sensitive_data` to `False` to return the raw setup payload.
345+
Set `mask_sensitive_data` to `False` to return the raw payloads.
342346
"""
343-
response = await self._get("setup")
347+
setup, action_groups = await asyncio.gather(
348+
self._get("setup"),
349+
self._get("actionGroups"),
350+
)
344351

345352
if mask_sensitive_data:
346-
return obfuscate_sensitive_data(response)
353+
setup = obfuscate_sensitive_data(setup)
354+
action_groups = obfuscate_sensitive_data(action_groups)
347355

348-
return response
356+
return {
357+
"setup": setup,
358+
"action_groups": action_groups,
359+
}
349360

350361
@retry_on_auth_error
351362
async def get_devices(self, refresh: bool = False) -> list[Device]:

pyoverkiz/obfuscate.py

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

55
import re
6-
from typing import Any
7-
8-
from pyoverkiz.types import JSON
6+
from typing import Any, cast
97

108

119
def obfuscate_id(id: str | None) -> str:
@@ -24,8 +22,15 @@ def obfuscate_string(input: str) -> str:
2422
return re.sub(r"[a-zA-Z0-9_.-]*", "*", str(input))
2523

2624

27-
def obfuscate_sensitive_data(data: dict[str, Any]) -> JSON:
25+
def obfuscate_sensitive_data(
26+
data: dict[str, Any] | list[dict[str, Any]],
27+
) -> dict[str, Any] | list[dict[str, Any]]:
2828
"""Mask Overkiz JSON data to remove sensitive data."""
29+
if isinstance(data, list):
30+
return cast(
31+
list[dict[str, Any]], [obfuscate_sensitive_data(item) for item in data]
32+
)
33+
2934
mask_next_value = False
3035

3136
for key, value in data.items():

tests/test_client.py

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -303,30 +303,65 @@ async def test_get_diagnostic_data(self, client: OverkizClient, fixture_name: st
303303
with (CURRENT_DIR / "fixtures" / "setup" / fixture_name).open(
304304
encoding="utf-8",
305305
) as setup_mock:
306-
resp = MockResponse(setup_mock.read())
306+
setup_resp = MockResponse(setup_mock.read())
307307

308-
with patch.object(aiohttp.ClientSession, "get", return_value=resp):
308+
with (
309+
CURRENT_DIR
310+
/ "fixtures"
311+
/ "action_groups"
312+
/ "action-group-tahoma-switch.json"
313+
).open(
314+
encoding="utf-8",
315+
) as ag_mock:
316+
ag_resp = MockResponse(ag_mock.read())
317+
318+
responses = iter([setup_resp, ag_resp])
319+
320+
with patch.object(
321+
aiohttp.ClientSession, "get", side_effect=lambda *a, **kw: next(responses)
322+
):
309323
diagnostics = await client.get_diagnostic_data()
310324
assert diagnostics
325+
assert "setup" in diagnostics
326+
assert "action_groups" in diagnostics
311327

312328
@pytest.mark.asyncio
313329
async def test_get_diagnostic_data_redacted_by_default(self, client: OverkizClient):
314330
"""Ensure diagnostics are redacted when no argument is provided."""
315331
with (CURRENT_DIR / "fixtures" / "setup" / "setup_tahoma_1.json").open(
316332
encoding="utf-8",
317333
) as setup_mock:
318-
resp = MockResponse(setup_mock.read())
334+
setup_resp = MockResponse(setup_mock.read())
319335

320336
with (
321-
patch.object(aiohttp.ClientSession, "get", return_value=resp),
337+
CURRENT_DIR
338+
/ "fixtures"
339+
/ "action_groups"
340+
/ "action-group-tahoma-switch.json"
341+
).open(
342+
encoding="utf-8",
343+
) as ag_mock:
344+
ag_resp = MockResponse(ag_mock.read())
345+
346+
responses = iter([setup_resp, ag_resp])
347+
348+
with (
349+
patch.object(
350+
aiohttp.ClientSession,
351+
"get",
352+
side_effect=lambda *a, **kw: next(responses),
353+
),
322354
patch(
323355
"pyoverkiz.client.obfuscate_sensitive_data",
324-
return_value={"masked": True},
356+
side_effect=[{"masked": True}, [{"masked": True}]],
325357
) as obfuscate,
326358
):
327359
diagnostics = await client.get_diagnostic_data()
328-
assert diagnostics == {"masked": True}
329-
obfuscate.assert_called_once()
360+
assert diagnostics == {
361+
"setup": {"masked": True},
362+
"action_groups": [{"masked": True}],
363+
}
364+
assert obfuscate.call_count == 2
330365

331366
@pytest.mark.asyncio
332367
async def test_get_diagnostic_data_without_masking(self, client: OverkizClient):
@@ -335,16 +370,67 @@ async def test_get_diagnostic_data_without_masking(self, client: OverkizClient):
335370
encoding="utf-8",
336371
) as setup_mock:
337372
raw_setup = setup_mock.read()
338-
resp = MockResponse(raw_setup)
373+
setup_resp = MockResponse(raw_setup)
339374

340375
with (
341-
patch.object(aiohttp.ClientSession, "get", return_value=resp),
376+
CURRENT_DIR
377+
/ "fixtures"
378+
/ "action_groups"
379+
/ "action-group-tahoma-switch.json"
380+
).open(
381+
encoding="utf-8",
382+
) as ag_mock:
383+
raw_ag = ag_mock.read()
384+
ag_resp = MockResponse(raw_ag)
385+
386+
responses = iter([setup_resp, ag_resp])
387+
388+
with (
389+
patch.object(
390+
aiohttp.ClientSession,
391+
"get",
392+
side_effect=lambda *a, **kw: next(responses),
393+
),
342394
patch("pyoverkiz.client.obfuscate_sensitive_data") as obfuscate,
343395
):
344396
diagnostics = await client.get_diagnostic_data(mask_sensitive_data=False)
345-
assert diagnostics == json.loads(raw_setup)
397+
assert diagnostics == {
398+
"setup": json.loads(raw_setup),
399+
"action_groups": json.loads(raw_ag),
400+
}
346401
obfuscate.assert_not_called()
347402

403+
@pytest.mark.asyncio
404+
async def test_get_diagnostic_data_returns_structured_dict(
405+
self, client: OverkizClient
406+
):
407+
"""Verify diagnostic data returns a dict with setup and action_groups sections."""
408+
with (CURRENT_DIR / "fixtures" / "setup" / "setup_tahoma_1.json").open(
409+
encoding="utf-8",
410+
) as setup_mock:
411+
setup_resp = MockResponse(setup_mock.read())
412+
413+
with (
414+
CURRENT_DIR
415+
/ "fixtures"
416+
/ "action_groups"
417+
/ "action-group-tahoma-switch.json"
418+
).open(
419+
encoding="utf-8",
420+
) as ag_mock:
421+
ag_resp = MockResponse(ag_mock.read())
422+
423+
responses = iter([setup_resp, ag_resp])
424+
425+
with patch.object(
426+
aiohttp.ClientSession, "get", side_effect=lambda *a, **kw: next(responses)
427+
):
428+
diagnostics = await client.get_diagnostic_data(mask_sensitive_data=False)
429+
430+
assert "setup" in diagnostics
431+
assert "action_groups" in diagnostics
432+
assert isinstance(diagnostics["action_groups"], list)
433+
348434
@pytest.mark.parametrize(
349435
("fixture_name", "exception", "status_code"),
350436
[

tests/test_obfuscate.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,17 @@ def test_obfuscate_list_with_none(self):
5757
]
5858
}
5959
assert obfuscate_sensitive_data(data) == data
60+
61+
def test_obfuscate_list_of_dicts(self):
62+
"""Ensure obfuscate_sensitive_data handles a list of dicts."""
63+
data = [
64+
{"label": "My Scene", "oid": "abc-123"},
65+
{"label": "Night Mode", "deviceURL": "io://1234-5678-1234/12345678"},
66+
]
67+
result = obfuscate_sensitive_data(data)
68+
assert isinstance(result, list)
69+
assert len(result) == 2
70+
assert result[0]["label"] != "My Scene"
71+
assert result[0]["oid"] == "abc-123" # oid is not a sensitive key
72+
assert result[1]["label"] != "Night Mode"
73+
assert result[1]["deviceURL"] != "io://1234-5678-1234/12345678"

0 commit comments

Comments
 (0)