ttlock-ble 0.1.1__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.
- ttlock_ble-0.1.1/.github/workflows/codeql.yml +41 -0
- ttlock_ble-0.1.1/.github/workflows/lint.yml +57 -0
- ttlock_ble-0.1.1/.github/workflows/release.yml +66 -0
- ttlock_ble-0.1.1/.github/workflows/tests.yml +33 -0
- ttlock_ble-0.1.1/.gitignore +14 -0
- ttlock_ble-0.1.1/.idea/.gitignore +3 -0
- ttlock_ble-0.1.1/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- ttlock_ble-0.1.1/.idea/misc.xml +6 -0
- ttlock_ble-0.1.1/.idea/modules.xml +8 -0
- ttlock_ble-0.1.1/.idea/pyProjectModel.xml +6 -0
- ttlock_ble-0.1.1/.idea/ttlock-ble.iml +19 -0
- ttlock_ble-0.1.1/.idea/vcs.xml +6 -0
- ttlock_ble-0.1.1/.pre-commit-config.yaml +23 -0
- ttlock_ble-0.1.1/.release-please-manifest.json +3 -0
- ttlock_ble-0.1.1/.ruff.toml +61 -0
- ttlock_ble-0.1.1/CHANGELOG.md +8 -0
- ttlock_ble-0.1.1/CLAUDE.md +28 -0
- ttlock_ble-0.1.1/CODE_STYLE.md +209 -0
- ttlock_ble-0.1.1/PKG-INFO +27 -0
- ttlock_ble-0.1.1/mypy.ini +22 -0
- ttlock_ble-0.1.1/pyproject.toml +56 -0
- ttlock_ble-0.1.1/pytest.ini +9 -0
- ttlock_ble-0.1.1/release-please-config.json +27 -0
- ttlock_ble-0.1.1/scripts/lint +11 -0
- ttlock_ble-0.1.1/src/ttlock_ble/__init__.py +40 -0
- ttlock_ble-0.1.1/src/ttlock_ble/_cloud_helpers.py +65 -0
- ttlock_ble-0.1.1/src/ttlock_ble/cli.py +193 -0
- ttlock_ble-0.1.1/src/ttlock_ble/client.py +524 -0
- ttlock_ble-0.1.1/src/ttlock_ble/cloud.py +254 -0
- ttlock_ble-0.1.1/src/ttlock_ble/commands.py +491 -0
- ttlock_ble-0.1.1/src/ttlock_ble/constants/__init__.py +14 -0
- ttlock_ble-0.1.1/src/ttlock_ble/constants/auto_lock_operate.py +12 -0
- ttlock_ble-0.1.1/src/ttlock_ble/constants/keyboard_pwd_type.py +14 -0
- ttlock_ble-0.1.1/src/ttlock_ble/constants/log_operate.py +48 -0
- ttlock_ble-0.1.1/src/ttlock_ble/constants/pwd_operate_type.py +15 -0
- ttlock_ble-0.1.1/src/ttlock_ble/crypto.py +132 -0
- ttlock_ble-0.1.1/src/ttlock_ble/exceptions/__init__.py +8 -0
- ttlock_ble-0.1.1/src/ttlock_ble/exceptions/cloud.py +18 -0
- ttlock_ble-0.1.1/src/ttlock_ble/exceptions/ttlock.py +7 -0
- ttlock_ble-0.1.1/src/ttlock_ble/models/__init__.py +19 -0
- ttlock_ble-0.1.1/src/ttlock_ble/models/cloud_credentials.py +14 -0
- ttlock_ble-0.1.1/src/ttlock_ble/models/lock_event.py +29 -0
- ttlock_ble-0.1.1/src/ttlock_ble/models/lock_version.py +58 -0
- ttlock_ble-0.1.1/src/ttlock_ble/models/log_entry.py +34 -0
- ttlock_ble-0.1.1/src/ttlock_ble/models/site_info.py +34 -0
- ttlock_ble-0.1.1/src/ttlock_ble/models/virtual_key.py +108 -0
- ttlock_ble-0.1.1/src/ttlock_ble/protocol/__init__.py +16 -0
- ttlock_ble-0.1.1/src/ttlock_ble/protocol/constants.py +9 -0
- ttlock_ble-0.1.1/src/ttlock_ble/protocol/frame.py +145 -0
- ttlock_ble-0.1.1/src/ttlock_ble/protocol/reassembler.py +44 -0
- ttlock_ble-0.1.1/src/ttlock_ble/py.typed +0 -0
- ttlock_ble-0.1.1/tests/__init__.py +0 -0
- ttlock_ble-0.1.1/tests/test_client.py +124 -0
- ttlock_ble-0.1.1/tests/test_crypto.py +111 -0
- ttlock_ble-0.1.1/tests/test_protocol.py +282 -0
- ttlock_ble-0.1.1/uv.lock +1342 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
schedule:
|
|
11
|
+
- cron: "0 0 * * 0"
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
actions: read
|
|
15
|
+
contents: read
|
|
16
|
+
security-events: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
analyze:
|
|
20
|
+
name: Analyze (${{ matrix.language }})
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
language:
|
|
26
|
+
- python
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout the repository
|
|
30
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
31
|
+
|
|
32
|
+
- name: Initialize CodeQL
|
|
33
|
+
uses: github/codeql-action/init@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3
|
|
34
|
+
with:
|
|
35
|
+
languages: ${{ matrix.language }}
|
|
36
|
+
queries: security-extended
|
|
37
|
+
|
|
38
|
+
- name: Perform CodeQL Analysis
|
|
39
|
+
uses: github/codeql-action/analyze@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3
|
|
40
|
+
with:
|
|
41
|
+
category: "/language:${{ matrix.language }}"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- "main"
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- "main"
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
ruff:
|
|
15
|
+
name: "Ruff"
|
|
16
|
+
runs-on: "ubuntu-latest"
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout the repository
|
|
19
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v3
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.14
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: uv sync --group dev
|
|
31
|
+
|
|
32
|
+
- name: Lint
|
|
33
|
+
run: uv run ruff check .
|
|
34
|
+
|
|
35
|
+
- name: Format
|
|
36
|
+
run: uv run ruff format . --check
|
|
37
|
+
|
|
38
|
+
mypy:
|
|
39
|
+
name: "Mypy"
|
|
40
|
+
runs-on: "ubuntu-latest"
|
|
41
|
+
steps:
|
|
42
|
+
- name: Checkout the repository
|
|
43
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
44
|
+
|
|
45
|
+
- name: Install uv
|
|
46
|
+
uses: astral-sh/setup-uv@v3
|
|
47
|
+
with:
|
|
48
|
+
enable-cache: true
|
|
49
|
+
|
|
50
|
+
- name: Set up Python
|
|
51
|
+
run: uv python install 3.14
|
|
52
|
+
|
|
53
|
+
- name: Install dependencies
|
|
54
|
+
run: uv sync --group dev
|
|
55
|
+
|
|
56
|
+
- name: Type check
|
|
57
|
+
run: uv run mypy src
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
tag:
|
|
10
|
+
description: "Tag to (re)publish to PyPI, e.g. v1.1.0. Skips release-please."
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
pull-requests: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release-please:
|
|
20
|
+
name: Release Please
|
|
21
|
+
if: github.event_name == 'push'
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
outputs:
|
|
24
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
25
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
26
|
+
steps:
|
|
27
|
+
- name: Run release-please
|
|
28
|
+
id: release
|
|
29
|
+
uses: googleapis/release-please-action@v5
|
|
30
|
+
with:
|
|
31
|
+
config-file: release-please-config.json
|
|
32
|
+
manifest-file: .release-please-manifest.json
|
|
33
|
+
|
|
34
|
+
publish-pypi:
|
|
35
|
+
name: Publish to PyPI
|
|
36
|
+
needs: release-please
|
|
37
|
+
if: |
|
|
38
|
+
always() && (
|
|
39
|
+
(github.event_name == 'push' && needs.release-please.outputs.release_created == 'true') ||
|
|
40
|
+
github.event_name == 'workflow_dispatch'
|
|
41
|
+
)
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/p/ttlock-ble
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write
|
|
48
|
+
steps:
|
|
49
|
+
- name: Checkout the released tag
|
|
50
|
+
uses: actions/checkout@v4
|
|
51
|
+
with:
|
|
52
|
+
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }}
|
|
53
|
+
|
|
54
|
+
- name: Set up Python
|
|
55
|
+
uses: actions/setup-python@v5
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.14"
|
|
58
|
+
|
|
59
|
+
- name: Install build tooling
|
|
60
|
+
run: python -m pip install --upgrade build
|
|
61
|
+
|
|
62
|
+
- name: Build sdist + wheel
|
|
63
|
+
run: python -m build
|
|
64
|
+
|
|
65
|
+
- name: Publish distribution to PyPI
|
|
66
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- "main"
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- "main"
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
pytest:
|
|
15
|
+
name: "Pytest"
|
|
16
|
+
runs-on: "ubuntu-latest"
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout the repository
|
|
19
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v3
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.14
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: uv sync --group dev
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: uv run pytest
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/ttlock-ble.iml" filepath="$PROJECT_DIR$/.idea/ttlock-ble.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="PYTHON_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/decompiled" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="jdk" jdkName="uv (ttlock-ble)" jdkType="Python SDK" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
<component name="PyDocumentationSettings">
|
|
13
|
+
<option name="format" value="PLAIN" />
|
|
14
|
+
<option name="myDocStringFormat" value="Plain" />
|
|
15
|
+
</component>
|
|
16
|
+
<component name="TestRunnerService">
|
|
17
|
+
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
|
18
|
+
</component>
|
|
19
|
+
</module>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: ruff-format
|
|
5
|
+
name: ruff format
|
|
6
|
+
entry: uv run ruff format
|
|
7
|
+
language: system
|
|
8
|
+
types_or: [python, pyi]
|
|
9
|
+
require_serial: true
|
|
10
|
+
|
|
11
|
+
- id: ruff-check
|
|
12
|
+
name: ruff check
|
|
13
|
+
entry: uv run ruff check --fix
|
|
14
|
+
language: system
|
|
15
|
+
types_or: [python, pyi]
|
|
16
|
+
require_serial: true
|
|
17
|
+
|
|
18
|
+
- id: mypy
|
|
19
|
+
name: mypy
|
|
20
|
+
entry: uv run mypy src
|
|
21
|
+
language: system
|
|
22
|
+
pass_filenames: false
|
|
23
|
+
types: [python]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
|
|
2
|
+
|
|
3
|
+
target-version = "py311"
|
|
4
|
+
line-length = 100
|
|
5
|
+
|
|
6
|
+
[lint]
|
|
7
|
+
select = [
|
|
8
|
+
"ALL",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
ignore = [
|
|
12
|
+
"ANN401", # typing.Any disallowed — not always avoidable at framework boundaries
|
|
13
|
+
"D203", # no-blank-line-before-class (incompatible with formatter)
|
|
14
|
+
"D212", # multi-line-summary-first-line (incompatible with formatter)
|
|
15
|
+
"D213", # mutually exclusive with D212; we pick D212-style summaries
|
|
16
|
+
"D107", # __init__ docstring covered by class docstring
|
|
17
|
+
"COM812", # incompatible with formatter
|
|
18
|
+
"ISC001", # incompatible with formatter
|
|
19
|
+
"EM101", # raise(str) is fine — format helps grep, no string variable needed
|
|
20
|
+
"EM102", # same for f-strings inside raise
|
|
21
|
+
"TRY003", # long exception messages are OK when the message carries diagnostics
|
|
22
|
+
"TID252", # relative imports between same-package siblings are intentional
|
|
23
|
+
"PLR2004", # magic byte values are the protocol — extracting them all hurts readability
|
|
24
|
+
"PLC0415", # lazy imports break a couple of circular deps; intentional
|
|
25
|
+
"FBT003", # boolean positional in typer.Option calls is the framework idiom
|
|
26
|
+
"S101", # we keep asserts as type-narrowing aids for mypy; cheap in CPython
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[lint.per-file-ignores]
|
|
30
|
+
"tests/**" = [
|
|
31
|
+
"S101", # asserts are the point of tests
|
|
32
|
+
"S105", # tests carry expected passcode/secret values inline
|
|
33
|
+
"SLF001", # tests intentionally exercise private members
|
|
34
|
+
"ARG001", # unused fixtures are normal
|
|
35
|
+
"PLR2004", # magic numbers in tests are fine
|
|
36
|
+
"D", # docstrings not required in tests
|
|
37
|
+
"ANN", # type annotations not required in tests
|
|
38
|
+
"ERA001", # protocol-byte-layout comments are valuable in payload tests
|
|
39
|
+
"PT011", # pytest.raises bare ValueError is fine when message isn't tested
|
|
40
|
+
"PT018", # composite assertion is normal in tests
|
|
41
|
+
"PLC0415", # local imports inside test methods are normal
|
|
42
|
+
"PYI034", # Self return type isn't relevant in test fixtures
|
|
43
|
+
]
|
|
44
|
+
"src/ttlock_ble/cli.py" = [
|
|
45
|
+
"D", # typer commands self-document via help=
|
|
46
|
+
"BLE001", # CLI handlers convert any error into typer.Exit
|
|
47
|
+
"FBT001", # boolean-positional in typer commands is the framework idiom
|
|
48
|
+
"FBT002",
|
|
49
|
+
]
|
|
50
|
+
"src/ttlock_ble/models/*.py" = [
|
|
51
|
+
"N815", # cloud-facing dataclass fields keep upstream camelCase
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[lint.flake8-pytest-style]
|
|
55
|
+
fixture-parentheses = false
|
|
56
|
+
|
|
57
|
+
[lint.pyupgrade]
|
|
58
|
+
keep-runtime-typing = true
|
|
59
|
+
|
|
60
|
+
[lint.mccabe]
|
|
61
|
+
max-complexity = 25
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1](https://github.com/roquerodrigo/ttlock-ble/compare/v0.1.0...v0.1.1) (2026-05-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* initial release of ttlock-ble Python SDK ([408a778](https://github.com/roquerodrigo/ttlock-ble/commit/408a778cd3dd067a09f892373b852ad87e418e68))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ttlock-ble
|
|
2
|
+
|
|
3
|
+
Python SDK for controlling **DLock-XP / TTLock** Bluetooth smart locks
|
|
4
|
+
without depending on the TTLock cloud at unlock time. The session AES key
|
|
5
|
+
is downloaded once from the TTLock cloud and cached locally; from then on
|
|
6
|
+
all lock/unlock/state operations run fully offline over BLE.
|
|
7
|
+
|
|
8
|
+
## Always read `CODE_STYLE.md` first
|
|
9
|
+
|
|
10
|
+
Before creating, renaming or restructuring any file/class/function, **read
|
|
11
|
+
[`CODE_STYLE.md`](./CODE_STYLE.md)**. It is the single source of truth for
|
|
12
|
+
conventions: language, file organisation, naming, typing, imports,
|
|
13
|
+
docstrings, comments, logging, error messages, public API surface,
|
|
14
|
+
pre-commit hooks, conventional commits, packaging, releasing, testing,
|
|
15
|
+
lint workflow.
|
|
16
|
+
|
|
17
|
+
## Verification workflow
|
|
18
|
+
|
|
19
|
+
After every code change, always run lint then tests, in that order, before
|
|
20
|
+
declaring the task done:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv run ruff format . && uv run ruff check . --fix && uv run mypy src
|
|
24
|
+
uv run pytest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Both gates mirror CI. Skip this only when the change literally cannot
|
|
28
|
+
affect lint or tests (e.g., README-only edits).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Code Style Guide
|
|
2
|
+
|
|
3
|
+
Style conventions for the `ttlock-ble` Python SDK. Run
|
|
4
|
+
`uv run ruff format . && uv run ruff check . --fix && uv run mypy src` before
|
|
5
|
+
committing — all three must exit cleanly. `uv run pytest` follows.
|
|
6
|
+
|
|
7
|
+
**Always read this file before adding or restructuring code.**
|
|
8
|
+
|
|
9
|
+
## Language
|
|
10
|
+
|
|
11
|
+
- Code is written in **English**: file names, class names, function names,
|
|
12
|
+
variable names, dictionary keys, identifier strings.
|
|
13
|
+
- The conversation language with the user can be Portuguese or anything else;
|
|
14
|
+
what is committed to disk stays English.
|
|
15
|
+
|
|
16
|
+
## File organization
|
|
17
|
+
|
|
18
|
+
- **Source layout is `src/ttlock_ble/`.** Tests in `tests/`, packaging in
|
|
19
|
+
`pyproject.toml`. Hatchling is the build backend.
|
|
20
|
+
- **One top-level class per file.** Multiple semantically related classes get
|
|
21
|
+
grouped into a package directory with one class per submodule and an
|
|
22
|
+
`__init__.py` re-exporting the public symbols.
|
|
23
|
+
- Example: `protocol/` could contain `frame.py`, `reassembler.py`,
|
|
24
|
+
plus `__init__.py`.
|
|
25
|
+
- Example: `models/` could contain `virtual_key.py`, `lock_version.py`,
|
|
26
|
+
`site_info.py`, plus `__init__.py`.
|
|
27
|
+
- **Public surface goes through the package `__init__.py`.** Anything not
|
|
28
|
+
re-exported there is internal — prefix with `_` if intended to stay private.
|
|
29
|
+
- **TypedDicts and `type` aliases do not count as "classes"** for this rule —
|
|
30
|
+
they live alongside related code.
|
|
31
|
+
- **Helper functions** may live in the same file as the single class that
|
|
32
|
+
uses them. Module-level private helpers are prefixed `_`.
|
|
33
|
+
|
|
34
|
+
## Naming
|
|
35
|
+
|
|
36
|
+
- Public classes are `CapWords`: `TTLockClient`, `TTLockCloud`, `VirtualKey`,
|
|
37
|
+
`LockVersion`, `Frame`, `FrameReassembler`.
|
|
38
|
+
- Exception classes end with `Error`: `TTLockError`, `CloudError`.
|
|
39
|
+
- Module names are `snake_case`. Subpackages are organized by concern
|
|
40
|
+
(`protocol`, `cloud`, `commands`).
|
|
41
|
+
- Private attributes / functions are prefixed with `_`.
|
|
42
|
+
|
|
43
|
+
## Typing
|
|
44
|
+
|
|
45
|
+
**Strict typing. No `Any`, no bare collection generics.** Mypy enforces this.
|
|
46
|
+
|
|
47
|
+
Banned: `typing.Any`, `object` as a value type, bare `dict` / `list` /
|
|
48
|
+
`tuple` / `set`, `dict[str, Any]`.
|
|
49
|
+
|
|
50
|
+
Required:
|
|
51
|
+
|
|
52
|
+
- `@dataclass` for structured records (`VirtualKey`, `LockVersion`,
|
|
53
|
+
`SiteInfo`, `CloudCredentials`).
|
|
54
|
+
- Named `type` aliases for shared shapes.
|
|
55
|
+
- `frozenset[str]` / `tuple[str, ...]` for fixed string collections.
|
|
56
|
+
- Always type return values explicitly. Never rely on type inference for
|
|
57
|
+
public APIs.
|
|
58
|
+
- Type-hinted module-level loggers: `log: logging.Logger = logging.getLogger(...)`.
|
|
59
|
+
|
|
60
|
+
The SDK ships a `py.typed` marker so downstream consumers get type info.
|
|
61
|
+
|
|
62
|
+
## Imports
|
|
63
|
+
|
|
64
|
+
- Always start every module with `from __future__ import annotations` so type
|
|
65
|
+
hints become lazy strings.
|
|
66
|
+
- Same-package relative imports (`from .module import …`) are the default.
|
|
67
|
+
- Move type-only imports into a `TYPE_CHECKING` block:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from __future__ import annotations
|
|
71
|
+
from typing import TYPE_CHECKING
|
|
72
|
+
|
|
73
|
+
if TYPE_CHECKING:
|
|
74
|
+
from collections.abc import Iterable
|
|
75
|
+
from .models import VirtualKey
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `noqa` comments require a written justification inline. Never silence to
|
|
79
|
+
"make ruff happy" — fix the underlying code.
|
|
80
|
+
|
|
81
|
+
## Docstrings
|
|
82
|
+
|
|
83
|
+
- Every public class, function, method (including `@property`) has a docstring.
|
|
84
|
+
- A single sentence is usually enough. Describe the *contract* or the *why*,
|
|
85
|
+
not the obvious implementation.
|
|
86
|
+
- Module-level docstring at the top of every `.py` file.
|
|
87
|
+
- Avoid restating the type — the signature already does that.
|
|
88
|
+
|
|
89
|
+
## Comments
|
|
90
|
+
|
|
91
|
+
- Default to **no comments**. Add one only when the *why* is not obvious from
|
|
92
|
+
the code: a hidden constraint, a workaround, a subtle invariant, a protocol
|
|
93
|
+
reference.
|
|
94
|
+
- Never describe *what* the code does — well-named identifiers handle that.
|
|
95
|
+
- **No section dividers** like `# --- Frame helpers ---` to group related
|
|
96
|
+
declarations. If a file has so many sections that you feel the need for
|
|
97
|
+
visual separators, split it into multiple files instead.
|
|
98
|
+
|
|
99
|
+
## Logging
|
|
100
|
+
|
|
101
|
+
- Module-level logger: `log: logging.Logger = logging.getLogger("ttlock_ble.<area>")`
|
|
102
|
+
(e.g. `"ttlock_ble.client"`, `"ttlock_ble.cloud"`). Don't use `__name__`
|
|
103
|
+
directly — the explicit dotted name lets users scope log levels precisely.
|
|
104
|
+
- Use **lazy `%`-formatting**, never f-strings:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
log.debug("Lock found: %s rssi=%d", name, rssi) # ✓
|
|
108
|
+
log.debug(f"Lock found: {name} rssi={rssi}") # ✗
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- Levels:
|
|
112
|
+
- `debug` — TX/RX bytes, GATT discovery, frame payload values.
|
|
113
|
+
- `info` — connection established, handshake completed, unlock success.
|
|
114
|
+
- `warning` — recoverable failures (one-off connect retry, weak signal).
|
|
115
|
+
- `error` / `exception` — unrecoverable. `exception` inside `except` blocks
|
|
116
|
+
captures the traceback.
|
|
117
|
+
- Never log raw `aesKeyStr`, `unlockKey`, `adminPs`, or decrypted payloads.
|
|
118
|
+
Truncate / hash if diagnostics need correlation.
|
|
119
|
+
|
|
120
|
+
## Error messages
|
|
121
|
+
|
|
122
|
+
- Format: `"Failed to <verb> <object>: <cause>"`. Keep them short and
|
|
123
|
+
grep-able.
|
|
124
|
+
- Custom exceptions form a hierarchy: `TTLockError` (BLE / protocol failures)
|
|
125
|
+
and `CloudError` (HTTP / signature / verification failures) are the public
|
|
126
|
+
errors. Wrap raw `bleak`, `httpx`, `OSError` errors at the transport
|
|
127
|
+
boundary so callers only catch this hierarchy.
|
|
128
|
+
- Pre-validate inputs before opening a connection so user-facing errors point
|
|
129
|
+
at the bad input, not a downstream traceback.
|
|
130
|
+
|
|
131
|
+
## Public API surface
|
|
132
|
+
|
|
133
|
+
- Anything imported in the package `__init__.py` is the public contract
|
|
134
|
+
(`TTLockClient`, `TTLockCloud`, `VirtualKey`, `LockVersion`, `SiteInfo`).
|
|
135
|
+
Renaming or removing those symbols is a `BREAKING CHANGE:`.
|
|
136
|
+
- Internal modules can change shape freely as long as the public re-exports
|
|
137
|
+
keep working.
|
|
138
|
+
|
|
139
|
+
## Pre-commit hooks
|
|
140
|
+
|
|
141
|
+
`pre-commit` is recommended. Add `.pre-commit-config.yaml` mirroring the
|
|
142
|
+
lint commands (ruff format, ruff check, mypy) and install once per clone:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pre-commit install
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The hook runs the same gates as CI on every commit. Skip it only on
|
|
149
|
+
emergency `git commit --no-verify` and immediately re-run the lint commands.
|
|
150
|
+
|
|
151
|
+
## Conventional commits
|
|
152
|
+
|
|
153
|
+
All commits follow [Conventional Commits](https://www.conventionalcommits.org/),
|
|
154
|
+
in **English**, which `release-please` parses to bump `pyproject.toml` `version`
|
|
155
|
+
and generate `CHANGELOG.md`:
|
|
156
|
+
|
|
157
|
+
| Type | Meaning | Bump |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `feat` | New feature | minor |
|
|
160
|
+
| `fix` | Bug fix | patch |
|
|
161
|
+
| `perf` | Performance improvement | patch |
|
|
162
|
+
| `deps` | Dependency bump | patch |
|
|
163
|
+
| `docs` | Documentation only | none |
|
|
164
|
+
| `refactor` | Refactor without behavior change | none |
|
|
165
|
+
| `test` | Test-only change | none |
|
|
166
|
+
| `ci` | CI / tooling change | none |
|
|
167
|
+
| `chore` | Anything else (rarely) | none |
|
|
168
|
+
|
|
169
|
+
- Subject line: imperative mood, lowercase, no trailing period.
|
|
170
|
+
- Use scopes when useful: `feat(ble): retry connect on first attempt`.
|
|
171
|
+
- A `BREAKING CHANGE:` footer (or `!` after type) bumps the major version.
|
|
172
|
+
|
|
173
|
+
## Packaging
|
|
174
|
+
|
|
175
|
+
- Build backend: `hatchling`. Wheel and sdist contain `src/ttlock_ble`.
|
|
176
|
+
- `requires-python = ">=3.11"`. Don't bump this without a `BREAKING CHANGE:`
|
|
177
|
+
footer.
|
|
178
|
+
- Public dependencies: keep them minimal and use `>=` lower bounds, not
|
|
179
|
+
pins.
|
|
180
|
+
- The `[dependency-groups] dev` group carries test-only deps.
|
|
181
|
+
- A `py.typed` marker ships in the wheel so consumers see type info.
|
|
182
|
+
|
|
183
|
+
## Releasing
|
|
184
|
+
|
|
185
|
+
- `release-please` runs on `main` and opens a release-PR with the next
|
|
186
|
+
version + `CHANGELOG.md`. Merging that PR triggers the publish job
|
|
187
|
+
(sdist + wheel via `python -m build`, published to PyPI via the
|
|
188
|
+
`pypi` GitHub Environment + Trusted Publisher — no token in repo secrets).
|
|
189
|
+
- Don't manually edit `pyproject.toml` `version` — release-please owns it.
|
|
190
|
+
|
|
191
|
+
## Testing
|
|
192
|
+
|
|
193
|
+
- Tests live in `tests/`. `uv run pytest` runs the suite. Aim for high
|
|
194
|
+
coverage on protocol/crypto/cloud layers since they're the byte-level
|
|
195
|
+
surface most likely to regress silently.
|
|
196
|
+
- Hardware-dependent tests (real BLE lock) are gated behind an env var and
|
|
197
|
+
skipped in CI; pure unit tests use captured byte fixtures.
|
|
198
|
+
|
|
199
|
+
## Linting and verification
|
|
200
|
+
|
|
201
|
+
- Ruff configuration lives in `.ruff.toml` with `select = ["ALL"]` and a
|
|
202
|
+
short list of justified ignores.
|
|
203
|
+
- Mypy configuration lives in `mypy.ini` (strict).
|
|
204
|
+
- Pytest configuration lives in `pytest.ini`, including a 50 % coverage
|
|
205
|
+
gate (BLE/HTTP code paths still need mocked tests; raise the gate as
|
|
206
|
+
coverage grows).
|
|
207
|
+
- `scripts/lint` runs `ruff format`, `ruff check --fix`, `mypy src` and
|
|
208
|
+
`pytest` in order. CI mirrors this via `.github/workflows/lint.yml`,
|
|
209
|
+
`tests.yml`, `codeql.yml`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ttlock-ble
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for controlling DLock-XP / TTLock smart locks over Bluetooth
|
|
5
|
+
Project-URL: Homepage, https://github.com/roquerodrigo/ttlock-ble
|
|
6
|
+
Project-URL: Repository, https://github.com/roquerodrigo/ttlock-ble
|
|
7
|
+
Project-URL: Issues, https://github.com/roquerodrigo/ttlock-ble/issues
|
|
8
|
+
Author-email: Rodrigo Roque <rodrigogoncalvesroque@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ble,bluetooth,home-automation,sdk,smart-lock,ttlock
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Home Automation
|
|
20
|
+
Classifier: Topic :: System :: Hardware
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: bleak-retry-connector>=4.6.0
|
|
23
|
+
Requires-Dist: bleak>=0.22
|
|
24
|
+
Requires-Dist: cryptography>=43
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0
|
|
27
|
+
Requires-Dist: typer>=0.12
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[mypy]
|
|
2
|
+
python_version = 3.11
|
|
3
|
+
explicit_package_bases = True
|
|
4
|
+
ignore_missing_imports = True
|
|
5
|
+
follow_imports = silent
|
|
6
|
+
strict_optional = True
|
|
7
|
+
warn_redundant_casts = True
|
|
8
|
+
warn_unused_ignores = True
|
|
9
|
+
warn_unreachable = True
|
|
10
|
+
no_implicit_optional = True
|
|
11
|
+
check_untyped_defs = True
|
|
12
|
+
disallow_untyped_defs = True
|
|
13
|
+
disallow_any_generics = True
|
|
14
|
+
|
|
15
|
+
[mypy-tests.*]
|
|
16
|
+
disallow_untyped_defs = False
|
|
17
|
+
check_untyped_defs = False
|
|
18
|
+
|
|
19
|
+
[mypy-ttlock_ble.client]
|
|
20
|
+
# bleak callback signatures resolve to `Any` without bundled stubs;
|
|
21
|
+
# silence the explicit-any complaints they trigger here.
|
|
22
|
+
disable_error_code = explicit-any
|