quilt-hp-python 0.1.1__py3-none-any.whl

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 (53) hide show
  1. quilt_hp/__init__.py +22 -0
  2. quilt_hp/_paths.py +26 -0
  3. quilt_hp/_proto/__init__.py +0 -0
  4. quilt_hp/_proto/quilt_device_pairing_pb2.py +56 -0
  5. quilt_hp/_proto/quilt_device_pairing_pb2.pyi +317 -0
  6. quilt_hp/_proto/quilt_device_pairing_pb2_grpc.py +24 -0
  7. quilt_hp/_proto/quilt_hds_pb2.py +292 -0
  8. quilt_hp/_proto/quilt_hds_pb2.pyi +3947 -0
  9. quilt_hp/_proto/quilt_hds_pb2_grpc.py +1732 -0
  10. quilt_hp/_proto/quilt_notifier_pb2.py +55 -0
  11. quilt_hp/_proto/quilt_notifier_pb2.pyi +258 -0
  12. quilt_hp/_proto/quilt_notifier_pb2_grpc.py +97 -0
  13. quilt_hp/_proto/quilt_services_pb2.py +171 -0
  14. quilt_hp/_proto/quilt_services_pb2.pyi +1320 -0
  15. quilt_hp/_proto/quilt_services_pb2_grpc.py +1188 -0
  16. quilt_hp/_proto/quilt_system_pb2.py +53 -0
  17. quilt_hp/_proto/quilt_system_pb2.pyi +164 -0
  18. quilt_hp/_proto/quilt_system_pb2_grpc.py +270 -0
  19. quilt_hp/auth.py +244 -0
  20. quilt_hp/cli/__init__.py +1 -0
  21. quilt_hp/cli/main.py +770 -0
  22. quilt_hp/cli/settings.py +123 -0
  23. quilt_hp/cli/store.py +105 -0
  24. quilt_hp/cli/tui.py +2677 -0
  25. quilt_hp/client.py +616 -0
  26. quilt_hp/const.py +57 -0
  27. quilt_hp/exceptions.py +23 -0
  28. quilt_hp/models/__init__.py +85 -0
  29. quilt_hp/models/comfort.py +47 -0
  30. quilt_hp/models/controller.py +135 -0
  31. quilt_hp/models/energy.py +31 -0
  32. quilt_hp/models/enums.py +298 -0
  33. quilt_hp/models/indoor_unit.py +412 -0
  34. quilt_hp/models/outdoor_unit.py +71 -0
  35. quilt_hp/models/qsm.py +105 -0
  36. quilt_hp/models/schedule.py +98 -0
  37. quilt_hp/models/sensor.py +92 -0
  38. quilt_hp/models/software_update.py +74 -0
  39. quilt_hp/models/space.py +177 -0
  40. quilt_hp/models/system.py +451 -0
  41. quilt_hp/py.typed +1 -0
  42. quilt_hp/services/__init__.py +1 -0
  43. quilt_hp/services/hds.py +480 -0
  44. quilt_hp/services/streaming.py +561 -0
  45. quilt_hp/services/system.py +95 -0
  46. quilt_hp/services/user.py +143 -0
  47. quilt_hp/tokens.py +119 -0
  48. quilt_hp/transport.py +192 -0
  49. quilt_hp_python-0.1.1.dist-info/METADATA +172 -0
  50. quilt_hp_python-0.1.1.dist-info/RECORD +53 -0
  51. quilt_hp_python-0.1.1.dist-info/WHEEL +4 -0
  52. quilt_hp_python-0.1.1.dist-info/entry_points.txt +2 -0
  53. quilt_hp_python-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,92 @@
