quilt-hp-python 0.2.1__tar.gz → 0.2.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 (113) hide show
  1. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/CHANGELOG.md +6 -0
  2. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/PKG-INFO +1 -1
  3. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/reference/client.md +1 -1
  4. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/pyproject.toml +1 -1
  5. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/__init__.py +1 -1
  6. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/cli/tui.py +13 -4
  7. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/system.py +6 -0
  8. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_cli_surfaces_extra.py +1 -1
  9. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_models_from_proto.py +103 -0
  10. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_tui_bindings.py +16 -1
  11. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/.github/copilot-instructions.md +0 -0
  12. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/.github/workflows/ci.yml +0 -0
  13. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/.github/workflows/docs-deploy.yml +0 -0
  14. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/.github/workflows/release.yml +0 -0
  15. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/.gitignore +0 -0
  16. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/LICENSE +0 -0
  17. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/README.md +0 -0
  18. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/explanation/architecture.md +0 -0
  19. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/explanation/authentication.md +0 -0
  20. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/explanation/grpc-and-protobuf.md +0 -0
  21. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/explanation/snapshot-and-stream.md +0 -0
  22. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/explanation/streaming-protocol.md +0 -0
  23. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/authenticate.md +0 -0
  24. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/automation-daemon.md +0 -0
  25. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/cli-scripting.md +0 -0
  26. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/configure-comfort-settings.md +0 -0
  27. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/configure-schedules.md +0 -0
  28. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/contribute.md +0 -0
  29. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/control-spaces.md +0 -0
  30. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/home-assistant.md +0 -0
  31. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/regenerate-protos.md +0 -0
  32. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/stream-updates.md +0 -0
  33. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/how-to/tui-app.md +0 -0
  34. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/index.md +0 -0
  35. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/reference/api-reference.md +0 -0
  36. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/reference/documentation-standards.md +0 -0
  37. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/reference/grpc-services-matrix.md +0 -0
  38. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/reference/hds-entities.md +0 -0
  39. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/reference/models.md +0 -0
  40. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/reference/token-management.md +0 -0
  41. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/docs/tutorial/get-started.md +0 -0
  42. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/mkdocs.yml +0 -0
  43. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/proto/cleaned/quilt_device_pairing.proto +0 -0
  44. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/proto/cleaned/quilt_hds.proto +0 -0
  45. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/proto/cleaned/quilt_notifier.proto +0 -0
  46. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/proto/cleaned/quilt_services.proto +0 -0
  47. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/proto/cleaned/quilt_system.proto +0 -0
  48. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/scripts/bump_version.py +0 -0
  49. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/scripts/check_docs_nav.py +0 -0
  50. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/scripts/generate_public_api_reference.py +0 -0
  51. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/scripts/regen_protos.sh +0 -0
  52. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_paths.py +0 -0
  53. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/__init__.py +0 -0
  54. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_device_pairing_pb2.py +0 -0
  55. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_device_pairing_pb2.pyi +0 -0
  56. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_device_pairing_pb2_grpc.py +0 -0
  57. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_hds_pb2.py +0 -0
  58. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_hds_pb2.pyi +0 -0
  59. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_hds_pb2_grpc.py +0 -0
  60. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_notifier_pb2.py +0 -0
  61. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_notifier_pb2.pyi +0 -0
  62. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_notifier_pb2_grpc.py +0 -0
  63. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_services_pb2.py +0 -0
  64. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_services_pb2.pyi +0 -0
  65. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_services_pb2_grpc.py +0 -0
  66. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_system_pb2.py +0 -0
  67. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_system_pb2.pyi +0 -0
  68. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/_proto/quilt_system_pb2_grpc.py +0 -0
  69. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/auth.py +0 -0
  70. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/cli/__init__.py +0 -0
  71. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/cli/main.py +0 -0
  72. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/cli/settings.py +0 -0
  73. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/cli/store.py +0 -0
  74. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/client.py +0 -0
  75. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/const.py +0 -0
  76. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/exceptions.py +0 -0
  77. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/__init__.py +0 -0
  78. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/comfort.py +0 -0
  79. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/controller.py +0 -0
  80. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/energy.py +0 -0
  81. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/enums.py +0 -0
  82. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/indoor_unit.py +0 -0
  83. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/outdoor_unit.py +0 -0
  84. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/qsm.py +0 -0
  85. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/schedule.py +0 -0
  86. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/sensor.py +0 -0
  87. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/software_update.py +0 -0
  88. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/models/space.py +0 -0
  89. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/py.typed +0 -0
  90. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/services/__init__.py +0 -0
  91. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/services/hds.py +0 -0
  92. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/services/streaming.py +0 -0
  93. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/services/system.py +0 -0
  94. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/services/user.py +0 -0
  95. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/tokens.py +0 -0
  96. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/src/quilt_hp/transport.py +0 -0
  97. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/__init__.py +0 -0
  98. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/conftest.py +0 -0
  99. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_auth.py +0 -0
  100. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_auth_store_settings_edges.py +0 -0
  101. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_cli_feature_completion.py +0 -0
  102. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_cli_login.py +0 -0
  103. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_client_cache.py +0 -0
  104. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_client_service_error_paths.py +0 -0
  105. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_hds_schedule_mapping.py +0 -0
  106. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_hds_service_branches.py +0 -0
  107. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_models.py +0 -0
  108. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_settings_store.py +0 -0
  109. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_streaming.py +0 -0
  110. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_streaming_reconnect_dispatch_extra.py +0 -0
  111. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_tokens.py +0 -0
  112. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_transport.py +0 -0
  113. {quilt_hp_python-0.2.1 → quilt_hp_python-0.2.2}/tests/test_transport_interceptor_extra.py +0 -0
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.2] - 2026-05-11
11
+
12
+ ### Fixed
13
+ - Corrected mapping of outdoor units to indoor units in SystemSnapshot
14
+ - Fixed TUI interaction issues with button handling and bindings
15
+
10
16
  ## [0.2.1] - 2026-05-10
