From c183540d245eb949db34cfedbc668bf9ac54d5e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Fri, 3 Jul 2026 23:39:15 -0700 Subject: [PATCH 1/3] feat: add fan speed control to CLI set command Add a --fan flag to `quilt set` so fan speed can be set from the CLI, matching the app's fan speed selection. The flag resolves the room's indoor unit(s) via a new SystemSnapshot.indoor_units_for_space() helper and applies the fan speed independently of mode/setpoint updates. The TUI already supports fan cycling via the `f` key; document both the CLI flag and TUI binding in the control-spaces how-to. --- docs/how-to/control-spaces.md | 10 +++++++++ src/quilt_hp/cli/main.py | 37 ++++++++++++++++++++++++++------- src/quilt_hp/models/system.py | 9 ++++++++ tests/test_models_from_proto.py | 29 ++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/docs/how-to/control-spaces.md b/docs/how-to/control-spaces.md index 703f2b6..2a7d04d 100644 --- a/docs/how-to/control-spaces.md +++ b/docs/how-to/control-spaces.md @@ -96,6 +96,16 @@ print(f"Fan speed: {updated.controls.fan_speed}") Available fan speeds: `FanSpeed.AUTO`, `QUIET`, `LOW`, `MEDIUM`, `HIGH`, `BLAST`. +From the CLI, set the fan speed for a room's indoor unit(s) with the `--fan` +flag on `quilt set` (it resolves the space's indoor units for you): + +```bash +quilt set "Living Room" --fan medium +quilt set "Living Room" --mode cool --cool 22 --fan auto +``` + +In the TUI, press `f` on a room screen to cycle through the fan speeds. + --- ## Set louver mode and position diff --git a/src/quilt_hp/cli/main.py b/src/quilt_hp/cli/main.py index 68b79a3..97cbdd4 100644 --- a/src/quilt_hp/cli/main.py +++ b/src/quilt_hp/cli/main.py @@ -23,7 +23,7 @@ from quilt_hp.cli.store import FileStore from quilt_hp.client import QuiltClient from quilt_hp.exceptions import QuiltAuthError, QuiltError -from quilt_hp.models.enums import HVACMode +from quilt_hp.models.enums import FanSpeed, HVACMode from quilt_hp.models.system import SystemSnapshot app = typer.Typer(help="Quilt HVAC command-line interface.") @@ -747,6 +747,7 @@ def set_space( mode: str | None = typer.Option(None, help="HVAC mode: COOL, HEAT, AUTO, FAN, DRY, STANDBY"), heat: float | None = typer.Option(None, help="Heating setpoint in °C"), cool: float | None = typer.Option(None, help="Cooling setpoint in °C"), + fan: str | None = typer.Option(None, help="Fan speed: AUTO, QUIET, LOW, MEDIUM, HIGH, BLAST"), email: str | None = typer.Option(None, envvar="QUILT_EMAIL", help="Quilt account email"), home: str | None = typer.Option(None, help="Specific home name to connect to"), ) -> None: @@ -773,12 +774,34 @@ async def _set() -> None: else: hvac_mode = None - await client.set_space( - space.id, - mode=hvac_mode, - heat_setpoint_c=heat, - cool_setpoint_c=cool, - ) + if fan: + try: + fan_speed: FanSpeed | None = FanSpeed[fan.upper()] + except KeyError: + valid = ", ".join(f.name.lower() for f in FanSpeed) + console.print(f"[red]Invalid fan speed {fan!r}. Valid: {valid}[/red]") + raise typer.Exit(1) from None + else: + fan_speed = None + + if hvac_mode is not None or heat is not None or cool is not None: + await client.set_space( + space.id, + mode=hvac_mode, + heat_setpoint_c=heat, + cool_setpoint_c=cool, + ) + + if fan_speed is not None: + idus = snap.indoor_units_for_space(space) + if not idus: + console.print( + f"[red]No indoor unit found for {space.name}; cannot set fan speed.[/red]" + ) + raise typer.Exit(1) + for idu in idus: + await client.set_indoor_unit(idu.id, fan_speed=fan_speed) + console.print(f"[green]✓ Updated {space.name}[/green]") _run(_set()) diff --git a/src/quilt_hp/models/system.py b/src/quilt_hp/models/system.py index 5e7214e..4398fe2 100644 --- a/src/quilt_hp/models/system.py +++ b/src/quilt_hp/models/system.py @@ -135,6 +135,15 @@ def away_comfort_setting(self, space: Space | str) -> ComfortSetting | None: return cs return None + def indoor_units_for_space(self, space: Space | str) -> list[IndoorUnit]: + """Return all indoor units linked to a space. + + Args: + space: A ``Space`` object or space ID string. + """ + space_id = space if isinstance(space, str) else space.id + return [u for u in self.indoor_units if u.space_id == space_id] + def enrich_space(self, space: Space) -> Space: """Resolve active_comfort_setting_type on a stream-updated Space. diff --git a/tests/test_models_from_proto.py b/tests/test_models_from_proto.py index b68b54f..03fd42f 100644 --- a/tests/test_models_from_proto.py +++ b/tests/test_models_from_proto.py @@ -1167,6 +1167,35 @@ def test_system_snapshot_rooms() -> None: assert rooms[0].name == "Living Room" +def test_system_snapshot_indoor_units_for_space() -> None: + """indoor_units_for_space filters IDUs by space id.""" + from quilt_hp.models.system import SystemSnapshot + + snap = SystemSnapshot( + spaces=[], + indoor_units=[ + _ns(space_id="room-1"), + _ns(space_id="room-2"), + _ns(space_id="room-1"), + ], + outdoor_units=[], + controllers=[], + quilt_smart_modules=[], + comfort_settings=[], + schedule_weeks=[], + schedule_days=[], + remote_sensors=[], + controller_remote_sensors=[], + software_update_infos=[], + locations=[], + timezone="UTC", + ) + result = snap.indoor_units_for_space("room-1") + assert len(result) == 2 + assert all(u.space_id == "room-1" for u in result) + assert snap.indoor_units_for_space("nope") == [] + + def test_system_snapshot_primary_location() -> None: from quilt_hp._proto import quilt_hds_pb2 as hds From 4df24a067fd18cf9ccd7b5d9110335138f533eca Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 4 Jul 2026 08:28:24 -0700 Subject: [PATCH 2/3] docs: update changelog for fan speed CLI and recent merges --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18162d0..6d33a99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,43 @@ ## [Unreleased] +Cross-checked the library against the Android app (com.quilt.android 1.0.29, +versionCode 242) by decompiling with jadx and decoding the compiled protobuf +`newMessageInfo` metadata for exact field numbers. + +### Fixed +- **`LocalCommsStatus` fields were mislabeled** (previously guessed from limited + captures). The compiled message proves the correct field semantics: field 2 + `health` → `status`, field 3 `link_state` → `visible_devices_count`, field 4 + `version` → `expected_devices_count`, field 5 `health_changed_ts` → + `last_session_change_ts`, field 6 `connection_state` (int32) → `reason` + (`LocalCommsHealthReason` enum). Decoding still worked by field number, but the + model exposed wrong names/semantics and discarded the reason diagnostics. + `QuiltSmartModule`/`Controller` now expose `local_comms_visible_devices`, + `local_comms_expected_devices`, `local_comms_reason`, and + `local_comms_last_session_change` (replacing `local_comms_link_state`, + `local_comms_connection_state`, and `local_comms_version`). + +### Added +- CLI `set` command gained a `--fan` option (`AUTO`, `QUIET`, `LOW`, `MEDIUM`, + `HIGH`, `BLAST`) that applies the chosen fan speed to every indoor unit in the + target space. +- `SystemSnapshot.indoor_units_for_space()` returns all indoor units linked to a + space (accepts a `Space` or space-ID string). +- `LocalCommsHealthReason` enum (12 APK-confirmed values). `ZENOH_SESSION_DOWN` + identifies the local mesh transport as **Eclipse Zenoh**, not NATS (port 7447 + is Zenoh's default). +- `IndoorUnitConditions.compressor_minimum_run_time` (proto field 13, + APK-confirmed 1.0.29). +- Marked `SpaceSettings.safety_heating=9` APK-confirmed (was "unconfirmed"). + +### CI +- Docs deploy workflow upgraded to the current Node 24 GitHub Pages actions + (`configure-pages@v6`, `upload-pages-artifact@v4`, `deploy-pages@v5`), + removing the deprecated Node 20 runtime path. +- Docs deploy now retries `deploy-pages` once on transient GitHub Pages backend + failures, failing the job only if both attempts fail. + ## [0.5.5] - 2026-07-03 ## [0.5.4] - 2026-07-03 From 4d2b592b40e0199e6fab2071904a045cc90ae5d4 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 4 Jul 2026 08:31:22 -0700 Subject: [PATCH 3/3] docs: remove app-decompilation references from changelog --- CHANGELOG.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d33a99..3b5bcdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,11 @@ ## [Unreleased] -Cross-checked the library against the Android app (com.quilt.android 1.0.29, -versionCode 242) by decompiling with jadx and decoding the compiled protobuf -`newMessageInfo` metadata for exact field numbers. - ### Fixed - **`LocalCommsStatus` fields were mislabeled** (previously guessed from limited - captures). The compiled message proves the correct field semantics: field 2 - `health` → `status`, field 3 `link_state` → `visible_devices_count`, field 4 - `version` → `expected_devices_count`, field 5 `health_changed_ts` → + captures). Corrected to the verified field semantics: field 2 `health` → + `status`, field 3 `link_state` → `visible_devices_count`, field 4 `version` → + `expected_devices_count`, field 5 `health_changed_ts` → `last_session_change_ts`, field 6 `connection_state` (int32) → `reason` (`LocalCommsHealthReason` enum). Decoding still worked by field number, but the model exposed wrong names/semantics and discarded the reason diagnostics. @@ -25,12 +21,11 @@ versionCode 242) by decompiling with jadx and decoding the compiled protobuf target space. - `SystemSnapshot.indoor_units_for_space()` returns all indoor units linked to a space (accepts a `Space` or space-ID string). -- `LocalCommsHealthReason` enum (12 APK-confirmed values). `ZENOH_SESSION_DOWN` +- `LocalCommsHealthReason` enum (12 values). `ZENOH_SESSION_DOWN` identifies the local mesh transport as **Eclipse Zenoh**, not NATS (port 7447 is Zenoh's default). -- `IndoorUnitConditions.compressor_minimum_run_time` (proto field 13, - APK-confirmed 1.0.29). -- Marked `SpaceSettings.safety_heating=9` APK-confirmed (was "unconfirmed"). +- `IndoorUnitConditions.compressor_minimum_run_time` (proto field 13). +- Marked `SpaceSettings.safety_heating=9` confirmed (was "unconfirmed"). ### CI - Docs deploy workflow upgraded to the current Node 24 GitHub Pages actions