python-rako-2025 0.5.0__tar.gz → 0.5.2__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 (73) hide show
  1. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.github/dependabot.yml +4 -6
  2. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/CHANGELOG.md +30 -1
  3. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/PKG-INFO +1 -1
  4. python_rako_2025-0.5.2/python_rako/__version__.py +1 -0
  5. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/state.py +19 -3
  6. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/resources/rako3.xml +80 -80
  7. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_state.py +73 -1
  8. python_rako_2025-0.5.0/python_rako/__version__.py +0 -1
  9. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.coveragerc +0 -0
  10. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.editorconfig +0 -0
  11. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.flake8 +0 -0
  12. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.gitattributes +0 -0
  13. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.github/workflows/ci.yml +0 -0
  14. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.github/workflows/release.yml +0 -0
  15. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.gitignore +0 -0
  16. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.isort.cfg +0 -0
  17. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.pre-commit-config.yaml +0 -0
  18. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/.yamllint +0 -0
  19. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/LICENSE +0 -0
  20. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/Makefile +0 -0
  21. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/README.md +0 -0
  22. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/RELEASING.md +0 -0
  23. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/catalog-info.yaml +0 -0
  24. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/conftest.py +0 -0
  25. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/doc/accessing-the-rako-bridge.pdf +0 -0
  26. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/examples/discover_all_devices.py +0 -0
  27. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/examples/example.py +0 -0
  28. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/examples/listen_status.py +0 -0
  29. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/examples/state_snapshot.py +0 -0
  30. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/examples/ventilation_example.py +0 -0
  31. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/examples/verified_commands.py +0 -0
  32. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/examples/view_updates.py +0 -0
  33. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/pyproject.toml +0 -0
  34. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/__init__.py +0 -0
  35. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/bridge.py +0 -0
  36. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/commands.py +0 -0
  37. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/const.py +0 -0
  38. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/exceptions.py +0 -0
  39. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/helpers.py +0 -0
  40. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/listener.py +0 -0
  41. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/model.py +0 -0
  42. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/pacing.py +0 -0
  43. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/protocol.py +0 -0
  44. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/python_rako/py.typed +0 -0
  45. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/scripts/bump_version.py +0 -0
  46. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/scripts/latency.py +0 -0
  47. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/scripts/listen.py +0 -0
  48. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/scripts/measure_interval.py +0 -0
  49. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/scripts/snapshot.py +0 -0
  50. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/README_TESTING.md +0 -0
  51. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/resources/rako.xml +0 -0
  52. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/resources/rako2.xml +0 -0
  53. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_async_lock.py +0 -0
  54. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_bridge.py +0 -0
  55. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_bridge_async_parsing.py +0 -0
  56. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_bridge_state.py +0 -0
  57. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_command_queue.py +0 -0
  58. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_commands.py +0 -0
  59. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_discovery.py +0 -0
  60. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_edge_cases.py +0 -0
  61. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_helpers.py +0 -0
  62. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_listener.py +0 -0
  63. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_measure_interval.py +0 -0
  64. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_model.py +0 -0
  65. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_protocol.py +0 -0
  66. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests/test_xml_parsing.py +0 -0
  67. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests_integration/__init__.py +0 -0
  68. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests_integration/conftest.py +0 -0
  69. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests_integration/test_bridge_info_live.py +0 -0
  70. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests_integration/test_command_live.py +0 -0
  71. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests_integration/test_discovery_live.py +0 -0
  72. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/tests_integration/test_listener_live.py +0 -0
  73. {python_rako_2025-0.5.0 → python_rako_2025-0.5.2}/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: daily
