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,85 @@
|
|
|
1
|
+
"""Pythonic data models wrapping protobuf messages."""
|
|
2
|
+
|
|
3
|
+
from quilt_hp.models.comfort import ComfortSetting
|
|
4
|
+
from quilt_hp.models.controller import Controller
|
|
5
|
+
from quilt_hp.models.energy import EnergyBucket, SpaceEnergyMetrics
|
|
6
|
+
from quilt_hp.models.enums import (
|
|
7
|
+
BoostMode,
|
|
8
|
+
ComfortSettingOverride,
|
|
9
|
+
ComfortSettingType,
|
|
10
|
+
ConditionState,
|
|
11
|
+
FallbackControlCommand,
|
|
12
|
+
FanSpeed,
|
|
13
|
+
HvacControllerType,
|
|
14
|
+
HVACMode,
|
|
15
|
+
HVACState,
|
|
16
|
+
LedAnimation,
|
|
17
|
+
LightPreset,
|
|
18
|
+
LouverAngle,
|
|
19
|
+
LouverMode,
|
|
20
|
+
OccupancyMode,
|
|
21
|
+
OccupancyState,
|
|
22
|
+
RemoteSensorControlMode,
|
|
23
|
+
SafetyHeatingMode,
|
|
24
|
+
)
|
|
25
|
+
from quilt_hp.models.indoor_unit import (
|
|
26
|
+
IndoorUnit,
|
|
27
|
+
IndoorUnitCommands,
|
|
28
|
+
IndoorUnitControls,
|
|
29
|
+
IndoorUnitSettings,
|
|
30
|
+
IndoorUnitState,
|
|
31
|
+
)
|
|
32
|
+
from quilt_hp.models.outdoor_unit import OutdoorUnit
|
|
33
|
+
from quilt_hp.models.schedule import ScheduleDay, ScheduleEvent, ScheduleWeek
|
|
34
|
+
from quilt_hp.models.sensor import ControllerRemoteSensor, RemoteSensor
|
|
35
|
+
from quilt_hp.models.software_update import SoftwareUpdateInfo
|
|
36
|
+
from quilt_hp.models.space import (
|
|
37
|
+
Space,
|
|
38
|
+
SpaceControls,
|
|
39
|
+
SpaceSettings,
|
|
40
|
+
SpaceState,
|
|
41
|
+
)
|
|
42
|
+
from quilt_hp.models.system import Location, SystemInfo, SystemSnapshot
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"BoostMode",
|
|
46
|
+
"ComfortSetting",
|
|
47
|
+
"ComfortSettingOverride",
|
|
48
|
+
"ComfortSettingType",
|
|
49
|
+
"ConditionState",
|
|
50
|
+
"Controller",
|
|
51
|
+
"ControllerRemoteSensor",
|
|
52
|
+
"EnergyBucket",
|
|
53
|
+
"FallbackControlCommand",
|
|
54
|
+
"FanSpeed",
|
|
55
|
+
"HVACMode",
|
|
56
|
+
"HVACState",
|
|
57
|
+
"HvacControllerType",
|
|
58
|
+
"IndoorUnit",
|
|
59
|
+
"IndoorUnitCommands",
|
|
60
|
+
"IndoorUnitControls",
|
|
61
|
+
"IndoorUnitSettings",
|
|
62
|
+
"IndoorUnitState",
|
|
63
|
+
"LedAnimation",
|
|
64
|
+
"LightPreset",
|
|
65
|
+
"Location",
|
|
66
|
+
"LouverAngle",
|
|
67
|
+
"LouverMode",
|
|
68
|
+
"OccupancyMode",
|
|
69
|
+
"OccupancyState",
|
|
70
|
+
"OutdoorUnit",
|
|
71
|
+
"RemoteSensor",
|
|
72
|
+
"RemoteSensorControlMode",
|
|
73
|
+
"SafetyHeatingMode",
|
|
74
|
+
"ScheduleDay",
|
|
75
|
+
"ScheduleEvent",
|
|
76
|
+
"ScheduleWeek",
|
|
77
|
+
"SoftwareUpdateInfo",
|
|
78
|
+
"Space",
|
|
79
|
+
"SpaceControls",
|
|
80
|
+
"SpaceEnergyMetrics",
|
|
81
|
+
"SpaceSettings",
|
|
82
|
+
"SpaceState",
|
|
83
|
+
"SystemInfo",
|
|
84
|
+
"SystemSnapshot",
|
|
85
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Comfort setting (named preset) model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from quilt_hp.models.enums import (
|
|
8
|
+
ComfortSettingType,
|
|
9
|
+
FanSpeed,
|
|
10
|
+
HVACMode,
|
|
11
|
+
LouverMode,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class ComfortSetting:
|
|
17
|
+
"""A named comfort preset (Active, Sleep, Away, etc.)."""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
system_id: str
|
|
21
|
+
space_id: str
|
|
22
|
+
name: str
|
|
23
|
+
type: ComfortSettingType
|
|
24
|
+
hvac_mode: HVACMode
|
|
25
|
+
heating_setpoint_c: float
|
|
26
|
+
cooling_setpoint_c: float
|
|
27
|
+
fan_speed: FanSpeed
|
|
28
|
+
louver_mode: LouverMode = LouverMode.UNSPECIFIED
|
|
29
|
+
louver_fixed_position: float = 0.0 # degrees, used when louver_mode=FIXED
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_proto(cls, proto: object) -> ComfortSetting:
|
|
33
|
+
"""Construct from a protobuf ComfortSetting message."""
|
|
34
|
+
a = proto.attributes # type: ignore[attr-defined]
|
|
35
|
+
return cls(
|
|
36
|
+
id=proto.header.object_id, # type: ignore[attr-defined]
|
|
37
|
+
system_id=proto.header.system_id, # type: ignore[attr-defined]
|
|
38
|
+
space_id=proto.relationships.space_id, # type: ignore[attr-defined]
|
|
39
|
+
name=a.name,
|
|
40
|
+
type=ComfortSettingType(a.type),
|
|
41
|
+
hvac_mode=HVACMode(a.hvac_mode),
|
|
42
|
+
heating_setpoint_c=a.heating_temperature_setpoint_c,
|
|
43
|
+
cooling_setpoint_c=a.cooling_temperature_setpoint_c,
|
|
44
|
+
fan_speed=FanSpeed.from_wire(a.fan_speed_mode, a.fan_speed_percent),
|
|
45
|
+
louver_mode=LouverMode(a.louver_mode) if a.louver_mode else LouverMode.UNSPECIFIED,
|
|
46
|
+
louver_fixed_position=a.louver_fixed_position,
|
|
47
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Controller (Quilt Dial thermostat) model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from quilt_hp.models.enums import RemoteSensorControlMode
|
|
10
|
+
from quilt_hp.models.qsm import WifiInfo
|
|
11
|
+
|
|
12
|
+
_ONLINE_THRESHOLD_S = 5 * 60 # 5 minutes, matching KMP IS_ONLINE_THRESHOLD_MINUTES
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class Controller:
|
|
17
|
+
"""A Quilt controller (Dial thermostat)."""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
system_id: str
|
|
21
|
+
space_id: str
|
|
22
|
+
name: str
|
|
23
|
+
raw_thermistor_c: float # ambient_temperature_c from raw Dial thermistor
|
|
24
|
+
pcb_temperature_a_c: float # temperature_f3 — PCB temp A (~30–50°C)
|
|
25
|
+
pcb_temperature_b_c: float # temperature_f4 — PCB temp B (hotter component, ~45–52°C)
|
|
26
|
+
calibrated_ambient_c: float # temperature_f5 — calibrated ext ambient sent to IDU
|
|
27
|
+
wifi_ssid: str | None
|
|
28
|
+
wifi_ip: str | None
|
|
29
|
+
wifi_signal_dbm: int | None
|
|
30
|
+
wifi_freq_mhz: int | None = None # e.g. 5745 → 5 GHz; 2437 → 2.4 GHz
|
|
31
|
+
wifi_last_seen: datetime | None = (
|
|
32
|
+
None # WifiState.updated_ts — when the dial last checked in over WiFi
|
|
33
|
+
)
|
|
34
|
+
ap_wifi: WifiInfo | None = None # AP-mode interface (device provisioning)
|
|
35
|
+
p2p_wifi: WifiInfo | None = None # peer-to-peer / Wi-Fi Direct
|
|
36
|
+
remote_sensor_mode: RemoteSensorControlMode = RemoteSensorControlMode.UNSPECIFIED
|
|
37
|
+
software_update_info_id: str | None = None
|
|
38
|
+
firmware_update_info_id: str | None = None
|
|
39
|
+
serial_number: str | None = None # ControllerHardware.attributes.serial_number
|
|
40
|
+
model_sku: str | None = None # ControllerHardware.attributes.model_sku
|
|
41
|
+
firmware_version: str | None = None # ControllerHardware.attributes.firmware_version
|
|
42
|
+
state_updated_at: datetime | None = None # ControllerState.updated_ts (field 1)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def ambient_temperature_c(self) -> float:
|
|
46
|
+
"""Calibrated ambient temperature used for system control.
|
|
47
|
+
|
|
48
|
+
Use this for display and logic. See also ``raw_thermistor_c`` for the
|
|
49
|
+
uncorrected on-chip reading (biased high by self-heating).
|
|
50
|
+
"""
|
|
51
|
+
return self.calibrated_ambient_c
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def wifi_band(self) -> str | None:
|
|
55
|
+
"""'5 GHz' or '2.4 GHz' based on frequency, or None if unknown."""
|
|
56
|
+
if self.wifi_freq_mhz is None:
|
|
57
|
+
return None
|
|
58
|
+
return "5 GHz" if self.wifi_freq_mhz > 5000 else "2.4 GHz"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_online(self) -> bool:
|
|
62
|
+
"""True if the controller is known to be online.
|
|
63
|
+
|
|
64
|
+
Uses ``ControllerState.updated_ts`` if available, with a 5-minute
|
|
65
|
+
threshold matching KMP ``IS_ONLINE_THRESHOLD_MINUTES = 5``.
|
|
66
|
+
|
|
67
|
+
The server does not currently send ``ControllerState.updated_ts``
|
|
68
|
+
(confirmed from wire captures — field 1 always absent). When no
|
|
69
|
+
timestamp is available we assume the controller is online; we only
|
|
70
|
+
report offline when we have positive evidence of a stale timestamp.
|
|
71
|
+
"""
|
|
72
|
+
if self.state_updated_at is None:
|
|
73
|
+
return True # no timestamp → unknown → assume online (fail-open)
|
|
74
|
+
age = (datetime.now(tz=UTC) - self.state_updated_at).total_seconds()
|
|
75
|
+
return age < _ONLINE_THRESHOLD_S
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_proto(cls, proto: object, hw_map: dict[str, object] | None = None) -> Controller:
|
|
79
|
+
"""Construct from a protobuf Controller message.
|
|
80
|
+
|
|
81
|
+
``hw_map`` maps hardware_id → ControllerHardware proto, built once from
|
|
82
|
+
``HomeDatastoreSystem.controller_hardware`` and passed in at snapshot
|
|
83
|
+
load time. Stream diffs won't have it; fields default to None.
|
|
84
|
+
"""
|
|
85
|
+
p = cast("Any", proto)
|
|
86
|
+
w = p.hosted_wifi_state
|
|
87
|
+
ts = p.state.updated_ts
|
|
88
|
+
updated_at: datetime | None = None
|
|
89
|
+
if ts.seconds != 0:
|
|
90
|
+
updated_at = datetime.fromtimestamp(ts.seconds, tz=UTC)
|
|
91
|
+
|
|
92
|
+
wifi_last_seen: datetime | None = None
|
|
93
|
+
if w.updated_ts.seconds != 0:
|
|
94
|
+
wifi_last_seen = datetime.fromtimestamp(w.updated_ts.seconds, tz=UTC)
|
|
95
|
+
|
|
96
|
+
def _wifi(wstate: object) -> WifiInfo | None:
|
|
97
|
+
info = WifiInfo.from_proto(wstate)
|
|
98
|
+
return info if info.connected else None
|
|
99
|
+
|
|
100
|
+
serial: str | None = None
|
|
101
|
+
model_sku: str | None = None
|
|
102
|
+
fw_ver: str | None = None
|
|
103
|
+
if hw_map:
|
|
104
|
+
hw_id = p.relationships.hardware_id
|
|
105
|
+
hw = hw_map.get(hw_id)
|
|
106
|
+
if hw is not None:
|
|
107
|
+
a = cast("Any", hw).attributes
|
|
108
|
+
serial = a.serial_number or None
|
|
109
|
+
model_sku = a.model_sku or None
|
|
110
|
+
fw_ver = a.firmware_version or None
|
|
111
|
+
|
|
112
|
+
return cls(
|
|
113
|
+
id=p.header.object_id,
|
|
114
|
+
system_id=p.header.system_id,
|
|
115
|
+
space_id=p.relationships.space_id,
|
|
116
|
+
name=p.settings.name,
|
|
117
|
+
raw_thermistor_c=p.state.ambient_temperature_c,
|
|
118
|
+
pcb_temperature_a_c=p.state.temperature_f3,
|
|
119
|
+
pcb_temperature_b_c=p.state.temperature_f4,
|
|
120
|
+
calibrated_ambient_c=p.state.temperature_f5,
|
|
121
|
+
wifi_ssid=w.ssid or None,
|
|
122
|
+
wifi_ip=w.ipv4_address or None,
|
|
123
|
+
wifi_signal_dbm=w.signal_level_dbm or None,
|
|
124
|
+
wifi_freq_mhz=w.frequency_mhz or None,
|
|
125
|
+
wifi_last_seen=wifi_last_seen,
|
|
126
|
+
ap_wifi=_wifi(p.ap_wifi_state),
|
|
127
|
+
p2p_wifi=_wifi(p.p2p_wifi_state),
|
|
128
|
+
remote_sensor_mode=RemoteSensorControlMode(p.controls.remote_sensor_control_mode),
|
|
129
|
+
software_update_info_id=p.relationships.software_update_info_id or None,
|
|
130
|
+
firmware_update_info_id=p.relationships.firmware_update_info_id or None,
|
|
131
|
+
serial_number=serial,
|
|
132
|
+
model_sku=model_sku,
|
|
133
|
+
firmware_version=fw_ver,
|
|
134
|
+
state_updated_at=updated_at,
|
|
135
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Energy metrics models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class EnergyBucket:
|
|
14
|
+
"""One hourly energy measurement slot."""
|
|
15
|
+
|
|
16
|
+
start_time: datetime
|
|
17
|
+
energy_kwh: float
|
|
18
|
+
status: int # 0=UNSPECIFIED, 1=COMPLETE, 2=INCOMPLETE
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class SpaceEnergyMetrics:
|
|
23
|
+
"""Hourly energy buckets for one space over a time range."""
|
|
24
|
+
|
|
25
|
+
space_id: str
|
|
26
|
+
buckets: list[EnergyBucket]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def total_kwh(self) -> float:
|
|
30
|
+
"""Sum of all bucket energy values."""
|
|
31
|
+
return sum(b.energy_kwh for b in self.buckets)
|
quilt_hp/models/enums.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Pythonic enums mirroring the Quilt protobuf enum values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HVACMode(IntEnum):
|
|
9
|
+
"""HVAC operating mode."""
|
|
10
|
+
|
|
11
|
+
UNSPECIFIED = 0
|
|
12
|
+
STANDBY = 1
|
|
13
|
+
COOL = 2
|
|
14
|
+
HEAT = 3
|
|
15
|
+
AUTO = 4
|
|
16
|
+
FAN = 5
|
|
17
|
+
FALLBACK_AUTO = 6
|
|
18
|
+
FALLBACK_OFF = 7
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return self.name
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HVACState(IntEnum):
|
|
25
|
+
"""Current HVAC state (what the system is actually doing)."""
|
|
26
|
+
|
|
27
|
+
UNSPECIFIED = 0
|
|
28
|
+
STANDBY = 1
|
|
29
|
+
COOL = 2
|
|
30
|
+
HEAT = 3
|
|
31
|
+
DRIFT = 4
|
|
32
|
+
FAN = 5
|
|
33
|
+
COOL_DEFERRED = 6
|
|
34
|
+
HEAT_DEFERRED = 7
|
|
35
|
+
FAN_DEFERRED = 8
|
|
36
|
+
COOL_PREPARING = 9
|
|
37
|
+
HEAT_PREPARING = 10
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
return self.name
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FanSpeed(IntEnum):
|
|
44
|
+
"""Discrete fan speed labels (maps to mode + percent on the wire)."""
|
|
45
|
+
|
|
46
|
+
AUTO = 0
|
|
47
|
+
QUIET = 1
|
|
48
|
+
LOW = 2
|
|
49
|
+
MEDIUM = 3
|
|
50
|
+
HIGH = 4
|
|
51
|
+
BLAST = 5
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
return self.name
|
|
55
|
+
|
|
56
|
+
def to_wire(self) -> tuple[int, float]:
|
|
57
|
+
"""Return (fan_speed_mode, fan_speed_percent) for the wire protocol."""
|
|
58
|
+
_MAP: dict[FanSpeed, tuple[int, float]] = {
|
|
59
|
+
FanSpeed.AUTO: (1, 0.0), # FAN_SPEED_MODE_AUTO
|
|
60
|
+
FanSpeed.QUIET: (2, 0.20), # FAN_SPEED_MODE_SETPOINT
|
|
61
|
+
FanSpeed.LOW: (2, 0.40),
|
|
62
|
+
FanSpeed.MEDIUM: (2, 0.60),
|
|
63
|
+
FanSpeed.HIGH: (2, 0.80),
|
|
64
|
+
FanSpeed.BLAST: (2, 1.00),
|
|
65
|
+
}
|
|
66
|
+
return _MAP[self]
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_wire(cls, mode: int, percent: float) -> FanSpeed:
|
|
70
|
+
"""Decode wire fan_speed_mode/fan_speed_percent to FanSpeed label."""
|
|
71
|
+
if mode != 2: # FAN_SPEED_MODE_SETPOINT
|
|
72
|
+
return cls.AUTO
|
|
73
|
+
if percent <= 0.21:
|
|
74
|
+
return cls.QUIET
|
|
75
|
+
if percent <= 0.41:
|
|
76
|
+
return cls.LOW
|
|
77
|
+
if percent <= 0.61:
|
|
78
|
+
return cls.MEDIUM
|
|
79
|
+
if percent <= 0.81:
|
|
80
|
+
return cls.HIGH
|
|
81
|
+
return cls.BLAST
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class LouverMode(IntEnum):
|
|
85
|
+
"""Indoor unit louver mode."""
|
|
86
|
+
|
|
87
|
+
UNSPECIFIED = 0
|
|
88
|
+
CLOSED = 1
|
|
89
|
+
SWEEP = 2
|
|
90
|
+
FIXED = 3
|
|
91
|
+
AUTO = 4
|
|
92
|
+
|
|
93
|
+
def __str__(self) -> str:
|
|
94
|
+
return self.name
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LouverAngle(IntEnum):
|
|
98
|
+
"""Discrete louver angle positions (when mode=FIXED)."""
|
|
99
|
+
|
|
100
|
+
ANGLE1 = 1
|
|
101
|
+
ANGLE2 = 2
|
|
102
|
+
ANGLE3 = 3
|
|
103
|
+
ANGLE4 = 4
|
|
104
|
+
ANGLE5 = 5
|
|
105
|
+
|
|
106
|
+
def to_wire(self) -> float:
|
|
107
|
+
"""Return the louver_fixed_position float for the wire."""
|
|
108
|
+
return {1: 0.20, 2: 0.40, 3: 0.60, 4: 0.80, 5: 1.00}[self.value]
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_wire(cls, position: float) -> LouverAngle:
|
|
112
|
+
"""Decode a wire louver_fixed_position to a LouverAngle."""
|
|
113
|
+
if position <= 0.21:
|
|
114
|
+
return cls.ANGLE1
|
|
115
|
+
if position <= 0.41:
|
|
116
|
+
return cls.ANGLE2
|
|
117
|
+
if position <= 0.61:
|
|
118
|
+
return cls.ANGLE3
|
|
119
|
+
if position <= 0.81:
|
|
120
|
+
return cls.ANGLE4
|
|
121
|
+
return cls.ANGLE5
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class LightPreset(IntEnum):
|
|
125
|
+
"""Built-in LED color presets (RGBW packed int32)."""
|
|
126
|
+
|
|
127
|
+
DAYLIGHT = 0x000000FF
|
|
128
|
+
WARM = 0xFF460064
|
|
129
|
+
SUNSET = 0xFF460024
|
|
130
|
+
SKY = 0x009CFF54
|
|
131
|
+
|
|
132
|
+
def __str__(self) -> str:
|
|
133
|
+
return self.name
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class LedAnimation(IntEnum):
|
|
137
|
+
"""Indoor unit LED animation modes.
|
|
138
|
+
|
|
139
|
+
Note: NONE = 1 (not 0). UNSPECIFIED = 0 means the field was absent from
|
|
140
|
+
the wire diff; NONE means the server explicitly set "no animation" (solid
|
|
141
|
+
colour). Callers should treat UNSPECIFIED the same as NONE for display.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
UNSPECIFIED = 0
|
|
145
|
+
NONE = 1
|
|
146
|
+
SPARKLE_FADE = 2
|
|
147
|
+
TWINKLE_FADE = 3
|
|
148
|
+
DANCE = 4
|
|
149
|
+
CHASE = 5
|
|
150
|
+
|
|
151
|
+
def __str__(self) -> str:
|
|
152
|
+
return self.name
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ComfortSettingType(IntEnum):
|
|
156
|
+
"""Type of comfort preset."""
|
|
157
|
+
|
|
158
|
+
UNSPECIFIED = 0
|
|
159
|
+
ACTIVE = 1
|
|
160
|
+
SLEEP = 2
|
|
161
|
+
AWAY = 3
|
|
162
|
+
STANDBY = 4
|
|
163
|
+
CUSTOM = 5
|
|
164
|
+
|
|
165
|
+
def __str__(self) -> str:
|
|
166
|
+
return self.name
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class OccupancyMode(IntEnum):
|
|
170
|
+
"""Space-level auto-away/return occupancy mode."""
|
|
171
|
+
|
|
172
|
+
UNSPECIFIED = 0
|
|
173
|
+
DISABLED = 1
|
|
174
|
+
ENABLED = 2
|
|
175
|
+
|
|
176
|
+
def __str__(self) -> str:
|
|
177
|
+
return self.name
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SafetyHeatingMode(IntEnum):
|
|
181
|
+
"""Freeze protection mode for a space."""
|
|
182
|
+
|
|
183
|
+
UNSPECIFIED = 0 # treated as ENABLED by the device
|
|
184
|
+
DISABLED = 1
|
|
185
|
+
ENABLED = 2
|
|
186
|
+
|
|
187
|
+
def __str__(self) -> str:
|
|
188
|
+
return self.name
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class OccupancyState(IntEnum):
|
|
192
|
+
"""Occupancy detection state."""
|
|
193
|
+
|
|
194
|
+
UNSPECIFIED = 0
|
|
195
|
+
UNDETECTED = 1
|
|
196
|
+
DETECTED = 2
|
|
197
|
+
|
|
198
|
+
def __str__(self) -> str:
|
|
199
|
+
return self.name
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class Presence(IntEnum):
|
|
203
|
+
"""Raw radar sensor presence detection."""
|
|
204
|
+
|
|
205
|
+
UNSPECIFIED = 0
|
|
206
|
+
UNDETECTED = 1
|
|
207
|
+
DETECTED = 2
|
|
208
|
+
|
|
209
|
+
def __str__(self) -> str:
|
|
210
|
+
return self.name
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class LightState(IntEnum):
|
|
214
|
+
"""Explicit LED on/off state (field 13 of IndoorUnitControls).
|
|
215
|
+
|
|
216
|
+
Sent when the ``mobile_led_scheduling_enabled`` Statsig gate is on.
|
|
217
|
+
When state=OFF, brightness is **preserved** server-side (not zeroed) so
|
|
218
|
+
toggling back on restores the prior brightness. When UNSPECIFIED, fall
|
|
219
|
+
back to brightness-based detection.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
UNSPECIFIED = 0
|
|
223
|
+
ON = 1
|
|
224
|
+
OFF = 2
|
|
225
|
+
|
|
226
|
+
def __str__(self) -> str:
|
|
227
|
+
return self.name
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class ConditionState(IntEnum):
|
|
231
|
+
"""Fault condition state."""
|
|
232
|
+
|
|
233
|
+
UNSPECIFIED = 0
|
|
234
|
+
INACTIVE = 1
|
|
235
|
+
ACTIVE = 2
|
|
236
|
+
|
|
237
|
+
def __str__(self) -> str:
|
|
238
|
+
return self.name
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class HvacControllerType(IntEnum):
|
|
242
|
+
"""Algorithm variant used by the controller to drive the IDU."""
|
|
243
|
+
|
|
244
|
+
UNSPECIFIED = 0
|
|
245
|
+
PASS_THROUGH_TEMPERATURE = 1
|
|
246
|
+
INTEGRAL_TEMPERATURE_V1 = 2
|
|
247
|
+
INTEGRAL_TEMPERATURE_V2 = 3
|
|
248
|
+
|
|
249
|
+
def __str__(self) -> str:
|
|
250
|
+
return self.name
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class BoostMode(IntEnum):
|
|
254
|
+
"""Boost (turbo) mode override for a space."""
|
|
255
|
+
|
|
256
|
+
UNSPECIFIED = 0
|
|
257
|
+
OFF = 1
|
|
258
|
+
ON = 2
|
|
259
|
+
|
|
260
|
+
def __str__(self) -> str:
|
|
261
|
+
return self.name
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ComfortSettingOverride(IntEnum):
|
|
265
|
+
"""Why the active comfort setting differs from the schedule."""
|
|
266
|
+
|
|
267
|
+
UNSPECIFIED = 0
|
|
268
|
+
NONE = 1
|
|
269
|
+
UNTIL_NEXT_SCHEDULE = 2
|
|
270
|
+
INDEFINITE = 3
|
|
271
|
+
SCHEDULE = 4
|
|
272
|
+
UNOCCUPIED = 5
|
|
273
|
+
OCCUPIED = 6
|
|
274
|
+
|
|
275
|
+
def __str__(self) -> str:
|
|
276
|
+
return self.name
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class FallbackControlCommand(IntEnum):
|
|
280
|
+
"""Command sent when the Dial loses cloud connectivity."""
|
|
281
|
+
|
|
282
|
+
UNSPECIFIED = 0
|
|
283
|
+
COMPLETE = 1
|
|
284
|
+
EXIT = 2
|
|
285
|
+
|
|
286
|
+
def __str__(self) -> str:
|
|
287
|
+
return self.name
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class RemoteSensorControlMode(IntEnum):
|
|
291
|
+
"""Whether the Dial acts as the zone temperature sensor."""
|
|
292
|
+
|
|
293
|
+
UNSPECIFIED = 0
|
|
294
|
+
DISABLED = 1
|
|
295
|
+
ENABLED = 2
|
|
296
|
+
|
|
297
|
+
def __str__(self) -> str:
|
|
298
|
+
return self.name
|