python-rako-2025 0.0.1__tar.gz → 0.1.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 (23) hide show
  1. {python_rako_2025-0.0.1/python_rako_2025.egg-info → python_rako_2025-0.1.0}/PKG-INFO +1 -1
  2. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/pyproject.toml +3 -2
  3. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/__init__.py +3 -0
  4. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/__version__.py +2 -1
  5. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/bridge.py +53 -18
  6. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/helpers.py +1 -1
  7. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/model.py +24 -1
  8. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0/python_rako_2025.egg-info}/PKG-INFO +1 -1
  9. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/setup.py +6 -3
  10. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/tests/test_bridge.py +45 -1
  11. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/LICENSE +0 -0
  12. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/README.md +0 -0
  13. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/const.py +0 -0
  14. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/exceptions.py +0 -0
  15. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/py.typed +0 -0
  16. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/SOURCES.txt +0 -0
  17. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/dependency_links.txt +0 -0
  18. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/not-zip-safe +0 -0
  19. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/requires.txt +0 -0
  20. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/top_level.txt +0 -0
  21. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/setup.cfg +0 -0
  22. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/tests/test_helpers.py +0 -0
  23. {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/tests/test_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-rako-2025
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: Asynchronous Python client for Rako Controls Lighting
5
5
  Home-page: https://github.com/simonleigh/python-rako
6
6
  Author: Simon Leigh
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-rako-2025"
7
- version = "0.0.1"
7
+ version = "0.1.0"
8
8
  description = "Asynchronous Python client for Rako Controls Lighting"
9
9
  readme = "README.md"
10
10
  keywords = ["rako", "controls", "api", "async", "client"]
@@ -81,6 +81,7 @@ warn_unused_ignores = true
81
81
  warn_return_any = true
82
82
  warn_unreachable = true
83
83
  strict_equality = true
84
+ enable_error_code = ["ignore-without-code"]
84
85
 
85
86
  [tool.pytest.ini_options]
86
- asyncio_mode = "auto"
87
+ asyncio_mode = "auto"
@@ -15,14 +15,17 @@ from python_rako.model import ( # noqa
15
15
  BridgeInfo,
16
16
  ChannelLight,
17
17
  ChannelStatusMessage,
18
+ ChannelVentilation,
18
19
  LevelCache,
19
20
  LevelCacheItem,
20
21
  Light,
21
22
  RoomChannel,
22
23
  RoomLight,
24
+ RoomVentilation,
23
25
  SceneCache,
24
26
  SceneStatusMessage,
25
27
  UnsupportedMessage,
28
+ Ventilation,
26
29
  )
27
30
 
28
31
  _LOGGER = logging.getLogger(__name__)
@@ -1,2 +1,3 @@
1
1
  """AUTOGENERATED ON RELEASE."""
2
- __version__ = "0.0.1"
2
+
3
+ __version__ = "0.1.0"
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from typing import Any, AsyncGenerator, Generator, Tuple
5
+ from collections.abc import AsyncGenerator, Generator
6
+ from typing import Any
6
7
 
7
8
  import aiohttp
8
9
  import xmltodict
@@ -24,6 +25,7 @@ from python_rako.helpers import (
24
25
  from python_rako.model import (
25
26
  BridgeInfo,
26
27
  ChannelLight,
28
+ ChannelVentilation,
27
29
  CommandHTTP,
28
30
  CommandLevelHTTP,
29
31
  CommandSceneHTTP,
@@ -32,7 +34,9 @@ from python_rako.model import (
32
34
  LevelCache,
33
35
  Light,
34
36
  RoomLight,
37
+ RoomVentilation,
35
38
  SceneCache,
39
+ Ventilation,
36
40
  )
37
41
 
38
42
  _LOGGER = logging.getLogger(__name__)
@@ -158,11 +162,19 @@ class Bridge:
158
162
 
159
163
  async def discover_lights(
160
164
  self, session: aiohttp.ClientSession
161
- ) -> AsyncGenerator[Light, None]:
165
+ ) -> AsyncGenerator[Light]:
162
166
  rako_xml = await self.get_rako_xml(session)
163
167
  for light in self.get_lights_from_discovery_xml(rako_xml):
164
168
  yield light
165
169
 
170
+ async def discover_ventilation(
171
+ self, session: aiohttp.ClientSession
172
+ ) -> AsyncGenerator[Ventilation]:
173
+ rako_xml = await self.get_rako_xml(session)
174
+ for device in self.get_devices_from_discovery_xml(rako_xml, "Ventilation"):
175
+ if isinstance(device, (RoomVentilation, ChannelVentilation)):
176
+ yield device
177
+
166
178
  async def get_info(self, session: aiohttp.ClientSession) -> BridgeInfo:
167
179
  try:
168
180
  rako_xml = await self.get_rako_xml(session)
@@ -192,18 +204,30 @@ class Bridge:
192
204
  )
193
205
 
194
206
  @staticmethod
195
- def get_lights_from_discovery_xml(xml: str) -> Generator[Light, None, None]:
196
- xml_dict = xmltodict.parse(xml, force_list={'Room'})
207
+ def get_lights_from_discovery_xml(xml: str) -> Generator[Light]:
208
+ for device in Bridge.get_devices_from_discovery_xml(xml, "Lights"):
209
+ if isinstance(device, (RoomLight, ChannelLight)):
210
+ yield device
211
+
212
+ @staticmethod
213
+ def get_devices_from_discovery_xml(
214
+ xml: str, device_type: str
215
+ ) -> Generator[Light | Ventilation]:
216
+ xml_dict = xmltodict.parse(xml, force_list={"Room"})
197
217
  for room in xml_dict["rako"]["rooms"]["Room"]:
198
218
  room_id = int(room["@id"])
199
219
  room_type = room.get("Type", "Lights")
200
- if room_type != "Lights":
201
- _LOGGER.info(
202
- "Unsupported room type. room_id=%s room_type=%s", room_id, room_type
203
- )
220
+ if room_type != device_type:
204
221
  continue
205
222
  room_title = room["Title"]
206
- yield RoomLight(room_id, room_title)
223
+
224
+ # Yield room-level device
225
+ if device_type == "Lights":
226
+ yield RoomLight(room_id, room_title)
227
+ elif device_type == "Ventilation":
228
+ yield RoomVentilation(room_id, room_title)
229
+
230
+ # Yield channel-level devices
207
231
  channels_section = room.get("Channel", [])
208
232
  channels = (
209
233
  channels_section
@@ -215,14 +239,25 @@ class Bridge:
215
239
  channel_type = channel.get("type", "Default")
216
240
  channel_name = channel["Name"]
217
241
  channel_levels = channel["Levels"]
218
- yield ChannelLight(
219
- room_id,
220
- room_title,
221
- channel_id,
222
- channel_type,
223
- channel_name,
224
- channel_levels,
225
- )
242
+
243
+ if device_type == "Lights":
244
+ yield ChannelLight(
245
+ room_id,
246
+ room_title,
247
+ channel_id,
248
+ channel_type,
249
+ channel_name,
250
+ channel_levels,
251
+ )
252
+ elif device_type == "Ventilation":
253
+ yield ChannelVentilation(
254
+ room_id,
255
+ room_title,
256
+ channel_id,
257
+ channel_type,
258
+ channel_name,
259
+ channel_levels,
260
+ )
226
261
 
227
262
  async def next_pushed_message(self, dg_listener: DatagramServer) -> Any | None:
228
263
  resp = await dg_listener.recv()
@@ -241,7 +276,7 @@ class Bridge:
241
276
 
242
277
  async def get_cache_state(
243
278
  self, cache_type: RequestType = RequestType.SCENE_LEVEL_CACHE
244
- ) -> Tuple[LevelCache, SceneCache]:
279
+ ) -> tuple[LevelCache, SceneCache]:
245
280
  scene_cache = SceneCache()
246
281
  level_cache = LevelCache()
247
282
  async with get_dg_commander(self.host, self.port) as dg_client:
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from collections.abc import AsyncIterator
4
5
  from contextlib import asynccontextmanager
5
- from typing import AsyncIterator
6
6
 
7
7
  import asyncio_dgram
8
8
  from asyncio_dgram.aio import DatagramClient, DatagramServer
@@ -1,5 +1,5 @@
1
+ from collections.abc import Iterable
1
2
  from dataclasses import dataclass
2
- from typing import Iterable
3
3
 
4
4
  from python_rako.const import CommandType, MessageType
5
5
 
@@ -33,6 +33,29 @@ class ChannelLight(Light):
33
33
  channel_levels: str
34
34
 
35
35
 
36
+ @dataclass
37
+ class Ventilation:
38
+ room_id: int
39
+ room_title: str
40
+ channel_id: int
41
+
42
+ @property
43
+ def room_channel(self) -> RoomChannel:
44
+ return RoomChannel(self.room_id, self.channel_id)
45
+
46
+
47
+ @dataclass
48
+ class RoomVentilation(Ventilation):
49
+ channel_id: int = 0
50
+
51
+
52
+ @dataclass
53
+ class ChannelVentilation(Ventilation):
54
+ channel_type: str
55
+ channel_name: str
56
+ channel_levels: str
57
+
58
+
36
59
  @dataclass(frozen=True)
37
60
  class BridgeInfo:
38
61
  version: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-rako-2025
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: Asynchronous Python client for Rako Controls Lighting
5
5
  Home-page: https://github.com/simonleigh/python-rako
6
6
  Author: Simon Leigh
@@ -1,18 +1,21 @@
1
1
  import os
2
+
2
3
  from setuptools import find_packages, setup
3
4
 
5
+
4
6
  def read(*parts):
5
7
  """Read file."""
6
8
  filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts)
