entr-ble 1.0.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.
- entr_ble-1.0.0/LICENSE +21 -0
- entr_ble-1.0.0/PKG-INFO +89 -0
- entr_ble-1.0.0/README.md +74 -0
- entr_ble-1.0.0/pyproject.toml +55 -0
- entr_ble-1.0.0/pyproject.toml.orig +46 -0
- entr_ble-1.0.0/src/entr_ble/README.md +110 -0
- entr_ble-1.0.0/src/entr_ble/__init__.py +5 -0
- entr_ble-1.0.0/src/entr_ble/advertising.py +77 -0
- entr_ble-1.0.0/src/entr_ble/client/__init__.py +30 -0
- entr_ble-1.0.0/src/entr_ble/client/access.py +58 -0
- entr_ble-1.0.0/src/entr_ble/client/config.py +119 -0
- entr_ble-1.0.0/src/entr_ble/client/diagnostics.py +221 -0
- entr_ble-1.0.0/src/entr_ble/client/fields.py +266 -0
- entr_ble-1.0.0/src/entr_ble/client/pairing.py +225 -0
- entr_ble-1.0.0/src/entr_ble/client/status.py +103 -0
- entr_ble-1.0.0/src/entr_ble/client/transport.py +344 -0
- entr_ble-1.0.0/src/entr_ble/client/users.py +139 -0
- entr_ble-1.0.0/src/entr_ble/const.py +205 -0
- entr_ble-1.0.0/src/entr_ble/crypto.py +50 -0
- entr_ble-1.0.0/src/entr_ble/framing.py +173 -0
- entr_ble-1.0.0/src/entr_ble/session_crypto.py +56 -0
entr_ble-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johann Lejeune
|
|
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.
|
entr_ble-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entr-ble
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for ENTR Bluetooth locks
|
|
5
|
+
Author: Johann Lejeune
|
|
6
|
+
Author-email: Johann Lejeune <tofupedia.fr@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: bleak>=3.0.2
|
|
10
|
+
Requires-Dist: cryptography>=48.0.1
|
|
11
|
+
Requires-Python: >=3.14
|
|
12
|
+
Project-URL: Repository, https://github.com/johannlejeune/entr-ble
|
|
13
|
+
Project-URL: Issues, https://github.com/johannlejeune/entr-ble/issues
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# ENTR BLE
|
|
17
|
+
|
|
18
|
+
**Your ENTR lock, over Bluetooth.** A Python library, a command-line interface, and a Home Assistant integration sharing the same protocol implementation.
|
|
19
|
+
|
|
20
|
+
> [!WARNING]
|
|
21
|
+
> This project is fully vibe coded, but it is being used in a working Home Assistant installation.\
|
|
22
|
+
> It was created to integrate the lock into Home Assistant and get rid of the horrible mobile app.
|
|
23
|
+
|
|
24
|
+
[](https://github.com/johannlejeune/entr-ble/releases/latest)
|
|
25
|
+
[](https://github.com/johannlejeune/entr-ble/actions/workflows/check.yml)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
## Choose your starting point
|
|
29
|
+
|
|
30
|
+
| Use ENTR BLE to… | Start here | Component |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Control a lock from dashboards and automations | [Home Assistant guide](custom_components/entr_ble/README.md) | HACS custom integration |
|
|
33
|
+
| Set up a lock, manage keys, or control it from a terminal | [CLI guide](packages/entr-ble-cli/README.md) | `entr-ble-cli` command |
|
|
34
|
+
| Build your own application | [Python library guide](src/entr_ble/README.md) | `entr-ble` → `EntrLockClient` |
|
|
35
|
+
|
|
36
|
+
The library is independent of the CLI and Home Assistant. Both frontends use its asynchronous API for pairing, encrypted sessions, and lock operations.
|
|
37
|
+
|
|
38
|
+
## What you can do
|
|
39
|
+
|
|
40
|
+
- Discover nearby locks and initialize a new lock or recover owner access.
|
|
41
|
+
- Lock and unlock, read battery and reported door/lock status.
|
|
42
|
+
- Create, activate, suspend, and revoke user keys.
|
|
43
|
+
- Change settings, calibrate the lock and magnet, and read diagnostics on supported firmware.
|
|
44
|
+
- Import CLI credentials into Home Assistant without enrolling again.
|
|
45
|
+
|
|
46
|
+
See [feature coverage](docs/FEATURES.md) for the complete command matrix and firmware requirements, and [protocol notes](docs/PROTOCOL.md) for the underlying BLE protocol.
|
|
47
|
+
|
|
48
|
+
## Before you start
|
|
49
|
+
|
|
50
|
+
The Python packages require **Python 3.14+** and a working Bluetooth adapter; the HACS integration requires **Home Assistant 2026.9.3+** and connectable Bluetooth coverage.
|
|
51
|
+
|
|
52
|
+
ENTR has a single owner slot. Recovering ownership replaces the previous owner's credential. To keep the mobile app as owner, redeem an additional user key instead. Each component's guide explains the choice before setup.
|
|
53
|
+
|
|
54
|
+
Hardware observations come from an **ENTR EURO with communication version 1.29r5**. Firmware support varies, and reported state can be stale after manual operation. This is an independent project, unaffiliated with ASSA ABLOY.
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
This repository is a uv workspace:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
src/entr_ble/ Python library
|
|
62
|
+
packages/entr-ble-cli/ CLI and local credential store
|
|
63
|
+
custom_components/entr_ble/ Home Assistant integration
|
|
64
|
+
docs/ Feature coverage and protocol reference
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
From the repository root, install all packages and the isolated Home Assistant development dependencies. The full suite requires Python 3.14.2+.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
uv sync --locked --all-packages --group hacs
|
|
71
|
+
uv run --no-sync ruff check .
|
|
72
|
+
uv run --no-sync ruff format --check .
|
|
73
|
+
uv run --no-sync basedpyright
|
|
74
|
+
uv run --no-sync python -m unittest discover -s tests -v
|
|
75
|
+
uv run --no-sync python -m unittest discover -s packages/entr-ble-cli/tests -v
|
|
76
|
+
uv build --all-packages --no-sources
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Tests simulate BLE responses and do not operate a physical lock. The [CI workflow](.github/workflows/check.yml) also validates the integration with Hassfest and HACS. Builds produce separate library and CLI wheels and source distributions in `dist/`; hardware behavior still needs verification on the target lock.
|
|
80
|
+
|
|
81
|
+
## Releases
|
|
82
|
+
|
|
83
|
+
All components release together with one version and [changelog](CHANGELOG.md). Release Please prepares a release PR; merging it runs the checks, publishes both Python packages to PyPI, and publishes the matching GitHub release for HACS. See [release setup and recovery](docs/RELEASING.md).
|
|
84
|
+
|
|
85
|
+
## Support and license
|
|
86
|
+
|
|
87
|
+
[Report an issue](https://github.com/johannlejeune/entr-ble/issues) with the component, software version, lock model, communication version if known, and the command or action that failed. Remove credentials and passwords from anything you share.
|
|
88
|
+
|
|
89
|
+
Licensed under the [MIT License](LICENSE).
|
entr_ble-1.0.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ENTR BLE
|
|
2
|
+
|
|
3
|
+
**Your ENTR lock, over Bluetooth.** A Python library, a command-line interface, and a Home Assistant integration sharing the same protocol implementation.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> This project is fully vibe coded, but it is being used in a working Home Assistant installation.\
|
|
7
|
+
> It was created to integrate the lock into Home Assistant and get rid of the horrible mobile app.
|
|
8
|
+
|
|
9
|
+
[](https://github.com/johannlejeune/entr-ble/releases/latest)
|
|
10
|
+
[](https://github.com/johannlejeune/entr-ble/actions/workflows/check.yml)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
|
|
13
|
+
## Choose your starting point
|
|
14
|
+
|
|
15
|
+
| Use ENTR BLE to… | Start here | Component |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Control a lock from dashboards and automations | [Home Assistant guide](custom_components/entr_ble/README.md) | HACS custom integration |
|
|
18
|
+
| Set up a lock, manage keys, or control it from a terminal | [CLI guide](packages/entr-ble-cli/README.md) | `entr-ble-cli` command |
|
|
19
|
+
| Build your own application | [Python library guide](src/entr_ble/README.md) | `entr-ble` → `EntrLockClient` |
|
|
20
|
+
|
|
21
|
+
The library is independent of the CLI and Home Assistant. Both frontends use its asynchronous API for pairing, encrypted sessions, and lock operations.
|
|
22
|
+
|
|
23
|
+
## What you can do
|
|
24
|
+
|
|
25
|
+
- Discover nearby locks and initialize a new lock or recover owner access.
|
|
26
|
+
- Lock and unlock, read battery and reported door/lock status.
|
|
27
|
+
- Create, activate, suspend, and revoke user keys.
|
|
28
|
+
- Change settings, calibrate the lock and magnet, and read diagnostics on supported firmware.
|
|
29
|
+
- Import CLI credentials into Home Assistant without enrolling again.
|
|
30
|
+
|
|
31
|
+
See [feature coverage](docs/FEATURES.md) for the complete command matrix and firmware requirements, and [protocol notes](docs/PROTOCOL.md) for the underlying BLE protocol.
|
|
32
|
+
|
|
33
|
+
## Before you start
|
|
34
|
+
|
|
35
|
+
The Python packages require **Python 3.14+** and a working Bluetooth adapter; the HACS integration requires **Home Assistant 2026.9.3+** and connectable Bluetooth coverage.
|
|
36
|
+
|
|
37
|
+
ENTR has a single owner slot. Recovering ownership replaces the previous owner's credential. To keep the mobile app as owner, redeem an additional user key instead. Each component's guide explains the choice before setup.
|
|
38
|
+
|
|
39
|
+
Hardware observations come from an **ENTR EURO with communication version 1.29r5**. Firmware support varies, and reported state can be stale after manual operation. This is an independent project, unaffiliated with ASSA ABLOY.
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
This repository is a uv workspace:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
src/entr_ble/ Python library
|
|
47
|
+
packages/entr-ble-cli/ CLI and local credential store
|
|
48
|
+
custom_components/entr_ble/ Home Assistant integration
|
|
49
|
+
docs/ Feature coverage and protocol reference
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
From the repository root, install all packages and the isolated Home Assistant development dependencies. The full suite requires Python 3.14.2+.
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uv sync --locked --all-packages --group hacs
|
|
56
|
+
uv run --no-sync ruff check .
|
|
57
|
+
uv run --no-sync ruff format --check .
|
|
58
|
+
uv run --no-sync basedpyright
|
|
59
|
+
uv run --no-sync python -m unittest discover -s tests -v
|
|
60
|
+
uv run --no-sync python -m unittest discover -s packages/entr-ble-cli/tests -v
|
|
61
|
+
uv build --all-packages --no-sources
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Tests simulate BLE responses and do not operate a physical lock. The [CI workflow](.github/workflows/check.yml) also validates the integration with Hassfest and HACS. Builds produce separate library and CLI wheels and source distributions in `dist/`; hardware behavior still needs verification on the target lock.
|
|
65
|
+
|
|
66
|
+
## Releases
|
|
67
|
+
|
|
68
|
+
All components release together with one version and [changelog](CHANGELOG.md). Release Please prepares a release PR; merging it runs the checks, publishes both Python packages to PyPI, and publishes the matching GitHub release for HACS. See [release setup and recovery](docs/RELEASING.md).
|
|
69
|
+
|
|
70
|
+
## Support and license
|
|
71
|
+
|
|
72
|
+
[Report an issue](https://github.com/johannlejeune/entr-ble/issues) with the component, software version, lock model, communication version if known, and the command or action that failed. Remove credentials and passwords from anything you share.
|
|
73
|
+
|
|
74
|
+
Licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "entr-ble"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python client for ENTR Bluetooth locks"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"bleak>=3.0.2",
|
|
11
|
+
"cryptography>=48.0.1",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "Johann Lejeune"
|
|
16
|
+
email = "tofupedia.fr@gmail.com"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/johannlejeune/entr-ble"
|
|
20
|
+
Issues = "https://github.com/johannlejeune/entr-ble/issues"
|
|
21
|
+
|
|
22
|
+
[tool.ruff.lint.per-file-ignores]
|
|
23
|
+
"src/entr_ble/client/*.py" = ["PLR0402"]
|
|
24
|
+
|
|
25
|
+
[tool.basedpyright]
|
|
26
|
+
typeCheckingMode = "standard"
|
|
27
|
+
include = [
|
|
28
|
+
"src",
|
|
29
|
+
"packages/entr-ble-cli/src",
|
|
30
|
+
"custom_components",
|
|
31
|
+
"tests",
|
|
32
|
+
"packages/entr-ble-cli/tests",
|
|
33
|
+
]
|
|
34
|
+
extraPaths = [
|
|
35
|
+
"src",
|
|
36
|
+
"packages/entr-ble-cli/src",
|
|
37
|
+
".",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.uv.workspace]
|
|
41
|
+
members = ["packages/entr-ble-cli"]
|
|
42
|
+
|
|
43
|
+
[tool.uv.dependency-groups.hacs]
|
|
44
|
+
requires-python = ">=3.14.2"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
48
|
+
build-backend = "uv_build"
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"basedpyright>=1.40.1",
|
|
53
|
+
"ruff>=0.16.9",
|
|
54
|
+
]
|
|
55
|
+
hacs = ["homeassistant==2026.9.4"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "entr-ble"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python client for ENTR Bluetooth locks"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Johann Lejeune", email = "tofupedia.fr@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"bleak>=3.0.2",
|
|
14
|
+
"cryptography>=48.0.1",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Repository = "https://github.com/johannlejeune/entr-ble"
|
|
19
|
+
Issues = "https://github.com/johannlejeune/entr-ble/issues"
|
|
20
|
+
|
|
21
|
+
[tool.ruff.lint.per-file-ignores]
|
|
22
|
+
"src/entr_ble/client/*.py" = ["PLR0402"]
|
|
23
|
+
|
|
24
|
+
[tool.basedpyright]
|
|
25
|
+
typeCheckingMode = "standard"
|
|
26
|
+
include = ["src", "packages/entr-ble-cli/src", "custom_components", "tests", "packages/entr-ble-cli/tests"]
|
|
27
|
+
extraPaths = ["src", "packages/entr-ble-cli/src", "."]
|
|
28
|
+
|
|
29
|
+
[tool.uv.workspace]
|
|
30
|
+
members = ["packages/entr-ble-cli"]
|
|
31
|
+
|
|
32
|
+
[tool.uv.dependency-groups]
|
|
33
|
+
hacs = { requires-python = ">=3.14.2" }
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"basedpyright>=1.40.1",
|
|
42
|
+
"ruff>=0.16.9",
|
|
43
|
+
]
|
|
44
|
+
hacs = [
|
|
45
|
+
"homeassistant==2026.9.4",
|
|
46
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ENTR BLE · Python library
|
|
2
|
+
|
|
3
|
+
**An asynchronous Python client for ENTR Bluetooth locks.** Discover locks, provision credentials, restore encrypted sessions, and send lock commands using Bleak.
|
|
4
|
+
|
|
5
|
+
[Project overview](../../README.md) · [CLI](../../packages/entr-ble-cli/README.md) · [Home Assistant](../../custom_components/entr_ble/README.md) · [Protocol reference](../../docs/PROTOCOL.md)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Requires **Python 3.14+** and a working Bluetooth adapter. On Linux, Bluetooth access uses BlueZ and the system D-Bus.
|
|
10
|
+
|
|
11
|
+
Add the library to your project:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
uv add entr-ble
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For environments using pip, use `pip install entr-ble`.
|
|
18
|
+
|
|
19
|
+
From a checkout, install the library into the repository's virtual environment:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
uv sync --locked
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
To install directly from the repository into your own environment:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pip install "git+https://github.com/johannlejeune/entr-ble.git"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package is named `entr-ble`; import it as `entr_ble`. For a terminal command and credential storage, install the separate [CLI](../../packages/entr-ble-cli/README.md).
|
|
32
|
+
|
|
33
|
+
## Read status with an existing key
|
|
34
|
+
|
|
35
|
+
`EntrLockClient` accepts a Bluetooth address or a Bleak `BLEDevice`. Connect, read the communication version, then restore the saved session before issuing encrypted commands:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from entr_ble import EntrLockClient
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def read_status(address, kdf_id, role, aes_key):
|
|
42
|
+
client = EntrLockClient(address)
|
|
43
|
+
try:
|
|
44
|
+
await client.connect()
|
|
45
|
+
await client.fetch_comm_version()
|
|
46
|
+
await client.kdf_resync(kdf_id, role, aes_key)
|
|
47
|
+
return await client.get_device_config()
|
|
48
|
+
finally:
|
|
49
|
+
await client.disconnect()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Call this function from your application's event loop. `aes_key` is the saved **16-byte key**; decode hexadecimal storage with `bytes.fromhex()` before passing it. `kdf_id` and `role` are the values returned during provisioning.
|
|
53
|
+
|
|
54
|
+
Locking and unlocking also require the saved user id, application id, and BLE lock key. Within the restored session, use:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
await client.unlock(user_id, app_id, ble_ekey)
|
|
58
|
+
await client.lock(user_id, app_id, ble_ekey)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The ids are each 16 bytes, and `ble_ekey` is 32 bytes. Keep one command in flight per client and disconnect after use. After reconnecting, restore the session again.
|
|
62
|
+
|
|
63
|
+
## Provisioning and credentials
|
|
64
|
+
|
|
65
|
+
For a new credential, the protocol starts with `pair()` and `handshake(app_id)`, followed by `set_owner()`, `recover_owner()`, or `get_new_key()`. These methods acknowledge provisioning responses; your application must persist the returned credential fields and the session key.
|
|
66
|
+
|
|
67
|
+
**Recovering the owner slot revokes the previous owner's credential.** Redeem an additional user key to preserve the existing owner. See [initial setup](../../docs/FEATURES.md#initial-setup) and the [CLI setup guide](../../packages/entr-ble-cli/README.md#2-choose-how-to-access-the-lock) for the available paths.
|
|
68
|
+
|
|
69
|
+
Credentials grant access to the lock. The library leaves persistence to the caller; protect keys and keep them out of logs and version control.
|
|
70
|
+
|
|
71
|
+
## API guide
|
|
72
|
+
|
|
73
|
+
Every public method and helper has a docstring describing inputs, results, and restrictions. Start with `help(EntrLockClient)` or `help(EntrLockClient.unlock)`. Command docstrings include protocol names and decimal command numbers, matching [const.py](const.py).
|
|
74
|
+
|
|
75
|
+
| Area | Implementation |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| Pairing, owner enrollment, key activation, session recovery | [client/pairing.py](client/pairing.py) |
|
|
78
|
+
| Lock and unlock | [client/access.py](client/access.py) |
|
|
79
|
+
| Configuration, battery, serial number, device info | [client/status.py](client/status.py) |
|
|
80
|
+
| User keys and permissions | [client/users.py](client/users.py) |
|
|
81
|
+
| Settings, calibration, factory reset | [client/config.py](client/config.py) |
|
|
82
|
+
| Clock, audit trail, error log | [client/diagnostics.py](client/diagnostics.py) |
|
|
83
|
+
| Field encoding and returned dictionary definitions | [client/fields.py](client/fields.py) |
|
|
84
|
+
| Identify locks from BLE advertisements without connecting | [advertising.py](advertising.py) |
|
|
85
|
+
|
|
86
|
+
See [feature coverage](../../docs/FEATURES.md) for firmware restrictions. Reported status may be stale after manual operation; see [protocol notes](../../docs/PROTOCOL.md).
|
|
87
|
+
|
|
88
|
+
## Errors and timeouts
|
|
89
|
+
|
|
90
|
+
| Exception | Meaning |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `ValueError` | Invalid field length or encoding |
|
|
93
|
+
| `EntrLockError` | Lock rejection; exposes numeric `category` and `detail` |
|
|
94
|
+
| `EntrProtocolError` | Malformed/unexpected response or missing session prerequisite; parent of `EntrLockError` |
|
|
95
|
+
| `TimeoutError` | No protocol response within eight seconds |
|
|
96
|
+
| Bleak backend exceptions | Bluetooth connection or I/O failure |
|
|
97
|
+
|
|
98
|
+
The constructor's `timeout` controls the Bleak **connection timeout**, separately from the protocol response timeout. Backend errors propagate to the caller.
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
From the repository root, run the library tests without Bluetooth hardware:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
uv run --no-sync python -m unittest discover -s tests -p 'test_library*.py' -v
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See the [development guide](../../README.md#development) for workspace setup, integration tests, and packaging.
|
|
109
|
+
|
|
110
|
+
Licensed under the [MIT License](../../LICENSE).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Decoding of the ENTR BLE advertisement.
|
|
2
|
+
|
|
3
|
+
Locks broadcast their name and initialisation state in the clear, so a scan can identify
|
|
4
|
+
them without connecting or authenticating.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from . import const
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class LockAdvertisement:
|
|
14
|
+
"""A discovered lock's BLE address, decoded name, raw state/version nibbles, and
|
|
15
|
+
RSSI in dBm.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
address: str
|
|
19
|
+
name: str
|
|
20
|
+
lock_state: int
|
|
21
|
+
advert_version: int
|
|
22
|
+
rssi: int
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def state_name(self) -> str:
|
|
26
|
+
"""Return the known state label, or ``unknown (<value>)`` for an unrecognized
|
|
27
|
+
state.
|
|
28
|
+
"""
|
|
29
|
+
return const.LOCK_STATE_NAMES.get(
|
|
30
|
+
self.lock_state, f"unknown ({self.lock_state})"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# The name sits in a fixed-width field: space padded, and followed by a trailing
|
|
35
|
+
# control byte that belongs to neither the name nor the padding.
|
|
36
|
+
PADDING = bytes(range(0x21))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_advertisement(address: str, adv) -> LockAdvertisement | None:
|
|
40
|
+
"""Return the first ENTR manufacturer payload as a ``LockAdvertisement``, or
|
|
41
|
+
``None`` when none matches.
|
|
42
|
+
|
|
43
|
+
``adv`` is a Bleak advertisement or an object exposing ``manufacturer_data``,
|
|
44
|
+
``local_name``, and ``rssi``. Empty payloads and product IDs without a following
|
|
45
|
+
state/version byte are skipped. Version 1 supplies an encoded name when present;
|
|
46
|
+
other layouts use the stripped local name, falling back to ``?``. Name decoding
|
|
47
|
+
replaces invalid characters. ``address`` is copied unchanged.
|
|
48
|
+
"""
|
|
49
|
+
for payload in adv.manufacturer_data.values():
|
|
50
|
+
if not payload or payload[0] != const.MANUFACTURER_PRODUCT_ID:
|
|
51
|
+
continue
|
|
52
|
+
custom = payload[1:]
|
|
53
|
+
if not custom:
|
|
54
|
+
continue
|
|
55
|
+
advert_version = (custom[0] >> 4) & 0x0F
|
|
56
|
+
lock_state = custom[0] & 0x0F
|
|
57
|
+
if advert_version == 1 and len(custom) > 4:
|
|
58
|
+
name = _decode_name(custom[3], custom[4:])
|
|
59
|
+
else:
|
|
60
|
+
name = (adv.local_name or "").strip() or "?"
|
|
61
|
+
return LockAdvertisement(
|
|
62
|
+
address=address,
|
|
63
|
+
name=name,
|
|
64
|
+
lock_state=lock_state,
|
|
65
|
+
advert_version=advert_version,
|
|
66
|
+
rssi=adv.rssi,
|
|
67
|
+
)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _decode_name(encoding_byte: int, raw: bytes) -> str:
|
|
72
|
+
# Use latin-1 for unknown encoding signs so every byte remains decodable.
|
|
73
|
+
encoding = const.LOCK_NAME_ENCODINGS.get(encoding_byte, "latin-1")
|
|
74
|
+
try:
|
|
75
|
+
return raw.strip(PADDING).decode(encoding, errors="replace")
|
|
76
|
+
except LookupError:
|
|
77
|
+
return raw.strip(PADDING).decode("latin-1", errors="replace")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .access import Access
|
|
2
|
+
from .config import Config
|
|
3
|
+
from .diagnostics import Diagnostics
|
|
4
|
+
from .fields import build_lock_name, settings_status_byte, user_id_bytes
|
|
5
|
+
from .pairing import Pairing
|
|
6
|
+
from .status import Status
|
|
7
|
+
from .transport import EntrLockError, EntrProtocolError
|
|
8
|
+
from .users import Users
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EntrLockClient(Pairing, Access, Status, Users, Config, Diagnostics):
|
|
12
|
+
"""Asynchronous ENTR lock client combining provisioning, access, status, users,
|
|
13
|
+
configuration and diagnostics.
|
|
14
|
+
|
|
15
|
+
Accepts a Bluetooth address or Bleak BLEDevice and an optional connection timeout in
|
|
16
|
+
seconds. Call connect(), then pair() and handshake() for provisioning or
|
|
17
|
+
kdf_resync() with saved credentials for normal use. Run one command at a time and
|
|
18
|
+
call disconnect() in a finally block. See TransportClient for shared session state
|
|
19
|
+
and command errors.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"EntrLockClient",
|
|
25
|
+
"EntrLockError",
|
|
26
|
+
"EntrProtocolError",
|
|
27
|
+
"build_lock_name",
|
|
28
|
+
"settings_status_byte",
|
|
29
|
+
"user_id_bytes",
|
|
30
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import entr_ble.const as const
|
|
2
|
+
|
|
3
|
+
from .fields import fixed_bytes
|
|
4
|
+
from .transport import EntrProtocolError, TransportClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Access(TransportClient):
|
|
8
|
+
"""Credential-based physical lock operations over an established session."""
|
|
9
|
+
|
|
10
|
+
async def unlock(
|
|
11
|
+
self, user_id: bytes, app_id: bytes, ble_ekey: bytes, mode: int | None = None
|
|
12
|
+
) -> None:
|
|
13
|
+
"""UNLOCK (17): unlock using a 16-byte user ID, 16-byte application ID and
|
|
14
|
+
32-byte BLE key.
|
|
15
|
+
|
|
16
|
+
Requires an established session and an active credential. Mode 0 selects
|
|
17
|
+
auto-lock and mode 1 manual operation; omitted mode follows cached status,
|
|
18
|
+
falling back to manual when status is unavailable. Returns None after an
|
|
19
|
+
operation-status response or explicit success; a missing confirmation raises
|
|
20
|
+
EntrProtocolError and lock rejection raises EntrLockError. Invalid field lengths
|
|
21
|
+
raise ValueError.
|
|
22
|
+
"""
|
|
23
|
+
if mode is None:
|
|
24
|
+
# Derive the mode from auto-lock: enabled sends 0, disabled sends 1.
|
|
25
|
+
auto_lock = self.status["auto_lock"] if self.status else False
|
|
26
|
+
mode = const.MODE_AUTO if auto_lock else const.MODE_MANUAL
|
|
27
|
+
fields = (
|
|
28
|
+
fixed_bytes(user_id, 16, "user id")
|
|
29
|
+
+ fixed_bytes(app_id, 16, "application id")
|
|
30
|
+
+ fixed_bytes(ble_ekey, 32, "lock key")
|
|
31
|
+
+ bytes([mode])
|
|
32
|
+
)
|
|
33
|
+
await self._operate(const.CMD_UNLOCK, fields)
|
|
34
|
+
|
|
35
|
+
async def lock(self, user_id: bytes, app_id: bytes, ble_ekey: bytes) -> None:
|
|
36
|
+
"""LOCK (18): lock using a 16-byte user ID, 16-byte application ID and 32-byte
|
|
37
|
+
BLE key.
|
|
38
|
+
|
|
39
|
+
Requires an established session and an active credential; the operation always
|
|
40
|
+
sends mode 0. Returns None after an operation-status response or explicit
|
|
41
|
+
success; a missing confirmation raises EntrProtocolError and lock rejection
|
|
42
|
+
raises EntrLockError. Invalid field lengths raise ValueError.
|
|
43
|
+
"""
|
|
44
|
+
# Lock always uses mode 0.
|
|
45
|
+
fields = (
|
|
46
|
+
fixed_bytes(user_id, 16, "user id")
|
|
47
|
+
+ fixed_bytes(app_id, 16, "application id")
|
|
48
|
+
+ fixed_bytes(ble_ekey, 32, "lock key")
|
|
49
|
+
+ bytes([const.MODE_AUTO])
|
|
50
|
+
)
|
|
51
|
+
await self._operate(const.CMD_LOCK, fields)
|
|
52
|
+
|
|
53
|
+
async def _operate(self, command, fields):
|
|
54
|
+
outer_command, payload = await self._exchange_encrypted(command, fields)
|
|
55
|
+
if outer_command != const.CMD_OP_STATUS and not self._is_explicit_success(
|
|
56
|
+
outer_command, payload, command
|
|
57
|
+
):
|
|
58
|
+
raise EntrProtocolError(f"lock did not confirm command {command}")
|