8
- time: "06:00"
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: daily
14
- time: "06:00"
15
- open-pull-requests-limit: 10
12
+ interval: weekly
13
+ open-pull-requests-limit: 5
@@ -9,6 +9,33 @@ 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.2] - 2026-11-04
13
+
14
+ ### Changed
15
+
16
+ - The `tests/resources/rako3.xml` fixture is anonymised: room titles, channel
17
+ names and scene names are now `Room <id>`, `Channel <id>` and `Scene <id>`.
18
+ Ids, levels and structure are unchanged, so every test runs against the same
19
+ topology. The fixture ships in the sdist, so releases from this one on carry
20
+ only the generic names.
21
+
22
+ ## [0.5.1] - 2026-11-04
23
+
24
+ ### Fixed
25
+
26
+ - `BridgeStateSnapshot.reconcile()` no longer wipes rooms that are missing
27
+ from the scene cache. `from_caches()` lists every room in the level table,
28
+ so a missing room arrived with `scene=None`, which `reconcile()` read as a
29
+ missed scene change, and reset the room and its channels to unknown. This is
30
+ the opposite of the documented rule that absence is not evidence. A bridge
31
+ that has just restarted refills its cache one scene selection at a time
32
+ (occupancy-sensor rooms within minutes), so the first non-empty read after a
33
+ restart reset every other room to unknown. Rooms missing from the cache now
34
+ only fill in rooms and channels nothing has reported yet. They keep a faded
35
+ room's `faded_from` guard, which the wipe used to discard, letting a later
36
+ stale cache entry bring back the pre-fade scene. The test for this rule
37
+ passed only because it used an empty level table; it now uses a real one.
38
+
12
39
  ## [0.5.0] - 2026-08-30
13
40
 
14
41
  ### Added
@@ -110,7 +137,9 @@ e.g.:
110
137
  - ...
111
138
  -->
112
139
 
113
- [Unreleased]: https://github.com/SimonLeigh/python-rako/compare/v0.5.0...master
140
+ [Unreleased]: https://github.com/SimonLeigh/python-rako/compare/v0.5.2...master
141
+ [0.5.2]: https://github.com/SimonLeigh/python-rako/compare/v0.5.1...v0.5.2
142
+ [0.5.1]: https://github.com/SimonLeigh/python-rako/compare/v0.5.0...v0.5.1
114
143
  [0.5.0]: https://github.com/SimonLeigh/python-rako/releases/tag/v0.5.0
115
144
 