7
- with open(filename, encoding="utf-8", mode="rt") as fp:
9
+ with open(filename, encoding="utf-8") as fp:
8
10
  return fp.read()
9
11
 
12
+
10
13
  with open("README.md") as readme_file:
11
14
  readme = readme_file.read()
12
15
 
13
16
  setup(
14
17
  name="python-rako-2025",
15
- version="0.0.1",
18
+ version="0.1.0",
16
19
  license="MIT",
17
20
  url="https://github.com/simonleigh/python-rako",
18
21
  author="Simon Leigh",
@@ -49,4 +52,4 @@ setup(
49
52
  "Development Status :: 4 - Beta",
50
53
  ],
51
54
  python_requires=">=3.10",
52
- )
55
+ )
@@ -1,5 +1,11 @@
1
1
  from python_rako.bridge import Bridge
2
- from python_rako.model import BridgeInfo, ChannelLight, RoomLight
2
+ from python_rako.model import (
3
+ BridgeInfo,
4
+ ChannelLight,
5
+ ChannelVentilation,
6
+ RoomLight,
7
+ RoomVentilation,
8
+ )
3
9
 
4
10
 
5
11
  def test_get_lights_from_discovery_xml(rako_xml):
@@ -97,3 +103,41 @@ def test_get_lights_from_discovery_xml2(rako_xml2):
97
103
  ]