1
+ """Remote sensor and ControllerRemoteSensor models.
2
+
3
+ RemoteSensor: standalone BLE temperature/humidity puck linked to an IndoorUnit.
4
+ - Proto field 12 in HomeDatastoreSystem (empty if no sensors paired).
5
+ - APK: C5534qL.java (proto), C3056e81.java (KMP model).
6
+
7
+ ControllerRemoteSensor: sensor capability of a Controller (Dial) for zones.
8
+ - Proto field 16 in HomeDatastoreSystem (empty if sensor mode not configured).
9
+ - APK: CI.java (proto), JD.java (KMP model).
10
+ - Shares RemoteSensorState and RemoteSensorAttributes with RemoteSensor.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+
17
+ from quilt_hp.models.enums import RemoteSensorControlMode
18
+
19
+
20
+ def _parse_state(
21
+ s: object,
22
+ ) -> tuple[float | None, float | None, float | None, int | None]:
23
+ """Return ambient temp, humidity, battery, and signal from proto state."""
24
+ return (
25
+ s.ambient_temperature_c or None, # type: ignore[attr-defined]
26
+ s.humidity_percent or None, # type: ignore[attr-defined]
27
+ s.battery_level_percent or None, # type: ignore[attr-defined]
28
+ s.signal_level_dbm or None, # type: ignore[attr-defined]
29
+ )
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class RemoteSensor:
34
+ """A standalone BLE remote sensor linked to an IndoorUnit."""
35
+
36
+ id: str
37
+ indoor_unit_id: str
38
+ mac: str | None
39
+ ambient_temperature_c: float | None
40
+ humidity_percent: float | None
41
+ battery_level_percent: float | None
42
+ signal_level_dbm: int | None
43
+ control_mode: RemoteSensorControlMode
44
+
45
+ @classmethod
46
+ def from_proto(cls, proto: object) -> RemoteSensor:
47
+ """Construct from a protobuf RemoteSensor message."""
48
+ at, hum, bat, sig = _parse_state(proto.state) # type: ignore[attr-defined]
49
+ return cls(
50
+ id=proto.header.object_id, # type: ignore[attr-defined]
51
+ indoor_unit_id=proto.relationships.indoor_unit_id, # type: ignore[attr-defined]
52
+ mac=proto.attributes.mac or None, # type: ignore[attr-defined]
53
+ ambient_temperature_c=at,
54
+ humidity_percent=hum,
55
+ battery_level_percent=bat,
56
+ signal_level_dbm=sig,
57
+ control_mode=RemoteSensorControlMode(proto.controls.control_mode), # type: ignore[attr-defined]
58
+ )
59
+
60
+
61
+ @dataclass(slots=True)
62
+ class ControllerRemoteSensor:
63
+ """The remote-sensor capability of a Controller (Dial).
64
+
65
+ When a Dial's remote_sensor_control_mode is ENABLED, the system creates a
66
+ ControllerRemoteSensor entity to expose its temperature/humidity readings
67
+ as a zone control input. Linked to a Controller via controller_id.
68
+ """
69
+
70
+ id: str
71
+ controller_id: str
72
+ mac: str | None
73
+ ambient_temperature_c: float | None
74
+ humidity_percent: float | None
75
+ battery_level_percent: float | None
76
+ signal_level_dbm: int | None
77
+ control_mode: RemoteSensorControlMode
78
+
79
+ @classmethod
80
+ def from_proto(cls, proto: object) -> ControllerRemoteSensor:
81
+ """Construct from a protobuf ControllerRemoteSensor message."""
82
+ at, hum, bat, sig = _parse_state(proto.state) # type: ignore[attr-defined]
83
+ return cls(
84
+ id=proto.header.object_id, # type: ignore[attr-defined]
85
+ controller_id=proto.relationships.controller_id, # type: ignore[attr-defined]
86
+ mac=proto.attributes.mac or None, # type: ignore[attr-defined]
87
+ ambient_temperature_c=at,
88
+ humidity_percent=hum,
89
+ battery_level_percent=bat,
90
+ signal_level_dbm=sig,
91
+ control_mode=RemoteSensorControlMode(proto.controls.control_mode), # type: ignore[attr-defined]
92
+ )
@@ -0,0 +1,74 @@
1
+ """Software/firmware update info model.
2
+
3
+ SoftwareUpdateInfo is returned at field 18 of HomeDatastoreSystem.
4
+ Each device (IDU, QSM, Controller, ODU) has two entries:
5
+ - one referenced by software_update_info_id (OS/app firmware)
6
+ - one referenced by firmware_update_info_id (device firmware)
7
+
8
+ When no update is pending, only updated_ts is populated; all version
9
+ fields and progress fields are empty/zero.
10
+
11
+ APK-confirmed: YL.java (proto), WL.java (attributes), OJ.java (field 18 in HDS).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from enum import IntEnum
18
+
19
+
20
+ class SoftwareUpdateState(IntEnum):
21
+ """Update state values (field 2, values still unconfirmed)."""
22
+
23
+ UNKNOWN = 0
24
+
25
+
26
+ class SoftwareUpdateStatus(IntEnum):
27
+ """Update status values (field 3, values still unconfirmed)."""
28
+
29
+ UNKNOWN = 0
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class SoftwareUpdateInfo:
34
+ """Update record for a single device firmware/software slot.
35
+
36
+ All device types carry both a ``software_update_info_id`` and a
37
+ ``firmware_update_info_id`` in their relationships; each ID
38
+ corresponds to one SoftwareUpdateInfo object in the snapshot.
39
+
40
+ When no update is pending all version strings are empty and
41
+ ``current_progress``/``total_progress`` are 0.0.
42
+ """
43
+
44
+ id: str
45
+ """Object UUID for software_update_info_id or firmware_update_info_id."""
46
+ state: int
47
+ """Raw update state integer (SoftwareUpdateState enum, TBD)."""
48
+ status: int
49
+ """Raw update status integer (SoftwareUpdateStatus enum, TBD)."""
50
+ current_version: str
51
+ """Installed version string; empty when no update is active."""
52
+ target_version: str
53
+ """Target version string; empty when no update is pending."""
54
+ current_progress: float
55
+ """Download/install progress in ``progress_unit`` units."""
56
+ total_progress: float
57
+ """Total work in ``progress_unit`` units."""
58
+ progress_unit: int
59
+ """Unit for progress values (enum TBD)."""
60
+
61
+ @classmethod
62
+ def from_proto(cls, proto: object) -> SoftwareUpdateInfo:
63
+ """Construct from a protobuf SoftwareUpdateInfo message."""
64
+ a = proto.attributes # type: ignore[attr-defined]
65
+ return cls(
66
+ id=proto.header.object_id, # type: ignore[attr-defined]
67
+ state=a.state,
68
+ status=a.status,
69
+ current_version=a.current_version or "",
70
+ target_version=a.target_version or "",
71
+ current_progress=a.current_progress,
72
+ total_progress=a.total_progress,
73
+ progress_unit=a.progress_unit,
74
+ )
@@ -0,0 +1,177 @@
1
+ """Space model — room-level HVAC zone."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, cast
7
+
8
+ from quilt_hp.models.enums import (
9
+ BoostMode,
10
+ ComfortSettingOverride,
11
+ ComfortSettingType,
12
+ HvacControllerType,
13
+ HVACMode,
14
+ HVACState,
15
+ OccupancyMode,
16
+ SafetyHeatingMode,
17
+ )
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class SpaceSettings:
22
+ """Per-space automation and safety settings."""
23
+
24
+ name: str # space display name (needed to round-trip UpdateSpace settings)
25
+ timezone: str # IANA timezone string (e.g. "America/Los_Angeles")
26
+ occupancy_mode: OccupancyMode
27
+ occupied_timeout_s: float # seconds of presence before "returned"; default 180s
28
+ unoccupied_timeout_s: float # seconds of no-presence before "away"; default 1200s
29
+ safety_heating: SafetyHeatingMode
30
+ hvac_controller_type: HvacControllerType = HvacControllerType.UNSPECIFIED
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class SpaceControls:
35
+ """Current HVAC control settings for a space."""
36
+
37
+ hvac_mode: HVACMode
38
+ temperature_setpoint_c: float
39
+ cooling_setpoint_c: float
40
+ heating_setpoint_c: float
41
+ comfort_setting_id: str
42
+ comfort_setting_override: ComfortSettingOverride
43
+ boost_mode: BoostMode = BoostMode.UNSPECIFIED
44
+
45
+ def display_setpoint_str(self, use_f: bool = False) -> str:
46
+ """Human-readable setpoint string respecting °C/°F preference."""
47
+
48
+ def fmt(val_c: float) -> str:
49
+ if use_f:
50
+ return f"{val_c * 9 / 5 + 32:.1f}°F"
51
+ return f"{val_c:.1f}°C"
52
+
53
+ mode = self.hvac_mode
54
+ if mode in (HVACMode.STANDBY, HVACMode.UNSPECIFIED, HVACMode.FAN):
55
+ return "--"
56
+ if mode == HVACMode.COOL and self.cooling_setpoint_c:
57
+ return fmt(self.cooling_setpoint_c)
58
+ if mode == HVACMode.HEAT and self.heating_setpoint_c:
59
+ return fmt(self.heating_setpoint_c)
60
+ if mode == HVACMode.AUTO and self.cooling_setpoint_c and self.heating_setpoint_c:
61
+ return f"{fmt(self.heating_setpoint_c)}–{fmt(self.cooling_setpoint_c)}"
62
+ best = self.temperature_setpoint_c or self.cooling_setpoint_c or self.heating_setpoint_c
63
+ return fmt(best) if best else "--"
64
+
65
+ @property
66
+ def display_setpoint(self) -> str:
67
+ """Human-readable setpoint string in °C.
68
+
69
+ Use display_setpoint_str(use_f) for unit-aware formatting.
70
+ """
71
+ return self.display_setpoint_str(use_f=False)
72
+
73
+
74
+ @dataclass(slots=True)
75
+ class SpaceState:
76
+ """Read-only state for a space."""
77
+
78
+ ambient_temperature_c: float | None
79
+ hvac_state: HVACState
80
+ setpoint_c: float | None
81
+ comfort_setting_id: str
82
+
83
+
84
+ @dataclass(slots=True)
85
+ class Space:
86
+ """A Quilt space (room / zone)."""
87
+
88
+ id: str
89
+ system_id: str
90
+ name: str
91
+ parent_space_id: str | None
92
+ settings: SpaceSettings
93
+ controls: SpaceControls
94
+ state: SpaceState
95
+ # Resolved from controls.comfort_setting_id at snapshot build time.
96
+ # None if the space was received via a stream update without enrichment.
97
+ active_comfort_setting_type: ComfortSettingType | None = field(default=None)
98
+
99
+ @property
100
+ def is_room(self) -> bool:
101
+ """True if this is a leaf space (room), not the root home space."""
102
+ return self.parent_space_id is not None and self.parent_space_id != ""
103
+
104
+ @property
105
+ def is_off(self) -> bool:
106
+ """True when the user has explicitly set this space to STANDBY.
107
+
108
+ The room will stay off regardless of occupancy or schedule.
109
+ """
110
+ return self.controls.hvac_mode == HVACMode.STANDBY and not self.is_away
111
+
112
+ @property
113
+ def is_away(self) -> bool:
114
+ """True when occupancy automation has temporarily suppressed an active mode.
115
+
116
+ Determined by active comfort setting type AWAY. The room
117
+ will re-activate automatically when someone enters.
118
+
119
+ Falls back to comparing controls vs state hvac values when no comfort
120
+ setting type is available (e.g. raw stream updates).
121
+ """
122
+ if self.active_comfort_setting_type is not None:
123
+ return self.active_comfort_setting_type == ComfortSettingType.AWAY
124
+ # Fallback: occupancy override shows STANDBY state while controls
125
+ # hold an active mode.
126
+ return self.state.hvac_state == HVACState.STANDBY and self.controls.hvac_mode not in (
127
+ HVACMode.STANDBY,
128
+ HVACMode.UNSPECIFIED,
129
+ )
130
+
131
+ @property
132
+ def ambient_temperature_f(self) -> float | None:
133
+ """Ambient temperature in °F, or None if unavailable."""
134
+ if self.state.ambient_temperature_c is None:
135
+ return None
136
+ return self.state.ambient_temperature_c * 9 / 5 + 32
137
+
138
+ @classmethod
139
+ def from_proto(cls, proto: object) -> Space:
140
+ """Construct a Space from a protobuf Space message."""
141
+ return _space_from_proto(proto)
142
+
143
+
144
+ def _space_from_proto(proto: object) -> Space:
145
+ """Internal: convert a proto Space to our model."""
146
+ p = cast("Any", proto)
147
+ sg = p.settings
148
+ return Space(
149
+ id=p.header.object_id,
150
+ system_id=p.header.system_id,
151
+ name=sg.name,
152
+ parent_space_id=p.relationships.parent_space_id or None,
153
+ settings=SpaceSettings(
154
+ name=sg.name,
155
+ timezone=sg.timezone,
156
+ occupancy_mode=OccupancyMode(sg.occupancy),
157
+ occupied_timeout_s=sg.occupied_timeout_s,
158
+ unoccupied_timeout_s=sg.unoccupied_timeout_s,
159
+ safety_heating=SafetyHeatingMode(sg.safety_heating),
160
+ hvac_controller_type=HvacControllerType(sg.hvac_controller_type),
161
+ ),
162
+ controls=SpaceControls(
163
+ hvac_mode=HVACMode(p.controls.hvac_mode),
164
+ temperature_setpoint_c=p.controls.temperature_setpoint_c,
165
+ cooling_setpoint_c=p.controls.cooling_temperature_setpoint_c,
166
+ heating_setpoint_c=p.controls.heating_temperature_setpoint_c,
167
+ comfort_setting_id=p.controls.comfort_setting_id_string,
168
+ comfort_setting_override=ComfortSettingOverride(p.controls.comfort_setting_override),
169
+ boost_mode=BoostMode(p.controls.boost_mode),
170
+ ),
171
+ state=SpaceState(
172
+ ambient_temperature_c=p.state.ambient_temperature_c if p.state.updated_ts else None,
173
+ hvac_state=HVACState(p.state.hvac_state),
174
+ setpoint_c=p.state.setpoint_temperature_c if p.state.updated_ts else None,
175
+ comfort_setting_id=p.state.comfort_setting_id,
176
+ ),
177
+ )