Skip to content

Commit b8a8369

Browse files
committed
feat: add UIProfileDefinition model and related classes, and implement UI profile fetching in generate_enums
1 parent d60fc03 commit b8a8369

File tree

4 files changed

+341
-35
lines changed

4 files changed

+341
-35
lines changed

pyoverkiz/client.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
ServerConfig,
6666
Setup,
6767
State,
68+
UIProfileDefinition,
6869
)
6970
from pyoverkiz.obfuscate import obfuscate_sensitive_data
7071
from pyoverkiz.serializers import prepare_payload
@@ -698,11 +699,19 @@ async def get_reference_ui_classifiers(self) -> list[str]:
698699
return await self.__get("reference/ui/classifiers")
699700

700701
@retry_on_auth_error
701-
async def get_reference_ui_profile(self, profile_name: str) -> JSON:
702-
"""Get a description of a given UI profile (or form-factor variant)."""
703-
return await self.__get(
702+
async def get_reference_ui_profile(self, profile_name: str) -> UIProfileDefinition:
703+
"""Get a description of a given UI profile (or form-factor variant).
704+
705+
Returns a profile definition containing:
706+
- name: Profile name
707+
- commands: Available commands with parameters and descriptions
708+
- states: Available states with value types and descriptions
709+
- form_factor: Whether profile is tied to a specific physical device type
710+
"""
711+
response = await self.__get(
704712
f"reference/ui/profile/{urllib.parse.quote_plus(profile_name)}"
705713
)
714+
return UIProfileDefinition(**humps.decamelize(response))
706715

707716
@retry_on_auth_error
708717
async def get_reference_ui_profile_names(self) -> list[str]:

pyoverkiz/models.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,3 +1047,154 @@ def __init__(self, id: int, prefix: str, name: str, label: str, **_: Any):
10471047
self.prefix = prefix
10481048
self.name = name
10491049
self.label = label
1050+
1051+
1052+
@define(init=False, kw_only=True)
1053+
class ValuePrototype:
1054+
"""Value prototype defining parameter/state value constraints."""
1055+
1056+
type: str
1057+
min_value: int | float | None = None
1058+
max_value: int | float | None = None
1059+
enum_values: list[str] | None = None
1060+
description: str | None = None
1061+
1062+
def __init__(
1063+
self,
1064+
type: str,
1065+
min_value: int | float | None = None,
1066+
max_value: int | float | None = None,
1067+
enum_values: list[str] | None = None,
1068+
description: str | None = None,
1069+
**_: Any,
1070+
):
1071+
"""Initialize ValuePrototype from API data."""
1072+
self.type = type
1073+
self.min_value = min_value
1074+
self.max_value = max_value
1075+
self.enum_values = enum_values
1076+
self.description = description
1077+
1078+
1079+
@define(init=False, kw_only=True)
1080+
class CommandParameter:
1081+
"""Command parameter definition."""
1082+
1083+
optional: bool
1084+
sensitive: bool
1085+
value_prototypes: list[ValuePrototype]
1086+
1087+
def __init__(
1088+
self,
1089+
optional: bool,
1090+
sensitive: bool,
1091+
value_prototypes: list[dict] | None = None,
1092+
**_: Any,
1093+
):
1094+
"""Initialize CommandParameter from API data."""
1095+
self.optional = optional
1096+
self.sensitive = sensitive
1097+
self.value_prototypes = (
1098+
[ValuePrototype(**vp) for vp in value_prototypes]
1099+
if value_prototypes
1100+
else []
1101+
)
1102+
1103+
1104+
@define(init=False, kw_only=True)
1105+
class CommandPrototype:
1106+
"""Command prototype defining parameters."""
1107+
1108+
parameters: list[CommandParameter]
1109+
1110+
def __init__(self, parameters: list[dict] | None = None, **_: Any):
1111+
"""Initialize CommandPrototype from API data."""
1112+
self.parameters = (
1113+
[CommandParameter(**p) for p in parameters] if parameters else []
1114+
)
1115+
1116+
1117+
@define(init=False, kw_only=True)
1118+
class UIProfileCommand:
1119+
"""UI profile command definition."""
1120+
1121+
name: str
1122+
prototype: CommandPrototype | None = None
1123+
description: str | None = None
1124+
1125+
def __init__(
1126+
self,
1127+
name: str,
1128+
prototype: dict | None = None,
1129+
description: str | None = None,
1130+
**_: Any,
1131+
):
1132+
"""Initialize UIProfileCommand from API data."""
1133+
self.name = name
1134+
self.prototype = CommandPrototype(**prototype) if prototype else None
1135+
self.description = description
1136+
1137+
1138+
@define(init=False, kw_only=True)
1139+
class StatePrototype:
1140+
"""State prototype defining value constraints."""
1141+
1142+
value_prototypes: list[ValuePrototype]
1143+
1144+
def __init__(self, value_prototypes: list[dict] | None = None, **_: Any):
1145+
"""Initialize StatePrototype from API data."""
1146+
self.value_prototypes = (
1147+
[ValuePrototype(**vp) for vp in value_prototypes]
1148+
if value_prototypes
1149+
else []
1150+
)
1151+
1152+
1153+
@define(init=False, kw_only=True)
1154+
class UIProfileState:
1155+
"""UI profile state definition."""
1156+
1157+
name: str
1158+
prototype: StatePrototype | None = None
1159+
description: str | None = None
1160+
1161+
def __init__(
1162+
self,
1163+
name: str,
1164+
prototype: dict | None = None,
1165+
description: str | None = None,
1166+
**_: Any,
1167+
):
1168+
"""Initialize UIProfileState from API data."""
1169+
self.name = name
1170+
self.prototype = StatePrototype(**prototype) if prototype else None
1171+
self.description = description
1172+
1173+
1174+
@define(init=False, kw_only=True)
1175+
class UIProfileDefinition:
1176+
"""UI profile definition from the reference API.
1177+
1178+
Describes device capabilities through available commands and states.
1179+
"""
1180+
1181+
name: str
1182+
commands: list[UIProfileCommand]
1183+
states: list[UIProfileState]
1184+
form_factor: bool
1185+
1186+
def __init__(
1187+
self,
1188+
name: str,
1189+
commands: list[dict] | None = None,
1190+
states: list[dict] | None = None,
1191+
form_factor: bool = False,
1192+
**_: Any,
1193+
):
1194+
"""Initialize UIProfileDefinition from API data."""
1195+
self.name = name
1196+
self.commands = (
1197+
[UIProfileCommand(**cmd) for cmd in commands] if commands else []
1198+
)
1199+
self.states = [UIProfileState(**s) for s in states] if states else []
1200+
self.form_factor = form_factor

tests/test_ui_profile.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for UIProfileDefinition models."""
2+
3+
from pyoverkiz.models import (
4+
CommandParameter,
5+
UIProfileCommand,
6+
UIProfileDefinition,
7+
UIProfileState,
8+
ValuePrototype,
9+
)
10+
11+
12+
def test_value_prototype_with_range():
13+
"""Test ValuePrototype with min/max range."""
14+
vp = ValuePrototype(type="INT", min_value=0, max_value=100)
15+
assert vp.type == "INT"
16+
assert vp.min_value == 0
17+
assert vp.max_value == 100
18+
assert vp.enum_values is None
19+
20+
21+
def test_value_prototype_with_enum():
22+
"""Test ValuePrototype with enum values."""
23+
vp = ValuePrototype(
24+
type="STRING", enum_values=["low", "high"], description="Fan speed mode"
25+
)
26+
assert vp.type == "STRING"
27+
assert vp.enum_values == ["low", "high"]
28+
assert vp.description == "Fan speed mode"
29+
30+
31+
def test_command_parameter():
32+
"""Test CommandParameter with value prototypes."""
33+
param = CommandParameter(
34+
optional=False,
35+
sensitive=False,
36+
value_prototypes=[{"type": "INT", "min_value": 0, "max_value": 100}],
37+
)
38+
assert param.optional is False
39+
assert param.sensitive is False
40+
assert len(param.value_prototypes) == 1
41+
assert param.value_prototypes[0].type == "INT"
42+
43+
44+
def test_ui_profile_command():
45+
"""Test UIProfileCommand with prototype."""
46+
cmd = UIProfileCommand(
47+
name="setFanSpeedLevel",
48+
prototype={
49+
"parameters": [
50+
{
51+
"optional": False,
52+
"sensitive": False,
53+
"value_prototypes": [
54+
{"type": "INT", "min_value": 0, "max_value": 100}
55+
],
56+
}
57+
]
58+
},
59+
description="Set the device fan speed level",
60+
)
61+
assert cmd.name == "setFanSpeedLevel"
62+
assert cmd.description == "Set the device fan speed level"
63+
assert cmd.prototype is not None
64+
assert len(cmd.prototype.parameters) == 1
65+
66+
67+
def test_ui_profile_state():
68+
"""Test UIProfileState with prototype."""
69+
state = UIProfileState(
70+
name="core:TemperatureState",
71+
prototype={
72+
"value_prototypes": [
73+
{"type": "FLOAT", "min_value": -100.0, "max_value": 100.0}
74+
]
75+
},
76+
description="Current room temperature",
77+
)
78+
assert state.name == "core:TemperatureState"
79+
assert state.description == "Current room temperature"
80+
assert state.prototype is not None
81+
assert len(state.prototype.value_prototypes) == 1
82+
83+
84+
def test_ui_profile_definition():
85+
"""Test complete UIProfileDefinition."""
86+
profile = UIProfileDefinition(
87+
name="AirFan",
88+
commands=[
89+
{
90+
"name": "setFanSpeedLevel",
91+
"prototype": {
92+
"parameters": [
93+
{
94+
"optional": False,
95+
"sensitive": False,
96+
"value_prototypes": [
97+
{"type": "INT", "min_value": 0, "max_value": 100}
98+
],
99+
}
100+
]
101+
},
102+
"description": "Set fan speed",
103+
}
104+
],
105+
states=[
106+
{
107+
"name": "core:FanSpeedState",
108+
"prototype": {
109+
"value_prototypes": [
110+
{"type": "INT", "min_value": 0, "max_value": 100}
111+
]
112+
},
113+
"description": "Current fan speed",
114+
}
115+
],
116+
form_factor=False,
117+
)
118+
119+
assert profile.name == "AirFan"
120+
assert len(profile.commands) == 1
121+
assert len(profile.states) == 1
122+
assert profile.form_factor is False
123+
124+
# Verify command structure
125+
cmd = profile.commands[0]
126+
assert cmd.name == "setFanSpeedLevel"
127+
assert cmd.description == "Set fan speed"
128+
assert cmd.prototype is not None
129+
assert len(cmd.prototype.parameters) == 1
130+
131+
# Verify state structure
132+
state = profile.states[0]
133+
assert state.name == "core:FanSpeedState"
134+
assert state.description == "Current fan speed"
135+
assert state.prototype is not None
136+
assert len(state.prototype.value_prototypes) == 1
137+
138+
139+
def test_ui_profile_definition_minimal():
140+
"""Test UIProfileDefinition with minimal data."""
141+
profile = UIProfileDefinition(name="MinimalProfile")
142+
143+
assert profile.name == "MinimalProfile"
144+
assert profile.commands == []
145+
assert profile.states == []
146+
assert profile.form_factor is False

0 commit comments

Comments
 (0)