11
17
 
12
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quilt-hp-python
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Async Python client for Quilt mini-split HVAC systems
5
5
  Project-URL: Repository, https://github.com/eman/quilt-hp-python
6
6
  Project-URL: Issues, https://github.com/eman/quilt-hp-python/issues
@@ -69,7 +69,7 @@ Raised when a requested resource does not exist (gRPC `NOT_FOUND`).
69
69
  ### `__version__`
70
70
 
71
71
  ```python
72
- __version__: str # e.g. "0.2.1"
72
+ __version__: str # e.g. "0.2.2"
73
73
  ```
74
74
 
75
75
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quilt-hp-python"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Async Python client for Quilt mini-split HVAC systems"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -9,7 +9,7 @@ from quilt_hp.exceptions import (
9
9
  QuiltNotFoundError,
10
10
  )
11
11
 
12
- __version__ = "0.2.1"
12
+ __version__ = "0.2.2"
13
13
 
14
14
  __all__ = [
15
15
  "Environment",
@@ -606,7 +606,7 @@ class DashboardScreen(Screen):
606
606
  self.app._persist()
607
607
 
608
608
  def action_system(self) -> None:
609
- self.app.push_screen(SystemScreen(self._snapshot, self._client))
609
+ self.app.push_screen(SystemScreen(self._snapshot, self._client, use_f=self.use_f))
610
610
 
611
611
  def action_select_room(self) -> None:
612
612
  lv = self.query_one(ListView)
@@ -633,7 +633,7 @@ class DashboardScreen(Screen):
633
633
  (c for c in self._snapshot.controllers if c.space_id == space_id),
634
634
  None,
635
635
  )
636
- odu = self._snapshot.outdoor_units[0] if self._snapshot.outdoor_units else None
636
+ odu = self._snapshot.odu_for_idu(idu) if idu else None
637
637
  qsm = self._snapshot.qsm_for_idu(idu) if idu else None
