aiopyronix 0.1.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.
- aiopyronix-0.1.0/.gitignore +13 -0
- aiopyronix-0.1.0/CHANGELOG.md +11 -0
- aiopyronix-0.1.0/LICENSE +21 -0
- aiopyronix-0.1.0/PKG-INFO +176 -0
- aiopyronix-0.1.0/README.md +150 -0
- aiopyronix-0.1.0/RELEASING.md +67 -0
- aiopyronix-0.1.0/pyproject.toml +107 -0
- aiopyronix-0.1.0/src/aiopyronix/__init__.py +37 -0
- aiopyronix-0.1.0/src/aiopyronix/client.py +537 -0
- aiopyronix-0.1.0/src/aiopyronix/protocol.py +268 -0
- aiopyronix-0.1.0/src/aiopyronix/py.typed +0 -0
- aiopyronix-0.1.0/tests/__init__.py +0 -0
- aiopyronix-0.1.0/tests/const.py +11 -0
- aiopyronix-0.1.0/tests/test_client.py +891 -0
- aiopyronix-0.1.0/tests/test_protocol.py +248 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project follows [Semantic Versioning](https://semver.org/). Until 1.0, a
|
|
4
|
+
minor version may change the public API.
|
|
5
|
+
|
|
6
|
+
## 0.1.0
|
|
7
|
+
|
|
8
|
+
First release. The PyronixCloud client and panel protocol move out of the
|
|
9
|
+
[Pyronix HomeControl integration](https://github.com/Herbertmt978/HA-Pyronix)
|
|
10
|
+
0.3.0 unchanged, with a public API, type information and full branch test
|
|
11
|
+
coverage.
|
aiopyronix-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Herbertmt978
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aiopyronix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Asynchronous PyronixCloud client for Pyronix alarm panels, with explicit sessions
|
|
5
|
+
Project-URL: Source, https://github.com/Herbertmt978/aiopyronix
|
|
6
|
+
Project-URL: Issues, https://github.com/Herbertmt978/aiopyronix/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Herbertmt978/aiopyronix/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Herbertmt978
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: alarm,asyncio,home-assistant,homecontrol,pyronix
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Home Automation
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.14
|
|
23
|
+
Requires-Dist: aiohttp>=3.13.0
|
|
24
|
+
Requires-Dist: cryptography>=47.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# aiopyronix
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/aiopyronix/)
|
|
30
|
+
[](https://github.com/Herbertmt978/aiopyronix/actions/workflows/ci.yml)
|
|
31
|
+
[](https://github.com/Herbertmt978/aiopyronix/blob/main/LICENSE)
|
|
32
|
+
|
|
33
|
+
An asynchronous Python client for Pyronix alarm panels reached through
|
|
34
|
+
PyronixCloud, the service used by the HomeControl 2.0 app.
|
|
35
|
+
|
|
36
|
+
It powers the [Pyronix HomeControl integration for Home Assistant](https://github.com/Herbertmt978/HA-Pyronix),
|
|
37
|
+
and it can be used on its own. This is independent community software. It is
|
|
38
|
+
not an official Pyronix, Hikvision or Home Assistant product.
|
|
39
|
+
|
|
40
|
+
## Design
|
|
41
|
+
|
|
42
|
+
The client controls an intruder alarm, so caution takes priority over convenience:
|
|
43
|
+
|
|
44
|
+
- **Explicit sessions.** Nothing connects when a `PanelClient` is created. A
|
|
45
|
+
session opens only when `connect()` is called. There is no polling or
|
|
46
|
+
automatic reconnection.
|
|
47
|
+
- **Idle sessions close.** A session closes after five minutes without a
|
|
48
|
+
`connect()` or `control()` call. Heartbeats keep the socket alive, but they
|
|
49
|
+
do not extend that limit.
|
|
50
|
+
- **Commands are sent once.** `control()` arms or disarms exactly one area and
|
|
51
|
+
waits for the panel to confirm it. When the outcome cannot be confirmed, it
|
|
52
|
+
raises `CommandUnconfirmedError`. The command is never repeated, even after
|
|
53
|
+
a lost connection.
|
|
54
|
+
- **A narrow command set.** Only single-area arm and disarm are possible. The
|
|
55
|
+
library has no forced omissions, outputs, installer functions or cloud
|
|
56
|
+
account changes.
|
|
57
|
+
- **Nothing sensitive is logged.** The library does not log. Its error
|
|
58
|
+
messages are fixed text and never contain credentials, panel data or wire
|
|
59
|
+
payloads.
|
|
60
|
+
|
|
61
|
+
Status updates and commands share one WebSocket. Each frame is encrypted with
|
|
62
|
+
a session key agreed through ECDH (P-256) and checked for integrity and
|
|
63
|
+
sequence.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install aiopyronix
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Python 3.14 or later is required.
|
|
72
|
+
|
|
73
|
+
## Connection details
|
|
74
|
+
|
|
75
|
+
`PanelClient` needs the connection details that the HomeControl 2.0 app saves
|
|
76
|
+
when you sign in:
|
|
77
|
+
|
|
78
|
+
| Field | Meaning |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `AppUser`, `PushId`, `DeviceId` | The app installation registered with PyronixCloud |
|
|
81
|
+
| `PanelId` | The eight-character panel identifier |
|
|
82
|
+
| `SystemName` | The panel name shown in the app |
|
|
83
|
+
| `PanelPwd` | The app password stored for the panel |
|
|
84
|
+
| `UserCode` | The panel user code, four to eight digits |
|
|
85
|
+
|
|
86
|
+
`AppVersion`, `AppType`, `OSName`, `OSVersion`, `NetworkType` and `AppLang`
|
|
87
|
+
are optional. Other keys are discarded. The Home Assistant integration
|
|
88
|
+
includes a [setup helper](https://github.com/Herbertmt978/HA-Pyronix#connect-your-own-alarm)
|
|
89
|
+
that reads these details from your own signed-in app.
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
import json
|
|
96
|
+
from pathlib import Path
|
|
97
|
+
|
|
98
|
+
import aiohttp
|
|
99
|
+
|
|
100
|
+
from aiopyronix import CommandUnconfirmedError, PanelClient
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def main() -> None:
|
|
104
|
+
auth = json.loads(Path("pyronix.json").read_text())
|
|
105
|
+
async with aiohttp.ClientSession() as http:
|
|
106
|
+
client = PanelClient(http, auth)
|
|
107
|
+
try:
|
|
108
|
+
snapshot = await client.connect()
|
|
109
|
+
for index, area in snapshot["areas"].items():
|
|
110
|
+
print(index, area["name"], area["status"])
|
|
111
|
+
|
|
112
|
+
if 0 in snapshot["can_arm"]:
|
|
113
|
+
try:
|
|
114
|
+
await client.control("arm", 0)
|
|
115
|
+
except CommandUnconfirmedError:
|
|
116
|
+
print("Check the panel before trying again")
|
|
117
|
+
finally:
|
|
118
|
+
await client.disconnect()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`connect()` returns once every area the user may control has reported its
|
|
125
|
+
state. Calling it on an open session returns the current state and restarts
|
|
126
|
+
the idle timer.
|
|
127
|
+
|
|
128
|
+
### Callbacks
|
|
129
|
+
|
|
130
|
+
- `on_update(snapshot)` runs whenever the panel reports new state.
|
|
131
|
+
- `on_state()` runs when `state`, `detail` or `idle_expires_at` changes.
|
|
132
|
+
- `create_task(coroutine)` starts the session's background reader. It defaults
|
|
133
|
+
to `asyncio.create_task`. Home Assistant passes
|
|
134
|
+
`entry.async_create_background_task`.
|
|
135
|
+
|
|
136
|
+
### State
|
|
137
|
+
|
|
138
|
+
- `snapshot()` returns an independent copy of the latest `Snapshot`: the
|
|
139
|
+
`areas`, the `can_arm` and `can_disarm` area sets, `permissions_received`,
|
|
140
|
+
`observed_at` and, when reported, `firmware`.
|
|
141
|
+
- `state` is `disconnected`, `connecting`, `connected` or `error`. After an
|
|
142
|
+
error, `detail` holds a fixed explanation.
|
|
143
|
+
- `idle_expires_at` is the wall-clock time at which an idle session will close.
|
|
144
|
+
|
|
145
|
+
Area `value` codes are listed in `STATUS`: `0` Unset, `1` Set, `2` Alarmed,
|
|
146
|
+
`3` Setting, `4` Cannot Set, `5` Can Override and `6` Set Cancelled. An arm
|
|
147
|
+
request counts as confirmed once the area reports Setting. Arming is refused
|
|
148
|
+
while an area reports Cannot Set or Can Override.
|
|
149
|
+
|
|
150
|
+
### Errors
|
|
151
|
+
|
|
152
|
+
All errors derive from `ProtocolError`:
|
|
153
|
+
|
|
154
|
+
| Exception | Meaning |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `AuthenticationError` | The cloud or panel rejected the credentials. Ask for new details; do not retry automatically. |
|
|
157
|
+
| `PanelBusyError` | Another command on this client is still waiting for its result. |
|
|
158
|
+
| `CommandUnconfirmedError` | The command may have reached the panel, but its outcome is unknown. `snapshot` holds the latest state, if the session is still open. |
|
|
159
|
+
| `ProtocolError` | Any other connection, validation or panel failure. |
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
python -m pip install -e . --group dev
|
|
165
|
+
ruff check .
|
|
166
|
+
ruff format --check .
|
|
167
|
+
mypy
|
|
168
|
+
pytest --cov
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The tests use a simulated panel and require 100% branch coverage. They never
|
|
172
|
+
contact PyronixCloud.
|
|
173
|
+
|
|
174
|
+
## Licence
|
|
175
|
+
|
|
176
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# aiopyronix
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/aiopyronix/)
|
|
4
|
+
[](https://github.com/Herbertmt978/aiopyronix/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/Herbertmt978/aiopyronix/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
An asynchronous Python client for Pyronix alarm panels reached through
|
|
8
|
+
PyronixCloud, the service used by the HomeControl 2.0 app.
|
|
9
|
+
|
|
10
|
+
It powers the [Pyronix HomeControl integration for Home Assistant](https://github.com/Herbertmt978/HA-Pyronix),
|
|
11
|
+
and it can be used on its own. This is independent community software. It is
|
|
12
|
+
not an official Pyronix, Hikvision or Home Assistant product.
|
|
13
|
+
|
|
14
|
+
## Design
|
|
15
|
+
|
|
16
|
+
The client controls an intruder alarm, so caution takes priority over convenience:
|
|
17
|
+
|
|
18
|
+
- **Explicit sessions.** Nothing connects when a `PanelClient` is created. A
|
|
19
|
+
session opens only when `connect()` is called. There is no polling or
|
|
20
|
+
automatic reconnection.
|
|
21
|
+
- **Idle sessions close.** A session closes after five minutes without a
|
|
22
|
+
`connect()` or `control()` call. Heartbeats keep the socket alive, but they
|
|
23
|
+
do not extend that limit.
|
|
24
|
+
- **Commands are sent once.** `control()` arms or disarms exactly one area and
|
|
25
|
+
waits for the panel to confirm it. When the outcome cannot be confirmed, it
|
|
26
|
+
raises `CommandUnconfirmedError`. The command is never repeated, even after
|
|
27
|
+
a lost connection.
|
|
28
|
+
- **A narrow command set.** Only single-area arm and disarm are possible. The
|
|
29
|
+
library has no forced omissions, outputs, installer functions or cloud
|
|
30
|
+
account changes.
|
|
31
|
+
- **Nothing sensitive is logged.** The library does not log. Its error
|
|
32
|
+
messages are fixed text and never contain credentials, panel data or wire
|
|
33
|
+
payloads.
|
|
34
|
+
|
|
35
|
+
Status updates and commands share one WebSocket. Each frame is encrypted with
|
|
36
|
+
a session key agreed through ECDH (P-256) and checked for integrity and
|
|
37
|
+
sequence.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install aiopyronix
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Python 3.14 or later is required.
|
|
46
|
+
|
|
47
|
+
## Connection details
|
|
48
|
+
|
|
49
|
+
`PanelClient` needs the connection details that the HomeControl 2.0 app saves
|
|
50
|
+
when you sign in:
|
|
51
|
+
|
|
52
|
+
| Field | Meaning |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `AppUser`, `PushId`, `DeviceId` | The app installation registered with PyronixCloud |
|
|
55
|
+
| `PanelId` | The eight-character panel identifier |
|
|
56
|
+
| `SystemName` | The panel name shown in the app |
|
|
57
|
+
| `PanelPwd` | The app password stored for the panel |
|
|
58
|
+
| `UserCode` | The panel user code, four to eight digits |
|
|
59
|
+
|
|
60
|
+
`AppVersion`, `AppType`, `OSName`, `OSVersion`, `NetworkType` and `AppLang`
|
|
61
|
+
are optional. Other keys are discarded. The Home Assistant integration
|
|
62
|
+
includes a [setup helper](https://github.com/Herbertmt978/HA-Pyronix#connect-your-own-alarm)
|
|
63
|
+
that reads these details from your own signed-in app.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import asyncio
|
|
69
|
+
import json
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
|
|
72
|
+
import aiohttp
|
|
73
|
+
|
|
74
|
+
from aiopyronix import CommandUnconfirmedError, PanelClient
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def main() -> None:
|
|
78
|
+
auth = json.loads(Path("pyronix.json").read_text())
|
|
79
|
+
async with aiohttp.ClientSession() as http:
|
|
80
|
+
client = PanelClient(http, auth)
|
|
81
|
+
try:
|
|
82
|
+
snapshot = await client.connect()
|
|
83
|
+
for index, area in snapshot["areas"].items():
|
|
84
|
+
print(index, area["name"], area["status"])
|
|
85
|
+
|
|
86
|
+
if 0 in snapshot["can_arm"]:
|
|
87
|
+
try:
|
|
88
|
+
await client.control("arm", 0)
|
|
89
|
+
except CommandUnconfirmedError:
|
|
90
|
+
print("Check the panel before trying again")
|
|
91
|
+
finally:
|
|
92
|
+
await client.disconnect()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
asyncio.run(main())
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`connect()` returns once every area the user may control has reported its
|
|
99
|
+
state. Calling it on an open session returns the current state and restarts
|
|
100
|
+
the idle timer.
|
|
101
|
+
|
|
102
|
+
### Callbacks
|
|
103
|
+
|
|
104
|
+
- `on_update(snapshot)` runs whenever the panel reports new state.
|
|
105
|
+
- `on_state()` runs when `state`, `detail` or `idle_expires_at` changes.
|
|
106
|
+
- `create_task(coroutine)` starts the session's background reader. It defaults
|
|
107
|
+
to `asyncio.create_task`. Home Assistant passes
|
|
108
|
+
`entry.async_create_background_task`.
|
|
109
|
+
|
|
110
|
+
### State
|
|
111
|
+
|
|
112
|
+
- `snapshot()` returns an independent copy of the latest `Snapshot`: the
|
|
113
|
+
`areas`, the `can_arm` and `can_disarm` area sets, `permissions_received`,
|
|
114
|
+
`observed_at` and, when reported, `firmware`.
|
|
115
|
+
- `state` is `disconnected`, `connecting`, `connected` or `error`. After an
|
|
116
|
+
error, `detail` holds a fixed explanation.
|
|
117
|
+
- `idle_expires_at` is the wall-clock time at which an idle session will close.
|
|
118
|
+
|
|
119
|
+
Area `value` codes are listed in `STATUS`: `0` Unset, `1` Set, `2` Alarmed,
|
|
120
|
+
`3` Setting, `4` Cannot Set, `5` Can Override and `6` Set Cancelled. An arm
|
|
121
|
+
request counts as confirmed once the area reports Setting. Arming is refused
|
|
122
|
+
while an area reports Cannot Set or Can Override.
|
|
123
|
+
|
|
124
|
+
### Errors
|
|
125
|
+
|
|
126
|
+
All errors derive from `ProtocolError`:
|
|
127
|
+
|
|
128
|
+
| Exception | Meaning |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `AuthenticationError` | The cloud or panel rejected the credentials. Ask for new details; do not retry automatically. |
|
|
131
|
+
| `PanelBusyError` | Another command on this client is still waiting for its result. |
|
|
132
|
+
| `CommandUnconfirmedError` | The command may have reached the panel, but its outcome is unknown. `snapshot` holds the latest state, if the session is still open. |
|
|
133
|
+
| `ProtocolError` | Any other connection, validation or panel failure. |
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python -m pip install -e . --group dev
|
|
139
|
+
ruff check .
|
|
140
|
+
ruff format --check .
|
|
141
|
+
mypy
|
|
142
|
+
pytest --cov
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The tests use a simulated panel and require 100% branch coverage. They never
|
|
146
|
+
contact PyronixCloud.
|
|
147
|
+
|
|
148
|
+
## Licence
|
|
149
|
+
|
|
150
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Releasing aiopyronix
|
|
2
|
+
|
|
3
|
+
Releases are built and uploaded by GitHub Actions. No PyPI API token exists:
|
|
4
|
+
PyPI trusts the `Release` workflow through
|
|
5
|
+
[trusted publishing](https://docs.pypi.org/trusted-publishers/), and each
|
|
6
|
+
upload carries a signed attestation of the workflow run that built it.
|
|
7
|
+
|
|
8
|
+
## One-time setup
|
|
9
|
+
|
|
10
|
+
- **GitHub:** the public repository `Herbertmt978/aiopyronix`, with issues
|
|
11
|
+
enabled. Its `pypi` environment accepts deployments only from tags matching
|
|
12
|
+
`v*`.
|
|
13
|
+
- **PyPI:** a trusted publisher for the `aiopyronix` project, bound to owner
|
|
14
|
+
`Herbertmt978`, repository `aiopyronix`, workflow `release.yml` and
|
|
15
|
+
environment `pypi`. Before the first upload it is a pending publisher; the
|
|
16
|
+
first successful upload creates the project.
|
|
17
|
+
|
|
18
|
+
Changing the workflow file name, the environment name or the repository owner
|
|
19
|
+
breaks publishing until the PyPI publisher is updated to match.
|
|
20
|
+
|
|
21
|
+
## Versioning
|
|
22
|
+
|
|
23
|
+
The project follows [Semantic Versioning](https://semver.org/). Before 1.0:
|
|
24
|
+
|
|
25
|
+
- a patch release fixes behaviour without changing the public API;
|
|
26
|
+
- a minor release adds or changes public API, or changes what reaches the
|
|
27
|
+
panel. The changelog calls out any change to commands, confirmation or
|
|
28
|
+
session lifetime.
|
|
29
|
+
|
|
30
|
+
The version is written once, in `pyproject.toml`. The release workflow stops
|
|
31
|
+
unless the tag is exactly `v` followed by that version.
|
|
32
|
+
|
|
33
|
+
## Cutting a release
|
|
34
|
+
|
|
35
|
+
1. Update `version` in `pyproject.toml` and add a `CHANGELOG.md` entry.
|
|
36
|
+
2. Run the checks locally:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ruff check .
|
|
40
|
+
ruff format --check .
|
|
41
|
+
mypy
|
|
42
|
+
pytest --cov
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Merge to `main` and wait for CI to pass.
|
|
46
|
+
4. Publish a GitHub release whose tag is `v<version>`, targeting `main`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gh release create v0.1.1 --target main --title 0.1.1 --notes-file notes.md
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
5. The `Release` workflow reruns CI, checks the tag, builds the sdist and
|
|
53
|
+
wheel, and uploads both to PyPI. Confirm the new version at
|
|
54
|
+
<https://pypi.org/project/aiopyronix/>.
|
|
55
|
+
6. In [HA-Pyronix](https://github.com/Herbertmt978/HA-Pyronix), update the
|
|
56
|
+
pin in `manifest.json` and `requirements-dev.txt`, then run its tests on
|
|
57
|
+
the minimum and current Home Assistant versions.
|
|
58
|
+
|
|
59
|
+
A published version cannot be replaced on PyPI. If a release is wrong, yank it
|
|
60
|
+
on PyPI and publish a new patch version.
|
|
61
|
+
|
|
62
|
+
## Licence
|
|
63
|
+
|
|
64
|
+
aiopyronix is MIT licensed, like the integration. Its runtime dependencies are
|
|
65
|
+
also OSI approved: aiohttp (Apache-2.0) and cryptography (Apache-2.0 or
|
|
66
|
+
BSD-3-Clause). The wheel records the licence as an SPDX expression and
|
|
67
|
+
includes the `LICENSE` file.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aiopyronix"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Asynchronous PyronixCloud client for Pyronix alarm panels, with explicit sessions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Herbertmt978" }]
|
|
13
|
+
requires-python = ">=3.14"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"aiohttp>=3.13.0",
|
|
16
|
+
"cryptography>=47.0.0",
|
|
17
|
+
]
|
|
18
|
+
keywords = ["pyronix", "homecontrol", "alarm", "asyncio", "home-assistant"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Framework :: AsyncIO",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Home Automation",
|
|
28
|
+
"Topic :: Security",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Source = "https://github.com/Herbertmt978/aiopyronix"
|
|
34
|
+
Issues = "https://github.com/Herbertmt978/aiopyronix/issues"
|
|
35
|
+
Changelog = "https://github.com/Herbertmt978/aiopyronix/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"mypy==2.4.1",
|
|
40
|
+
"pytest==9.0.3",
|
|
41
|
+
"pytest-asyncio==1.4.0",
|
|
42
|
+
"pytest-cov==7.1.0",
|
|
43
|
+
"ruff==0.17.3",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = ["/src", "/tests", "/CHANGELOG.md", "/RELEASING.md"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
asyncio_mode = "auto"
|
|
51
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
# Unhandled task exceptions and resource warnings are defects here, not noise.
|
|
54
|
+
filterwarnings = ["error"]
|
|
55
|
+
|
|
56
|
+
[tool.coverage.run]
|
|
57
|
+
branch = true
|
|
58
|
+
source = ["aiopyronix"]
|
|
59
|
+
|
|
60
|
+
[tool.coverage.report]
|
|
61
|
+
fail_under = 100
|
|
62
|
+
show_missing = true
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
python_version = "3.14"
|
|
66
|
+
strict = true
|
|
67
|
+
files = ["src/aiopyronix"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
target-version = "py314"
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
# Home Assistant Core's rule selection, so the library reads like the integration.
|
|
74
|
+
select = [
|
|
75
|
+
"A001", "ASYNC", "B", "BLE", "C", "COM818", "D", "DTZ003", "DTZ004", "DTZ011", "E", "F",
|
|
76
|
+
"FLY", "FURB", "G", "I", "INP", "ISC", "ICN001", "ICN002", "LOG", "N804", "N805", "N806",
|
|
77
|
+
"N815", "PERF", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S102", "S103",
|
|
78
|
+
"S107", "S108", "S301", "S306", "S307", "S313", "S314", "S315", "S316", "S317", "S318",
|
|
79
|
+
"S319", "S601", "S602", "S604", "S608", "S609", "SIM", "SLF", "SLOT", "T100", "T20", "TC",
|
|
80
|
+
"TID", "TRY", "UP", "W",
|
|
81
|
+
]
|
|
82
|
+
ignore = [
|
|
83
|
+
"D202", "D203", "D213", "D406", "D407", "D417", "E501", "PLC1901", "PLR0911", "PLR0912",
|
|
84
|
+
"PLR0913", "PLR0915", "PLR0917", "PLR2004", "PLW0108", "PLW2901", "PT011", "PT018",
|
|
85
|
+
"RUF001", "RUF012", "SIM102", "SIM103", "SIM108", "TC001", "TC002", "TC003", "TRY003",
|
|
86
|
+
"COM812",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[tool.ruff.lint.isort]
|
|
90
|
+
force-sort-within-sections = true
|
|
91
|
+
known-first-party = ["aiopyronix", "tests"]
|
|
92
|
+
combine-as-imports = true
|
|
93
|
+
split-on-trailing-comma = false
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
96
|
+
"__future__.annotations".msg = "Not needed; the library requires Python 3.14"
|
|
97
|
+
"async_timeout".msg = "use asyncio.timeout instead"
|
|
98
|
+
|
|
99
|
+
[tool.ruff.lint.mccabe]
|
|
100
|
+
max-complexity = 25
|
|
101
|
+
|
|
102
|
+
[tool.ruff.lint.pydocstyle]
|
|
103
|
+
convention = "google"
|
|
104
|
+
|
|
105
|
+
[tool.ruff.lint.per-file-ignores]
|
|
106
|
+
# Tests exercise private session state directly.
|
|
107
|
+
"tests/*.py" = ["D", "SLF001"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Asynchronous PyronixCloud client for Pyronix alarm panels.
|
|
2
|
+
|
|
3
|
+
A session opens only when the caller asks for it. Each arm or disarm request is
|
|
4
|
+
sent once and never repeated. Connection fields and wire payloads are never logged.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import (
|
|
8
|
+
OPTIONAL_AUTH,
|
|
9
|
+
REQUIRED_AUTH,
|
|
10
|
+
STATUS,
|
|
11
|
+
AreaState,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
CommandUnconfirmedError,
|
|
14
|
+
ConnectionState,
|
|
15
|
+
PanelBusyError,
|
|
16
|
+
PanelClient,
|
|
17
|
+
Snapshot,
|
|
18
|
+
empty_snapshot,
|
|
19
|
+
validate_auth,
|
|
20
|
+
)
|
|
21
|
+
from .protocol import ProtocolError
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"OPTIONAL_AUTH",
|
|
25
|
+
"REQUIRED_AUTH",
|
|
26
|
+
"STATUS",
|
|
27
|
+
"AreaState",
|
|
28
|
+
"AuthenticationError",
|
|
29
|
+
"CommandUnconfirmedError",
|
|
30
|
+
"ConnectionState",
|
|
31
|
+
"PanelBusyError",
|
|
32
|
+
"PanelClient",
|
|
33
|
+
"ProtocolError",
|
|
34
|
+
"Snapshot",
|
|
35
|
+
"empty_snapshot",
|
|
36
|
+
"validate_auth",
|
|
37
|
+
]
|