python-rako-2025 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {python_rako_2025-0.1.0/python_rako_2025.egg-info → python_rako_2025-0.3.0}/PKG-INFO +1 -8
  2. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/pyproject.toml +4 -1
  3. python_rako_2025-0.3.0/python_rako/__version__.py +1 -0
  4. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako/bridge.py +49 -6
  5. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0/python_rako_2025.egg-info}/PKG-INFO +1 -8
  6. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako_2025.egg-info/SOURCES.txt +0 -1
  7. python_rako_2025-0.3.0/setup.py +3 -0
  8. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/tests/test_bridge.py +82 -0
  9. python_rako_2025-0.1.0/python_rako/__version__.py +0 -3
  10. python_rako_2025-0.1.0/python_rako_2025.egg-info/not-zip-safe +0 -1
  11. python_rako_2025-0.1.0/setup.py +0 -55
  12. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/LICENSE +0 -0
  13. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/README.md +0 -0
  14. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako/__init__.py +0 -0
  15. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako/const.py +0 -0
  16. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako/exceptions.py +0 -0
  17. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako/helpers.py +0 -0
  18. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako/model.py +0 -0
  19. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako/py.typed +0 -0
  20. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako_2025.egg-info/dependency_links.txt +0 -0
  21. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako_2025.egg-info/requires.txt +0 -0
  22. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/python_rako_2025.egg-info/top_level.txt +0 -0
  23. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/setup.cfg +0 -0
  24. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/tests/test_helpers.py +0 -0
  25. {python_rako_2025-0.1.0 → python_rako_2025-0.3.0}/tests/test_model.py +0 -0
@@ -1,15 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-rako-2025
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Asynchronous Python client for Rako Controls Lighting
5
- Home-page: https://github.com/simonleigh/python-rako
6
- Author: Simon Leigh
7
5
  Author-email: Simon Leigh <simonleigh@users.noreply.github.com>
8
6
  Project-URL: Homepage, https://github.com/simonleigh/python-rako
9
7
  Project-URL: Repository, https://github.com/simonleigh/python-rako
10
8
  Project-URL: Issues, https://github.com/simonleigh/python-rako/issues
11
9
  Keywords: rako,controls,api,async,client
12
- Platform: any
13
10
  Classifier: Development Status :: 4 - Beta
14
11
  Classifier: Framework :: AsyncIO
15
12
  Classifier: Intended Audience :: Developers
@@ -28,11 +25,7 @@ License-File: LICENSE
28
25
  Requires-Dist: aiohttp>=3.10.0
29
26
  Requires-Dist: asyncio-dgram>=2.2.0
30
27
  Requires-Dist: xmltodict>=0.13.0
31
- Dynamic: author
32
- Dynamic: home-page
33
28
  Dynamic: license-file
34
- Dynamic: platform
35
- Dynamic: requires-python
36
29
 
37
30
  # Python: Rako Controls API Client
38
31
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-rako-2025"
7
- version = "0.1.0"
7
+ dynamic = ["version"]
8
8
  description = "Asynchronous Python client for Rako Controls Lighting"
9
9
  readme = "README.md"
10
10
  keywords = ["rako", "controls", "api", "async", "client"]
@@ -44,6 +44,9 @@ include = ["python_rako*"]
44
44
  [tool.setuptools.package-data]
45
45
  python_rako = ["py.typed"]
46
46
 
47
+ [tool.setuptools.dynamic]
48
+ version = {file = "python_rako/__version__.py"}
49
+
47
50
  [tool.ruff]
48
51
  target-version = "py310"
49
52
  line-length = 100
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -160,9 +160,30 @@ class Bridge:
160
160
  rako_xml: str = await response.text()
161
161
  return rako_xml
162
162
 