638
638
  self.app.push_screen(
639
639
  RoomScreen(
@@ -2239,10 +2239,13 @@ class SystemScreen(Screen):
2239
2239
  self,
2240
2240
  snapshot: SystemSnapshot,
2241
2241
  client: QuiltClient,
2242
+ *,
2243
+ use_f: bool = False,
2242
2244
  ) -> None:
2243
2245
  super().__init__()
2244
2246
  self._snapshot = snapshot
2245
2247
  self._client = client
2248
+ self.use_f = use_f
2246
2249
 
2247
2250
  def compose(self) -> ComposeResult:
2248
2251
  yield Header(show_clock=True)
@@ -2283,7 +2286,6 @@ class SystemScreen(Screen):
2283
2286
  yield Footer()
2284
2287
 
2285
2288
  def on_mount(self) -> None:
2286
- self.use_f = False
2287
2289
  self._populate()
2288
2290
 
2289
2291
  def _populate(self) -> None:
@@ -2486,6 +2488,11 @@ class SystemScreen(Screen):
2486
2488
  self._populate()
2487
2489
 
2488
2490
  def action_toggle_units(self) -> None:
2491
+ self.use_f = not self.use_f
2492
+ self._populate()
2493
+ self.app._persist()
2494
+
2495
+ def action_toggle_schedule(self) -> None:
2489
2496
  loc = self._snapshot.primary_location
2490
2497
  if loc is None:
2491
2498
  self.notify("No location found", severity="error")
@@ -2645,7 +2652,9 @@ class QuiltApp(App[None]):
2645
2652
  if self._snapshot:
2646
2653
  odu = self._snapshot.apply_outdoor_unit(odu)
2647
2654
  screen = self.screen
2648
- if isinstance(screen, (DashboardScreen, RoomScreen, SystemScreen)):
2655
+ if isinstance(screen, (DashboardScreen, SystemScreen)) or (
2656
+ isinstance(screen, RoomScreen) and screen._odu and screen._odu.id == odu.id
2657
+ ):
2649
2658
  screen.update_odu(odu)
2650
2659
 
2651
2660
  def _dispatch_ctrl(self, ctrl: Controller) -> None:
@@ -416,6 +416,12 @@ class SystemSnapshot:
416
416
  self.controller_remote_sensors.append(crs)
417
417
  return crs
418
418
 
419
+ def odu_for_idu(self, idu: IndoorUnit) -> OutdoorUnit | None:
420
+ """Return the OutdoorUnit connected to the given IDU, or None."""
421
+ if not idu.outdoor_unit_id:
422
+ return None
423
+ return next((u for u in self.outdoor_units if u.id == idu.outdoor_unit_id), None)
424
+
419
425
  def qsm_for_idu(self, idu: IndoorUnit) -> QuiltSmartModule | None:
420
426
  """Return the QSM embedded in the given IDU, or None."""
421
427
  if not idu.qsm_id:
@@ -15,7 +15,7 @@ runner = CliRunner()
15
15
  def test_version_option_outputs_package_version() -> None:
16
16
  result = runner.invoke(cli_main.app, ["--version"])
17
17
  assert result.exit_code == 0
18
- assert result.stdout.strip() == "0.2.1"
18
+ assert result.stdout.strip() == "0.2.2"
19
19
 
20
20
 
21
21
  class _FakeClient:
@@ -1216,3 +1216,106 @@ def test_away_setpoints_reflected_in_controls_when_away() -> None:
1216
1216
  assert s1_away.is_away is True
1217
1217
  assert s1_away.controls.heating_setpoint_c == pytest.approx(away.heating_setpoint_c)
1218
1218
  assert s1_away.controls.cooling_setpoint_c == pytest.approx(away.cooling_setpoint_c)
1219
+
1220
+
1221
+ # ─── SystemSnapshot.odu_for_idu ───────────────────────────────────────────────
1222
+
1223
+
1224
+ def _make_odu_proto(odu_id: str, space_id: str = "space-1") -> SimpleNamespace:
1225
+ return _ns(
1226
+ header=_make_header(odu_id),
1227
+ relationships=_ns(space_id=space_id, hardware_id="", firmware_update_info_id=""),
1228
+ state=_ns(hvac_state=0),
1229
+ performance_data=_ns(
1230
+ ambient_temperature_c=0.0,
1231
+ compressor_frequency_hz=0.0,
1232
+ energy_measurement_j=0.0,
1233
+ ),
1234
+ )
1235
+
1236
+
1237
+ def _make_snap_with_multiple_odus() -> SystemSnapshot:
1238
+ from quilt_hp._proto import quilt_hds_pb2 as hds
1239
+
1240
+ loc = _ns(
1241
+ header=_make_header("loc-1"),
1242
+ attributes=_ns(name="", tz_identifier="UTC"),
1243
+ controls=_ns(schedule_execution=hds.SCHEDULE_EXECUTION_RUNNING),
1244
+ )
1245
+
1246
+ def _idu(idu_id: str, space_id: str, odu_id: str) -> SimpleNamespace:
1247
+ proto = _make_idu_proto(idu_id, space_id)
1248
+ proto.relationships = _ns(
1249
+ space_id=space_id,
1250
+ outdoor_unit_id=odu_id,
1251
+ hardware_id="hw-1",
1252
+ quilt_smart_module_id="",
1253
+ firmware_update_info_id="",
1254
+ )
1255
+ return proto
1256
+
1257
+ return SystemSnapshot.from_proto(
1258
+ _ns(
1259
+ spaces=[
1260
+ _make_space_proto("s1", "Room 1", parent_space_id="root"),
1261
+ _make_space_proto("s2", "Room 2", parent_space_id="root"),
1262
+ _make_space_proto("s3", "Room 3", parent_space_id="root"),
1263
+ ],
1264
+ indoor_units=[
1265
+ _idu("idu-1", "s1", "odu-1"),
1266
+ _idu("idu-2", "s2", "odu-2"),
1267
+ _idu("idu-3", "s3", "odu-1"), # shares odu-1 with room 1
1268
+ ],
1269
+ outdoor_units=[
1270
+ _make_odu_proto("odu-1"),
1271
+ _make_odu_proto("odu-2"),
1272
+ _make_odu_proto("odu-3"), # present but not linked to any IDU
1273
+ ],
1274
+ outdoor_unit_hardware=[],
1275
+ controller_hardware=[],
1276
+ controllers=[],
1277
+ quilt_smart_modules=[],
1278
+ comfort_settings=[],
1279
+ schedule_weeks=[],
1280
+ schedule_days=[],
1281
+ remote_sensors=[],
1282
+ controller_remote_sensors=[],
1283
+ software_update_infos=[],
1284
+ locations=[loc],
1285
+ )
1286
+ )
1287
+
1288
+
1289
+ def test_odu_for_idu_returns_correct_odu() -> None:
1290
+ snap = _make_snap_with_multiple_odus()
1291
+ idu1 = next(u for u in snap.indoor_units if u.id == "idu-1")
1292
+ idu2 = next(u for u in snap.indoor_units if u.id == "idu-2")
1293
+ idu3 = next(u for u in snap.indoor_units if u.id == "idu-3")
1294
+
1295
+ assert snap.odu_for_idu(idu1) is not None
1296
+ assert snap.odu_for_idu(idu1).id == "odu-1" # type: ignore[union-attr]
1297
+
1298
+ assert snap.odu_for_idu(idu2) is not None
1299
+ assert snap.odu_for_idu(idu2).id == "odu-2" # type: ignore[union-attr]
1300
+
1301
+ assert snap.odu_for_idu(idu3) is not None
1302
+ assert snap.odu_for_idu(idu3).id == "odu-1" # type: ignore[union-attr]
1303
+
1304
+
1305
+ def test_odu_for_idu_no_outdoor_unit_id_returns_none() -> None:
1306
+ snap = _make_snap_with_multiple_odus()
1307
+ idu1 = next(u for u in snap.indoor_units if u.id == "idu-1")
1308
+ from dataclasses import replace
1309
+
1310
+ idu_no_odu = replace(idu1, outdoor_unit_id=None)
1311
+ assert snap.odu_for_idu(idu_no_odu) is None
1312
+
1313
+
1314
+ def test_odu_for_idu_unlinked_odu_not_returned() -> None:
1315
+ """ODU-3 exists in the snapshot but is not linked to any IDU."""
1316
+ snap = _make_snap_with_multiple_odus()
1317
+ assert len(snap.outdoor_units) == 3
1318
+ assert all(
1319
+ u.id != "odu-3"
1320
+ for u in (snap.odu_for_idu(idu) for idu in snap.indoor_units if idu is not None)
1321
+ )
@@ -4,7 +4,7 @@ import pytest
4
4
 
5
5
  pytest.importorskip("textual")
6
6
 
7
- from quilt_hp.cli.tui import RoomScreen
7
+ from quilt_hp.cli.tui import RoomScreen, SystemScreen
8
8
 
9
9
 
10
10
  def _key_action_map() -> dict[str, set[str]]:
@@ -21,3 +21,18 @@ def test_fan_key_does_not_overlap_with_fence_adjustment() -> None:
21
21
  assert "fence_fwd_inc" not in keymap["f"]
22
22
  assert "ctrl+up" in keymap
23
23
  assert keymap["ctrl+up"] == {"fence_fwd_inc"}
24
+
25
+
26
+ def test_system_bindings_map_units_and_schedule_actions() -> None:
27
+ keymap: dict[str, set[str]] = {}
28
+ for binding in SystemScreen.BINDINGS:
29
+ for key in binding.key.split(","):
30
+ keymap.setdefault(key, set()).add(binding.action)
31
+
32
+ assert keymap["u"] == {"toggle_units"}
33
+ assert keymap["p"] == {"toggle_schedule"}
34
+
35
+
36
+ def test_system_screen_accepts_initial_unit_preference() -> None:
37
+ screen = SystemScreen(snapshot=object(), client=object(), use_f=True)
38
+ assert screen.use_f is True
File without changes