Skip to content

Commit 0b8a6a3

Browse files
iMicknlCopilot
andcommitted
Improve Device helpers (#1932)
Small changes to the helpers to have better naming and more aligned to what Home Assistant needs --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent e50b86d commit 0b8a6a3

File tree

3 files changed

+143
-55
lines changed

3 files changed

+143
-55
lines changed

docs/device-control.md

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,20 @@ from pyoverkiz.enums import OverkizState
4545
devices = await client.get_devices()
4646
device = devices[0]
4747

48-
# Get the value of the first matching state
49-
slats_orientation = device.get_state_value([OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION])
48+
# Get the value of a single state
49+
slats_orientation = device.get_state_value(OverkizState.CORE_SLATS_ORIENTATION)
5050
print(f"Orientation: {slats_orientation}")
5151

52-
# Check if a state has a non-None value
53-
if device.has_state_value([OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION]):
52+
# Get the value of the first matching state from a list (fallback pattern)
53+
slats_orientation = device.select_first_state_value([OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION])
54+
print(f"Orientation: {slats_orientation}")
55+
56+
# Check if a single state has a non-None value
57+
if device.has_state_value(OverkizState.CORE_SLATS_ORIENTATION):
58+
print("Device has a slats orientation")
59+
60+
# Check if any of the states have non-None values
61+
if device.has_any_state_value([OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION]):
5462
print("Device has a slats orientation")
5563
```
5664

@@ -60,8 +68,14 @@ if device.has_state_value([OverkizState.CORE_SLATS_ORIENTATION, OverkizState.COR
6068
devices = await client.get_devices()
6169
device = devices[0]
6270

63-
# Get the state definition for querying type, valid values, etc.
64-
state_def = device.get_state_definition([OverkizState.CORE_OPEN_CLOSED, OverkizState.CORE_SLATS_OPEN_CLOSED])
71+
# Get the state definition for a single state
72+
state_def = device.get_state_definition(OverkizState.CORE_OPEN_CLOSED)
73+
if state_def:
74+
print(f"Type: {state_def.type}")
75+
print(f"Valid values: {state_def.values}")
76+
77+
# Get the first matching state definition from a list
78+
state_def = device.select_first_state_definition([OverkizState.CORE_OPEN_CLOSED, OverkizState.CORE_SLATS_OPEN_CLOSED])
6579
if state_def:
6680
print(f"Type: {state_def.type}")
6781
print(f"Valid values: {state_def.values}")
@@ -75,12 +89,16 @@ from pyoverkiz.enums import OverkizCommand
7589
devices = await client.get_devices()
7690
device = devices[0]
7791

92+
# Check if device supports a single command
93+
if device.supports_command(OverkizCommand.OPEN):
94+
print("Device supports open command")
95+
7896
# Check if device supports any of the given commands
79-
if device.has_supported_command([OverkizCommand.OPEN, OverkizCommand.CLOSE]):
97+
if device.supports_any_command([OverkizCommand.OPEN, OverkizCommand.CLOSE]):
8098
print("Device supports open/close commands")
8199

82100
# Get the first supported command from a list
83-
supported_cmd = device.get_supported_command_name(
101+
supported_cmd = device.select_first_command(
84102
[OverkizCommand.SET_CLOSURE, OverkizCommand.OPEN, OverkizCommand.CLOSE]
85103
)
86104
if supported_cmd:
@@ -93,9 +111,14 @@ if supported_cmd:
93111
devices = await client.get_devices()
94112
device = devices[0]
95113

96-
# Get the value of device attributes (like firmware)
97-
firmware = device.get_attribute_value([
114+
# Get the value of a single attribute
115+
firmware = device.get_attribute_value(OverkizAttribute.CORE_FIRMWARE_REVISION)
116+
print(f"Firmware: {firmware}")
117+
118+
# Get the value of the first matching attribute from a list
119+
firmware = device.select_first_attribute_value([
98120
OverkizAttribute.CORE_FIRMWARE_REVISION,
121+
OverkizAttribute.CORE_MANUFACTURER,
99122
])
100123
print(f"Firmware: {firmware}")
101124
```

pyoverkiz/models.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -287,30 +287,50 @@ def widget(self) -> UIWidget:
287287
return UIWidget(self.definition.widget_name)
288288
raise ValueError(f"Device {self.device_url} has no widget defined")
289289

290-
def get_supported_command_name(
291-
self, commands: list[str | OverkizCommand]
292-
) -> str | None:
293-
"""Return the first command name that exists in this device's definition."""
294-
return self.definition.commands.select(commands)
290+
def supports_command(self, command: str | OverkizCommand) -> bool:
291+
"""Check if device supports a command."""
292+
return str(command) in self.definition.commands
295293

296-
def has_supported_command(self, commands: list[str | OverkizCommand]) -> bool:
297-
"""Return True if any of the given commands exist in this device's definition."""
294+
def supports_any_command(self, commands: list[str | OverkizCommand]) -> bool:
295+
"""Check if device supports any of the commands."""
298296
return self.definition.commands.has_any(commands)
299297

300-
def get_state_value(self, states: list[str]) -> StateType | None:
301-
"""Return the value of the first state that exists with a non-None value."""
298+
def select_first_command(self, commands: list[str | OverkizCommand]) -> str | None:
299+
"""Return first supported command name from list, or None."""
300+
return self.definition.commands.select(commands)
301+
302+
def get_state_value(self, state: str) -> StateType | None:
303+
"""Get value of a single state, or None if not found or None."""
304+
return self.states.select_value([state])
305+
306+
def select_first_state_value(self, states: list[str]) -> StateType | None:
307+
"""Return value of first state with non-None value from list, or None."""
302308
return self.states.select_value(states)
303309

304-
def has_state_value(self, states: list[str]) -> bool:
305-
"""Return True if any of the given states exist with a non-None value."""
310+
def has_state_value(self, state: str) -> bool:
311+
"""Check if a state exists with a non-None value."""
312+
return self.states.has_any([state])
313+
314+
def has_any_state_value(self, states: list[str]) -> bool:
315+
"""Check if any of the states exist with non-None values."""
306316
return self.states.has_any(states)
307317

308-
def get_state_definition(self, states: list[str]) -> StateDefinition | None:
309-
"""Return the first StateDefinition that matches, from the device definition."""
318+
def get_state_definition(self, state: str) -> StateDefinition | None:
319+
"""Get StateDefinition for a single state name, or None."""
320+
return self.definition.get_state_definition([state])
321+
322+
def select_first_state_definition(
323+
self, states: list[str]
324+
) -> StateDefinition | None:
325+
"""Return first matching StateDefinition from list, or None."""
310326
return self.definition.get_state_definition(states)
311327

312-
def get_attribute_value(self, attributes: list[str]) -> StateType:
313-
"""Return the value of the first attribute that exists with a non-None value."""
328+
def get_attribute_value(self, attribute: str) -> StateType | None:
329+
"""Get value of a single attribute, or None if not found or None."""
330+
return self.attributes.select_value([attribute])
331+
332+
def select_first_attribute_value(self, attributes: list[str]) -> StateType | None:
333+
"""Return value of first attribute with non-None value from list, or None."""
314334
return self.attributes.select_value(attributes)
315335

316336

tests/test_models.py

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -200,47 +200,92 @@ def test_none_states(self):
200200
device = Device(**hump_device)
201201
assert not device.states.get(STATE)
202202

203-
def test_get_supported_command_name(self):
204-
"""Device.get_supported_command_name() delegates to commands.select()."""
203+
def test_select_first_command(self):
204+
"""Device.select_first_command() returns first supported command from list."""
205205
hump_device = humps.decamelize(RAW_DEVICES)
206206
device = Device(**hump_device)
207-
assert (
208-
device.get_supported_command_name(["nonexistent", "open", "close"])
209-
== "open"
210-
)
211-
assert device.get_supported_command_name(["nonexistent"]) is None
207+
assert device.select_first_command(["nonexistent", "open", "close"]) == "open"
208+
assert device.select_first_command(["nonexistent"]) is None
209+
210+
def test_supports_command(self):
211+
"""Device.supports_command() checks if device supports a single command."""
212+
hump_device = humps.decamelize(RAW_DEVICES)
213+
device = Device(**hump_device)
214+
assert device.supports_command("open")
215+
assert not device.supports_command("nonexistent")
212216

213-
def test_has_supported_command(self):
214-
"""Device.has_supported_command() delegates to commands.has_any()."""
217+
def test_supports_any_command(self):
218+
"""Device.supports_any_command() checks if device supports any command."""
215219
hump_device = humps.decamelize(RAW_DEVICES)
216220
device = Device(**hump_device)
217-
assert device.has_supported_command(["nonexistent", "open"])
218-
assert not device.has_supported_command(["nonexistent"])
221+
assert device.supports_any_command(["nonexistent", "open"])
222+
assert not device.supports_any_command(["nonexistent"])
219223

220224
def test_get_state_value(self):
221-
"""Device.get_state_value() returns the value of the first matching state."""
225+
"""Device.get_state_value() returns value of a single state."""
226+
hump_device = humps.decamelize(RAW_DEVICES)
227+
device = Device(**hump_device)
228+
value = device.get_state_value("core:ClosureState")
229+
assert value == 100
230+
assert device.get_state_value("nonexistent") is None
231+
232+
def test_select_first_state_value(self):
233+
"""Device.select_first_state_value() returns value of first matching state from list."""
222234
hump_device = humps.decamelize(RAW_DEVICES)
223235
device = Device(**hump_device)
224-
value = device.get_state_value(["nonexistent", "core:ClosureState"])
236+
value = device.select_first_state_value(["nonexistent", "core:ClosureState"])
225237
assert value == 100
226238

227239
def test_has_state_value(self):
228-
"""Device.has_state_value() returns True if any state exists with non-None value."""
240+
"""Device.has_state_value() checks if a single state exists with non-None value."""
241+
hump_device = humps.decamelize(RAW_DEVICES)
242+
device = Device(**hump_device)
243+
assert device.has_state_value("core:ClosureState")
244+
assert not device.has_state_value("nonexistent")
245+
246+
def test_has_any_state_value(self):
247+
"""Device.has_any_state_value() checks if any state exists with non-None value."""
229248
hump_device = humps.decamelize(RAW_DEVICES)
230249
device = Device(**hump_device)
231-
assert device.has_state_value(["nonexistent", "core:ClosureState"])
232-
assert not device.has_state_value(["nonexistent"])
250+
assert device.has_any_state_value(["nonexistent", "core:ClosureState"])
251+
assert not device.has_any_state_value(["nonexistent"])
233252

234253
def test_get_state_definition(self):
235-
"""Device.get_state_definition() returns the first matching StateDefinition."""
254+
"""Device.get_state_definition() returns StateDefinition for a single state."""
255+
hump_device = humps.decamelize(RAW_DEVICES)
256+
device = Device(**hump_device)
257+
state_def = device.get_state_definition("core:ClosureState")
258+
assert state_def is not None
259+
assert state_def.qualified_name == "core:ClosureState"
260+
assert device.get_state_definition("nonexistent") is None
261+
262+
def test_select_first_state_definition(self):
263+
"""Device.select_first_state_definition() returns first matching StateDefinition from list."""
236264
hump_device = humps.decamelize(RAW_DEVICES)
237265
device = Device(**hump_device)
238-
state_def = device.get_state_definition(["nonexistent", "core:ClosureState"])
266+
state_def = device.select_first_state_definition(
267+
["nonexistent", "core:ClosureState"]
268+
)
239269
assert state_def is not None
240270
assert state_def.qualified_name == "core:ClosureState"
241271

242-
def test_get_attribute_value_returns_first_match(self):
243-
"""Device.get_attribute_value() returns the value of the first matching attribute."""
272+
def test_get_attribute_value(self):
273+
"""Device.get_attribute_value() returns value of a single attribute."""
274+
test_device = {
275+
**RAW_DEVICES,
276+
"attributes": [
277+
{"name": "core:Manufacturer", "type": 3, "value": "VELUX"},
278+
{"name": "core:Model", "type": 3, "value": "WINDOW 100"},
279+
],
280+
}
281+
hump_device = humps.decamelize(test_device)
282+
device = Device(**hump_device)
283+
value = device.get_attribute_value("core:Model")
284+
assert value == "WINDOW 100"
285+
assert device.get_attribute_value("nonexistent") is None
286+
287+
def test_select_first_attribute_value_returns_first_match(self):
288+
"""Device.select_first_attribute_value() returns value of first matching attribute from list."""
244289
test_device = {
245290
**RAW_DEVICES,
246291
"attributes": [
@@ -250,28 +295,28 @@ def test_get_attribute_value_returns_first_match(self):
250295
}
251296
hump_device = humps.decamelize(test_device)
252297
device = Device(**hump_device)
253-
value = device.get_attribute_value(
298+
value = device.select_first_attribute_value(
254299
["nonexistent", "core:Model", "core:Manufacturer"]
255300
)
256301
assert value == "WINDOW 100"
257302

258-
def test_get_attribute_value_returns_none_when_no_match(self):
259-
"""Device.get_attribute_value() returns None when no attribute matches."""
303+
def test_select_first_attribute_value_returns_none_when_no_match(self):
304+
"""Device.select_first_attribute_value() returns None when no attribute matches."""
260305
hump_device = humps.decamelize(RAW_DEVICES)
261306
device = Device(**hump_device)
262-
value = device.get_attribute_value(["nonexistent", "also_nonexistent"])
307+
value = device.select_first_attribute_value(["nonexistent", "also_nonexistent"])
263308
assert value is None
264309

265-
def test_get_attribute_value_empty_attributes(self):
266-
"""Device.get_attribute_value() returns None for devices with no attributes."""
310+
def test_select_first_attribute_value_empty_attributes(self):
311+
"""Device.select_first_attribute_value() returns None for devices with no attributes."""
267312
test_device = {**RAW_DEVICES, "attributes": []}
268313
hump_device = humps.decamelize(test_device)
269314
device = Device(**hump_device)
270-
value = device.get_attribute_value(["core:Manufacturer"])
315+
value = device.select_first_attribute_value(["core:Manufacturer"])
271316
assert value is None
272317

273-
def test_get_attribute_value_with_none_values(self):
274-
"""Device.get_attribute_value() skips attributes with None values."""
318+
def test_select_first_attribute_value_with_none_values(self):
319+
"""Device.select_first_attribute_value() skips attributes with None values."""
275320
test_device = {
276321
**RAW_DEVICES,
277322
"attributes": [
@@ -281,7 +326,7 @@ def test_get_attribute_value_with_none_values(self):
281326
}
282327
hump_device = humps.decamelize(test_device)
283328
device = Device(**hump_device)
284-
value = device.get_attribute_value(["core:Model", "core:Manufacturer"])
329+
value = device.select_first_attribute_value(["core:Model", "core:Manufacturer"])
285330
assert value == "VELUX"
286331

287332

0 commit comments

Comments
 (0)