163
+ async def discover_devices(
164
+ self, session: aiohttp.ClientSession
165
+ ) -> tuple[list[Light], list[Ventilation]]:
166
+ """
167
+ Discover all devices by fetching XML once and parsing all device types.
168
+ Returns a tuple of (lights, ventilation) to avoid race conditions.
169
+ """
170
+ rako_xml = await self.get_rako_xml(session)
171
+
172
+ lights = []
173
+ ventilation = []
174
+
175
+ for device in self.get_devices_from_discovery_xml(rako_xml):
176
+ if isinstance(device, (RoomLight, ChannelLight)):
177
+ lights.append(device)
178
+ elif isinstance(device, (RoomVentilation, ChannelVentilation)):
179
+ ventilation.append(device)
180
+
181
+ return lights, ventilation
182
+
163
183
  async def discover_lights(
164
184
  self, session: aiohttp.ClientSession
165
185
  ) -> AsyncGenerator[Light]:
186
+ """Discover lights by fetching XML once and filtering for lights."""
166
187
  rako_xml = await self.get_rako_xml(session)
167
188
  for light in self.get_lights_from_discovery_xml(rako_xml):
168
189
  yield light
@@ -170,11 +191,20 @@ class Bridge:
170
191
  async def discover_ventilation(
171
192
  self, session: aiohttp.ClientSession
172
193
  ) -> AsyncGenerator[Ventilation]:
194
+ """Discover ventilation by fetching XML once and filtering for ventilation."""
173
195
  rako_xml = await self.get_rako_xml(session)
174
196
  for device in self.get_devices_from_discovery_xml(rako_xml, "Ventilation"):
175
197
  if isinstance(device, (RoomVentilation, ChannelVentilation)):
176
198
  yield device
177
199
 
200
+ async def discover_all_devices(
201
+ self, session: aiohttp.ClientSession
202
+ ) -> AsyncGenerator[Light | Ventilation]:
203
+ """Discover all devices by fetching XML once."""
204
+ rako_xml = await self.get_rako_xml(session)
205
+ for device in self.get_devices_from_discovery_xml(rako_xml):
206
+ yield device
207
+
178
208
  async def get_info(self, session: aiohttp.ClientSession) -> BridgeInfo:
179
209
  try:
180
210
  rako_xml = await self.get_rako_xml(session)
@@ -209,22 +239,35 @@ class Bridge:
209
239
  if isinstance(device, (RoomLight, ChannelLight)):
210
240
  yield device
211
241
 
242
+ @staticmethod
243
+ def get_all_devices_from_discovery_xml(xml: str) -> Generator[Light | Ventilation]:
244
+ """Get all devices (lights and ventilation) from discovery XML."""
245
+ return Bridge.get_devices_from_discovery_xml(xml)
246
+
212
247
  @staticmethod
213
248
  def get_devices_from_discovery_xml(
214
- xml: str, device_type: str
249
+ xml: str, device_types: str | list[str] | None = None
215
250
  ) -> Generator[Light | Ventilation]:
251
+ # Handle different input types for backward compatibility
252
+ if device_types is None or device_types == "All":
253
+ target_types = {"Lights", "Ventilation"}
254
+ elif isinstance(device_types, str):
255
+ target_types = {device_types}
256
+ else:
257
+ target_types = set(device_types)
258
+
216
259
  xml_dict = xmltodict.parse(xml, force_list={"Room"})
217
260
  for room in xml_dict["rako"]["rooms"]["Room"]:
218
261
  room_id = int(room["@id"])
219
262
  room_type = room.get("Type", "Lights")
220
- if room_type != device_type:
263
+ if room_type not in target_types:
221
264
  continue
222
265
  room_title = room["Title"]
223
266
 
224
267
  # Yield room-level device
225
- if device_type == "Lights":
268
+ if room_type == "Lights":
226
269
  yield RoomLight(room_id, room_title)
227
- elif device_type == "Ventilation":
270
+ elif room_type == "Ventilation":
228
271
  yield RoomVentilation(room_id, room_title)
229
272
 
230
273
  # Yield channel-level devices
@@ -240,7 +283,7 @@ class Bridge:
240
283
  channel_name = channel["Name"]
241
284
  channel_levels = channel["Levels"]
242
285
 
243
- if device_type == "Lights":
286
+ if room_type == "Lights":
244
287
  yield ChannelLight(
245
288
  room_id,
246
289
  room_title,
@@ -249,7 +292,7 @@ class Bridge:
249
292
  channel_name,
250
293
  channel_levels,
251
294
  )
