python-rako-2025 0.3.7__tar.gz → 0.3.9__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.
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/PKG-INFO +1 -1
- python_rako_2025-0.3.9/python_rako/__version__.py +1 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako/bridge.py +56 -36
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako_2025.egg-info/PKG-INFO +1 -1
- python_rako_2025-0.3.7/python_rako/__version__.py +0 -1
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/LICENSE +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/README.md +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/pyproject.toml +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako/__init__.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako/const.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako/exceptions.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako/helpers.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako/model.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako/py.typed +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako_2025.egg-info/SOURCES.txt +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako_2025.egg-info/dependency_links.txt +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako_2025.egg-info/requires.txt +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako_2025.egg-info/top_level.txt +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/setup.cfg +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/setup.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/tests/test_bridge.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/tests/test_helpers.py +0 -0
- {python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/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.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Asynchronous Python client for Rako Controls Lighting
|
|
5
5
|
Author-email: Simon Leigh <simonleigh@users.noreply.github.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/simonleigh/python-rako
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.9
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
import threading
|
|
5
6
|
from collections.abc import AsyncGenerator, Generator
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -32,15 +33,16 @@ from python_rako.model import (
|
|
|
32
33
|
CommandUDP,
|
|
33
34
|
EOFResponse,
|
|
34
35
|
LevelCache,
|
|
35
|
-
Light,
|
|
36
36
|
RoomLight,
|
|
37
37
|
RoomVentilation,
|
|
38
38
|
SceneCache,
|
|
39
|
-
Ventilation,
|
|
40
39
|
)
|
|
41
40
|
|
|
42
41
|
_LOGGER = logging.getLogger(__name__)
|
|
43
42
|
|
|
43
|
+
# Thread lock for XML parsing to ensure concurrency safety
|
|
44
|
+
_XML_PARSE_LOCK = threading.Lock()
|
|
45
|
+
|
|
44
46
|
|
|
45
47
|
class _BridgeCommander:
|
|
46
48
|
def __init__(self, host: str, port: int):
|
|
@@ -150,64 +152,76 @@ class Bridge:
|
|
|
150
152
|
)
|
|
151
153
|
self.level_cache: LevelCache = LevelCache()
|
|
152
154
|
self.scene_cache: SceneCache = SceneCache()
|
|
155
|
+
self._cached_xml: str | None = None
|
|
153
156
|
|
|
154
157
|
@property
|
|
155
158
|
def _discovery_url(self) -> str:
|
|
156
159
|
return f"http://{self.host}/rako.xml"
|
|
157
160
|
|
|
158
|
-
async def get_rako_xml(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
async def get_rako_xml(
|
|
162
|
+
self, session: aiohttp.ClientSession, force_refresh: bool = False
|
|
163
|
+
) -> str:
|
|
164
|
+
if self._cached_xml is None or force_refresh:
|
|
165
|
+
async with session.get(self._discovery_url) as response:
|
|
166
|
+
self._cached_xml = await response.text()
|
|
167
|
+
assert self._cached_xml is not None
|
|
168
|
+
return self._cached_xml
|
|
162
169
|
|
|
163
170
|
async def discover_devices(
|
|
164
|
-
self, session: aiohttp.ClientSession
|
|
165
|
-
) -> tuple[
|
|
171
|
+
self, session: aiohttp.ClientSession, force_refresh: bool = False
|
|
172
|
+
) -> tuple[
|
|
173
|
+
list[RoomLight | ChannelLight], list[RoomVentilation | ChannelVentilation]
|
|
174
|
+
]:
|
|
166
175
|
"""Discover all devices by fetching XML once and parsing all device types.
|
|
167
176
|
|
|
168
177
|
Returns a tuple of (lights, ventilation) to avoid race conditions.
|
|
169
178
|
"""
|
|
170
|
-
rako_xml = await self.get_rako_xml(session)
|
|
179
|
+
rako_xml = await self.get_rako_xml(session, force_refresh)
|
|
171
180
|
|
|
172
|
-
lights: list[
|
|
173
|
-
ventilation: list[
|
|
181
|
+
lights: list[RoomLight | ChannelLight] = []
|
|
182
|
+
ventilation: list[RoomVentilation | ChannelVentilation] = []
|
|
174
183
|
|
|
175
184
|
for device in self.get_devices_from_discovery_xml(rako_xml):
|
|
176
185
|
if isinstance(device, (RoomLight, ChannelLight)):
|
|
177
|
-
lights.append(device)
|
|
186
|
+
lights.append(device)
|
|
178
187
|
elif isinstance(device, (RoomVentilation, ChannelVentilation)):
|
|
179
|
-
ventilation.append(device)
|
|
188
|
+
ventilation.append(device)
|
|
180
189
|
|
|
181
190
|
return lights, ventilation
|
|
182
191
|
|
|
183
192
|
async def discover_lights(
|
|
184
|
-
self, session: aiohttp.ClientSession
|
|
185
|
-
) -> AsyncGenerator[RoomLight | ChannelLight
|
|
193
|
+
self, session: aiohttp.ClientSession, force_refresh: bool = False
|
|
194
|
+
) -> AsyncGenerator[RoomLight | ChannelLight]:
|
|
186
195
|
"""Discover lights by fetching XML once and filtering for lights."""
|
|
187
|
-
|
|
188
|
-
for light in
|
|
196
|
+
lights, _ = await self.discover_devices(session, force_refresh)
|
|
197
|
+
for light in lights:
|
|
189
198
|
yield light
|
|
190
199
|
|
|
191
200
|
async def discover_ventilation(
|
|
192
|
-
self, session: aiohttp.ClientSession
|
|
193
|
-
) -> AsyncGenerator[RoomVentilation | ChannelVentilation
|
|
201
|
+
self, session: aiohttp.ClientSession, force_refresh: bool = False
|
|
202
|
+
) -> AsyncGenerator[RoomVentilation | ChannelVentilation]:
|
|
194
203
|
"""Discover ventilation by fetching XML once and filtering for ventilation."""
|
|
195
|
-
|
|
196
|
-
for
|
|
197
|
-
|
|
198
|
-
yield device
|
|
204
|
+
_, ventilation = await self.discover_devices(session, force_refresh)
|
|
205
|
+
for vent in ventilation:
|
|
206
|
+
yield vent
|
|
199
207
|
|
|
200
208
|
async def discover_all_devices(
|
|
201
|
-
self, session: aiohttp.ClientSession
|
|
202
|
-
) -> AsyncGenerator[
|
|
209
|
+
self, session: aiohttp.ClientSession, force_refresh: bool = False
|
|
210
|
+
) -> AsyncGenerator[
|
|
211
|
+
RoomLight | ChannelLight | RoomVentilation | ChannelVentilation
|
|
212
|
+
]:
|
|
203
213
|
"""Discover all devices by fetching XML once."""
|
|
204
|
-
|
|
205
|
-
for
|
|
206
|
-
yield
|
|
214
|
+
lights, ventilation = await self.discover_devices(session, force_refresh)
|
|
215
|
+
for light in lights:
|
|
216
|
+
yield light
|
|
217
|
+
for vent in ventilation:
|
|
218
|
+
yield vent
|
|
207
219
|
|
|
208
|
-
async def get_info(
|
|
220
|
+
async def get_info(
|
|
221
|
+
self, session: aiohttp.ClientSession, force_refresh: bool = False
|
|
222
|
+
) -> BridgeInfo:
|
|
209
223
|
try:
|
|
210
|
-
rako_xml = await self.get_rako_xml(session)
|
|
224
|
+
rako_xml = await self.get_rako_xml(session, force_refresh)
|
|
211
225
|
info = self.get_bridge_info_from_discovery_xml(rako_xml)
|
|
212
226
|
except (KeyError, ValueError) as ex:
|
|
213
227
|
raise RakoBridgeError(f"unsupported bridge: {ex}")
|
|
@@ -217,7 +231,8 @@ class Bridge:
|
|
|
217
231
|
|
|
218
232
|
@staticmethod
|
|
219
233
|
def get_bridge_info_from_discovery_xml(xml: str) -> BridgeInfo:
|
|
220
|
-
|
|
234
|
+
with _XML_PARSE_LOCK:
|
|
235
|
+
xml_dict = xmltodict.parse(xml)
|
|
221
236
|
info = xml_dict["rako"].get("info", dict())
|
|
222
237
|
config = xml_dict["rako"].get("config", dict())
|
|
223
238
|
return BridgeInfo(
|
|
@@ -234,20 +249,24 @@ class Bridge:
|
|
|
234
249
|
)
|
|
235
250
|
|
|
236
251
|
@staticmethod
|
|
237
|
-
def get_lights_from_discovery_xml(
|
|
252
|
+
def get_lights_from_discovery_xml(
|
|
253
|
+
xml: str,
|
|
254
|
+
) -> Generator[RoomLight | ChannelLight]:
|
|
238
255
|
for device in Bridge.get_devices_from_discovery_xml(xml, "Lights"):
|
|
239
256
|
if isinstance(device, (RoomLight, ChannelLight)):
|
|
240
257
|
yield device
|
|
241
258
|
|
|
242
259
|
@staticmethod
|
|
243
|
-
def get_all_devices_from_discovery_xml(
|
|
260
|
+
def get_all_devices_from_discovery_xml(
|
|
261
|
+
xml: str,
|
|
262
|
+
) -> Generator[RoomLight | ChannelLight | RoomVentilation | ChannelVentilation]:
|
|
244
263
|
"""Get all devices (lights and ventilation) from discovery XML."""
|
|
245
264
|
return Bridge.get_devices_from_discovery_xml(xml)
|
|
246
265
|
|
|
247
266
|
@staticmethod
|
|
248
267
|
def get_devices_from_discovery_xml(
|
|
249
268
|
xml: str, device_types: str | list[str] | None = None
|
|
250
|
-
) -> Generator[RoomLight | ChannelLight | RoomVentilation | ChannelVentilation
|
|
269
|
+
) -> Generator[RoomLight | ChannelLight | RoomVentilation | ChannelVentilation]:
|
|
251
270
|
# Handle different input types for backward compatibility
|
|
252
271
|
if device_types is None or device_types == "All":
|
|
253
272
|
target_types = {"Lights", "Ventilation"}
|
|
@@ -256,7 +275,8 @@ class Bridge:
|
|
|
256
275
|
else:
|
|
257
276
|
target_types = set(device_types)
|
|
258
277
|
|
|
259
|
-
|
|
278
|
+
with _XML_PARSE_LOCK:
|
|
279
|
+
xml_dict = xmltodict.parse(xml, force_list={"Room"})
|
|
260
280
|
for room in xml_dict["rako"]["rooms"]["Room"]:
|
|
261
281
|
room_id = int(room["@id"])
|
|
262
282
|
room_type = room.get("Type", "Lights")
|
|
@@ -312,7 +332,7 @@ class Bridge:
|
|
|
312
332
|
if remote_ip != self.host:
|
|
313
333
|
return None
|
|
314
334
|
|
|
315
|
-
byte_list = list(data)
|
|
335
|
+
byte_list = list(data)
|
|
316
336
|
_LOGGER.debug("Received bytes: %s", byte_list)
|
|
317
337
|
message = deserialise_byte_list(byte_list)
|
|
318
338
|
_LOGGER.debug("Deserialised received message as: %s", message)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-rako-2025
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Asynchronous Python client for Rako Controls Lighting
|
|
5
5
|
Author-email: Simon Leigh <simonleigh@users.noreply.github.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/simonleigh/python-rako
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.7
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_rako_2025-0.3.7 → python_rako_2025-0.3.9}/python_rako_2025.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|