python-rako-2025 0.5.0__tar.gz → 0.5.1__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.5.0 → python_rako_2025-0.5.1}/.github/dependabot.yml +4 -6
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/CHANGELOG.md +19 -1
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/PKG-INFO +1 -1
- python_rako_2025-0.5.1/python_rako/__version__.py +1 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/state.py +19 -3
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_state.py +73 -1
- python_rako_2025-0.5.0/python_rako/__version__.py +0 -1
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.coveragerc +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.editorconfig +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.flake8 +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.gitattributes +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.github/workflows/ci.yml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.github/workflows/release.yml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.gitignore +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.isort.cfg +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.pre-commit-config.yaml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/.yamllint +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/LICENSE +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/Makefile +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/README.md +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/RELEASING.md +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/catalog-info.yaml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/conftest.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/doc/accessing-the-rako-bridge.pdf +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/examples/discover_all_devices.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/examples/example.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/examples/listen_status.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/examples/state_snapshot.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/examples/ventilation_example.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/examples/verified_commands.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/examples/view_updates.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/pyproject.toml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/__init__.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/bridge.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/commands.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/const.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/exceptions.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/helpers.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/listener.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/model.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/pacing.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/protocol.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/python_rako/py.typed +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/scripts/bump_version.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/scripts/latency.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/scripts/listen.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/scripts/measure_interval.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/scripts/snapshot.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/README_TESTING.md +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/resources/rako.xml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/resources/rako2.xml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/resources/rako3.xml +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_async_lock.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_bridge.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_bridge_async_parsing.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_bridge_state.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_command_queue.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_commands.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_discovery.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_edge_cases.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_helpers.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_listener.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_measure_interval.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_model.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_protocol.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests/test_xml_parsing.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/__init__.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/conftest.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/test_bridge_info_live.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/test_command_live.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/test_discovery_live.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/test_listener_live.py +0 -0
- {python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/test_state_live.py +0 -0
|
@@ -4,12 +4,10 @@ updates:
|
|
|
4
4
|
- package-ecosystem: pip
|
|
5
5
|
directory: "/"
|
|
6
6
|
schedule:
|
|
7
|
-
interval:
|
|
8
|
-
|
|
9
|
-
open-pull-requests-limit: 10
|
|
7
|
+
interval: weekly
|
|
8
|
+
open-pull-requests-limit: 5
|
|
10
9
|
- package-ecosystem: "github-actions"
|
|
11
10
|
directory: "/"
|
|
12
11
|
schedule:
|
|
13
|
-
interval:
|
|
14
|
-
|
|
15
|
-
open-pull-requests-limit: 10
|
|
12
|
+
interval: weekly
|
|
13
|
+
open-pull-requests-limit: 5
|
|
@@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
Nothing yet.
|
|
11
11
|
|
|
12
|
+
## [0.5.1] - 2026-11-04
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `BridgeStateSnapshot.reconcile()` no longer wipes rooms that are missing
|
|
17
|
+
from the scene cache. `from_caches()` lists every room in the level table,
|
|
18
|
+
so a missing room arrived with `scene=None`, which `reconcile()` read as a
|
|
19
|
+
missed scene change, and reset the room and its channels to unknown. This is
|
|
20
|
+
the opposite of the documented rule that absence is not evidence. A bridge
|
|
21
|
+
that has just restarted refills its cache one scene selection at a time
|
|
22
|
+
(occupancy-sensor rooms within minutes), so the first non-empty read after a
|
|
23
|
+
restart reset every other room to unknown. Rooms missing from the cache now
|
|
24
|
+
only fill in rooms and channels nothing has reported yet. They keep a faded
|
|
25
|
+
room's `faded_from` guard, which the wipe used to discard, letting a later
|
|
26
|
+
stale cache entry bring back the pre-fade scene. The test for this rule
|
|
27
|
+
passed only because it used an empty level table; it now uses a real one.
|
|
28
|
+
|
|
12
29
|
## [0.5.0] - 2026-08-30
|
|
13
30
|
|
|
14
31
|
### Added
|
|
@@ -110,7 +127,8 @@ e.g.:
|
|
|
110
127
|
- ...
|
|
111
128
|
-->
|
|
112
129
|
|
|
113
|
-
[Unreleased]: https://github.com/SimonLeigh/python-rako/compare/v0.5.
|
|
130
|
+
[Unreleased]: https://github.com/SimonLeigh/python-rako/compare/v0.5.1...master
|
|
131
|
+
[0.5.1]: https://github.com/SimonLeigh/python-rako/compare/v0.5.0...v0.5.1
|
|
114
132
|
[0.5.0]: https://github.com/SimonLeigh/python-rako/releases/tag/v0.5.0
|
|
115
133
|
|
|
116
134
|
<!--
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-rako-2025
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -328,9 +328,16 @@ class BridgeStateSnapshot:
|
|
|
328
328
|
Had the cache said scene 3, the scene change would have been one we
|
|
329
329
|
missed, and the room would be rebuilt from the cache instead.
|
|
330
330
|
|
|
331
|
-
Rooms absent from
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
Rooms absent from the scene cache are left exactly as they are, whether
|
|
332
|
+
they are missing from ``fresh`` altogether or -- as
|
|
333
|
+
:meth:`from_caches` lists every room in the level table -- present with
|
|
334
|
+
``scene=None``. Absence is not evidence of anything: the bridge
|
|
335
|
+
deletes fade-controlled rooms from its scene cache by design, and after
|
|
336
|
+
a restart its cache starts empty and refills one scene selection at a
|
|
337
|
+
time, so a read taken then lists only the rooms used since. Treating
|
|
338
|
+
``None`` as a scene change would wipe every other room to unknown.
|
|
339
|
+
Such a room only fills in rooms and channels we have never heard of,
|
|
340
|
+
as unknown; it never overwrites anything already tracked.
|
|
334
341
|
|
|
335
342
|
**Fades get one extra guard.** A fade sets the room's scene to
|
|
336
343
|
``None``, which would otherwise always "differ" from a cached scene and
|
|
@@ -351,6 +358,15 @@ class BridgeStateSnapshot:
|
|
|
351
358
|
rc: state for rc, state in fresh.channels.items() if rc.room_id == room
|
|
352
359
|
}
|
|
353
360
|
|
|
361
|
+
if fresh_room.scene is None:
|
|
362
|
+
# Not in the scene cache: no news. Keep everything we know --
|
|
363
|
+
# including a faded room's faded_from guard -- and only fill in
|
|
364
|
+
# rooms and channels we have never heard of, as unknown.
|
|
365
|
+
rooms.setdefault(room, replace(fresh_room, updated_at=now))
|
|
366
|
+
for room_channel, state in fresh_channels.items():
|
|
367
|
+
channels.setdefault(room_channel, replace(state, updated_at=now))
|
|
368
|
+
continue
|
|
369
|
+
|
|
354
370
|
if tracked is not None and tracked.is_faded:
|
|
355
371
|
if fresh_room.scene == tracked.faded_from:
|
|
356
372
|
_LOGGER.debug(
|
|
@@ -304,7 +304,11 @@ def test_reconcile_does_not_overwrite_a_command_echo():
|
|
|
304
304
|
def test_reconcile_leaves_rooms_absent_from_the_cache_alone():
|
|
305
305
|
"""Absence from the scene cache means fade-controlled, not off."""
|
|
306
306
|
tracked = empty_snapshot().apply(ChannelStatusMessage(9, 1, 200))
|
|
307
|
-
|
|
307
|
+
# The real level table, as get_state_snapshot() passes it: room 9 is then
|
|
308
|
+
# listed in ``fresh`` with scene=None rather than missing from it.
|
|
309
|
+
fresh = BridgeStateSnapshot.from_caches(SceneCache({6: 1}), LEVEL_TABLE)
|
|
310
|
+
assert fresh.room_scene(9) is None
|
|
311
|
+
assert 9 in fresh.rooms
|
|
308
312
|
|
|
309
313
|
reconciled = tracked.reconcile(fresh)
|
|
310
314
|
|
|
@@ -312,6 +316,74 @@ def test_reconcile_leaves_rooms_absent_from_the_cache_alone():
|
|
|
312
316
|
assert reconciled.channel_state(9, 1).source is StateSource.LEVEL_BROADCAST
|
|
313
317
|
|
|
314
318
|
|
|
319
|
+
def test_reconcile_survives_a_bridge_restart_refilling_its_cache():
|
|
320
|
+
"""A restarted bridge's cache lists only the rooms used since the restart.
|
|
321
|
+
|
|
322
|
+
Observed 2026-11-03: the bridge's cache came back empty, then partly
|
|
323
|
+
refilled by occupancy-sensor rooms within minutes. That read is not empty,
|
|
324
|
+
so it passes an emptiness check, yet every other room appears with
|
|
325
|
+
scene=None. None of them changed; none of them may be wiped to unknown.
|
|
326
|
+
"""
|
|
327
|
+
tracked = (
|
|
328
|
+
empty_snapshot()
|
|
329
|
+
.apply(SceneStatusMessage(6, 0, 2))
|
|
330
|
+
.apply(ChannelStatusMessage(6, 1, 129))
|
|
331
|
+
.apply(SceneStatusMessage(7, 0, 0))
|
|
332
|
+
)
|
|
333
|
+
refilling = BridgeStateSnapshot.from_caches(SceneCache({9: 1}), LEVEL_TABLE)
|
|
334
|
+
|
|
335
|
+
reconciled = tracked.reconcile(refilling)
|
|
336
|
+
|
|
337
|
+
assert reconciled.room_scene(6) == 2
|
|
338
|
+
assert reconciled.channel_level(6, 1) == 129
|
|
339
|
+
assert reconciled.channel_state(6, 1).source is StateSource.LEVEL_BROADCAST
|
|
340
|
+
assert reconciled.channel_level(6, 2) == 192
|
|
341
|
+
assert reconciled.room_scene(7) == 0
|
|
342
|
+
assert reconciled.channel_level(7, 2) == 0
|
|
343
|
+
# The room that is in the cache is still reconciled normally.
|
|
344
|
+
assert reconciled.room_scene(9) == 1
|
|
345
|
+
assert reconciled.channel_level(9, 1) == 255
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_reconcile_keeps_the_fade_guard_while_the_room_is_absent():
|
|
349
|
+
"""The bridge drops a faded room from its cache; that must not reset the guard.
|
|
350
|
+
|
|
351
|
+
Otherwise the next read that still carries the pre-fade scene would be
|
|
352
|
+
adopted as a "missed scene change" and resurrect levels the fade invalidated.
|
|
353
|
+
"""
|
|
354
|
+
tracked = (
|
|
355
|
+
empty_snapshot()
|
|
356
|
+
.apply(SceneStatusMessage(6, 0, 1))
|
|
357
|
+
.apply(FadeMessage(6, 0, direction=FadeDirection.UP, command=CommandType.FADE))
|
|
358
|
+
)
|
|
359
|
+
absent = BridgeStateSnapshot.from_caches(SceneCache({7: 1}), LEVEL_TABLE)
|
|
360
|
+
|
|
361
|
+
after_absent = tracked.reconcile(absent)
|
|
362
|
+
|
|
363
|
+
assert after_absent.rooms[6].is_faded
|
|
364
|
+
assert after_absent.rooms[6].faded_from == 1
|
|
365
|
+
|
|
366
|
+
stale = BridgeStateSnapshot.from_caches(SceneCache({6: 1}), LEVEL_TABLE)
|
|
367
|
+
reconciled = after_absent.reconcile(stale)
|
|
368
|
+
|
|
369
|
+
assert reconciled.room_scene(6) is None
|
|
370
|
+
assert reconciled.channel_level(6, 1) is None
|
|
371
|
+
assert reconciled.channel_state(6, 1).source is StateSource.UNKNOWN_AFTER_FADE
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_reconcile_adopts_absent_rooms_it_has_never_seen_as_unknown():
|
|
375
|
+
"""A room nothing has told us about is added -- as unknown, never off."""
|
|
376
|
+
tracked = empty_snapshot().apply(SceneStatusMessage(6, 0, 1))
|
|
377
|
+
fresh = BridgeStateSnapshot.from_caches(SceneCache({6: 1}), LEVEL_TABLE)
|
|
378
|
+
|
|
379
|
+
reconciled = tracked.reconcile(fresh)
|
|
380
|
+
|
|
381
|
+
assert 9 in reconciled.rooms
|
|
382
|
+
assert reconciled.room_scene(9) is None
|
|
383
|
+
assert reconciled.channel_level(9, 1) is None
|
|
384
|
+
assert reconciled.channel_state(9, 1).source is StateSource.UNKNOWN_AFTER_FADE
|
|
385
|
+
|
|
386
|
+
|
|
315
387
|
def test_reconcile_adopts_rooms_we_have_never_seen():
|
|
316
388
|
tracked = empty_snapshot()
|
|
317
389
|
fresh = BridgeStateSnapshot.from_caches(SceneCache({6: 3}), LEVEL_TABLE)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.0"
|
|
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
|
|
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
|
{python_rako_2025-0.5.0 → python_rako_2025-0.5.1}/tests_integration/test_bridge_info_live.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|