pydaitem 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.
- pydaitem-0.1.0/.github/workflows/publish.yml +49 -0
- pydaitem-0.1.0/.github/workflows/test.yml +37 -0
- pydaitem-0.1.0/.gitignore +23 -0
- pydaitem-0.1.0/CHANGELOG.md +44 -0
- pydaitem-0.1.0/LICENSE +21 -0
- pydaitem-0.1.0/PKG-INFO +301 -0
- pydaitem-0.1.0/README.md +274 -0
- pydaitem-0.1.0/docs/api-daitem.md +415 -0
- pydaitem-0.1.0/docs/openapi.yaml +1403 -0
- pydaitem-0.1.0/docs/project-architecture.md +101 -0
- pydaitem-0.1.0/pyproject.toml +75 -0
- pydaitem-0.1.0/samples/control.py +66 -0
- pydaitem-0.1.0/samples/persist_token.py +42 -0
- pydaitem-0.1.0/samples/read_state.py +40 -0
- pydaitem-0.1.0/scripts/probe_headers.py +119 -0
- pydaitem-0.1.0/scripts/smoke.py +151 -0
- pydaitem-0.1.0/src/pydaitem/__init__.py +78 -0
- pydaitem-0.1.0/src/pydaitem/cli/__init__.py +39 -0
- pydaitem-0.1.0/src/pydaitem/cli/__main__.py +8 -0
- pydaitem-0.1.0/src/pydaitem/cli/app.py +84 -0
- pydaitem-0.1.0/src/pydaitem/cli/commands.py +118 -0
- pydaitem-0.1.0/src/pydaitem/cli/exit_codes.py +9 -0
- pydaitem-0.1.0/src/pydaitem/cli/output.py +35 -0
- pydaitem-0.1.0/src/pydaitem/cli/parser.py +102 -0
- pydaitem-0.1.0/src/pydaitem/cli/schedule.py +115 -0
- pydaitem-0.1.0/src/pydaitem/client/__init__.py +102 -0
- pydaitem-0.1.0/src/pydaitem/client/account.py +26 -0
- pydaitem-0.1.0/src/pydaitem/client/auth.py +193 -0
- pydaitem-0.1.0/src/pydaitem/client/commands.py +68 -0
- pydaitem-0.1.0/src/pydaitem/client/logbook.py +43 -0
- pydaitem-0.1.0/src/pydaitem/client/panel.py +103 -0
- pydaitem-0.1.0/src/pydaitem/client/pkce.py +26 -0
- pydaitem-0.1.0/src/pydaitem/client/schedule.py +46 -0
- pydaitem-0.1.0/src/pydaitem/client/state.py +58 -0
- pydaitem-0.1.0/src/pydaitem/client/transport.py +121 -0
- pydaitem-0.1.0/src/pydaitem/const/__init__.py +90 -0
- pydaitem-0.1.0/src/pydaitem/const/errors.py +26 -0
- pydaitem-0.1.0/src/pydaitem/const/semantics.py +137 -0
- pydaitem-0.1.0/src/pydaitem/const/wire.py +18 -0
- pydaitem-0.1.0/src/pydaitem/credentials.py +99 -0
- pydaitem-0.1.0/src/pydaitem/exceptions.py +61 -0
- pydaitem-0.1.0/src/pydaitem/models/__init__.py +28 -0
- pydaitem-0.1.0/src/pydaitem/models/_shared.py +19 -0
- pydaitem-0.1.0/src/pydaitem/models/account.py +30 -0
- pydaitem-0.1.0/src/pydaitem/models/inventory.py +126 -0
- pydaitem-0.1.0/src/pydaitem/models/schedule.py +68 -0
- pydaitem-0.1.0/src/pydaitem/models/status.py +93 -0
- pydaitem-0.1.0/src/pydaitem/py.typed +0 -0
- pydaitem-0.1.0/src/pydaitem/system/__init__.py +97 -0
- pydaitem-0.1.0/src/pydaitem/system/capabilities.py +101 -0
- pydaitem-0.1.0/src/pydaitem/system/commands.py +67 -0
- pydaitem-0.1.0/src/pydaitem/system/schedule.py +43 -0
- pydaitem-0.1.0/src/pydaitem/system/state.py +13 -0
- pydaitem-0.1.0/src/pydaitem/tokens.py +106 -0
- pydaitem-0.1.0/tests/test_cli.py +283 -0
- pydaitem-0.1.0/tests/test_errors.py +204 -0
- pydaitem-0.1.0/tests/test_login.py +85 -0
- pydaitem-0.1.0/tests/test_schedule.py +158 -0
- pydaitem-0.1.0/tests/test_semantics.py +137 -0
- pydaitem-0.1.0/tests/test_system.py +182 -0
- pydaitem-0.1.0/tests/test_tokens.py +218 -0
- pydaitem-0.1.0/tests/test_units.py +144 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# PyPI publishing via trusted publishing (OIDC): no token to store.
|
|
2
|
+
#
|
|
3
|
+
# One-time setup at https://pypi.org/manage/account/publishing/: declare this repository
|
|
4
|
+
# as a trusted publisher for the "pydaitem" project, naming the workflow (publish.yml) and
|
|
5
|
+
# the environment (pypi).
|
|
6
|
+
#
|
|
7
|
+
# Ordering matters: publish the library BEFORE releasing the ha-daitem version that pins
|
|
8
|
+
# it, otherwise installation breaks for users.
|
|
9
|
+
|
|
10
|
+
name: Publish to PyPI
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
release:
|
|
14
|
+
types: [published]
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
|
|
27
|
+
- name: Build artifacts
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip build
|
|
30
|
+
python -m build
|
|
31
|
+
|
|
32
|
+
- uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: pypi
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write # required for trusted publishing
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
|
|
49
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Lint (ruff)
|
|
29
|
+
run: |
|
|
30
|
+
ruff check .
|
|
31
|
+
ruff format --check .
|
|
32
|
+
|
|
33
|
+
- name: Type check (mypy)
|
|
34
|
+
run: mypy src/pydaitem
|
|
35
|
+
|
|
36
|
+
- name: Unit tests
|
|
37
|
+
run: pytest -q
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Environments and build
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
|
|
8
|
+
# Python
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
|
|
15
|
+
# Secrets: never commit credentials, alarm codes or persisted tokens
|
|
16
|
+
.env
|
|
17
|
+
secrets.*
|
|
18
|
+
*.credentials
|
|
19
|
+
credentials.json
|
|
20
|
+
# Note: do not use a broad "credentials*" rule. It would also exclude source modules
|
|
21
|
+
# named credentials.py, which silently drops them from the built package.
|
|
22
|
+
*.token
|
|
23
|
+
.daitem.token
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
|
|
4
|
+
versioning.
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-09-13
|
|
7
|
+
|
|
8
|
+
First public release.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `DaitemClient`, the async low-level client for the private Daitem Secure API, grouped by
|
|
13
|
+
resource: `account`, `panel`, `commands`, `logbook`, `schedule`. Authentication uses
|
|
14
|
+
OAuth2 authorization_code + PKCE, driven headlessly. The client identifies itself
|
|
15
|
+
honestly rather than impersonating the mobile app: only `X-App-Name: eNova` is kept
|
|
16
|
+
as-is, a product routing key the gateway requires.
|
|
17
|
+
- `DaitemSystem`, the semantic façade binding a client, a system id and an alarm code, and
|
|
18
|
+
`connect()`, an async context manager yielding a ready one in one step. Grouped into
|
|
19
|
+
`capabilities`, `commands`, `schedule`, it owns the session protocol, preset indices and
|
|
20
|
+
group identifiers, so a change on the Daitem side never reaches consumer code.
|
|
21
|
+
- `PanelState`, `ArmMode` and `Fault`: semantic vocabularies owned by the library, mapped
|
|
22
|
+
from the raw API in one place. An unmapped value degrades to `UNKNOWN`
|
|
23
|
+
(`Anomalies.unknown_keys` for faults) with a one-off warning, rather than failing or
|
|
24
|
+
silently reading as absent.
|
|
25
|
+
- Full, presence/partial-preset and direct per-group arming and disarming
|
|
26
|
+
(`system.commands.*`), mirroring the mobile app. Commands are never retried: a replayed
|
|
27
|
+
request could arm or disarm twice.
|
|
28
|
+
- Recurring arm/disarm programs (`Schedule`, `ScheduleProgram`, `system.schedule.*`).
|
|
29
|
+
Owner-only, confirmed live: a restricted account gets `DaitemForbiddenError`. Creating a
|
|
30
|
+
new program has never been observed on the live API and is not supported; `set` only
|
|
31
|
+
updates an existing `id`.
|
|
32
|
+
- The exception hierarchy rooted at `DaitemError` covers every failure the library raises,
|
|
33
|
+
credential resolution included (`MissingCredentials`).
|
|
34
|
+
- Refresh token persistence through a pluggable `TokenStore` protocol, with
|
|
35
|
+
`FileTokenStore` (atomic write, mode `0600`) and `MemoryTokenStore`. A client with a
|
|
36
|
+
store resumes its session instead of replaying the Keycloak login form on every start.
|
|
37
|
+
- `resolve_credentials()`, resolving from arguments, environment variables then an env
|
|
38
|
+
file, and a `pydaitem` command line (`status`, `devices`, `presets`, `arm`, `disarm`,
|
|
39
|
+
`schedule ...`) with `--json` output and documented exit codes, so cron jobs, shell
|
|
40
|
+
scripts and MQTT bridges need no Python. Built on argparse, adding no dependency.
|
|
41
|
+
- `py.typed` (PEP 561), and a documented public API contract in the README separating the
|
|
42
|
+
stable surface from what tracks the API.
|
|
43
|
+
- `samples/`: runnable examples for reading state, persisting the token, and controlling
|
|
44
|
+
the alarm.
|
pydaitem-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yannick Olympio
|
|
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.
|
pydaitem-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pydaitem
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial async client for the Daitem Secure API (Atral Topaze platform)
|
|
5
|
+
Project-URL: Homepage, https://github.com/tomahoax/pydaitem
|
|
6
|
+
Project-URL: Issues, https://github.com/tomahoax/pydaitem/issues
|
|
7
|
+
Author: Yannick Olympio
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: alarm,atral,daitem,home-assistant,home-automation,topaze
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Home Automation
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: aiohttp>=3.13.4
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# pydaitem
|
|
29
|
+
|
|
30
|
+
**Unofficial** async Python client for the private Daitem Secure API (Atral "Topaze"
|
|
31
|
+
platform). It reads and controls a Daitem alarm from code, without going through the
|
|
32
|
+
mobile app.
|
|
33
|
+
|
|
34
|
+
Usable on its own from a script, a cron job, an MQTT bridge or the shell, and used as the
|
|
35
|
+
foundation of the [ha-daitem](https://github.com/tomahoax/ha-daitem) Home Assistant
|
|
36
|
+
integration. Nothing in the library depends on Home Assistant.
|
|
37
|
+
|
|
38
|
+
> **Not affiliated with Daitem or the Atral group.** The API it consumes is private,
|
|
39
|
+
> undocumented and non-contractual: it may change without notice with any app update. Use
|
|
40
|
+
> at your own risk, on your own hardware and with your own account.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install pydaitem
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Only dependency: `aiohttp`, already present in Home Assistant.
|
|
49
|
+
|
|
50
|
+
## Command line
|
|
51
|
+
|
|
52
|
+
Reading a state or driving the alarm from a shell, a cron job or a Node-RED flow needs no
|
|
53
|
+
Python:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pydaitem status --json
|
|
57
|
+
pydaitem devices
|
|
58
|
+
pydaitem presets
|
|
59
|
+
pydaitem arm --mode away --yes
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`presets` prints the raw partial-arming presets (`id` and `name`), the installation's own
|
|
63
|
+
wording before `capabilities.arm_modes()` resolves it into the stable `presence`/`partial`
|
|
64
|
+
vocabulary — useful to see what an installer named a preset, or what it was renamed to in
|
|
65
|
+
the app.
|
|
66
|
+
|
|
67
|
+
Credentials come from the environment or `--env-file`, and `--token-file` caches the
|
|
68
|
+
refresh token so repeated runs reuse a session.
|
|
69
|
+
|
|
70
|
+
Exit codes let a caller branch without parsing text, and are part of the compatibility
|
|
71
|
+
promise:
|
|
72
|
+
|
|
73
|
+
| Code | Meaning |
|
|
74
|
+
|------|---------|
|
|
75
|
+
| 0 | Success |
|
|
76
|
+
| 1 | Generic failure |
|
|
77
|
+
| 2 | Usage error, or confirmation impossible |
|
|
78
|
+
| 3 | Panel session held by another device |
|
|
79
|
+
| 4 | Authentication failure |
|
|
80
|
+
|
|
81
|
+
Code 3 is not a failure: it is the normal outcome when someone has the mobile app open, so
|
|
82
|
+
an automation should retry rather than alert.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Arm at night, tolerating someone using the app.
|
|
86
|
+
pydaitem arm --yes || [ $? -eq 3 ] && echo "retry later"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Arming and disarming ask for confirmation.** Without a terminal and without `--yes` the
|
|
90
|
+
command refuses rather than proceeding: a scheduled job must state its intent, and silently
|
|
91
|
+
arming a house because nobody could answer a prompt is the wrong default.
|
|
92
|
+
|
|
93
|
+
## Schedules
|
|
94
|
+
|
|
95
|
+
Recurring arm/disarm programs, the same feature the app calls "planning":
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pydaitem schedule list --json
|
|
99
|
+
pydaitem schedule set 1 --hour 23 --minute 30 --yes # only the given fields change
|
|
100
|
+
pydaitem schedule set 1 --disarm --yes
|
|
101
|
+
pydaitem schedule delete 3 --yes
|
|
102
|
+
pydaitem schedule activate --yes # master switch for the whole schedule
|
|
103
|
+
pydaitem schedule deactivate --yes
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Creating a new program is not supported.** It was never observed on the live API (no
|
|
107
|
+
official documentation exists to confirm the endpoint), so `schedule set` can only update
|
|
108
|
+
an existing program's `id` — read it from `schedule list` first. `set`, `delete`,
|
|
109
|
+
`activate` and `deactivate` ask for confirmation like `arm`/`disarm`: they drive the alarm
|
|
110
|
+
indirectly, by changing when it will arm or disarm itself.
|
|
111
|
+
|
|
112
|
+
**Owner-only, confirmed live.** A restricted account (the kind recommended for the
|
|
113
|
+
`ha-daitem` integration) gets a `DaitemForbiddenError` on every schedule call — use the
|
|
114
|
+
account's owner credentials instead.
|
|
115
|
+
|
|
116
|
+
## Usage from Python
|
|
117
|
+
|
|
118
|
+
The quickest path is `connect()`, which owns the client and picks the account's first
|
|
119
|
+
installation:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import asyncio
|
|
123
|
+
from pydaitem import connect
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def main() -> None:
|
|
127
|
+
async with connect("me@example.com", "password", "1234") as system:
|
|
128
|
+
status = await system.read_status()
|
|
129
|
+
print(status.panel_state, status.active_groups)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
asyncio.run(main())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For finer control, work through `DaitemSystem`. It hides the endpoints, the session protocol, preset indices
|
|
136
|
+
and group identifiers, so a change on the Daitem side is absorbed here rather than in your
|
|
137
|
+
code.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import asyncio
|
|
141
|
+
from pydaitem import DaitemClient, DaitemSystem
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def main() -> None:
|
|
145
|
+
async with DaitemClient("me@example.com", "password") as client:
|
|
146
|
+
systems = await client.account.list_systems()
|
|
147
|
+
system = DaitemSystem(client, systems[0].id, "1234")
|
|
148
|
+
|
|
149
|
+
status = await system.read_status()
|
|
150
|
+
print(status.panel_state, status.active_groups)
|
|
151
|
+
|
|
152
|
+
inventory = await system.read_inventory()
|
|
153
|
+
print(len(inventory.sensors), "detectors")
|
|
154
|
+
print(inventory.central_anomalies.faults)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
asyncio.run(main())
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Commands are single calls; the session is opened and released for you:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
await system.commands.arm_away()
|
|
164
|
+
await system.commands.arm_presence()
|
|
165
|
+
await system.commands.disarm()
|
|
166
|
+
|
|
167
|
+
# Only advertise what the installation supports.
|
|
168
|
+
modes = await system.capabilities.arm_modes()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Reusing a session between runs
|
|
172
|
+
|
|
173
|
+
By default every process start replays the Keycloak login form. Pass a token store and the
|
|
174
|
+
client resumes from the stored refresh token instead, falling back to a full login only if
|
|
175
|
+
that token is rejected.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from pydaitem import DaitemClient, FileTokenStore
|
|
179
|
+
|
|
180
|
+
store = FileTokenStore("~/.daitem.token")
|
|
181
|
+
async with DaitemClient(email, password, token_store=store) as client:
|
|
182
|
+
...
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`FileTokenStore` writes atomically with mode `0600`: the refresh token is a credential and
|
|
186
|
+
should be treated like a password. It does real disk I/O, so inside Home Assistant back the
|
|
187
|
+
store with the config entry instead; the protocol is async precisely so that blocking I/O
|
|
188
|
+
never reaches the event loop.
|
|
189
|
+
|
|
190
|
+
Credentials themselves can be resolved from arguments, environment variables then an env
|
|
191
|
+
file:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from pydaitem import resolve_credentials
|
|
195
|
+
|
|
196
|
+
creds = resolve_credentials(env_file="~/.daitem.env")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
See `samples/` for runnable examples.
|
|
200
|
+
|
|
201
|
+
## Public API contract
|
|
202
|
+
|
|
203
|
+
The point of this library is that **a change in the Daitem API should never force a change
|
|
204
|
+
in your code**. That only holds if you stay on the stable surface.
|
|
205
|
+
|
|
206
|
+
**Stable.** Breaking changes here get a major version bump.
|
|
207
|
+
|
|
208
|
+
- `DaitemSystem`: its plain reads (`read_status`, `read_inventory`, `system_id`), and its
|
|
209
|
+
resource namespaces `capabilities`, `commands`, `schedule`.
|
|
210
|
+
- `PanelState`, `ArmMode`, `Fault`: semantic vocabularies owned by this library.
|
|
211
|
+
- `TokenStore`, `FileTokenStore`, `MemoryTokenStore`, `connect()` and
|
|
212
|
+
`resolve_credentials`.
|
|
213
|
+
- The command line: its subcommands, its `--json` shape, and its exit codes.
|
|
214
|
+
- The exception hierarchy rooted at `DaitemError`.
|
|
215
|
+
- Model attributes and the semantic properties: `SystemStatus.panel_state`, `.is_armed`,
|
|
216
|
+
`.is_arming`, `.active_groups`, `Anomalies.faults`, `.has()`, `Inventory.sensors`,
|
|
217
|
+
`Inventory.controls`, and so on.
|
|
218
|
+
|
|
219
|
+
**Not stable.** These track the API and may change in any release.
|
|
220
|
+
|
|
221
|
+
- `DaitemClient` low-level methods, which mirror endpoints one for one.
|
|
222
|
+
- `SystemStatus.state`: the raw API string. Use `panel_state` instead.
|
|
223
|
+
- `Anomalies.flags` and `.active`: raw JSON keys. Use `faults` or `has()`.
|
|
224
|
+
- Any `.raw` payload, and anything underscore-prefixed.
|
|
225
|
+
- `SystemState`, deliberately absent from `pydaitem.__all__`.
|
|
226
|
+
|
|
227
|
+
**Unknown values degrade, they do not break.** A state this library does not know maps to
|
|
228
|
+
`PanelState.UNKNOWN`, an unmapped fault key lands in `Anomalies.unknown_keys`, and both log
|
|
229
|
+
a warning once. A Daitem update therefore shows up in your logs instead of silently
|
|
230
|
+
reading as "no state".
|
|
231
|
+
|
|
232
|
+
## API constraints worth knowing before you design around it
|
|
233
|
+
|
|
234
|
+
These are not library choices, they are measured properties of the API, and they shape
|
|
235
|
+
any integration.
|
|
236
|
+
|
|
237
|
+
**One session per panel, across all accounts.** Calling `connect` while another device
|
|
238
|
+
(typically the mobile app) holds the session returns `DaitemSessionBusyError`. Creating a
|
|
239
|
+
secondary account does not work around it. Hence `client.panel.session()`, which releases
|
|
240
|
+
as soon as possible, and `client.panel.get_status()`, which reads without opening anything
|
|
241
|
+
when it can.
|
|
242
|
+
|
|
243
|
+
**Reading the state requires an existing session**, opened by any device. Without one,
|
|
244
|
+
`client.panel.get_state()` raises `DaitemNoSessionError`. `get_status()` handles that case.
|
|
245
|
+
|
|
246
|
+
**No live per-detector state.** The inventory gives identity, group, inhibition and
|
|
247
|
+
**faults** (battery, tamper, radio, masking), but not the open/closed state of a contact.
|
|
248
|
+
An opening only shows up in the panel history, while the system is armed.
|
|
249
|
+
|
|
250
|
+
**Faults live in the inventory**, so `read_inventory()` must be called on every poll cycle
|
|
251
|
+
for fault reporting to stay live. It needs no panel session, so this is cheap.
|
|
252
|
+
|
|
253
|
+
**Browser-flow authentication.** The Keycloak realm refuses the `password` grant, so the
|
|
254
|
+
library drives the `authorization_code` + PKCE flow server-side, without a browser.
|
|
255
|
+
|
|
256
|
+
**The logbook and the schedule are owner-only.** A restricted account gets a 403 on both
|
|
257
|
+
(confirmed live for the schedule endpoints).
|
|
258
|
+
|
|
259
|
+
## System states
|
|
260
|
+
|
|
261
|
+
| `PanelState` | Meaning |
|
|
262
|
+
|--------------|---------|
|
|
263
|
+
| `DISARMED` | Disarmed |
|
|
264
|
+
| `ARMING` | An exit delay is running |
|
|
265
|
+
| `ARMED_FULL` | Every group armed |
|
|
266
|
+
| `ARMED_PRESENCE` | Partially armed through a named preset |
|
|
267
|
+
| `ARMED_GROUPS` | Partially armed by direct group activation |
|
|
268
|
+
| `TRIGGERED` | Alarm going off (raw value never captured yet) |
|
|
269
|
+
| `UNKNOWN` | A state this library does not know about |
|
|
270
|
+
|
|
271
|
+
## Client identification
|
|
272
|
+
|
|
273
|
+
The client identifies itself honestly rather than posing as the mobile app:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
X-App-Name: eNova
|
|
277
|
+
X-App-Platform: python
|
|
278
|
+
X-App-Version: <library version>
|
|
279
|
+
User-Agent: pydaitem/<version> (+https://github.com/tomahoax/pydaitem) aiohttp/<v> Python/<v>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`X-App-Name` keeps the app value because the server requires it to route to the right
|
|
283
|
+
product line; every other header is our own. This was verified by probing: without
|
|
284
|
+
`X-App-Name` the gateway returns 400, and any other value returns 500.
|
|
285
|
+
|
|
286
|
+
## Development
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
python -m venv .venv && source .venv/bin/activate
|
|
290
|
+
pip install -e ".[dev]"
|
|
291
|
+
pytest # unit tests, no network, no alarm
|
|
292
|
+
ruff check .
|
|
293
|
+
mypy src/pydaitem
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The bundled tests never call the API and never drive an alarm. `scripts/smoke.py` does
|
|
297
|
+
exercise the live API and is excluded from the published package.
|
|
298
|
+
|
|
299
|
+
## Licence
|
|
300
|
+
|
|
301
|
+
MIT.
|