116
145
  <!--
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-rako-2025
3
- Version: 0.5.0
3
+ Version: 0.5.2
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.2"
@@ -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 ``fresh`` are left exactly as they are: the bridge
332
- deletes fade-controlled rooms from its scene cache, so absence is not
333
- evidence of anything.
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(
@@ -17,365 +17,365 @@
17
17
  <rooms>
18
18
  <Room id="0">
19
19
  <Type>Lights</Type>
20
- <Title>House Master Controls</Title>
20
+ <Title>Room 0</Title>
21
21
  </Room>
22
22
  <Room id="1">
23
23
  <Type>Lights</Type>
24
- <Title>Global Scenes</Title>
24
+ <Title>Room 1</Title>
25
25
  <mode>Named+OFF</mode>
26
26
  <Scene id="1">
27
- <Name>Welcome Home</Name>
27
+ <Name>Scene 1</Name>
28
28
  </Scene>
29
29
  <Scene id="2">
30
- <Name>Away</Name>
30
+ <Name>Scene 2</Name>
31
31
  </Scene>
32
32
  </Room>
33
33
  <Room id="13">
34
34
  <Type>Lights</Type>
35
- <Title>Hallway</Title>
35
+ <Title>Room 13</Title>
36
36
  <Channel id="1">
37
37
  <type>Slider</type>
38
- <Name>Spots</Name>
38
+ <Name>Channel 1</Name>
39
39
  <Levels>FFBF7F3F000000000000000000000000</Levels>
40
40
  </Channel>
41
41
  </Room>
42
42
  <Room id="65">
43
43
  <Type>Lights</Type>
44
- <Title>House Den</Title>
44
+ <Title>Room 65</Title>
45
45
  <Channel id="1">
46
46
  <type>Slider</type>
47
- <Name>Lounge Spots</Name>
47
+ <Name>Channel 1</Name>
48
48
  <Levels>67000000000000000000000000000000</Levels>
49
49
  </Channel>
50
50
  <Channel id="2">
51
51
  <type>Slider</type>
52
- <Name>Coridor Spots</Name>
52
+ <Name>Channel 2</Name>
53
53
  <Levels>FF005DFF000000000000000000000000</Levels>
54
54
  </Channel>
55
55
  <Channel id="3">
56
56
  <type>Slider</type>
57
- <Name>Pendent</Name>
57
+ <Name>Channel 3</Name>
58
58
  <Levels>0058FF00000000000000000000000000</Levels>
59
59
  </Channel>
60
60
  </Room>
61
61
  <Room id="153">
62
62
  <Type>Lights</Type>
63
- <Title>Shoe Cupboard</Title>
63
+ <Title>Room 153</Title>
64
64
  <Channel id="1">
65
65
  <type>Default</type>
66
- <Name>Spots</Name>
66
+ <Name>Channel 1</Name>
67
67
  <Levels>FFBF7F3F000000000000000000000000</Levels>
68
68
  </Channel>
69
69
  </Room>
70
70
  <Room id="14">
71
71
  <Type>Lights</Type>
72
- <Title>Link</Title>
72
+ <Title>Room 14</Title>
73
73
  <Channel id="1">
74
74
  <type>Default</type>
75
- <Name>Pelmet</Name>
75
+ <Name>Channel 1</Name>
76
76
  <Levels>FFBF7F3F000000000000000000000000</Levels>
77
77
  </Channel>
78
78
  <Channel id="2">
79
79
  <type>Default</type>
80
- <Name>Floor Lights</Name>
80
+ <Name>Channel 2</Name>
81
81
  <Levels>00BF7F3F000000000000000000000000</Levels>
82
82
  </Channel>
83
83
  <Channel id="3">
84
84
  <type>Default</type>
85
- <Name>Spots</Name>
85
+ <Name>Channel 3</Name>
86
86
  <Levels>FFBF7F3F000000000000000000000000</Levels>
87
87
  </Channel>
88
88
  </Room>
89
89
  <Room id="5">
90
90
  <Type>Lights</Type>
91
- <Title>Barn Kitchen</Title>
91
+ <Title>Room 5</Title>
92
92
  <Channel id="1">
93
93
  <type>switch</type>
94
- <Name>Brekfast Bar Pendants</Name>
94
+ <Name>Channel 1</Name>
95
95
  <Levels>FFBF7F3F000000000000000000000000</Levels>
96
96
  </Channel>
97
97
  <Channel id="2">
98
98
  <type>switch</type>
99
- <Name>Spots</Name>
99
+ <Name>Channel 2</Name>
100
100
  <Levels>00007F3F000000000000000000000000</Levels>
101
101
  </Channel>
102
102
  <Channel id="3">
103
103
  <type>switch</type>
104
- <Name>LED High</Name>
104
+ <Name>Channel 3</Name>
105
105
  <Levels>FFFF7F3F000000000000000000000000</Levels>
106
106
  </Channel>
107
107
  <Channel id="4">
108
108
  <type>Slider</type>
109
- <Name>Wall Lights</Name>
109
+ <Name>Channel 4</Name>
110
110
  <Levels>FF007F3F000000000000000000000000</Levels>
111
111
  </Channel>
112
112
  <Channel id="5">
113
113
  <type>Slider</type>
114
- <Name>Island LED</Name>
114
+ <Name>Channel 5</Name>
115
115
  <Levels>FFFF7F3F000000000000000000000000</Levels>
116
116
  </Channel>
117
117
  </Room>
118
118
  <Room id="6">
119
119
  <Type>Lights</Type>
120
- <Title>Barn Lounge</Title>
120
+ <Title>Room 6</Title>
121
121
  <Channel id="1">
122
122
  <type>Default</type>
123
- <Name>Spots</Name>
123
+ <Name>Channel 1</Name>
124
124
  <Levels>00000000000000000000000000000000</Levels>
125
125
  </Channel>
126
126
  <Channel id="2">
127
127
  <type>switch</type>
128
- <Name>Wall Lights</Name>
128
+ <Name>Channel 2</Name>
129
129
  <Levels>00BF00FF000000000000000000000000</Levels>
130
130
  </Channel>
131
131
  <Channel id="3">
132
132
  <type>Slider</type>
133
- <Name>TV LED</Name>
133
+ <Name>Channel 3</Name>
134
134
  <Levels>C0BFFF00000000000000000000000000</Levels>
135
135
  </Channel>
136
136
  </Room>
137
137
  <Room id="7">
138
138
  <Type>Lights</Type>
139
- <Title>Barn Dining</Title>
139
+ <Title>Room 7</Title>
140
140
  <Channel id="1">
141
141
  <type>Default</type>
142
- <Name>Wall Lights</Name>
142
+ <Name>Channel 1</Name>
143
143
  <Levels>FFBF7F3F000000000000000000000000</Levels>
144
144
  </Channel>
145
145
  <Channel id="2">
146
146
  <type>Default</type>
147
- <Name>Pendants</Name>
147
+ <Name>Channel 2</Name>
148
148
  <Levels>00BF7F3F000000000000000000000000</Levels>
149
149
  </Channel>
150
150
  </Room>
151
151
  <Room id="145">
152
152
  <Type>Lights</Type>
153
- <Title>Larder</Title>
153
+ <Title>Room 145</Title>
154
154
  <Channel id="1">
155
155
  <type>Slider</type>
156
- <Name>Spots</Name>
156
+ <Name>Channel 1</Name>
157
157
  <Levels>FFBF7F3F000000000000000000000000</Levels>
158
158
  </Channel>
159
159
  </Room>
160
160
  <Room id="9">
161
161
  <Type>Lights</Type>
162
- <Title>Utility Room</Title>
162
+ <Title>Room 9</Title>
163
163
  <Channel id="1">
164
164
  <type>switch</type>
165
- <Name>Spots</Name>
165
+ <Name>Channel 1</Name>
166
166
  <Levels>FFBF7F3F000000000000000000000000</Levels>
167
167
  </Channel>
168
168
  <Channel id="2">
169
169
  <type>Default</type>
170
- <Name>Under Cupboard LED</Name>
170
+ <Name>Channel 2</Name>
171
171
  <Levels>FFBF7F3F000000000000000000000000</Levels>
172
172
  </Channel>
173
173
  <Channel id="3">
174
174
  <type>Slider</type>
175
- <Name>Corridor Spots</Name>
175
+ <Name>Channel 3</Name>
176
176
  <Levels>FFBF7F3F000000000000000000000000</Levels>
177
177
  </Channel>
178
178
  </Room>
179
179
  <Room id="10">
180
180
  <Type>Lights</Type>
181
- <Title>Utility WC</Title>
181
+ <Title>Room 10</Title>
182
182
  <Channel id="1">
183
183
  <type>Default</type>
184
- <Name>Spots</Name>
184
+ <Name>Channel 1</Name>
185
185
  <Levels>FFBF7F3F000000000000000000000000</Levels>
186
186
  </Channel>
187
187
  </Room>
188
188
  <Room id="17">
189
189
  <Type>Lights</Type>
190
- <Title>Master Bedroom</Title>
190
+ <Title>Room 17</Title>
191
191
  <Channel id="1">
192
192
  <type>Default</type>
193
- <Name>Bedroom</Name>
193
+ <Name>Channel 1</Name>
194
194
  <Levels>FFFF7F00000000000000000000000000</Levels>
195
195
  </Channel>
196
196
  <Channel id="2">
197
197
  <type>Default</type>
198
- <Name>Corridor</Name>
198
+ <Name>Channel 2</Name>
199
199
  <Levels>00007F3F000000000000000000000000</Levels>
200
200
  </Channel>
201
201
  </Room>
202
202
  <Room id="18">
203
203
  <Type>Lights</Type>
204
- <Title>Master Ensuite</Title>
204
+ <Title>Room 18</Title>
205
205
  <Channel id="1">
206
206
  <type>Default</type>
207
- <Name>Spots</Name>
207
+ <Name>Channel 1</Name>
208
208
  <Levels>FFBF7F3F000000000000000000000000</Levels>
209
209
  </Channel>
210
210
  </Room>
211
211
  <Room id="19">
212
212
  <Type>Lights</Type>
213
- <Title>Master Dressing</Title>
213
+ <Title>Room 19</Title>
214
214
  <Channel id="1">
215
215
  <type>Default</type>
216
- <Name>Spots</Name>
216
+ <Name>Channel 1</Name>
217
217
  <Levels>FF2F7F3F000000000000000000000000</Levels>
218
218
  </Channel>
219
219
  </Room>
220
220
  <Room id="73">
221
221
  <Type>Lights</Type>
222
- <Title>Simon Study</Title>
222
+ <Title>Room 73</Title>
223
223
  <Channel id="1">
224
224
  <type>Slider</type>
225
- <Name>Spots</Name>
225
+ <Name>Channel 1</Name>
226
226
  <Levels>FF00FFFF000000000000000000000000</Levels>
227
227
  </Channel>
228
228
  <Channel id="2">
229
229
  <type>Default</type>
230
- <Name>Dress Spots</Name>
230
+ <Name>Channel 2</Name>
231
231
  <Levels>FFFF0000000000000000000000000000</Levels>
232
232
  </Channel>
233
233
  </Room>
234
234
  <Room id="137">
235
235
  <Type>Lights</Type>
236
- <Title>Simon Ensuite</Title>
236
+ <Title>Room 137</Title>
237
237
  <Channel id="1">
238
238
  <type>Default</type>
239
- <Name>Spots</Name>
239
+ <Name>Channel 1</Name>
240
240
  <Levels>FFBF7F3F000000000000000000000000</Levels>
241
241
  </Channel>
242
242
  </Room>
243
243
  <Room id="15">
244
244
  <Type>Lights</Type>
245
- <Title>Landing</Title>
245
+ <Title>Room 15</Title>
246
246
  <Channel id="1">
247
247
  <type>Default</type>
248
- <Name>Spots</Name>
248
+ <Name>Channel 1</Name>
249
249
  <Levels>FFBF7F3F000000000000000000000000</Levels>
250
250
  </Channel>
251
251
  <Channel id="2">
252
252
  <type>Default</type>
253
- <Name>Chandelier</Name>
253
+ <Name>Channel 2</Name>
254
254
  <Levels>FFBF7F3F000000000000000000000000</Levels>
255
255
  </Channel>
256
256
  </Room>
257
257
  <Room id="89">
258
258
  <Type>Lights</Type>
259
- <Title>Lisa Study</Title>
259
+ <Title>Room 89</Title>
260
260
  <Channel id="1">
261
261
  <type>Default</type>
262
- <Name>Spots</Name>
262
+ <Name>Channel 1</Name>
263
263
  <Levels>FFBF7F3F000000000000000000000000</Levels>
264
264
  </Channel>
265
265
  </Room>
266
266
  <Room id="113">
267
267
  <Type>Lights</Type>
268
- <Title>Lisa Sewing</Title>
268
+ <Title>Room 113</Title>
269
269
  <Channel id="1">
270
270
  <type>Default</type>
271
- <Name>Spots</Name>
271
+ <Name>Channel 1</Name>
272
272
  <Levels>FFBF7F3F000000000000000000000000</Levels>
273
273
  </Channel>
274
274
  </Room>
275
275
  <Room id="105">
276
276
  <Type>Lights</Type>
277
- <Title>Family Bathroom</Title>
277
+ <Title>Room 105</Title>
278
278
  <Channel id="1">
279
279
  <type>Default</type>
280
- <Name>Spots</Name>
280
+ <Name>Channel 1</Name>
281
281
  <Levels>FFBF7F3F000000000000000000000000</Levels>
282
282
  </Channel>
283
283
  </Room>
284
284
  <Room id="97">
285
285
  <Type>Lights</Type>
286
- <Title>Guest Bedroom</Title>
286
+ <Title>Room 97</Title>
287
287
  <Channel id="1">
288
288
  <type>Default</type>
289
- <Name>Spots</Name>
289
+ <Name>Channel 1</Name>
290
290
  <Levels>FFBF7F3F000000000000000000000000</Levels>
291
291
  </Channel>
292
292
  </Room>
293
293
  <Room id="121">
294
294
  <Type>Lights</Type>
295
- <Title>Guest Ensuite</Title>
295
+ <Title>Room 121</Title>
296
296
  <Channel id="1">
297
297
  <type>Default</type>
298
- <Name>Spots</Name>
298
+ <Name>Channel 1</Name>
299
299
  <Levels>FFBF7F3F000000000000000000000000</Levels>
300
300
  </Channel>
301
301
  </Room>
302
302
  <Room id="156">
303
303
  <Type>Lights</Type>
304
- <Title>Outside Master Controls</Title>
304
+ <Title>Room 156</Title>
305
305
  <mode>Named+OFF</mode>
306
306
  <Scene id="1">
307
- <Name>On</Name>
307
+ <Name>Scene 1</Name>
308
308
  </Scene>
309
309
  </Room>
310
310
  <Room id="157">
311
311
  <Type>Lights</Type>
312
- <Title>Front Drive</Title>
312
+ <Title>Room 157</Title>
313
313
  <Channel id="1">
314
314
  <type>switch</type>
315
- <Name>Front Door Right Wall</Name>
315
+ <Name>Channel 1</Name>
316
316
  <Levels>FF00FF00000000000000000000000000</Levels>
317
317
  </Channel>
318
318
  <Channel id="2">
319
319
  <type>Slider</type>
320
- <Name>Posts</Name>
320
+ <Name>Channel 2</Name>
321
321
  <Levels>FF000000000000000000000000000000</Levels>
322
322
  </Channel>
323
323
  <Channel id="3">
324
324
  <type>switch</type>
325
- <Name>Front Henry's Window + Sid</Name>
325
+ <Name>Channel 3</Name>
326
326
  <Levels>0000FFFF000000000000000000000000</Levels>
327
327
  </Channel>
328
328
  <Channel id="5">
329
329
  <type>switch</type>
330
- <Name>Front Right Wall</Name>
330
+ <Name>Channel 5</Name>
331
331
  <Levels>FFFFFF00000000000000000000000000</Levels>
332
332
  </Channel>
333
333
  <Channel id="7">
334
334
  <type>switch</type>
335
- <Name>Mystery Light</Name>
335
+ <Name>Channel 7</Name>
336
336
  <Levels>FFBFFF00000000000000000000000000</Levels>
337
337
  </Channel>
338
338
  </Room>
339
339
  <Room id="158">
340
340
  <Type>Lights</Type>
341
- <Title>Courtyard</Title>
341
+ <Title>Room 158</Title>
342
342
  <Channel id="1">
343
343
  <type>switch</type>
344
- <Name>Front Wall</Name>
344
+ <Name>Channel 1</Name>
345
345
  <Levels>FFBF7F3F000000000000000000000000</Levels>
346
346
  </Channel>
347
347
  <Channel id="2">
348
348
  <type>switch</type>
349
- <Name>Back Wall</Name>
349
+ <Name>Channel 2</Name>
350
350
  <Levels>FFBF7F3F000000000000000000000000</Levels>
351
351
  </Channel>
352
352
  <Channel id="3">
353
353
  <type>switch</type>
354
- <Name>Pond</Name>
354
+ <Name>Channel 3</Name>
355
355
  <Levels>FFBF7F3F000000000000000000000000</Levels>
356
356
  </Channel>
357
357
  <Channel id="5">
358
358
  <type>switch</type>
359
- <Name>Wall by Garage</Name>
359
+ <Name>Channel 5</Name>
360
360
  <Levels>FFBF7F3F000000000000000000000000</Levels>
361
361
  </Channel>
362
362
  </Room>
363
363
  <Room id="159">
364
364
  <Type>Lights</Type>
365
- <Title>Garden</Title>
365
+ <Title>Room 159</Title>
366
366
  <Channel id="1">
367
367
  <type>switch</type>
368
- <Name>House Wall</Name>
368
+ <Name>Channel 1</Name>
369
369
  <Levels>FFBF7F3F000000000000000000000000</Levels>
370
370
  </Channel>
371
371
  <Channel id="2">
372
372
  <type>switch</type>
373
- <Name>Step Lights</Name>
373
+ <Name>Channel 2</Name>
374
374
  <Levels>FFBF7F3F000000000000000000000000</Levels>
375
375
  </Channel>
376
376
  <Channel id="3">
377
377
  <type>switch</type>
378
- <Name>Patio Wall</Name>
378
+ <Name>Channel 3</Name>
379
379
  <Levels>FFBF7F3F000000000000000000000000</Levels>
380
380
  </Channel>
381
381
  </Room>
@@ -384,7 +384,7 @@
384
384
  <Title>Fans</Title>
385
385
  <mode>Named+OFF</mode>
386
386
  <Scene id="1">
387
- <Name>On</Name>
387
+ <Name>Scene 1</Name>
388
388
  </Scene>
389
389
  <Channel id="1">
390
390
  <type>switch</type>
@@ -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
- fresh = BridgeStateSnapshot.from_caches(SceneCache({6: 1}), LevelCache())
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"