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.
- quilt_hp/__init__.py +22 -0
- quilt_hp/_paths.py +26 -0
- quilt_hp/_proto/__init__.py +0 -0
- quilt_hp/_proto/quilt_device_pairing_pb2.py +56 -0
- quilt_hp/_proto/quilt_device_pairing_pb2.pyi +317 -0
- quilt_hp/_proto/quilt_device_pairing_pb2_grpc.py +24 -0
- quilt_hp/_proto/quilt_hds_pb2.py +292 -0
- quilt_hp/_proto/quilt_hds_pb2.pyi +3947 -0
- quilt_hp/_proto/quilt_hds_pb2_grpc.py +1732 -0
- quilt_hp/_proto/quilt_notifier_pb2.py +55 -0
- quilt_hp/_proto/quilt_notifier_pb2.pyi +258 -0
- quilt_hp/_proto/quilt_notifier_pb2_grpc.py +97 -0
- quilt_hp/_proto/quilt_services_pb2.py +171 -0
- quilt_hp/_proto/quilt_services_pb2.pyi +1320 -0
- quilt_hp/_proto/quilt_services_pb2_grpc.py +1188 -0
- quilt_hp/_proto/quilt_system_pb2.py +53 -0
- quilt_hp/_proto/quilt_system_pb2.pyi +164 -0
- quilt_hp/_proto/quilt_system_pb2_grpc.py +270 -0
- quilt_hp/auth.py +244 -0
- quilt_hp/cli/__init__.py +1 -0
- quilt_hp/cli/main.py +770 -0
- quilt_hp/cli/settings.py +123 -0
- quilt_hp/cli/store.py +105 -0
- quilt_hp/cli/tui.py +2677 -0
- quilt_hp/client.py +616 -0
- quilt_hp/const.py +57 -0
- quilt_hp/exceptions.py +23 -0
- quilt_hp/models/__init__.py +85 -0
- quilt_hp/models/comfort.py +47 -0
- quilt_hp/models/controller.py +135 -0
- quilt_hp/models/energy.py +31 -0
- quilt_hp/models/enums.py +298 -0
- quilt_hp/models/indoor_unit.py +412 -0
- quilt_hp/models/outdoor_unit.py +71 -0
- quilt_hp/models/qsm.py +105 -0
- quilt_hp/models/schedule.py +98 -0
- quilt_hp/models/sensor.py +92 -0
- quilt_hp/models/software_update.py +74 -0
- quilt_hp/models/space.py +177 -0
- quilt_hp/models/system.py +451 -0
- quilt_hp/py.typed +1 -0
- quilt_hp/services/__init__.py +1 -0
- quilt_hp/services/hds.py +480 -0
- quilt_hp/services/streaming.py +561 -0
- quilt_hp/services/system.py +95 -0
- quilt_hp/services/user.py +143 -0
- quilt_hp/tokens.py +119 -0
- quilt_hp/transport.py +192 -0
- quilt_hp_python-0.1.1.dist-info/METADATA +172 -0
- quilt_hp_python-0.1.1.dist-info/RECORD +53 -0
- quilt_hp_python-0.1.1.dist-info/WHEEL +4 -0
- quilt_hp_python-0.1.1.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|
quilt_hp/models/space.py
ADDED
|
@@ -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
|
+
)
|