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.
- {python_rako_2025-0.0.1/python_rako_2025.egg-info → python_rako_2025-0.1.0}/PKG-INFO +1 -1
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/pyproject.toml +3 -2
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/__init__.py +3 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/__version__.py +2 -1
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/bridge.py +53 -18
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/helpers.py +1 -1
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/model.py +24 -1
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0/python_rako_2025.egg-info}/PKG-INFO +1 -1
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/setup.py +6 -3
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/tests/test_bridge.py +45 -1
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/LICENSE +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/README.md +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/const.py +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/exceptions.py +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako/py.typed +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/SOURCES.txt +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/dependency_links.txt +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/not-zip-safe +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/requires.txt +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/python_rako_2025.egg-info/top_level.txt +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/setup.cfg +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/tests/test_helpers.py +0 -0
- {python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/tests/test_model.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-rako-2025"
|
|
7
|
-
version = "0.0
|
|
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__)
|
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from
|
|
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
|
|
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
|
|
196
|
-
|
|
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 !=
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
) ->
|
|
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,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"
|
|
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
|
|
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
|
|
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}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_rako_2025-0.0.1 → python_rako_2025-0.1.0}/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
|