Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

## [Unreleased]

### Fixed
- **`LocalCommsStatus` fields were mislabeled** (previously guessed from limited
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.
`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 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).
- Marked `SpaceSettings.safety_heating=9` 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
Expand Down
10 changes: 10 additions & 0 deletions docs/how-to/control-spaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 32 additions & 9 deletions src/quilt_hp/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,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.")
Expand Down Expand Up @@ -786,13 +786,14 @@ def set_space(
max=SETPOINT_MAX_C,
help=f"Cooling setpoint in °C ({SETPOINT_MIN_C:.0f}–{SETPOINT_MAX_C:.0f})",
),
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:
"""Update HVAC mode and setpoints for a room."""
if mode is None and heat is None and cool is None:
if mode is None and heat is None and cool is None and fan is None:
console.print(
"[red]Nothing to update:[/red] provide at least one of --mode, --heat, --cool."
"[red]Nothing to update:[/red] provide at least one of --mode, --heat, --cool, --fan."
)
raise typer.Exit(1)

Expand Down Expand Up @@ -820,12 +821,34 @@ async def _set() -> None:
console.print(f"[red]Room {space_name!r} not found.[/red]")
raise typer.Exit(1)

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())
Expand Down
9 changes: 9 additions & 0 deletions src/quilt_hp/models/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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.

Expand Down
29 changes: 29 additions & 0 deletions tests/test_models_from_proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,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

Expand Down