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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ tokens.json
.env
.env.*


# uv lockfile — not tracked: this is a library (consumers resolve from
# pyproject.toml) and CI does not use the lock. Commit it only if dev/CI
# standardize on `uv sync --locked`.
uv.lock
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

## [Unreleased]

### Added
- **Diagnostics view.** `QuiltClient.get_diagnostics()` and
`SystemSnapshot.diagnostics()` return a new `SystemDiagnostics` — the
installer-style diagnostic picture assembled from data the cloud API already
returns on the indoor units: the per-IDU fault/condition matrix (including the
outdoor-unit / refrigerant conditions surfaced through each IDU, e.g.
`outdoor_unit_communication_error`, `defrost_cycle`, `oil_return`),
refrigerant-circuit temperatures (coil / gas-pipe / liquid-pipe / inlet /
outlet) and humidity, and per-unit power. New models `IndoorUnitDiagnostics`,
`OutdoorUnitDiagnostics`, `SystemDiagnostics`, plus `IndoorUnitConditions.active`
/ `.states()` helpers. New CLI command `quilt diagnostics` (with `--faults-only`
and `--output json`). The outdoor unit's own raw sensors (compressor Hz,
pressures, discharge temp) remain withheld from the cloud plane and are flagged
as such rather than reported.
- `QuiltClient.request_fast_updates(reason=..., system_id=...)` and the
underlying `CommandService.request_fast_updates()` wrapper — calls the new
`core.protos.home_datastore.CommandService/RequestFastUpdates` RPC to ask the
cloud to raise a system's telemetry cadence. This is the same lever the Quilt
mobile app pulls (new in app versionCode 255) when the user is active or a
device's local mesh is degraded; the effect is a faster stream of updates over
`NotifierStream`. New `FastUpdateReason` enum (`UNSPECIFIED`,
`LOCAL_COMMS_UNHEALTHY`, `USER_ACTIVITY`) in `quilt_hp.models.enums`.

## [0.5.7] - 2026-07-20

### Added
Expand Down
19 changes: 19 additions & 0 deletions docs/how-to/cli-scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ quilt snapshot | jq '[.indoor_units[] | select(.state.is_online == false)]'

---

## Check diagnostics

```bash
# Human-readable: per-IDU fault conditions, refrigerant temps, and power
quilt diagnostics

# Only indoor units with an active fault
quilt diagnostics --faults-only

# JSON for scripting — e.g. list every active fault system-wide
quilt diagnostics --output json | jq '.indoor_units[] | select(.active_faults | length > 0) | {name, active_faults}'
```

The outdoor unit's own raw sensors (compressor Hz, pressures, discharge temp)
are withheld from the cloud plane; the diagnostic conditions and refrigerant
pipe temperatures for each ODU circuit are surfaced through its indoor units.

---

## Control spaces from the shell

```bash
Expand Down
53 changes: 53 additions & 0 deletions docs/reference/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,29 @@ def invalidate_snapshot(self) -> None

Discards the cached snapshot. The next `get_snapshot()` call fetches fresh data from the server.

---

### `get_diagnostics`

```python
async def get_diagnostics(self, system_id: str | None = None) -> SystemDiagnostics
```

Fetches the installer-style diagnostic view — a convenience wrapper over
`get_snapshot().diagnostics()`. Returns a [`SystemDiagnostics`](models.md#systemdiagnostics)
with, per indoor unit: the fault/condition matrix (including the outdoor-unit and
refrigerant conditions surfaced through each IDU), refrigerant-circuit temperatures
(coil / gas-pipe / liquid-pipe / inlet / outlet) and humidity, and per-unit power.

The outdoor unit's own raw sensors (compressor Hz, pressures, discharge temp) are
withheld from the cloud plane and are **not** included; `OutdoorUnitDiagnostics`
reports `raw_sensors_available=False`.

**Parameters:**
- `system_id`: explicit system ID; defaults to the client's resolved system.

**Raises:** `QuiltError` if the RPC fails.

### `close`

```python
Expand Down Expand Up @@ -510,6 +533,36 @@ Returns hourly energy consumption for all spaces.

