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.
Files changed (48) hide show
  1. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.claude/settings.local.json +2 -1
  2. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/PKG-INFO +30 -49
  3. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/README.md +29 -48
  4. python_rako_2025-0.4.4/python_rako/__version__.py +1 -0
  5. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/bridge.py +42 -15
  6. python_rako_2025-0.4.4/python_rako/exceptions.py +12 -0
  7. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/helpers.py +45 -1
  8. python_rako_2025-0.4.3/python_rako/__version__.py +0 -1
  9. python_rako_2025-0.4.3/python_rako/exceptions.py +0 -2
  10. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.coveragerc +0 -0
  11. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.editorconfig +0 -0
  12. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.flake8 +0 -0
  13. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.gitattributes +0 -0
  14. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.github/dependabot.yml +0 -0
  15. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.github/workflows/ci.yml +0 -0
  16. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.github/workflows/release.yml +0 -0
  17. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.gitignore +0 -0
  18. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.isort.cfg +0 -0
  19. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.pre-commit-config.yaml +0 -0
  20. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/.yamllint +0 -0
  21. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/LICENSE +0 -0
  22. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/Makefile +0 -0
  23. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/catalog-info.yaml +0 -0
  24. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/conftest.py +0 -0
  25. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/doc/accessing-the-rako-bridge.pdf +0 -0
  26. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/discover_all_devices.py +0 -0
  27. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/example.py +0 -0
  28. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/ventilation_example.py +0 -0
  29. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/examples/view_updates.py +0 -0
  30. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/pyproject.toml +0 -0
  31. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/__init__.py +0 -0
  32. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/const.py +0 -0
  33. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/model.py +0 -0
  34. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/python_rako/py.typed +0 -0
  35. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/README_TESTING.md +0 -0
  36. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/resources/rako.xml +0 -0
  37. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/resources/rako2.xml +0 -0
  38. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/resources/rako3.xml +0 -0
  39. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_async_lock.py +0 -0
  40. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_bridge.py +0 -0
  41. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_edge_cases.py +0 -0
  42. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_helpers.py +0 -0
  43. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_model.py +0 -0
  44. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests/test_xml_parsing.py +0 -0
  45. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/__init__.py +0 -0
  46. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/conftest.py +0 -0
  47. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/test_command_live.py +0 -0
  48. {python_rako_2025-0.4.3 → python_rako_2025-0.4.4}/tests_integration/test_general_live.py +0 -0
@@ -6,7 +6,8 @@
6
6
  "mcp__ide__getDiagnostics",
7
7
  "WebFetch(domain:docs.python.org)",
8
8
  "Bash(python -m pytest tests/ -v)",
9
- "Bash(pytest:*)"
9
+ "Bash(pytest:*)",
10
+ "Bash(git fetch:*)"
10
11
  ],
11
12
  "deny": []
12
13
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-rako-2025
3
- Version: 0.4.3
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-2025
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
- # Discover bridge on network
78
- bridge_info = await discover_bridge()
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.run(main())
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 Ruff linting and formatting checks.
149
- make format Format code with Ruff.
150
- make typecheck Run type checking with MyPy.
151
- make check Run all checks (linting, formatting, and type checking).
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 PyPI.
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/simonleigh/python-rako/workflows/Continuous%20Integration/badge.svg
182
- [build]: https://github.com/simonleigh/python-rako/actions
183
- [code-quality-shield]: https://img.shields.io/lgtm/grade/python/g/simonleigh/python-rako.svg?logo=lgtm&logoWidth=18
184
- [code-quality]: https://lgtm.com/projects/g/simonleigh/python-rako/context:python
185
- [codecov-shield]: https://codecov.io/gh/simonleigh/python-rako/branch/master/graph/badge.svg
186
- [codecov]: https://codecov.io/gh/simonleigh/python-rako
187
- [contributors]: https://github.com/simonleigh/python-rako/graphs/contributors
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/simonleigh/python-rako.svg
192
- [maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg
193
- [project-stage-shield]: https://img.shields.io/badge/project%20stage-stable-green.svg
194
- [releases-shield]: https://img.shields.io/github/release/simonleigh/python-rako.svg
195
- [releases]: https://github.com/simonleigh/python-rako/releases
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-2025
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
- # Discover bridge on network
39
- bridge_info = await discover_bridge()
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.run(main())
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 Ruff linting and formatting checks.
110
- make format Format code with Ruff.
111
- make typecheck Run type checking with MyPy.
112
- make check Run all checks (linting, formatting, and type checking).
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 PyPI.
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/simonleigh/python-rako/workflows/Continuous%20Integration/badge.svg
143
- [build]: https://github.com/simonleigh/python-rako/actions
144
- [code-quality-shield]: https://img.shields.io/lgtm/grade/python/g/simonleigh/python-rako.svg?logo=lgtm&logoWidth=18
145
- [code-quality]: https://lgtm.com/projects/g/simonleigh/python-rako/context:python
146
- [codecov-shield]: https://codecov.io/gh/simonleigh/python-rako/branch/master/graph/badge.svg
147
- [codecov]: https://codecov.io/gh/simonleigh/python-rako
148
- [contributors]: https://github.com/simonleigh/python-rako/graphs/contributors
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/simonleigh/python-rako.svg
153
- [maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg
154
- [project-stage-shield]: https://img.shields.io/badge/project%20stage-stable-green.svg
155
- [releases-shield]: https://img.shields.io/github/release/simonleigh/python-rako.svg
156
- [releases]: https://github.com/simonleigh/python-rako/releases
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
- async with get_dg_commander(self.host, self.port) as dg_client:
93
- _LOGGER.debug("Sending command bytes: %s", byte_list)
94
- await dg_client.send(bytes(byte_list))
95
- data, _ = await dg_client.recv()
96
-
97
- # Rako bridges return responses in Windows-1252 encoding, not UTF-8
98
- try:
99
- decoded_data = data.decode("windows-1252").strip()
100
- except UnicodeDecodeError:
101
- # Fallback to latin-1 which accepts all byte values
102
- decoded_data = data.decode("latin-1").strip()
103
-
104
- if decoded_data != COMMAND_SUCCESS_RESPONSE:
105
- _LOGGER.warning("Bad response after command %s %s", command, data)
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)
@@ -0,0 +1,12 @@
1
+ class RakoBridgeError(Exception):
2
+ pass
3
+
4
+
5
+ class RakoConnectionError(RakoBridgeError):
6
+ """Raised when bridge connection fails."""
7
+ pass
8
+
9
+
10
+ class RakoCommandError(RakoBridgeError):
11
+ """Raised when command execution fails."""
12
+ pass
@@ -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
- server = await asyncio_dgram.bind((listen_host, port))
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"
@@ -1,2 +0,0 @@
1
- class RakoBridgeError(Exception):
2
- pass