python-rako-2025 0.4.3__tar.gz → 0.4.4__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.4.3 → python_rako_2025-0.4.4}/.claude/settings.local.json +2 -1
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/PKG-INFO +30 -49
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/README.md +29 -48
- python_rako_2025-0.4.4/python_rako/__version__.py +1 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/bridge.py +42 -15
- python_rako_2025-0.4.4/python_rako/exceptions.py +12 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/helpers.py +45 -1
- python_rako_2025-0.4.3/python_rako/__version__.py +0 -1
- python_rako_2025-0.4.3/python_rako/exceptions.py +0 -2
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.coveragerc +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.editorconfig +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.flake8 +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.gitattributes +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.github/dependabot.yml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.github/workflows/ci.yml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.github/workflows/release.yml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.gitignore +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.isort.cfg +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.pre-commit-config.yaml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.yamllint +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/LICENSE +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/Makefile +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/catalog-info.yaml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/conftest.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/doc/accessing-the-rako-bridge.pdf +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/discover_all_devices.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/example.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/ventilation_example.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/view_updates.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/pyproject.toml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/__init__.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/const.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/model.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/py.typed +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/README_TESTING.md +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/resources/rako.xml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/resources/rako2.xml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/resources/rako3.xml +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_async_lock.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_bridge.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_edge_cases.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_helpers.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_model.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_xml_parsing.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/__init__.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/conftest.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/test_command_live.py +0 -0
- {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/test_general_live.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-rako-2025
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Asynchronous Python client for Rako Controls Lighting
|
|
5
5
|
Project-URL: Homepage, https://github.com/simonleigh/python-rako
|
|
6
6
|
Project-URL: Repository, https://github.com/simonleigh/python-rako
|
|
@@ -48,11 +48,10 @@ Description-Content-Type: text/markdown
|
|
|
48
48
|
[![Code Coverage][codecov-shield]][codecov]
|
|
49
49
|
[![Code Quality][code-quality-shield]][code-quality]
|
|
50
50
|
|
|
51
|
+
[![Buy me a coffee][buymeacoffee-shield]][buymeacoffee]
|
|
51
52
|
|
|
52
53
|
Asynchronous Python client for Rako Controls.
|
|
53
54
|
|
|
54
|
-
**This is a modernized fork** that has been updated to Python 3.12+ best practices, includes async concurrency fixes for Home Assistant integration, and uses modern tooling (Ruff, Hatchling, etc.).
|
|
55
|
-
|
|
56
55
|
## About
|
|
57
56
|
|
|
58
57
|
This package allows you to control and monitor Rako Controls devices
|
|
@@ -62,42 +61,23 @@ their behavior.
|
|
|
62
61
|
## Installation
|
|
63
62
|
|
|
64
63
|
```bash
|
|
65
|
-
pip install python-rako
|
|
64
|
+
pip install python-rako
|
|
66
65
|
```
|
|
67
66
|
|
|
68
67
|
## Usage
|
|
69
68
|
|
|
70
69
|
```python
|
|
71
70
|
import asyncio
|
|
72
|
-
import aiohttp
|
|
73
|
-
from python_rako import discover_bridge, Bridge
|
|
74
71
|
|
|
75
72
|
|
|
76
73
|
async def main():
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
print(f"Found bridge: {bridge_info}")
|
|
80
|
-
|
|
81
|
-
# Create bridge instance
|
|
82
|
-
bridge = Bridge(
|
|
83
|
-
host=bridge_info["host"],
|
|
84
|
-
port=bridge_info["port"],
|
|
85
|
-
name=bridge_info["name"],
|
|
86
|
-
mac=bridge_info["mac"]
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Get bridge information and discover devices
|
|
90
|
-
async with aiohttp.ClientSession() as session:
|
|
91
|
-
info = await bridge.get_info(session)
|
|
92
|
-
print(f"Bridge version: {info.version}")
|
|
93
|
-
|
|
94
|
-
# Discover all lights
|
|
95
|
-
async for light in bridge.discover_lights(session):
|
|
96
|
-
print(f"Found light: {light.title} (Room {light.room_id})")
|
|
74
|
+
# TODO
|
|
75
|
+
pass
|
|
97
76
|
|
|
98
77
|
|
|
99
78
|
if __name__ == "__main__":
|
|
100
|
-
asyncio.
|
|
79
|
+
loop = asyncio.get_event_loop()
|
|
80
|
+
loop.run_until_complete(main())
|
|
101
81
|
```
|
|
102
82
|
|
|
103
83
|
## Changelog & Releases
|
|
@@ -145,10 +125,11 @@ Asynchronous Python client for Rako Controls Lighting.
|
|
|
145
125
|
Usage:
|
|
146
126
|
make help Shows this message.
|
|
147
127
|
make dev Set up a development environment.
|
|
148
|
-
make lint Run
|
|
149
|
-
make
|
|
150
|
-
make
|
|
151
|
-
make
|
|
128
|
+
make lint Run all linters.
|
|
129
|
+
make lint-black Run linting using black & blacken-docs.
|
|
130
|
+
make lint-flake8 Run linting using flake8 (pycodestyle/pydocstyle).
|
|
131
|
+
make lint-pylint Run linting using PyLint.
|
|
132
|
+
make lint-mypy Run linting using MyPy.
|
|
152
133
|
make test Run tests quickly with the default Python.
|
|
153
134
|
make coverage Check code coverage quickly with the default Python.
|
|
154
135
|
make install Install the package to the active Python's site-packages.
|
|
@@ -159,17 +140,13 @@ Usage:
|
|
|
159
140
|
make clean-test Removes test and coverage artifacts.
|
|
160
141
|
make clean-venv Removes Python virtual environment artifacts.
|
|
161
142
|
make dist Builds source and wheel package.
|
|
162
|
-
make release Release build on
|
|
143
|
+
make release Release build on PyP
|
|
163
144
|
make venv Create Python venv environment.
|
|
164
|
-
make bump-patch Bump patch version (x.y.Z) and commit.
|
|
165
|
-
make bump-minor Bump minor version (x.Y.z) and commit.
|
|
166
|
-
make bump-major Bump major version (X.y.z) and commit.
|
|
167
145
|
```
|
|
168
146
|
|
|
169
147
|
## Authors & contributors
|
|
170
148
|
|
|
171
149
|
The original setup of this repository is by [Ben Marengo][marengaz].
|
|
172
|
-
Currently maintained by [Simon Leigh][simonleigh].
|
|
173
150
|
|
|
174
151
|
For a full list of all authors and contributors,
|
|
175
152
|
check [the contributor's page][contributors].
|
|
@@ -178,19 +155,23 @@ check [the contributor's page][contributors].
|
|
|
178
155
|
|
|
179
156
|
[License](LICENSE)
|
|
180
157
|
|
|
181
|
-
[build-shield]: https://github.com/
|
|
182
|
-
[build]: https://github.com/
|
|
183
|
-
[code-quality-shield]: https://img.shields.io/lgtm/grade/python/g/
|
|
184
|
-
[code-quality]: https://lgtm.com/projects/g/
|
|
185
|
-
[codecov-shield]: https://codecov.io/gh/
|
|
186
|
-
[codecov]: https://codecov.io/gh/
|
|
187
|
-
[contributors]: https://github.com/
|
|
158
|
+
[build-shield]: https://github.com/marengaz/python-rako/workflows/Continuous%20Integration/badge.svg
|
|
159
|
+
[build]: https://github.com/marengaz/python-rako/actions
|
|
160
|
+
[code-quality-shield]: https://img.shields.io/lgtm/grade/python/g/marengaz/python-rako.svg?logo=lgtm&logoWidth=18
|
|
161
|
+
[code-quality]: https://lgtm.com/projects/g/marengaz/python-rako/context:python
|
|
162
|
+
[codecov-shield]: https://codecov.io/gh/marengaz/python-rako/branch/master/graph/badge.svg
|
|
163
|
+
[codecov]: https://codecov.io/gh/marengaz/python-rako
|
|
164
|
+
[contributors]: https://github.com/marengaz/python-rako/graphs/contributors
|
|
188
165
|
[marengaz]: https://github.com/marengaz
|
|
189
|
-
[simonleigh]: https://github.com/simonleigh
|
|
190
166
|
[keepchangelog]: http://keepachangelog.com/en/1.0.0/
|
|
191
|
-
[license-shield]: https://img.shields.io/github/license/
|
|
192
|
-
[maintenance-shield]: https://img.shields.io/maintenance/yes/
|
|
193
|
-
[project-stage-shield]: https://img.shields.io/badge/project%20stage-
|
|
194
|
-
[releases-shield]: https://img.shields.io/github/release/
|
|
195
|
-
[releases]: https://github.com/
|
|
167
|
+
[license-shield]: https://img.shields.io/github/license/marengaz/python-rako.svg
|
|
168
|
+
[maintenance-shield]: https://img.shields.io/maintenance/yes/2021.svg
|
|
169
|
+
[project-stage-shield]: https://img.shields.io/badge/project%20stage-experimental-yellow.svg
|
|
170
|
+
[releases-shield]: https://img.shields.io/github/release/marengaz/python-rako.svg
|
|
171
|
+
[releases]: https://github.com/marengaz/python-rako/releases
|
|
196
172
|
[semver]: http://semver.org/spec/v2.0.0.html
|
|
173
|
+
|
|
174
|
+
[buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-2.svg
|
|
175
|
+
[buymeacoffee]: https://www.buymeacoffee.com/marengaz
|
|
176
|
+
[github-actions-shield]: https://github.com/marengaz/rakomqtt/workflows/Test%20RakoMQTT/badge.svg?branch=master
|
|
177
|
+
[github-actions]: https://github.com/marengaz/rakomqtt/actions?query=workflow%3A%22Test+RakoMQTT%22+branch%3Amaster
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
[![Code Coverage][codecov-shield]][codecov]
|
|
10
10
|
[![Code Quality][code-quality-shield]][code-quality]
|
|
11
11
|
|
|
12
|
+
[![Buy me a coffee][buymeacoffee-shield]][buymeacoffee]
|
|
12
13
|
|
|
13
14
|
Asynchronous Python client for Rako Controls.
|
|
14
15
|
|
|
15
|
-
**This is a modernized fork** that has been updated to Python 3.12+ best practices, includes async concurrency fixes for Home Assistant integration, and uses modern tooling (Ruff, Hatchling, etc.).
|
|
16
|
-
|
|
17
16
|
## About
|
|
18
17
|
|
|
19
18
|
This package allows you to control and monitor Rako Controls devices
|
|
@@ -23,42 +22,23 @@ their behavior.
|
|
|
23
22
|
## Installation
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
|
-
pip install python-rako
|
|
25
|
+
pip install python-rako
|
|
27
26
|
```
|
|
28
27
|
|
|
29
28
|
## Usage
|
|
30
29
|
|
|
31
30
|
```python
|
|
32
31
|
import asyncio
|
|
33
|
-
import aiohttp
|
|
34
|
-
from python_rako import discover_bridge, Bridge
|
|
35
32
|
|
|
36
33
|
|
|
37
34
|
async def main():
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
print(f"Found bridge: {bridge_info}")
|
|
41
|
-
|
|
42
|
-
# Create bridge instance
|
|
43
|
-
bridge = Bridge(
|
|
44
|
-
host=bridge_info["host"],
|
|
45
|
-
port=bridge_info["port"],
|
|
46
|
-
name=bridge_info["name"],
|
|
47
|
-
mac=bridge_info["mac"]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# Get bridge information and discover devices
|
|
51
|
-
async with aiohttp.ClientSession() as session:
|
|
52
|
-
info = await bridge.get_info(session)
|
|
53
|
-
print(f"Bridge version: {info.version}")
|
|
54
|
-
|
|
55
|
-
# Discover all lights
|
|
56
|
-
async for light in bridge.discover_lights(session):
|
|
57
|
-
print(f"Found light: {light.title} (Room {light.room_id})")
|
|
35
|
+
# TODO
|
|
36
|
+
pass
|
|
58
37
|
|
|
59
38
|
|
|
60
39
|
if __name__ == "__main__":
|
|
61
|
-
asyncio.
|
|
40
|
+
loop = asyncio.get_event_loop()
|
|
41
|
+
loop.run_until_complete(main())
|
|
62
42
|
```
|
|
63
43
|
|
|
64
44
|
## Changelog & Releases
|
|
@@ -106,10 +86,11 @@ Asynchronous Python client for Rako Controls Lighting.
|
|
|
106
86
|
Usage:
|
|
107
87
|
make help Shows this message.
|
|
108
88
|
make dev Set up a development environment.
|
|
109
|
-
make lint Run
|
|
110
|
-
make
|
|
111
|
-
make
|
|
112
|
-
make
|
|
89
|
+
make lint Run all linters.
|
|
90
|
+
make lint-black Run linting using black & blacken-docs.
|
|
91
|
+
make lint-flake8 Run linting using flake8 (pycodestyle/pydocstyle).
|
|
92
|
+
make lint-pylint Run linting using PyLint.
|
|
93
|
+
make lint-mypy Run linting using MyPy.
|
|
113
94
|
make test Run tests quickly with the default Python.
|
|
114
95
|
make coverage Check code coverage quickly with the default Python.
|
|
115
96
|
make install Install the package to the active Python's site-packages.
|
|
@@ -120,17 +101,13 @@ Usage:
|
|
|
120
101
|
make clean-test Removes test and coverage artifacts.
|
|
121
102
|
make clean-venv Removes Python virtual environment artifacts.
|
|
122
103
|
make dist Builds source and wheel package.
|
|
123
|
-
make release Release build on
|
|
104
|
+
make release Release build on PyP
|
|
124
105
|
make venv Create Python venv environment.
|
|
125
|
-
make bump-patch Bump patch version (x.y.Z) and commit.
|
|
126
|
-
make bump-minor Bump minor version (x.Y.z) and commit.
|
|
127
|
-
make bump-major Bump major version (X.y.z) and commit.
|
|
128
106
|
```
|
|
129
107
|
|
|
130
108
|
## Authors & contributors
|
|
131
109
|
|
|
132
110
|
The original setup of this repository is by [Ben Marengo][marengaz].
|
|
133
|
-
Currently maintained by [Simon Leigh][simonleigh].
|
|
134
111
|
|
|
135
112
|
For a full list of all authors and contributors,
|
|
136
113
|
check [the contributor's page][contributors].
|
|
@@ -139,19 +116,23 @@ check [the contributor's page][contributors].
|
|
|
139
116
|
|
|
140
117
|
[License](LICENSE)
|
|
141
118
|
|
|
142
|
-
[build-shield]: https://github.com/
|
|
143
|
-
[build]: https://github.com/
|
|
144
|
-
[code-quality-shield]: https://img.shields.io/lgtm/grade/python/g/
|
|
145
|
-
[code-quality]: https://lgtm.com/projects/g/
|
|
146
|
-
[codecov-shield]: https://codecov.io/gh/
|
|
147
|
-
[codecov]: https://codecov.io/gh/
|
|
148
|
-
[contributors]: https://github.com/
|
|
119
|
+
[build-shield]: https://github.com/marengaz/python-rako/workflows/Continuous%20Integration/badge.svg
|
|
120
|
+
[build]: https://github.com/marengaz/python-rako/actions
|
|
121
|
+
[code-quality-shield]: https://img.shields.io/lgtm/grade/python/g/marengaz/python-rako.svg?logo=lgtm&logoWidth=18
|
|
122
|
+
[code-quality]: https://lgtm.com/projects/g/marengaz/python-rako/context:python
|
|
123
|
+
[codecov-shield]: https://codecov.io/gh/marengaz/python-rako/branch/master/graph/badge.svg
|
|
124
|
+
[codecov]: https://codecov.io/gh/marengaz/python-rako
|
|
125
|
+
[contributors]: https://github.com/marengaz/python-rako/graphs/contributors
|
|
149
126
|
[marengaz]: https://github.com/marengaz
|
|
150
|
-
[simonleigh]: https://github.com/simonleigh
|
|
151
127
|
[keepchangelog]: http://keepachangelog.com/en/1.0.0/
|
|
152
|
-
[license-shield]: https://img.shields.io/github/license/
|
|
153
|
-
[maintenance-shield]: https://img.shields.io/maintenance/yes/
|
|
154
|
-
[project-stage-shield]: https://img.shields.io/badge/project%20stage-
|
|
155
|
-
[releases-shield]: https://img.shields.io/github/release/
|
|
156
|
-
[releases]: https://github.com/
|
|
128
|
+
[license-shield]: https://img.shields.io/github/license/marengaz/python-rako.svg
|
|
129
|
+
[maintenance-shield]: https://img.shields.io/maintenance/yes/2021.svg
|
|
130
|
+
[project-stage-shield]: https://img.shields.io/badge/project%20stage-experimental-yellow.svg
|
|
131
|
+
[releases-shield]: https://img.shields.io/github/release/marengaz/python-rako.svg
|
|
132
|
+
[releases]: https://github.com/marengaz/python-rako/releases
|
|
157
133
|
[semver]: http://semver.org/spec/v2.0.0.html
|
|
134
|
+
|
|
135
|
+
[buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-2.svg
|
|
136
|
+
[buymeacoffee]: https://www.buymeacoffee.com/marengaz
|
|
137
|
+
[github-actions-shield]: https://github.com/marengaz/rakomqtt/workflows/Test%20RakoMQTT/badge.svg?branch=master
|
|
138
|
+
[github-actions]: https://github.com/marengaz/rakomqtt/actions?query=workflow%3A%22Test+RakoMQTT%22+branch%3Amaster
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.4"
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
import threading
|
|
6
|
+
import time
|
|
6
7
|
from typing import TYPE_CHECKING, Any, cast
|
|
7
8
|
|
|
8
9
|
import aiohttp
|
|
@@ -15,7 +16,7 @@ from python_rako.const import (
|
|
|
15
16
|
MessageType,
|
|
16
17
|
RequestType,
|
|
17
18
|
)
|
|
18
|
-
from python_rako.exceptions import RakoBridgeError
|
|
19
|
+
from python_rako.exceptions import RakoBridgeError, RakoConnectionError, RakoCommandError
|
|
19
20
|
from python_rako.helpers import (
|
|
20
21
|
command_to_byte_list,
|
|
21
22
|
deserialise_byte_list,
|
|
@@ -87,22 +88,36 @@ class BridgeCommanderUDP(_BridgeCommander):
|
|
|
87
88
|
await self._send_command(command)
|
|
88
89
|
|
|
89
90
|
async def _send_command(self, command: CommandUDP) -> None:
|
|
91
|
+
"""Send command with retry logic."""
|
|
92
|
+
await self._send_command_with_retry(command, max_retries=2)
|
|
93
|
+
|
|
94
|
+
async def _send_command_with_retry(self, command: CommandUDP, max_retries: int = 2) -> bool:
|
|
95
|
+
"""Send command with retry logic."""
|
|
90
96
|
_LOGGER.debug("Sending command: %s", command)
|
|
91
97
|
byte_list = command_to_byte_list(command)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
|
|
99
|
+
for attempt in range(max_retries + 1):
|
|
100
|
+
try:
|
|
101
|
+
async with get_dg_commander(self.host, self.port) as dg_client:
|
|
102
|
+
_LOGGER.debug("Sending command bytes: %s (attempt %d)", byte_list, attempt + 1)
|
|
103
|
+
await dg_client.send(bytes(byte_list))
|
|
104
|
+
try:
|
|
105
|
+
# Add timeout to prevent indefinite blocking
|
|
106
|
+
data, _ = await asyncio.wait_for(dg_client.recv(), timeout=3.0)
|
|
107
|
+
if data.decode("utf8").strip() != COMMAND_SUCCESS_RESPONSE:
|
|
108
|
+
_LOGGER.warning("Bad response after command %s: %s", command, data)
|
|
109
|
+
return True
|
|
110
|
+
except TimeoutError:
|
|
111
|
+
# Many bridges don't send consistent responses, just log and continue
|
|
112
|
+
_LOGGER.debug("No response received for command %s (timeout after 3s)", command)
|
|
113
|
+
return True # Consider timeout as success for UDP commands
|
|
114
|
+
except (ConnectionError, OSError) as e:
|
|
115
|
+
if attempt == max_retries:
|
|
116
|
+
_LOGGER.error("Command failed after %d attempts: %s", max_retries + 1, e)
|
|
117
|
+
raise RakoConnectionError(f"Failed to send command after {max_retries + 1} attempts: {e}") from e
|
|
118
|
+
_LOGGER.warning("Command attempt %d failed, retrying: %s", attempt + 1, e)
|
|
119
|
+
await asyncio.sleep(0.5 * (2 ** attempt)) # Exponential backoff
|
|
120
|
+
return False
|
|
106
121
|
|
|
107
122
|
|
|
108
123
|
class BridgeCommanderHTTP(_BridgeCommander):
|
|
@@ -158,6 +173,7 @@ class Bridge:
|
|
|
158
173
|
self.scene_cache: SceneCache = SceneCache()
|
|
159
174
|
self._cached_xml: str | None = None
|
|
160
175
|
self._xml_fetch_lock = asyncio.Lock()
|
|
176
|
+
self._last_cache_refresh: float = 0
|
|
161
177
|
|
|
162
178
|
@property
|
|
163
179
|
def _discovery_url(self) -> str:
|
|
@@ -342,6 +358,17 @@ class Bridge:
|
|
|
342
358
|
|
|
343
359
|
return level_cache, scene_cache
|
|
344
360
|
|
|
361
|
+
async def refresh_cache_if_stale(self, max_age_seconds: int = 300) -> None:
|
|
362
|
+
"""Refresh level and scene cache if older than max_age_seconds."""
|
|
363
|
+
current_time = time.time()
|
|
364
|
+
if current_time - self._last_cache_refresh > max_age_seconds:
|
|
365
|
+
try:
|
|
366
|
+
self.level_cache, self.scene_cache = await self.get_cache_state()
|
|
367
|
+
self._last_cache_refresh = current_time
|
|
368
|
+
_LOGGER.debug("📚 Cache refreshed for bridge %s", self.mac)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
_LOGGER.warning("Failed to refresh cache for bridge %s: %s", self.mac, e)
|
|
371
|
+
|
|
345
372
|
async def set_room_scene(self, room_id: int, scene: int) -> None:
|
|
346
373
|
"""Set the scene of a room."""
|
|
347
374
|
await self._bridge_commander.set_room_scene(room_id, scene)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
5
|
+
import socket
|
|
6
|
+
import time
|
|
4
7
|
from contextlib import asynccontextmanager
|
|
5
8
|
from typing import TYPE_CHECKING
|
|
6
9
|
|
|
@@ -38,7 +41,13 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
38
41
|
async def get_dg_listener(port: int, listen_host: str = "0.0.0.0") -> AsyncIterator[DatagramServer]:
|
|
39
42
|
server: DatagramServer | None = None
|
|
40
43
|
try:
|
|
41
|
-
|
|
44
|
+
# Create socket with broadcast capability for receiving status messages
|
|
45
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
46
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
47
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
48
|
+
sock.bind((listen_host, port))
|
|
49
|
+
|
|
50
|
+
server = await asyncio_dgram.from_socket(sock)
|
|
42
51
|
yield server
|
|
43
52
|
finally:
|
|
44
53
|
if server:
|
|
@@ -194,3 +203,38 @@ def convert_to_scene(brightness: int) -> int:
|
|
|
194
203
|
|
|
195
204
|
scene = next(k for k, v in _scene_windows.items() if v["low"] <= brightness < v["high"])
|
|
196
205
|
return scene
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_predicted_channel_brightness(level_cache: LevelCache, room: int, channel: int, scene: int) -> int:
|
|
209
|
+
"""Predict channel brightness from scene using cache, fallback to conversion."""
|
|
210
|
+
# Try cache first
|
|
211
|
+
cached_brightness = level_cache.get_channel_level(
|
|
212
|
+
RoomChannel(room, channel), scene
|
|
213
|
+
)
|
|
214
|
+
if cached_brightness > 0:
|
|
215
|
+
return cached_brightness
|
|
216
|
+
|
|
217
|
+
# Fallback to scene-to-brightness conversion
|
|
218
|
+
return convert_to_brightness(scene)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class UDPMessageRateLimit:
|
|
222
|
+
"""Rate limiter for UDP message processing to prevent flooding."""
|
|
223
|
+
|
|
224
|
+
def __init__(self, max_messages_per_second: int = 50):
|
|
225
|
+
self.max_messages = max_messages_per_second
|
|
226
|
+
self.message_times: list[float] = []
|
|
227
|
+
self.lock = asyncio.Lock()
|
|
228
|
+
|
|
229
|
+
async def should_process_message(self) -> bool:
|
|
230
|
+
"""Rate limit UDP message processing."""
|
|
231
|
+
async with self.lock:
|
|
232
|
+
now = time.time()
|
|
233
|
+
# Remove messages older than 1 second
|
|
234
|
+
self.message_times = [t for t in self.message_times if now - t < 1.0]
|
|
235
|
+
|
|
236
|
+
if len(self.message_times) >= self.max_messages:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
self.message_times.append(now)
|
|
240
|
+
return True
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.3"
|
|
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
|
|
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
|
|
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
|
|
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
|