steamloop 1.0.0__tar.gz → 1.2.0__tar.gz

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 (40) hide show
  1. steamloop-1.2.0/PKG-INFO +229 -0
  2. steamloop-1.2.0/README.md +205 -0
  3. {steamloop-1.0.0 → steamloop-1.2.0}/pyproject.toml +85 -26
  4. steamloop-1.2.0/src/steamloop/__init__.py +31 -0
  5. steamloop-1.2.0/src/steamloop/__main__.py +5 -0
  6. steamloop-1.2.0/src/steamloop/certs.py +189 -0
  7. steamloop-1.2.0/src/steamloop/cli.py +322 -0
  8. steamloop-1.2.0/src/steamloop/connection.py +774 -0
  9. steamloop-1.2.0/src/steamloop/const.py +49 -0
  10. steamloop-1.2.0/src/steamloop/exceptions.py +21 -0
  11. steamloop-1.2.0/src/steamloop/models.py +149 -0
  12. steamloop-1.2.0/src/steamloop.egg-info/PKG-INFO +229 -0
  13. {steamloop-1.0.0 → steamloop-1.2.0}/src/steamloop.egg-info/SOURCES.txt +12 -2
  14. steamloop-1.2.0/src/steamloop.egg-info/entry_points.txt +2 -0
  15. steamloop-1.2.0/src/steamloop.egg-info/requires.txt +1 -0
  16. steamloop-1.2.0/tests/test_certs.py +35 -0
  17. steamloop-1.2.0/tests/test_cli.py +658 -0
  18. steamloop-1.2.0/tests/test_connection.py +995 -0
  19. steamloop-1.2.0/tests/test_const.py +48 -0
  20. {steamloop-1.0.0 → steamloop-1.2.0}/tests/test_dunder_main.py +6 -3
  21. steamloop-1.2.0/tests/test_exceptions.py +30 -0
  22. steamloop-1.2.0/tests/test_models.py +47 -0
  23. steamloop-1.2.0/tests/test_pairing.py +55 -0
  24. steamloop-1.2.0/tests/test_protocol.py +132 -0
  25. steamloop-1.0.0/PKG-INFO +0 -96
  26. steamloop-1.0.0/README.md +0 -69
  27. steamloop-1.0.0/src/steamloop/__init__.py +0 -1
  28. steamloop-1.0.0/src/steamloop/__main__.py +0 -5
  29. steamloop-1.0.0/src/steamloop/cli.py +0 -12
  30. steamloop-1.0.0/src/steamloop/main.py +0 -11
  31. steamloop-1.0.0/src/steamloop.egg-info/PKG-INFO +0 -96
  32. steamloop-1.0.0/src/steamloop.egg-info/entry_points.txt +0 -2
  33. steamloop-1.0.0/src/steamloop.egg-info/requires.txt +0 -2
  34. steamloop-1.0.0/tests/test_cli.py +0 -12
  35. steamloop-1.0.0/tests/test_main.py +0 -6
  36. {steamloop-1.0.0 → steamloop-1.2.0}/LICENSE +0 -0
  37. {steamloop-1.0.0 → steamloop-1.2.0}/setup.cfg +0 -0
  38. {steamloop-1.0.0 → steamloop-1.2.0}/src/steamloop/py.typed +0 -0
  39. {steamloop-1.0.0 → steamloop-1.2.0}/src/steamloop.egg-info/dependency_links.txt +0 -0
  40. {steamloop-1.0.0 → steamloop-1.2.0}/src/steamloop.egg-info/top_level.txt +0 -0
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: steamloop
3
+ Version: 1.2.0
4
+ Summary: Local control for choochoo based thermostats
5
+ Author-email: "J. Nick Koston" <nick@koston.org>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Bug Tracker, https://github.com/hvaclibs/steamloop/issues
8
+ Project-URL: Changelog, https://github.com/hvaclibs/steamloop/blob/main/CHANGELOG.md
9
+ Project-URL: documentation, https://steamloop.readthedocs.io
10
+ Project-URL: repository, https://github.com/hvaclibs/steamloop
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: orjson>=3.10
23
+ Dynamic: license-file
24
+
25
+ # steamloop
26
+
27
+ <p align="center">
28
+ <a href="https://github.com/hvaclibs/steamloop/actions/workflows/ci.yml?query=branch%3Amain">
29
+ <img src="https://img.shields.io/github/actions/workflow/status/hvaclibs/steamloop/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
30
+ </a>
31
+ <a href="https://steamloop.readthedocs.io">
32
+ <img src="https://img.shields.io/readthedocs/steamloop.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
33
+ </a>
34
+ <a href="https://codecov.io/gh/hvaclibs/steamloop">
35
+ <img src="https://img.shields.io/codecov/c/github/hvaclibs/steamloop.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
36
+ </a>
37
+ </p>
38
+ <p align="center">
39
+ <a href="https://github.com/astral-sh/uv">
40
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
41
+ </a>
42
+ <a href="https://github.com/astral-sh/ruff">
43
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
44
+ </a>
45
+ <a href="https://github.com/pre-commit/pre-commit">
46
+ <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
47
+ </a>
48
+ </p>
49
+ <p align="center">
50
+ <a href="https://pypi.org/project/steamloop/">
51
+ <img src="https://img.shields.io/pypi/v/steamloop.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
52
+ </a>
53
+ <img src="https://img.shields.io/pypi/pyversions/steamloop.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
54
+ <img src="https://img.shields.io/pypi/l/steamloop.svg?style=flat-square" alt="License">
55
+ </p>
56
+
57
+ ---
58
+
59
+ Async Python library for local control of thermostat devices over mTLS (port 7878).
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ pip install steamloop
65
+ ```
66
+
67
+ ## CLI
68
+
69
+ ### Pairing
70
+
71
+ Put the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair), then:
72
+
73
+ ```bash
74
+ steamloop 192.168.1.100 --pair
75
+ ```
76
+
77
+ This saves a pairing file in the current directory with the secret key.
78
+
79
+ ### Monitoring
80
+
81
+ ```bash
82
+ steamloop 192.168.1.100
83
+ ```
84
+
85
+ If already paired, you can pass the secret key directly to skip the pairing file:
86
+
87
+ ```bash
88
+ steamloop 192.168.1.100 --key YOUR_SECRET_KEY
89
+ ```
90
+
91
+ Interactive commands: `status`, `heat <temp>`, `cool <temp>`, `mode <off|auto|cool|heat>`, `fan <auto|on|circulate>`, `eheat <on|off>`, `help`.
92
+
93
+ ## Library Usage
94
+
95
+ ```python
96
+ import asyncio
97
+ from steamloop import ThermostatConnection, ZoneMode, FanMode
98
+
99
+ async def main():
100
+ conn = ThermostatConnection(
101
+ "192.168.1.100",
102
+ secret_key="your-secret-key-from-pairing",
103
+ )
104
+ async with conn:
105
+ # State is populated automatically from thermostat events
106
+ for zone_id, zone in conn.state.zones.items():
107
+ print(f"{zone.name}: {zone.indoor_temperature}°F")
108
+
109
+ # Send commands (sync — no await needed)
110
+ conn.set_temperature_setpoint("1", heat_setpoint="72")
111
+ conn.set_zone_mode("1", ZoneMode.COOL)
112
+ conn.set_fan_mode(FanMode.AUTO)
113
+
114
+ asyncio.run(main())
115
+ ```
116
+
117
+ ### Pairing Programmatically
118
+
119
+ `pair()` returns the secret key directly — store it however you like:
120
+
121
+ ```python
122
+ from steamloop import ThermostatConnection
123
+
124
+ async def pair(ip: str) -> str:
125
+ conn = ThermostatConnection(ip, secret_key="")
126
+ try:
127
+ await conn.connect()
128
+ ssk = await conn.pair()
129
+ return ssk["secret_key"] # store in a database, config entry, etc.
130
+ finally:
131
+ await conn.disconnect()
132
+ ```
133
+
134
+ Or use the built-in file helpers to save/load pairing data to disk:
135
+
136
+ ```python
137
+ from steamloop import ThermostatConnection, save_pairing, load_pairing
138
+
139
+ # Save after pairing
140
+ await save_pairing(ip, {
141
+ "secret_key": secret_key,
142
+ "device_type": "automation",
143
+ "device_id": "module",
144
+ })
145
+
146
+ # Load later
147
+ pairing = await load_pairing(ip)
148
+ conn = ThermostatConnection(ip, secret_key=pairing["secret_key"])
149
+ ```
150
+
151
+ ### Event Callbacks
152
+
153
+ ```python
154
+ def on_event(msg):
155
+ print("Received:", msg)
156
+
157
+ remove = conn.add_event_callback(on_event)
158
+ # later: remove() to unregister
159
+ ```
160
+
161
+ ## Home Assistant Integration
162
+
163
+ Key design points for using steamloop in a Home Assistant integration:
164
+
165
+ - **Commands are sync** — `set_zone_mode()`, `set_fan_mode()`, `set_temperature_setpoint()` use `transport.write()` internally, so they won't block the event loop. No `await` needed.
166
+ - **State is always fresh** — the `asyncio.Protocol` receives events via `data_received()` and updates `conn.state` automatically. Just read properties directly.
167
+ - **Auto-reconnect** — after calling `start_background_tasks()`, the connection automatically reconnects with exponential backoff (5s, 10s, 20s, ... up to 5 min).
168
+ - **Event callbacks** — use `add_event_callback()` to trigger `async_write_ha_state()` when the thermostat pushes updates.
169
+ - **Multi-zone** — create one `ClimateEntity` per `conn.state.zones` entry. Zones are populated automatically after login.
170
+
171
+ ## API Reference
172
+
173
+ ### `ThermostatConnection(ip, port=7878, *, secret_key, cert_set=None, device_type="automation", device_id="module")`
174
+
175
+ | Method | Async | Description |
176
+ | ------------------------------------------------------------------------------- | ----- | ----------------------------------------------------- |
177
+ | `connect()` | yes | Establish mTLS connection |
178
+ | `login()` | yes | Authenticate with secret key |
179
+ | `pair()` | yes | Pair and receive secret key |
180
+ | `start_background_tasks()` | no | Start heartbeat + auto-reconnect |
181
+ | `disconnect()` | yes | Close connection and stop tasks |
182
+ | `set_temperature_setpoint(zone_id, *, heat_setpoint, cool_setpoint, hold_type)` | no | Set zone temperature |
183
+ | `set_zone_mode(zone_id, mode)` | no | Set zone HVAC mode |
184
+ | `set_fan_mode(mode)` | no | Set fan mode |
185
+ | `set_emergency_heat(enabled)` | no | Toggle emergency heat |
186
+ | `add_event_callback(fn)` | no | Register event listener (returns unregister callable) |
187
+
188
+ Supports `async with` for automatic connect/login/disconnect:
189
+
190
+ ```python
191
+ async with ThermostatConnection(ip, secret_key=key) as conn:
192
+ ... # connected, logged in, background tasks running
193
+ # automatically disconnected
194
+ ```
195
+
196
+ ### Enums
197
+
198
+ - `ZoneMode` — `OFF`, `AUTO`, `COOL`, `HEAT`
199
+ - `FanMode` — `AUTO`, `ALWAYS_ON`, `CIRCULATE`
200
+ - `HoldType` — `UNDEFINED`, `MANUAL`, `SCHEDULE`, `HOLD`
201
+
202
+ ### State
203
+
204
+ - `conn.state.zones` — `dict[str, Zone]` with temperature, setpoints, mode per zone
205
+ - `conn.state.fan_mode` — current `FanMode`
206
+ - `conn.state.supported_modes` — `list[ZoneMode]`
207
+ - `conn.state.emergency_heat` / `relative_humidity` / `cooling_active` / `heating_active`
208
+
209
+ ## Contributors
210
+
211
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
212
+
213
+ <!-- prettier-ignore-start -->
214
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
215
+ <!-- markdownlint-disable -->
216
+ <!-- markdownlint-enable -->
217
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
218
+ <!-- prettier-ignore-end -->
219
+
220
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
221
+
222
+ ## Credits
223
+
224
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier)
225
+
226
+ This package was created with
227
+ [Copier](https://copier.readthedocs.io/) and the
228
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
229
+ project template.
@@ -0,0 +1,205 @@
1
+ # steamloop
2
+
3
+ <p align="center">
4
+ <a href="https://github.com/hvaclibs/steamloop/actions/workflows/ci.yml?query=branch%3Amain">
5
+ <img src="https://img.shields.io/github/actions/workflow/status/hvaclibs/steamloop/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
6
+ </a>
7
+ <a href="https://steamloop.readthedocs.io">
8
+ <img src="https://img.shields.io/readthedocs/steamloop.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
9
+ </a>
10
+ <a href="https://codecov.io/gh/hvaclibs/steamloop">
11
+ <img src="https://img.shields.io/codecov/c/github/hvaclibs/steamloop.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
12
+ </a>
13
+ </p>
14
+ <p align="center">
15
+ <a href="https://github.com/astral-sh/uv">
16
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
17
+ </a>
18
+ <a href="https://github.com/astral-sh/ruff">
19
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
20
+ </a>
21
+ <a href="https://github.com/pre-commit/pre-commit">
22
+ <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
23
+ </a>
24
+ </p>
25
+ <p align="center">
26
+ <a href="https://pypi.org/project/steamloop/">
27
+ <img src="https://img.shields.io/pypi/v/steamloop.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
28
+ </a>
29
+ <img src="https://img.shields.io/pypi/pyversions/steamloop.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
30
+ <img src="https://img.shields.io/pypi/l/steamloop.svg?style=flat-square" alt="License">
31
+ </p>
32
+
33
+ ---
34
+
35
+ Async Python library for local control of thermostat devices over mTLS (port 7878).
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install steamloop
41
+ ```
42
+
43
+ ## CLI
44
+
45
+ ### Pairing
46
+
47
+ Put the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair), then:
48
+
49
+ ```bash
50
+ steamloop 192.168.1.100 --pair
51
+ ```
52
+
53
+ This saves a pairing file in the current directory with the secret key.
54
+
55
+ ### Monitoring
56
+
57
+ ```bash
58
+ steamloop 192.168.1.100
59
+ ```
60
+
61
+ If already paired, you can pass the secret key directly to skip the pairing file:
62
+
63
+ ```bash
64
+ steamloop 192.168.1.100 --key YOUR_SECRET_KEY
65
+ ```
66
+
67
+ Interactive commands: `status`, `heat <temp>`, `cool <temp>`, `mode <off|auto|cool|heat>`, `fan <auto|on|circulate>`, `eheat <on|off>`, `help`.
68
+
69
+ ## Library Usage
70
+
71
+ ```python
72
+ import asyncio
73
+ from steamloop import ThermostatConnection, ZoneMode, FanMode
74
+
75
+ async def main():
76
+ conn = ThermostatConnection(
77
+ "192.168.1.100",
78
+ secret_key="your-secret-key-from-pairing",
79
+ )
80
+ async with conn:
81
+ # State is populated automatically from thermostat events
82
+ for zone_id, zone in conn.state.zones.items():
83
+ print(f"{zone.name}: {zone.indoor_temperature}°F")
84
+
85
+ # Send commands (sync — no await needed)
86
+ conn.set_temperature_setpoint("1", heat_setpoint="72")
87
+ conn.set_zone_mode("1", ZoneMode.COOL)
88
+ conn.set_fan_mode(FanMode.AUTO)
89
+
90
+ asyncio.run(main())
91
+ ```
92
+
93
+ ### Pairing Programmatically
94
+
95
+ `pair()` returns the secret key directly — store it however you like:
96
+
97
+ ```python
98
+ from steamloop import ThermostatConnection
99
+
100
+ async def pair(ip: str) -> str:
101
+ conn = ThermostatConnection(ip, secret_key="")
102
+ try:
103
+ await conn.connect()
104
+ ssk = await conn.pair()
105
+ return ssk["secret_key"] # store in a database, config entry, etc.
106
+ finally:
107
+ await conn.disconnect()
108
+ ```
109
+
110
+ Or use the built-in file helpers to save/load pairing data to disk:
111
+
112
+ ```python
113
+ from steamloop import ThermostatConnection, save_pairing, load_pairing
114
+
115
+ # Save after pairing
116
+ await save_pairing(ip, {
117
+ "secret_key": secret_key,
118
+ "device_type": "automation",
119
+ "device_id": "module",
120
+ })
121
+
122
+ # Load later
123
+ pairing = await load_pairing(ip)
124
+ conn = ThermostatConnection(ip, secret_key=pairing["secret_key"])
125
+ ```
126
+
127
+ ### Event Callbacks
128
+
129
+ ```python
130
+ def on_event(msg):
131
+ print("Received:", msg)
132
+
133
+ remove = conn.add_event_callback(on_event)
134
+ # later: remove() to unregister
135
+ ```
136
+
137
+ ## Home Assistant Integration
138
+
139
+ Key design points for using steamloop in a Home Assistant integration:
140
+
141
+ - **Commands are sync** — `set_zone_mode()`, `set_fan_mode()`, `set_temperature_setpoint()` use `transport.write()` internally, so they won't block the event loop. No `await` needed.
142
+ - **State is always fresh** — the `asyncio.Protocol` receives events via `data_received()` and updates `conn.state` automatically. Just read properties directly.
143
+ - **Auto-reconnect** — after calling `start_background_tasks()`, the connection automatically reconnects with exponential backoff (5s, 10s, 20s, ... up to 5 min).
144
+ - **Event callbacks** — use `add_event_callback()` to trigger `async_write_ha_state()` when the thermostat pushes updates.
145
+ - **Multi-zone** — create one `ClimateEntity` per `conn.state.zones` entry. Zones are populated automatically after login.
146
+
147
+ ## API Reference
148
+
149
+ ### `ThermostatConnection(ip, port=7878, *, secret_key, cert_set=None, device_type="automation", device_id="module")`
150
+
151
+ | Method | Async | Description |
152
+ | ------------------------------------------------------------------------------- | ----- | ----------------------------------------------------- |
153
+ | `connect()` | yes | Establish mTLS connection |
154
+ | `login()` | yes | Authenticate with secret key |
155
+ | `pair()` | yes | Pair and receive secret key |
156
+ | `start_background_tasks()` | no | Start heartbeat + auto-reconnect |
157
+ | `disconnect()` | yes | Close connection and stop tasks |
158
+ | `set_temperature_setpoint(zone_id, *, heat_setpoint, cool_setpoint, hold_type)` | no | Set zone temperature |
159
+ | `set_zone_mode(zone_id, mode)` | no | Set zone HVAC mode |
160
+ | `set_fan_mode(mode)` | no | Set fan mode |
161
+ | `set_emergency_heat(enabled)` | no | Toggle emergency heat |
162
+ | `add_event_callback(fn)` | no | Register event listener (returns unregister callable) |
163
+
164
+ Supports `async with` for automatic connect/login/disconnect:
165
+
166
+ ```python
167
+ async with ThermostatConnection(ip, secret_key=key) as conn:
168
+ ... # connected, logged in, background tasks running
169
+ # automatically disconnected
170
+ ```
171
+
172
+ ### Enums
173
+
174
+ - `ZoneMode` — `OFF`, `AUTO`, `COOL`, `HEAT`
175
+ - `FanMode` — `AUTO`, `ALWAYS_ON`, `CIRCULATE`
176
+ - `HoldType` — `UNDEFINED`, `MANUAL`, `SCHEDULE`, `HOLD`
177
+
178
+ ### State
179
+
180
+ - `conn.state.zones` — `dict[str, Zone]` with temperature, setpoints, mode per zone
181
+ - `conn.state.fan_mode` — current `FanMode`
182
+ - `conn.state.supported_modes` — `list[ZoneMode]`
183
+ - `conn.state.emergency_heat` / `relative_humidity` / `cooling_active` / `heating_active`
184
+
185
+ ## Contributors
186
+
187
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
188
+
189
+ <!-- prettier-ignore-start -->
190
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
191
+ <!-- markdownlint-disable -->
192
+ <!-- markdownlint-enable -->
193
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
194
+ <!-- prettier-ignore-end -->
195
+
196
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
197
+
198
+ ## Credits
199
+
200
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier)
201
+
202
+ This package was created with
203
+ [Copier](https://copier.readthedocs.io/) and the
204
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
205
+ project template.
@@ -4,21 +4,19 @@ requires = [ "setuptools>=77.0.3" ]
4
4
 
5
5
  [project]
6
6
  name = "steamloop"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = "Local control for choochoo based thermostats"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
11
  authors = [
12
12
  { name = "J. Nick Koston", email = "nick@koston.org" },
13
13
  ]
14
- requires-python = ">=3.10"
14
+ requires-python = ">=3.12"
15
15
  classifiers = [
16
16
  "Development Status :: 2 - Pre-Alpha",
17
17
  "Intended Audience :: Developers",
18
18
  "Natural Language :: English",
19
19
  "Operating System :: OS Independent",
20
- "Programming Language :: Python :: 3.10",
21
- "Programming Language :: Python :: 3.11",
22
20
  "Programming Language :: Python :: 3.12",
23
21
  "Programming Language :: Python :: 3.13",
24
22
  "Programming Language :: Python :: 3.14",
@@ -26,19 +24,21 @@ classifiers = [
26
24
  ]
27
25
 
28
26
  dependencies = [
29
- "rich>=10",
30
- "typer>=0.15,<1",
27
+ "orjson>=3.10",
31
28
  ]
29
+
32
30
  urls."Bug Tracker" = "https://github.com/hvaclibs/steamloop/issues"
33
31
  urls.Changelog = "https://github.com/hvaclibs/steamloop/blob/main/CHANGELOG.md"
34
32
  urls.documentation = "https://steamloop.readthedocs.io"
35
33
  urls.repository = "https://github.com/hvaclibs/steamloop"
36
- scripts.steamloop = "steamloop.cli:app"
34
+
35
+ [project.scripts]
36
+ steamloop = "steamloop.cli:main"
37
37
 
38
38
  [dependency-groups]
39
39
  dev = [
40
40
  "pytest==9.*",
41
- "sybil[pytest]",
41
+ "pytest-asyncio",
42
42
  "coverage",
43
43
  "pytest-cov==7.*",
44
44
  ]
@@ -50,29 +50,70 @@ docs = [
50
50
  ]
51
51
 
52
52
  [tool.ruff]
53
+ target-version = "py312"
53
54
  line-length = 88
54
55
  lint.select = [
55
- "B", # flake8-bugbear
56
- "D", # flake8-docstrings
57
- "C4", # flake8-comprehensions
58
- "S", # flake8-bandit
59
- "F", # pyflake
60
- "E", # pycodestyle
61
- "W", # pycodestyle
62
- "UP", # pyupgrade
63
- "I", # isort
64
- "RUF", # ruff specific
56
+ "ASYNC", # async rules
57
+ "B", # flake8-bugbear
58
+ "BLE", # flake8-blind-except
59
+ "C", # complexity + comprehensions
60
+ "COM818", # Trailing comma on bare tuple prohibited
61
+ "D", # flake8-docstrings
62
+ "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow()
63
+ "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts)
64
+ "E", # pycodestyle
65
+ "F", # pyflakes
66
+ "FLY", # flynt
67
+ "FURB", # refurb
68
+ "G", # flake8-logging-format
69
+ "I", # isort
70
+ "ICN001", # import conventions; {name} should be imported as {asname}
71
+ "INP", # flake8-no-pep420
72
+ "ISC", # flake8-implicit-str-concat
73
+ "LOG", # flake8-logging
74
+ "N804", # First argument of a class method should be named cls
75
+ "N805", # First argument of a method should be named self
76
+ "N815", # Variable {name} in class scope should not be mixedCase
77
+ "PERF", # Perflint
78
+ "PGH", # pygrep-hooks
79
+ "PIE", # flake8-pie
80
+ "PL", # pylint
81
+ "PT", # flake8-pytest-style
82
+ "PTH", # flake8-pathlib
83
+ "PYI", # flake8-pyi
84
+ "RET", # flake8-return
85
+ "RSE", # flake8-raise
86
+ "RUF", # ruff specific
87
+ "S", # flake8-bandit
88
+ "SIM", # flake8-simplify
89
+ "SLF", # flake8-self
90
+ "SLOT", # flake8-slots
91
+ "T100", # Trace found: {name} used
92
+ "T20", # flake8-print
93
+ "TC", # flake8-type-checking
94
+ "TID", # Tidy imports
95
+ "TRY", # tryceratops
96
+ "UP", # pyupgrade
97
+ "W", # pycodestyle
65
98
  ]
66
99
  lint.ignore = [
67
- "D203", # 1 blank line required before class docstring
68
- "D212", # Multi-line docstring summary should start at the first line
69
- "D100", # Missing docstring in public module
70
- "D104", # Missing docstring in public package
71
- "D107", # Missing docstring in `__init__`
72
- "D401", # First line of docstring should be in imperative mood
100
+ "D203", # 1 blank line required before class docstring
101
+ "D212", # Multi-line docstring summary should start at the first line
102
+ "D213", # Multi-line docstring summary should start at the second line
103
+ "D100", # Missing docstring in public module
104
+ "D104", # Missing docstring in public package
105
+ "D107", # Missing docstring in `__init__`
106
+ "D401", # First line of docstring should be in imperative mood
107
+ "COM812", # May conflict with the formatter
108
+ "ISC001", # May conflict with the formatter
109
+ "TRY003", # Avoid specifying long messages outside the exception class
110
+ "TRY301", # raise within try — used for exception wrapping
111
+ "PLR0913", # Too many arguments to function call
112
+ "PLR0911", # Too many return statements
113
+ "SIM110", # Reimplementing builtin — can be slower
73
114
  ]
74
115
  lint.per-file-ignores."conftest.py" = [ "D100" ]
75
- lint.per-file-ignores."docs/conf.py" = [ "D100" ]
116
+ lint.per-file-ignores."docs/conf.py" = [ "D100", "INP001" ]
76
117
  lint.per-file-ignores."setup.py" = [ "D100" ]
77
118
  lint.per-file-ignores."tests/**/*" = [
78
119
  "D100",
@@ -80,14 +121,31 @@ lint.per-file-ignores."tests/**/*" = [
80
121
  "D102",
81
122
  "D103",
82
123
  "D104",
124
+ "INP001",
125
+ "PLC0415",
126
+ "PLR2004",
127
+ "PT011",
128
+ "PT012",
83
129
  "S101",
130
+ "S105",
131
+ "S106",
132
+ "SLF001",
133
+ "TC002",
134
+ "TRY003",
135
+ ]
136
+ lint.per-file-ignores."src/steamloop/cli.py" = [
137
+ "C901", # _handle_command is inherently a command dispatcher
138
+ "PLR0912", # _handle_command has many branches by design
139
+ "PLR0915", # _handle_command has many statements by design
140
+ "PLR2004", # magic values in CLI arg counts are clear
141
+ "T201", # print() is the CLI output mechanism
84
142
  ]
85
143
  lint.isort.known-first-party = [ "steamloop", "tests" ]
86
144
 
87
145
  [tool.pyproject-fmt]
88
146
  max_supported_python = "3.14"
89
147
 
90
- [tool.pytest]
148
+ [tool.pytest.ini_options]
91
149
  minversion = "9.0"
92
150
  addopts = [
93
151
  "-v",
@@ -98,6 +156,7 @@ addopts = [
98
156
  "--cov-report=xml"
99
157
  ]
100
158
  pythonpath = [ "src" ]
159
+ asyncio_mode = "auto"
101
160
 
102
161
  [tool.coverage]
103
162
  run.branch = true
@@ -0,0 +1,31 @@
1
+ """Local control for thermostat devices over mTLS."""
2
+
3
+ __version__ = "1.2.0"
4
+
5
+ from .connection import ThermostatConnection, load_pairing, save_pairing
6
+ from .const import DEFAULT_PORT, FanMode, HoldType, ZoneMode
7
+ from .exceptions import (
8
+ AuthenticationError,
9
+ CommandError,
10
+ PairingError,
11
+ SteamloopConnectionError,
12
+ SteamloopError,
13
+ )
14
+ from .models import ThermostatState, Zone
15
+
16
+ __all__ = [
17
+ "DEFAULT_PORT",
18
+ "AuthenticationError",
19
+ "CommandError",
20
+ "FanMode",
21
+ "HoldType",
22
+ "PairingError",
23
+ "SteamloopConnectionError",
24
+ "SteamloopError",
25
+ "ThermostatConnection",
26
+ "ThermostatState",
27
+ "Zone",
28
+ "ZoneMode",
29
+ "load_pairing",
30
+ "save_pairing",
31
+ ]
@@ -0,0 +1,5 @@
1
+ """Allow running as ``python -m steamloop``."""
2
+
3
+ from steamloop.cli import main
4
+
5
+ main()