252
- elif device_type == "Ventilation":
295
+ elif room_type == "Ventilation":
253
296
  yield ChannelVentilation(
254
297
  room_id,
255
298
  room_title,
@@ -1,15 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-rako-2025
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Asynchronous Python client for Rako Controls Lighting
5
- Home-page: https://github.com/simonleigh/python-rako
6
- Author: Simon Leigh
7
5
  Author-email: Simon Leigh <simonleigh@users.noreply.github.com>
8
6
  Project-URL: Homepage, https://github.com/simonleigh/python-rako
9
7
  Project-URL: Repository, https://github.com/simonleigh/python-rako
10
8
  Project-URL: Issues, https://github.com/simonleigh/python-rako/issues
11
9
  Keywords: rako,controls,api,async,client
12
- Platform: any
13
10
  Classifier: Development Status :: 4 - Beta
14
11
  Classifier: Framework :: AsyncIO
15
12
  Classifier: Intended Audience :: Developers
@@ -28,11 +25,7 @@ License-File: LICENSE
28
25
  Requires-Dist: aiohttp>=3.10.0
29
26
  Requires-Dist: asyncio-dgram>=2.2.0
30
27
  Requires-Dist: xmltodict>=0.13.0
31
- Dynamic: author
32
- Dynamic: home-page
33
28
  Dynamic: license-file
34
- Dynamic: platform
35
- Dynamic: requires-python
36
29
 
37
30
  # Python: Rako Controls API Client
38
31
 
@@ -13,7 +13,6 @@ python_rako/py.typed
13
13
  python_rako_2025.egg-info/PKG-INFO
14
14
  python_rako_2025.egg-info/SOURCES.txt
15
15
  python_rako_2025.egg-info/dependency_links.txt
16
- python_rako_2025.egg-info/not-zip-safe
17
16
  python_rako_2025.egg-info/requires.txt
18
17
  python_rako_2025.egg-info/top_level.txt
19
18
  tests/test_bridge.py
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()
@@ -141,3 +141,85 @@ def test_ventilation_command_compatibility():
141
141
  # Test that ventilation channel can use lighting level commands
142
142
  channel_level_command = CommandLevelHTTP(room=161, channel=1, level=128)
143
143
  assert channel_level_command.as_params() == {"room": 161, "ch": 1, "lev": 128}
144
+
145
+
146
+ def test_get_all_devices_from_discovery_xml(rako_xml3):
147
+ """Test discovering all device types from XML"""
148
+ all_devices = list(Bridge.get_all_devices_from_discovery_xml(rako_xml3))
149
+
150
+ # Should contain both lights and ventilation
151
+ lights = [dev for dev in all_devices if isinstance(dev, (RoomLight, ChannelLight))]
152
+ ventilation = [
153
+ dev
154
+ for dev in all_devices
155
+ if isinstance(dev, (RoomVentilation, ChannelVentilation))
156
+ ]
157
+
158
+ # Should have many lights and 2 ventilation devices
159
+ assert len(lights) > 10 # rako3.xml has many light rooms
160
+ assert len(ventilation) == 2 # 1 RoomVentilation + 1 ChannelVentilation
161
+
162
+ # Check specific ventilation devices are included
163
+ ventilation_room = next(
164
+ (dev for dev in ventilation if isinstance(dev, RoomVentilation)), None
165
+ )
166
+ assert ventilation_room is not None
167
+ assert ventilation_room.room_id == 161
168
+ assert ventilation_room.room_title == "Fans"
169
+
170
+
171
+ def test_get_devices_with_specific_types(rako_xml3):
172
+ """Test discovering specific device types using list parameter"""
173
+ # Test with list of device types
174
+ lights_only = list(Bridge.get_devices_from_discovery_xml(rako_xml3, ["Lights"]))
175
+ ventilation_only = list(
176
+ Bridge.get_devices_from_discovery_xml(rako_xml3, ["Ventilation"])
177
+ )
178
+ both_types = list(
179
+ Bridge.get_devices_from_discovery_xml(rako_xml3, ["Lights", "Ventilation"])
180
+ )
181
+
182
+ # Verify filtering works correctly
183
+ assert all(isinstance(dev, (RoomLight, ChannelLight)) for dev in lights_only)
184
+ assert all(
185
+ isinstance(dev, (RoomVentilation, ChannelVentilation))
186
+ for dev in ventilation_only
187
+ )
188
+ assert len(both_types) == len(lights_only) + len(ventilation_only)
189
+
190
+
191
+ def test_get_devices_backward_compatibility(rako_xml3):
192
+ """Test that existing string parameter still works"""
193
+ # Test backward compatibility with string parameter
194
+ lights_str = list(Bridge.get_devices_from_discovery_xml(rako_xml3, "Lights"))
195
+ lights_list = list(Bridge.get_devices_from_discovery_xml(rako_xml3, ["Lights"]))
196
+
197
+ # Should return the same devices
198
+ assert lights_str == lights_list
199
+
200
+ # Test ventilation backward compatibility
201
+ ventilation_str = list(
202
+ Bridge.get_devices_from_discovery_xml(rako_xml3, "Ventilation")
203
+ )
204
+ ventilation_list = list(
205
+ Bridge.get_devices_from_discovery_xml(rako_xml3, ["Ventilation"])
206
+ )
207
+
208
+ assert ventilation_str == ventilation_list
209
+
210
+
211
+ def test_get_devices_all_parameter_variants(rako_xml3):
212
+ """Test different ways to get all devices"""
213
+ all_devices_none = list(Bridge.get_devices_from_discovery_xml(rako_xml3, None))
214
+ all_devices_all = list(Bridge.get_devices_from_discovery_xml(rako_xml3, "All"))
215
+ all_devices_method = list(Bridge.get_all_devices_from_discovery_xml(rako_xml3))
216
+
217
+ # All should return the same devices
218
+ assert all_devices_none == all_devices_all == all_devices_method
219
+
220
+ # Should contain both lights and ventilation
221
+ assert any(isinstance(dev, (RoomLight, ChannelLight)) for dev in all_devices_none)
222
+ assert any(
223
+ isinstance(dev, (RoomVentilation, ChannelVentilation))
224
+ for dev in all_devices_none
225
+ )
@@ -1,3 +0,0 @@
1
- """AUTOGENERATED ON RELEASE."""
2
-
3
- __version__ = "0.1.0"
@@ -1,55 +0,0 @@
1
- import os
2
-
3
- from setuptools import find_packages, setup
4
-
5
-
6
- def read(*parts):
7
- """Read file."""
8
- filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts)
9
- with open(filename, encoding="utf-8") as fp:
10
- return fp.read()
11
-
12
-
13
- with open("README.md") as readme_file:
14
- readme = readme_file.read()
15
-
16
- setup(
17
- name="python-rako-2025",
18
- version="0.1.0",
19
- license="MIT",
20
- url="https://github.com/simonleigh/python-rako",
21
- author="Simon Leigh",
22
- author_email="simonleigh@users.noreply.github.com",
23
- description="Asynchronous Python client for Rako Controls Lighting.",
24
- keywords=["rako", "controls", "api", "async", "client"],
25
- long_description=readme,
26
- long_description_content_type="text/markdown",
27
- packages=find_packages(include=["python_rako"]),
28
- zip_safe=False,
29
- include_package_data=True,
30
- platforms="any",
31
- package_data={
32
- "python_rako": ["py.typed"],
33
- },
34
- install_requires=[
35
- "aiohttp>=3.10.0",
36
- "asyncio-dgram>=2.2.0",
37
- "xmltodict>=0.13.0",
38
- ],
39
- classifiers=[
40
- "Intended Audience :: Developers",
41
- "License :: OSI Approved :: MIT License",
42
- "Operating System :: OS Independent",
43
- "Programming Language :: Python",
44
- "Programming Language :: Python :: 3",
45
- "Programming Language :: Python :: 3.10",
46
- "Programming Language :: Python :: 3.11",
47
- "Programming Language :: Python :: 3.12",
48
- "Programming Language :: Python :: 3.13",
49
- "Natural Language :: English",
50
- "Topic :: Software Development :: Libraries :: Python Modules",
51
- "Framework :: AsyncIO",
52
- "Development Status :: 4 - Beta",
53
- ],
54
- python_requires=">=3.10",
55
- )