---

## Telemetry cadence

### `request_fast_updates`

```python
async def request_fast_updates(
self,
*,
reason: FastUpdateReason = FastUpdateReason.USER_ACTIVITY,
system_id: str | None = None,
) -> None
```

Asks the cloud to raise the telemetry cadence for a system by calling
`CommandService/RequestFastUpdates` — the same lever the mobile app pulls when
the user is active or a device's local mesh is degraded. New in the Quilt app
versionCode 255.

The RPC response is empty; the effect is a faster stream of state updates over
a `NotifierStream`, so pair this with `stream()`. There is no local endpoint —
the request flows through the cloud API like every other call.

**Parameters:**
- `reason`: `FastUpdateReason.USER_ACTIVITY` (default), `LOCAL_COMMS_UNHEALTHY`, or `UNSPECIFIED`. Imported from `quilt_hp.models.enums`.
- `system_id`: explicit system ID; defaults to the client's resolved system.

**Raises:** `QuiltError` if the client is not connected or the RPC fails.

---

## Streaming

### `stream`
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/grpc-services-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ Defined in `quilt_hds.proto`. Package: `core.protos.home_datastore`.
| `DeleteScheduleWeek` | `DeleteScheduleWeekRequest` | `Empty` | `HomeDatastoreService.delete_schedule_week()` |
| `UpdateLocation` | `UpdateLocationRequest` | `Location` | `HomeDatastoreService.update_location_schedule_execution()`; pauses/resumes schedules |

## CommandService

Defined in `quilt_hds.proto`. Package: `core.protos.home_datastore`. New in the Quilt app versionCode 255; cloud stub only (no local endpoint).

| Method | Request | Response | Library wrapper |
| --- | --- | --- | --- |
| `RequestFastUpdates` | `RequestFastUpdatesRequest` | `Empty` | `CommandService.request_fast_updates()` → `None`; also `QuiltClient.request_fast_updates()` |

## SystemInformationService

Defined in `quilt_services.proto`. Package: `core.protos.app`.
Expand Down
84 changes: 84 additions & 0 deletions docs/reference/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ service = UserService(channel)
| `get_user_attributes()` | Returns `UserAttributes` including declared user type. |
| `patch_user_attributes(declared_user_type)` | Updates user attributes. |

### `CommandService`

```python
from quilt_hp.services.command import CommandService

service = CommandService(channel)
```

Wraps the `CommandService` gRPC stub (new in the Quilt app versionCode 255).
Registered in the cloud stub only — there is no local endpoint.

| Method | Description |
|--------|-------------|
| `request_fast_updates(system_id, reason=FastUpdateReason.USER_ACTIVITY)` | Asks the cloud to raise the telemetry cadence for a system. Returns `None`; the effect is a faster stream of updates over `NotifierStream`. |

### `NotifierStream`

```python
Expand Down Expand Up @@ -573,6 +588,74 @@ Firmware/software update record associated with an indoor unit, outdoor unit, co

---

### SystemDiagnostics

```python
@dataclass(slots=True)
class SystemDiagnostics:
indoor_units: list[IndoorUnitDiagnostics]
outdoor_units: list[OutdoorUnitDiagnostics]

