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.
Files changed (56) hide show
  1. ttlock_ble-0.1.1/.github/workflows/codeql.yml +41 -0
  2. ttlock_ble-0.1.1/.github/workflows/lint.yml +57 -0
  3. ttlock_ble-0.1.1/.github/workflows/release.yml +66 -0
  4. ttlock_ble-0.1.1/.github/workflows/tests.yml +33 -0
  5. ttlock_ble-0.1.1/.gitignore +14 -0
  6. ttlock_ble-0.1.1/.idea/.gitignore +3 -0
  7. ttlock_ble-0.1.1/.idea/inspectionProfiles/profiles_settings.xml +6 -0
  8. ttlock_ble-0.1.1/.idea/misc.xml +6 -0
  9. ttlock_ble-0.1.1/.idea/modules.xml +8 -0
  10. ttlock_ble-0.1.1/.idea/pyProjectModel.xml +6 -0
  11. ttlock_ble-0.1.1/.idea/ttlock-ble.iml +19 -0
  12. ttlock_ble-0.1.1/.idea/vcs.xml +6 -0
  13. ttlock_ble-0.1.1/.pre-commit-config.yaml +23 -0
  14. ttlock_ble-0.1.1/.release-please-manifest.json +3 -0
  15. ttlock_ble-0.1.1/.ruff.toml +61 -0
  16. ttlock_ble-0.1.1/CHANGELOG.md +8 -0
  17. ttlock_ble-0.1.1/CLAUDE.md +28 -0
  18. ttlock_ble-0.1.1/CODE_STYLE.md +209 -0
  19. ttlock_ble-0.1.1/PKG-INFO +27 -0
  20. ttlock_ble-0.1.1/mypy.ini +22 -0
  21. ttlock_ble-0.1.1/pyproject.toml +56 -0
  22. ttlock_ble-0.1.1/pytest.ini +9 -0
  23. ttlock_ble-0.1.1/release-please-config.json +27 -0
  24. ttlock_ble-0.1.1/scripts/lint +11 -0
  25. ttlock_ble-0.1.1/src/ttlock_ble/__init__.py +40 -0
  26. ttlock_ble-0.1.1/src/ttlock_ble/_cloud_helpers.py +65 -0
  27. ttlock_ble-0.1.1/src/ttlock_ble/cli.py +193 -0
  28. ttlock_ble-0.1.1/src/ttlock_ble/client.py +524 -0
  29. ttlock_ble-0.1.1/src/ttlock_ble/cloud.py +254 -0
  30. ttlock_ble-0.1.1/src/ttlock_ble/commands.py +491 -0
  31. ttlock_ble-0.1.1/src/ttlock_ble/constants/__init__.py +14 -0
  32. ttlock_ble-0.1.1/src/ttlock_ble/constants/auto_lock_operate.py +12 -0
  33. ttlock_ble-0.1.1/src/ttlock_ble/constants/keyboard_pwd_type.py +14 -0
  34. ttlock_ble-0.1.1/src/ttlock_ble/constants/log_operate.py +48 -0
  35. ttlock_ble-0.1.1/src/ttlock_ble/constants/pwd_operate_type.py +15 -0
  36. ttlock_ble-0.1.1/src/ttlock_ble/crypto.py +132 -0
  37. ttlock_ble-0.1.1/src/ttlock_ble/exceptions/__init__.py +8 -0
  38. ttlock_ble-0.1.1/src/ttlock_ble/exceptions/cloud.py +18 -0
  39. ttlock_ble-0.1.1/src/ttlock_ble/exceptions/ttlock.py +7 -0
  40. ttlock_ble-0.1.1/src/ttlock_ble/models/__init__.py +19 -0
  41. ttlock_ble-0.1.1/src/ttlock_ble/models/cloud_credentials.py +14 -0
  42. ttlock_ble-0.1.1/src/ttlock_ble/models/lock_event.py +29 -0
  43. ttlock_ble-0.1.1/src/ttlock_ble/models/lock_version.py +58 -0
  44. ttlock_ble-0.1.1/src/ttlock_ble/models/log_entry.py +34 -0
  45. ttlock_ble-0.1.1/src/ttlock_ble/models/site_info.py +34 -0
  46. ttlock_ble-0.1.1/src/ttlock_ble/models/virtual_key.py +108 -0
  47. ttlock_ble-0.1.1/src/ttlock_ble/protocol/__init__.py +16 -0
  48. ttlock_ble-0.1.1/src/ttlock_ble/protocol/constants.py +9 -0
  49. ttlock_ble-0.1.1/src/ttlock_ble/protocol/frame.py +145 -0
  50. ttlock_ble-0.1.1/src/ttlock_ble/protocol/reassembler.py +44 -0
  51. ttlock_ble-0.1.1/src/ttlock_ble/py.typed +0 -0
  52. ttlock_ble-0.1.1/tests/__init__.py +0 -0
  53. ttlock_ble-0.1.1/tests/test_client.py +124 -0
  54. ttlock_ble-0.1.1/tests/test_crypto.py +111 -0
  55. ttlock_ble-0.1.1/tests/test_protocol.py +282 -0
  56. 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,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ decompiled/
6
+ DLock-XP_*.xapk
7
+ /tmp/
8
+ *.log
9
+ .DS_Store
10
+ .coverage
11
+ htmlcov/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .pytest_cache/
@@ -0,0 +1,3 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="uv (ttlock-ble)" />
5
+ </component>
6
+ </project>
@@ -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,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="PyProjectModelSettings">
4
+ <option name="showConfigurationNotification" value="false" />
5
+ </component>
6
+ </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,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -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,3 @@
1
+ {
2
+ ".": "0.1.1"
3
+ }
@@ -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