98
104
 
99
105
  assert list(lights) == expected_lights
106
+
107
+
108
+ def test_get_ventilation_from_discovery_xml(rako_xml3):
109
+ ventilation_devices = Bridge.get_devices_from_discovery_xml(
110
+ rako_xml3, "Ventilation"
111
+ )
112
+
113
+ expected_ventilation = [
114
+ RoomVentilation(room_id=161, room_title="Fans", channel_id=0),
115
+ ChannelVentilation(
116
+ room_id=161,
117
+ room_title="Fans",
118
+ channel_id=1,
119
+ channel_type="switch",
120
+ channel_name="Fans",
121
+ channel_levels="FFBF7F3F000000000000000000000000",
122
+ ),
123
+ ]
124
+
125
+ ventilation_list = [
126
+ dev
127
+ for dev in ventilation_devices
128
+ if isinstance(dev, (RoomVentilation, ChannelVentilation))
129
+ ]
130
+ assert ventilation_list == expected_ventilation
131
+
132
+
133
+ def test_ventilation_command_compatibility():
134
+ """Test that ventilation can use the same commands as lighting"""
135
+ from python_rako.model import CommandLevelHTTP, CommandSceneHTTP
136
+
137
+ # Test that ventilation room can use lighting commands
138
+ room_scene_command = CommandSceneHTTP(room=161, channel=0, scene=1)
139
+ assert room_scene_command.as_params() == {"room": 161, "ch": 0, "sc": 1}
140
+
141
+ # Test that ventilation channel can use lighting level commands
142
+ channel_level_command = CommandLevelHTTP(room=161, channel=1, level=128)
143
+ assert channel_level_command.as_params() == {"room": 161, "ch": 1, "lev": 128}