@property
def active_faults(self) -> list[tuple[str, str]]: ... # (indoor_unit_id, condition_name)
@property
def has_faults(self) -> bool: ...
```

The installer-style diagnostic view, assembled from data the cloud API already
returns. Obtain one from `SystemSnapshot.diagnostics()` or
`QuiltClient.get_diagnostics()`.

#### IndoorUnitDiagnostics

```python
@dataclass(slots=True)
class IndoorUnitDiagnostics:
indoor_unit_id: str
name: str
space_id: str
space_name: str
online: bool
hvac_state: HVACState
active_faults: list[str] # condition names currently ACTIVE
conditions: dict[str, ConditionState] # every condition → state (empty if none reported)
coil_temperature_c: float | None
gas_pipe_temperature_c: float | None
liquid_pipe_temperature_c: float | None
inlet_temperature_c: float | None
outlet_temperature_c: float | None
inlet_humidity_pct: float | None
hvac_power_w: float | None
```

Per-indoor-unit diagnostics. The condition matrix includes the outdoor-unit and
refrigerant conditions surfaced through the IDU (`outdoor_unit_communication_error`,
`abnormal_outdoor_air_temperature`, `defrost_cycle`, `oil_return`, `coil_preheat`,
`modbus_communication_error`, `compressor_minimum_run_time`, …). Refrigerant temps
and power come from `IndoorUnit.performance_data` / `performance_metrics` and are
`None` when the unit reported none.

The underlying `IndoorUnitConditions` model exposes `active` (list of ACTIVE
condition names) and `states()` (dict of every condition → `ConditionState`).

#### OutdoorUnitDiagnostics

```python
@dataclass(slots=True)
class OutdoorUnitDiagnostics:
outdoor_unit_id: str
hvac_state: HVACState
raw_sensors_available: bool
```

Per-outdoor-unit diagnostics. `raw_sensors_available` is `False` over the cloud
plane — the ODU's own `performance_data` (compressor Hz, suction/discharge
pressures, coil/discharge temps) is withheld from the mobile API; that telemetry
is only reachable on the local/hardware track. The refrigerant conditions and
pipe temperatures for this ODU's circuit are surfaced through its indoor units.

---

## Enum types

All enums live in `quilt_hp.models.enums` and subclass `IntEnum`, mirroring Quilt's wire values.
Expand All @@ -596,5 +679,6 @@ All enums live in `quilt_hp.models.enums` and subclass `IntEnum`, mirroring Quil
| `HvacControllerType` | Controller algorithm variant | `PASS_THROUGH_TEMPERATURE`, `INTEGRAL_TEMPERATURE_V1`, `INTEGRAL_TEMPERATURE_V2` |
| `FallbackControlCommand` | Offline fallback command sent to an IDU | `COMPLETE`, `EXIT` |
| `RemoteSensorControlMode` | Whether a remote sensor participates in control | `DISABLED`, `ENABLED` |
| `FastUpdateReason` | Why `request_fast_updates()` is asking the cloud to raise the telemetry cadence | `UNSPECIFIED`, `LOCAL_COMMS_UNHEALTHY`, `USER_ACTIVITY` |

`FanSpeed.to_wire()` and `FanSpeed.from_wire()` handle the Quilt protocol's `(fan_speed_mode, fan_speed_percent)` encoding. `LouverAngle.to_wire()` and `LouverAngle.from_wire()` do the same for fixed louver positions.
28 changes: 28 additions & 0 deletions proto/cleaned/quilt_hds.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1045,3 +1045,31 @@ service HomeDatastoreService {
rpc ListLocations(ListLocationsRequest) returns (ListLocationsResponse);
}

// ---------------------------------------------------------------------------
// CommandService — imperative device commands (new in com.quilt.android 255)
// ---------------------------------------------------------------------------
//
// A separate service in the same home_datastore package. Registered in the
// cloud gRPC stub only (api.prod.quilt.cloud) — there is no local endpoint.
// Field numbers, method name, and enum values APK-confirmed from
// com.quilt.android versionCode 255 (request class sh1, reason enum qh1).

// Why the app is asking the cloud to raise the per-system telemetry cadence.
enum FastUpdateReason {
FAST_UPDATE_REASON_UNSPECIFIED = 0;
// Local Zenoh mesh is degraded/offline, so the app leans on the cloud to
// poll the devices faster instead (there is no app-side local fast path).
FAST_UPDATE_REASON_LOCAL_COMMS_UNHEALTHY = 1;
// The user is actively interacting in the app.
FAST_UPDATE_REASON_USER_ACTIVITY = 2;
}

message RequestFastUpdatesRequest {
string system_id = 1;
FastUpdateReason reason = 2;
}

service CommandService {
rpc RequestFastUpdates(RequestFastUpdatesRequest) returns (google.protobuf.Empty);
}

100 changes: 53 additions & 47 deletions src/quilt_hp/_proto/quilt_hds_pb2.py

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions src/quilt_hp/_proto/quilt_hds_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,42 @@ WIFI_STATE_GROUP_HANDSHAKE: WifiConnectionState.ValueType # 9
WIFI_STATE_WPA_COMPLETED: WifiConnectionState.ValueType # 10
Global___WifiConnectionState: _TypeAlias = WifiConnectionState # noqa: Y015

class _FastUpdateReason:
ValueType = _typing.NewType("ValueType", _builtins.int)
V: _TypeAlias = ValueType # noqa: Y015

class _FastUpdateReasonEnumTypeWrapper(_enum_type_wrapper._EnumTypeWrapper[_FastUpdateReason.ValueType], _builtins.type):
DESCRIPTOR: _descriptor.EnumDescriptor
FAST_UPDATE_REASON_UNSPECIFIED: _FastUpdateReason.ValueType # 0
FAST_UPDATE_REASON_LOCAL_COMMS_UNHEALTHY: _FastUpdateReason.ValueType # 1
"""Local Zenoh mesh is degraded/offline, so the app leans on the cloud to
poll the devices faster instead (there is no app-side local fast path).
"""
FAST_UPDATE_REASON_USER_ACTIVITY: _FastUpdateReason.ValueType # 2
"""The user is actively interacting in the app."""

class FastUpdateReason(_FastUpdateReason, metaclass=_FastUpdateReasonEnumTypeWrapper):
"""---------------------------------------------------------------------------
CommandService — imperative device commands (new in com.quilt.android 255)
---------------------------------------------------------------------------

A separate service in the same home_datastore package. Registered in the
cloud gRPC stub only (api.prod.quilt.cloud) — there is no local endpoint.
Field numbers, method name, and enum values APK-confirmed from
com.quilt.android versionCode 255 (request class sh1, reason enum qh1).

Why the app is asking the cloud to raise the per-system telemetry cadence.
"""

FAST_UPDATE_REASON_UNSPECIFIED: FastUpdateReason.ValueType # 0
FAST_UPDATE_REASON_LOCAL_COMMS_UNHEALTHY: FastUpdateReason.ValueType # 1
"""Local Zenoh mesh is degraded/offline, so the app leans on the cloud to
poll the devices faster instead (there is no app-side local fast path).
"""
FAST_UPDATE_REASON_USER_ACTIVITY: FastUpdateReason.ValueType # 2
"""The user is actively interacting in the app."""
Global___FastUpdateReason: _TypeAlias = FastUpdateReason # noqa: Y015

@_typing.final
class LocalCommsStatus(_message.Message):
"""Local communications status for a QSM or Controller mesh node.
Expand Down Expand Up @@ -3995,3 +4031,25 @@ class ListLocationsResponse(_message.Message):
def WhichOneof(self, oneof_group: _Never) -> None: ...

Global___ListLocationsResponse: _TypeAlias = ListLocationsResponse # noqa: Y015

@_typing.final
class RequestFastUpdatesRequest(_message.Message):
DESCRIPTOR: _descriptor.Descriptor

SYSTEM_ID_FIELD_NUMBER: _builtins.int
REASON_FIELD_NUMBER: _builtins.int
system_id: _builtins.str
reason: Global___FastUpdateReason.ValueType
def __init__(
self,
*,
system_id: _builtins.str = ...,
reason: Global___FastUpdateReason.ValueType = ...,
) -> None: ...
_HasFieldArgType: _TypeAlias = _Never # noqa: Y015
def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["reason", b"reason", "system_id", b"system_id"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
def WhichOneof(self, oneof_group: _Never) -> None: ...

Global___RequestFastUpdatesRequest: _TypeAlias = RequestFastUpdatesRequest # noqa: Y015
Loading