Skip to content

Commit a78876f

Browse files
iMicknlCopilot
andauthored
Improve Scenario class/model and handle missing label (optional field) (#1859)
* Fix missing label initialization in Scenario class * Improve scenario and add tests * Update README * Fix scenario label handling by decamelizing response keys * Improve test * Improve models * Improve models * Add additional tests * Improve documentation * Add test * Update pyoverkiz/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update pyoverkiz/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2466ee3 commit a78876f

File tree

6 files changed

+350
-6
lines changed

6 files changed

+350
-6
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,20 @@ This package powers the Overkiz integration in [Home Assistant Core](https://www
148148

149149
We welcome contributions! To get started with setting up this project for development, follow the steps below.
150150

151-
### Dev Container (recommended)
151+
152+
### Project setup
153+
#### Dev Container (recommended)
152154

153155
If you use Visual Studio Code with Docker or GitHub Codespaces, you can take advantage of the included [Dev Container](https://code.visualstudio.com/docs/devcontainers/containers). This environment comes pre-configured with all necessary dependencies and tools, including the correct Python version, making setup simple and straightforward.
154156

155-
### Manual
157+
#### Manual setup
156158

157-
- Ensure Python 3.12 is installed on your system.
158159
- Install [uv](https://docs.astral.sh/uv/getting-started/installation).
159160
- Clone this repository and navigate to it: `cd python-overkiz-api`
160161
- Initialize the project with `uv sync`, then run `uv run pre-commit install`
162+
163+
#### Tests
164+
165+
```bash
166+
uv run pytest
167+
```

pyoverkiz/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ async def execute_commands(
745745
async def get_scenarios(self) -> list[Scenario]:
746746
"""List the scenarios"""
747747
response = await self.__get("actionGroups")
748-
return [Scenario(**scenario) for scenario in response]
748+
return [Scenario(**scenario) for scenario in humps.decamelize(response)]
749749

750750
@backoff.on_exception(
751751
backoff.expo,

pyoverkiz/models.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,13 +539,69 @@ def __init__(
539539
self.action_group = action_group
540540

541541

542+
@define(init=False, kw_only=True)
543+
class Action:
544+
"""
545+
An action consists of multiple commands related to a single device, identified by its device URL.
546+
"""
547+
548+
device_url: str
549+
commands: list[Command]
550+
551+
def __init__(self, device_url: str, commands: list[dict[str, Any]]):
552+
self.device_url = device_url
553+
self.commands = [Command(**c) for c in commands] if commands else []
554+
555+
542556
@define(init=False, kw_only=True)
543557
class Scenario:
558+
"""
559+
An action group is composed of one or more actions.
560+
Each action is related to a single setup device (designated by its device URL) and
561+
is composed of one or more commands to be executed on that device.
562+
"""
563+
564+
id: str = field(repr=obfuscate_id)
565+
creation_time: int
566+
last_update_time: int | None = None
544567
label: str = field(repr=obfuscate_string)
568+
metadata: str
569+
shortcut: bool | None = None
570+
notification_type_mask: int | None = None
571+
notification_condition: str | None = None
572+
notification_text: str | None = None
573+
notification_title: str | None = None
574+
actions: list[Action]
545575
oid: str = field(repr=obfuscate_id)
546576

547-
def __init__(self, label: str, oid: str, **_: Any):
548-
self.label = label
577+
def __init__(
578+
self,
579+
creation_time: int,
580+
metadata: str,
581+
actions: list[dict[str, Any]],
582+
oid: str,
583+
last_update_time: int | None = None,
584+
label: str | None = None,
585+
shortcut: bool | None = None,
586+
notification_type_mask: int | None = None,
587+
notification_condition: str | None = None,
588+
notification_text: str | None = None,
589+
notification_title: str | None = None,
590+
**_: Any,
591+
) -> None:
592+
self.id = oid
593+
self.creation_time = creation_time
594+
self.last_update_time = last_update_time
595+
self.label = (
596+
label or ""
597+
) # for backwards compatibility we set label to empty string if None
598+
self.metadata = metadata
599+
self.shortcut = shortcut
600+
self.notification_type_mask = notification_type_mask
601+
self.notification_condition = notification_condition
602+
self.notification_text = notification_text
603+
self.notification_title = notification_title
604+
self.actions = [Action(**action) for action in actions]
549605
self.oid = oid
550606

551607

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
[
2+
{
3+
"creationTime": 1763489130000,
4+
"lastUpdateTime": 1763489130000,
5+
"label": "Label 1",
6+
"metadata": "a56b34aa-2f6f-4e24-b73a-c2ed23f0ac91",
7+
"shortcut": false,
8+
"notificationTypeMask": 0,
9+
"notificationCondition": "NEVER",
10+
"actions": [
11+
{
12+
"deviceURL": "io://1234-5678-1234/13880042",
13+
"commands": [
14+
{
15+
"type": 1,
16+
"name": "setHeatingLevel",
17+
"parameters": [
18+
"eco"
19+
]
20+
}
21+
]
22+
}
23+
],
24+
"oid": "0a0589bb-9471-4667-a2a9-4602beb2a2e8"
25+
},
26+
{
27+
"creationTime": 1763214463000,
28+
"lastUpdateTime": 1763214463000,
29+
"label": "Label 2",
30+
"metadata": "f6206578-b730-4f66-b322-e8d0a5625407",
31+
"shortcut": false,
32+
"notificationTypeMask": 0,
33+
"notificationCondition": "NEVER",
34+
"actions": [
35+
{
36+
"deviceURL": "io://1234-5678-1234/15581081",
37+
"commands": [
38+
{
39+
"type": 1,
40+
"name": "setHeatingLevel",
41+
"parameters": [
42+
"comfort"
43+
]
44+
}
45+
]
46+
}
47+
],
48+
"oid": "50d39fc3-9368-49c9-bcbf-c74f3ce1678a"
49+
},
50+
{
51+
"creationTime": 1763214533000,
52+
"lastUpdateTime": 1763214533000,
53+
"label": "Label 3",
54+
"metadata": "c1f2a9b2-e959-4cc2-a9ec-7c12f13d684b",
55+
"shortcut": false,
56+
"notificationTypeMask": 0,
57+
"notificationCondition": "NEVER",
58+
"actions": [
59+
{
60+
"deviceURL": "io://1234-5678-1234/13933322",
61+
"commands": [
62+
{
63+
"type": 1,
64+
"name": "setHeatingLevel",
65+
"parameters": [
66+
"eco"
67+
]
68+
}
69+
]
70+
}
71+
],
72+
"oid": "6ed08671-9967-4f97-ba80-9561fdff8342"
73+
},
74+
{
75+
"creationTime": 1763489115000,
76+
"lastUpdateTime": 1763489115000,
77+
"label": "Label 4",
78+
"metadata": "a56b34aa-2f6f-4e24-b73a-c2ed23f0ac91",
79+
"shortcut": false,
80+
"notificationTypeMask": 0,
81+
"notificationCondition": "NEVER",
82+
"actions": [
83+
{
84+
"deviceURL": "io://1234-5678-1234/13880042",
85+
"commands": [
86+
{
87+
"type": 1,
88+
"name": "setHeatingLevel",
89+
"parameters": [
90+
"comfort"
91+
]
92+
}
93+
]
94+
}
95+
],
96+
"oid": "902cb626-2b30-45e1-9993-48c12b28b7e8"
97+
},
98+
{
99+
"creationTime": 1645911835000,
100+
"lastUpdateTime": 1763207152000,
101+
"metadata": "0d84c6d3-e22f-4ec6-b064-24f47ae6a27f",
102+
"shortcut": false,
103+
"notificationTypeMask": 0,
104+
"notificationCondition": "NEVER",
105+
"actions": [
106+
{
107+
"deviceURL": "io://1234-5678-1234/13880042",
108+
"commands": [
109+
{
110+
"type": 1,
111+
"name": "setHeatingLevel",
112+
"parameters": [
113+
"eco"
114+
]
115+
}
116+
]
117+
}
118+
],
119+
"oid": "a6123547-b740-4233-b819-37d6bfdb262f"
120+
},
121+
{
122+
"creationTime": 1645911837000,
123+
"lastUpdateTime": 1763207151000,
124+
"metadata": "0d84c6d3-e22f-4ec6-b064-24f47ae6a27f",
125+
"shortcut": false,
126+
"notificationTypeMask": 0,
127+
"notificationCondition": "NEVER",
128+
"actions": [
129+
{
130+
"deviceURL": "io://1234-5678-1234/13880042",
131+
"commands": [
132+
{
133+
"type": 1,
134+
"name": "setHeatingLevel",
135+
"parameters": [
136+
"comfort"
137+
]
138+
}
139+
]
140+
}
141+
],
142+
"oid": "cd65aa96-e520-4dac-a10b-2cf2318282c4"
143+
},
144+
{
145+
"creationTime": 1763214461000,
146+
"lastUpdateTime": 1763214461000,
147+
"label": "Label 7",
148+
"metadata": "f6206578-b730-4f66-b322-e8d0a5625407",
149+
"shortcut": false,
150+
"notificationTypeMask": 0,
151+
"notificationCondition": "NEVER",
152+
"actions": [
153+
{
154+
"deviceURL": "io://1234-5678-1234/15581081",
155+
"commands": [
156+
{
157+
"type": 1,
158+
"name": "setHeatingLevel",
159+
"parameters": [
160+
"eco"
161+
]
162+
}
163+
]
164+
}
165+
],
166+
"oid": "ebc17489-236b-460e-8312-e6aab8f041e9"
167+
},
168+
{
169+
"creationTime": 1763214535000,
170+
"lastUpdateTime": 1763214535000,
171+
"label": "Label 8",
172+
"metadata": "c1f2a9b2-e959-4cc2-a9ec-7c12f13d684b",
173+
"shortcut": false,
174+
"notificationTypeMask": 0,
175+
"notificationCondition": "NEVER",
176+
"actions": [
177+
{
178+
"deviceURL": "io://1234-5678-1234/13933322",
179+
"commands": [
180+
{
181+
"type": 1,
182+
"name": "setHeatingLevel",
183+
"parameters": [
184+
"comfort"
185+
]
186+
}
187+
]
188+
}
189+
],
190+
"oid": "f33627ed-6d64-4c9e-8830-314e46803d81"
191+
},
192+
{
193+
"creationTime": 1763207186000,
194+
"lastUpdateTime": 1763207186000,
195+
"label": "Label 9",
196+
"metadata": "a6363499-f520-431d-812b-ef37fa9db84c",
197+
"shortcut": false,
198+
"notificationTypeMask": 0,
199+
"notificationCondition": "NEVER",
200+
"actions": [
201+
{
202+
"deviceURL": "io://1234-5678-1234/13880042",
203+
"commands": [
204+
{
205+
"type": 1,
206+
"name": "setHeatingLevel",
207+
"parameters": [
208+
"comfort"
209+
]
210+
}
211+
]
212+
}
213+
],
214+
"oid": "f51acc08-084e-4850-a93f-b69368187d1e"
215+
}
216+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[
2+
{
3+
"creationTime": 1766929020000,
4+
"lastUpdateTime": 1766929020000,
5+
"label": "I'm arriving",
6+
"metadata": "{\"tahoma\":{\"version\":1,\"icon\":\"at_home\",\"template\":\"arriving\"}}",
7+
"shortcut": false,
8+
"notificationTypeMask": 16,
9+
"notificationCondition": "ON_ERROR",
10+
"notificationText": "Your scene I'm arriving could not be played correctly due to an error in one of your products.",
11+
"notificationTitle": "[$[setupLabel]] : Impossible to play the scene I'm arriving",
12+
"actions": [
13+
{
14+
"deviceURL": "rts://1234-5678-1234/16756006",
15+
"commands": [
16+
{
17+
"type": 1,
18+
"name": "open",
19+
"parameters": [
20+
1
21+
]
22+
}
23+
]
24+
}
25+
],
26+
"oid": "d1b689e1-4087-473d-b726-d3b24770856f"
27+
}
28+
]

tests/test_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,43 @@ async def test_get_setup_option(
396396
else:
397397
assert isinstance(option, instance)
398398

399+
@pytest.mark.parametrize(
400+
"fixture_name, scenario_count",
401+
[
402+
("action-group-cozytouch.json", 9),
403+
("action-group-tahoma-switch.json", 1),
404+
],
405+
)
406+
@pytest.mark.asyncio
407+
async def test_get_scenarios(
408+
self,
409+
client: OverkizClient,
410+
fixture_name: str,
411+
scenario_count: int,
412+
):
413+
with open(
414+
os.path.join(CURRENT_DIR, "fixtures/action_groups/" + fixture_name),
415+
encoding="utf-8",
416+
) as action_group_mock:
417+
resp = MockResponse(action_group_mock.read())
418+
419+
with patch.object(aiohttp.ClientSession, "get", return_value=resp):
420+
scenarios = await client.get_scenarios()
421+
422+
assert len(scenarios) == scenario_count
423+
424+
for scenario in scenarios:
425+
assert scenario.oid
426+
assert scenario.label is not None
427+
assert scenario.actions
428+
429+
for action in scenario.actions:
430+
assert action.device_url
431+
assert action.commands
432+
433+
for command in action.commands:
434+
assert command.name
435+
399436

400437
class MockResponse:
401438
def __init__(self, text, status=200, url=""):

0 commit comments

Comments
 (0)