gatt-link-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 (36) hide show
  1. gatt_link_ble-0.1.1/.github/agents/phase-builder.agent.md +11 -0
  2. gatt_link_ble-0.1.1/.github/copilot-instructions.md +8 -0
  3. gatt_link_ble-0.1.1/.github/workflows/ci.yml +35 -0
  4. gatt_link_ble-0.1.1/.github/workflows/publish.yml +49 -0
  5. gatt_link_ble-0.1.1/.github/workflows/release.yml +61 -0
  6. gatt_link_ble-0.1.1/.gitignore +8 -0
  7. gatt_link_ble-0.1.1/CHANGELOG.md +4 -0
  8. gatt_link_ble-0.1.1/LICENSE +21 -0
  9. gatt_link_ble-0.1.1/PKG-INFO +133 -0
  10. gatt_link_ble-0.1.1/README.md +121 -0
  11. gatt_link_ble-0.1.1/pyproject.toml +72 -0
  12. gatt_link_ble-0.1.1/scripts/live_scan.py +99 -0
  13. gatt_link_ble-0.1.1/src/gatt_link_ble/__init__.py +21 -0
  14. gatt_link_ble-0.1.1/src/gatt_link_ble/client.py +406 -0
  15. gatt_link_ble-0.1.1/src/gatt_link_ble/const.py +23 -0
  16. gatt_link_ble-0.1.1/src/gatt_link_ble/exceptions.py +13 -0
  17. gatt_link_ble-0.1.1/src/gatt_link_ble/models.py +45 -0
  18. gatt_link_ble-0.1.1/src/gatt_link_ble/parsers/__init__.py +13 -0
  19. gatt_link_ble-0.1.1/src/gatt_link_ble/parsers/base.py +42 -0
  20. gatt_link_ble-0.1.1/src/gatt_link_ble/parsers/battery.py +29 -0
  21. gatt_link_ble-0.1.1/src/gatt_link_ble/parsers/device_info.py +52 -0
  22. gatt_link_ble-0.1.1/src/gatt_link_ble/parsers/environmental.py +42 -0
  23. gatt_link_ble-0.1.1/src/gatt_link_ble/parsers/thingy_env.py +49 -0
  24. gatt_link_ble-0.1.1/src/gatt_link_ble/registry.py +37 -0
  25. gatt_link_ble-0.1.1/tests/conftest.py +114 -0
  26. gatt_link_ble-0.1.1/tests/parsers/test_battery.py +34 -0
  27. gatt_link_ble-0.1.1/tests/parsers/test_device_info.py +44 -0
  28. gatt_link_ble-0.1.1/tests/parsers/test_environmental.py +49 -0
  29. gatt_link_ble-0.1.1/tests/parsers/test_thingy_env.py +67 -0
  30. gatt_link_ble-0.1.1/tests/test_client.py +411 -0
  31. gatt_link_ble-0.1.1/tests/test_const.py +22 -0
  32. gatt_link_ble-0.1.1/tests/test_exceptions.py +6 -0
  33. gatt_link_ble-0.1.1/tests/test_models.py +45 -0
  34. gatt_link_ble-0.1.1/tests/test_registry.py +60 -0
  35. gatt_link_ble-0.1.1/tests/test_scaffold.py +5 -0
  36. gatt_link_ble-0.1.1/uv.lock +849 -0
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Executes a single implementation phase from ARCHITECTURE.md with automated lint, typecheck, and pytest verification.
3
+ tools: ['edit', 'runNotebooks', 'search', 'new', 'runCommands', 'problems', 'usages', 'findTestFiles']
4
+ ---
5
+
6
+ You are an expert Python and Open Home Foundation developer.
7
+ 1. Read `ARCHITECTURE.md` and inspect the current directory tree to see which phases are already complete.
8
+ 2. Implement ONLY the requested phase (or the next incomplete phase) cleanly and completely.
9
+ 3. Write unit/functional tests covering edge cases (negative numbers, malformed payloads, disconnects) alongside the implementation.
10
+ 4. Run the verification suite in terminal (`uv run ruff check --fix . && uv run ruff format . && uv run mypy . && uv run pytest`) and resolve any failures before responding.
11
+ 5. Keep your final summary concise (under 10 bullet points) to conserve tokens.
@@ -0,0 +1,8 @@
1
+ # Copilot Instructions — `gatt-link-ble`
2
+
3
+ - **Architecture:** Always follow `ARCHITECTURE.md` in the repo root. Execute one phase at a time.
4
+ - **Strict Boundary:** NEVER import `homeassistant` or `habluetooth`. This is a pure Python PyPI library.
5
+ - **Tooling:** Always use `uv run` for commands (`uv run pytest`, `uv run mypy src tests`, `uv run ruff check .`). Never use raw `pip`.
6
+ - **BLE Rules:** Use `bleak-retry-connector` (`establish_connection` + `BleakClientWithServiceCache`). Unpack binary payloads strictly with Python's `struct` module—never slice hex strings.
7
+ - **Code Quality:** Enforce Python 3.12+ type annotations (`mypy --strict`) and immutable `@dataclass(frozen=True)` state models.
8
+ - **Verification:** Before finishing any task, run `uv run ruff check . && uv run mypy src tests && uv run pytest` and fix all errors in the same turn.
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
15
+ runs-on: ${{ matrix.os }}
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ os: [ubuntu-24.04, windows-2025]
20
+ python-version: ["3.12", "3.13"]
21
+ steps:
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
+
24
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
29
+ with:
30
+ enable-cache: true
31
+
32
+ - run: uv sync --locked --all-groups
33
+ - run: uv run --locked ruff check .
34
+ - run: uv run --locked mypy src tests
35
+ - run: uv run --locked pytest
@@ -0,0 +1,49 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ tag:
10
+ description: Existing version tag to publish
11
+ required: true
12
+ type: string
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ publish:
19
+ name: Build, verify, and publish
20
+ runs-on: ubuntu-24.04
21
+ environment:
22
+ name: pypi
23
+ url: https://pypi.org/project/gatt-link-ble/
24
+ permissions:
25
+ contents: read
26
+ id-token: write
27
+ steps:
28
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
29
+ with:
30
+ ref: ${{ inputs.tag || github.ref }}
31
+
32
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
33
+ with:
34
+ python-version: "3.12"
35
+
36
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
37
+ with:
38
+ enable-cache: true
39
+
40
+ - name: Install locked dependencies
41
+ run: uv sync --locked --all-groups
42
+
43
+ - run: uv run --locked ruff check .
44
+ - run: uv run --locked mypy src tests
45
+ - run: uv run --locked pytest
46
+ - run: uv build
47
+
48
+ - name: Publish to PyPI using OIDC
49
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
@@ -0,0 +1,61 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: ["CI"]
6
+ types: [completed]
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: release-main
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ release:
18
+ if: >-
19
+ github.event.workflow_run.conclusion == 'success' &&
20
+ github.event.workflow_run.head_branch == 'main' &&
21
+ !startsWith(github.event.workflow_run.head_commit.message, 'chore(release):')
22
+ name: Create semantic release
23
+ runs-on: ubuntu-24.04
24
+ steps:
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26
+ with:
27
+ ref: main
28
+ fetch-depth: 0
29
+ token: ${{ secrets.RELEASE_PAT }}
30
+
31
+ - name: Confirm CI checked the current main commit
32
+ id: current-main
33
+ env:
34
+ CI_SHA: ${{ github.event.workflow_run.head_sha }}
35
+ run: |
36
+ if [[ "$(git rev-parse HEAD)" == "$CI_SHA" ]]; then
37
+ echo "current=true" >> "$GITHUB_OUTPUT"
38
+ else
39
+ echo "CI result is stale; main has advanced"
40
+ echo "current=false" >> "$GITHUB_OUTPUT"
41
+ fi
42
+
43
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
44
+ if: steps.current-main.outputs.current == 'true'
45
+ with:
46
+ python-version: "3.12"
47
+
48
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
49
+ if: steps.current-main.outputs.current == 'true'
50
+ with:
51
+ enable-cache: true
52
+
53
+ - name: Install uv.lock
54
+ if: steps.current-main.outputs.current == 'true'
55
+ run: uv sync --locked --all-groups
56
+
57
+ - name: Release with Python Semantic Release
58
+ if: steps.current-main.outputs.current == 'true'
59
+ env:
60
+ GH_TOKEN: ${{ secrets.RELEASE_PAT }}
61
+ run: uv run --with python-semantic-release==10.7.0 --no-project semantic-release version
@@ -0,0 +1,8 @@
1
+ ARCHITECTURE.md
2
+ .venv/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .coverage
8
+ dist/
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ Release notes are generated from Conventional Commit messages by
4
+ Python Semantic Release.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 infoyouth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.5
2
+ Name: gatt-link-ble
3
+ Version: 0.1.1
4
+ Summary: Async BLE GATT discovery, parsing, and state updates.
5
+ Author: infoyouth
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: bleak-retry-connector>=3.6.0
10
+ Requires-Dist: bleak>=0.22.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # gatt-link-ble
14
+
15
+ An asynchronous, framework-independent Python library for discovering BLE GATT
16
+ services, decoding sensor values, and receiving device-state updates.
17
+
18
+ The package owns BLE transport and parsing. It has no Home Assistant, MQTT, or
19
+ UI-framework dependencies.
20
+
21
+ ## Requirements and installation
22
+
23
+ - Python 3.12 or newer
24
+ - BLE adapter and a device advertising supported GATT services
25
+
26
+ Install from PyPI:
27
+
28
+ ```shell
29
+ python -m pip install gatt-link-ble
30
+ ```
31
+
32
+ ## Poll once
33
+
34
+ ```python
35
+ import asyncio
36
+
37
+ from bleak import BleakScanner
38
+
39
+ from gatt_link_ble import ConnectionMode, GattLinkClient
40
+
41
+
42
+ async def main() -> None:
43
+ address = "AA:BB:CC:DD:EE:FF"
44
+ device = await BleakScanner.find_device_by_address(address, timeout=10)
45
+ if device is None:
46
+ raise RuntimeError(f"BLE device not found: {address}")
47
+
48
+ client = GattLinkClient(device, mode=ConnectionMode.POLL_AND_DISCONNECT)
49
+ state = await client.poll()
50
+ print(state)
51
+
52
+
53
+ asyncio.run(main())
54
+ ```
55
+
56
+ ## Receive push updates
57
+
58
+ ```python
59
+ import asyncio
60
+
61
+ from bleak import BleakScanner
62
+
63
+ from gatt_link_ble import GattLinkClient
64
+
65
+
66
+ async def main() -> None:
67
+ address = "AA:BB:CC:DD:EE:FF"
68
+ device = await BleakScanner.find_device_by_address(address, timeout=10)
69
+ if device is None:
70
+ raise RuntimeError(f"BLE device not found: {address}")
71
+
72
+ client = GattLinkClient(device, state_callback=print)
73
+ await client.start()
74
+ try:
75
+ await asyncio.Event().wait()
76
+ finally:
77
+ await client.stop()
78
+
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ `PUSH` keeps the connection open, sends an initial state after connecting, and
84
+ reconnects after unexpected disconnects. `POLL_AND_DISCONNECT` reads available
85
+ characteristics once and disconnects before returning.
86
+
87
+ ## Supported profiles
88
+
89
+ | Profile | Service UUID | Characteristics |
90
+ | --- | --- | --- |
91
+ | Battery Service | `0x180F` | Battery Level `0x2A19` |
92
+ | Device Information | `0x180A` | Manufacturer `0x2A29`, Model `0x2A24`, Firmware `0x2A26`, Hardware `0x2A27` |
93
+ | Environmental Sensing | `0x181A` | Temperature `0x2A6E`, Humidity `0x2A6F`, Pressure `0x2A6D` |
94
+ | Nordic Thingy Environment | `ef680200-9b35-4933-9b10-52ffa9740042` | Temperature, pressure, humidity, eCO2, TVOC |
95
+
96
+ Parsed values are available through the immutable `GattDeviceState`,
97
+ `DeviceMetadata`, and `GattSensorData` models.
98
+
99
+ ## Inspect a device
100
+
101
+ The hardware diagnostic script lists nearby devices and, when passed an
102
+ address, dumps raw bytes from readable characteristics:
103
+
104
+ ```shell
105
+ uv run python scripts/live_scan.py
106
+ uv run python scripts/live_scan.py AA:BB:CC:DD:EE:FF --timeout 10
107
+ ```
108
+
109
+ ## Development
110
+
111
+ ```shell
112
+ uv sync --locked --all-groups
113
+ uv run ruff check .
114
+ uv run mypy src tests
115
+ uv run pytest
116
+ uv build
117
+ ```
118
+
119
+ Tests use mocked BLE clients and GATT services; no hardware is needed.
120
+ `ARCHITECTURE.md` is a local implementation guide and is intentionally not
121
+ included in the public repository.
122
+
123
+ ## Releases
124
+
125
+ After CI succeeds on `main`, Python Semantic Release uses Conventional Commits
126
+ (`feat:` for minor, `fix:` or `perf:` for patch, and `!` for breaking changes)
127
+ to update the version, create a GitHub release, and push a tag. Configure the
128
+ repository Actions secret `RELEASE_PAT` with a GitHub token that can write to
129
+ this repository and whose account is allowed to bypass the `main` branch rules.
130
+
131
+ The PAT is only for GitHub release operations; it is not a PyPI token. PyPI
132
+ publishing uses OIDC Trusted Publishing. Configure the PyPI publisher for this
133
+ repository's `publish.yml` workflow before the first upload.
@@ -0,0 +1,121 @@
1
+ # gatt-link-ble
2
+
3
+ An asynchronous, framework-independent Python library for discovering BLE GATT
4
+ services, decoding sensor values, and receiving device-state updates.
5
+
6
+ The package owns BLE transport and parsing. It has no Home Assistant, MQTT, or
7
+ UI-framework dependencies.
8
+
9
+ ## Requirements and installation
10
+
11
+ - Python 3.12 or newer
12
+ - BLE adapter and a device advertising supported GATT services
13
+
14
+ Install from PyPI:
15
+
16
+ ```shell
17
+ python -m pip install gatt-link-ble
18
+ ```
19
+
20
+ ## Poll once
21
+
22
+ ```python
23
+ import asyncio
24
+
25
+ from bleak import BleakScanner
26
+
27
+ from gatt_link_ble import ConnectionMode, GattLinkClient
28
+
29
+
30
+ async def main() -> None:
31
+ address = "AA:BB:CC:DD:EE:FF"
32
+ device = await BleakScanner.find_device_by_address(address, timeout=10)
33
+ if device is None:
34
+ raise RuntimeError(f"BLE device not found: {address}")
35
+
36
+ client = GattLinkClient(device, mode=ConnectionMode.POLL_AND_DISCONNECT)
37
+ state = await client.poll()
38
+ print(state)
39
+
40
+
41
+ asyncio.run(main())
42
+ ```
43
+
44
+ ## Receive push updates
45
+
46
+ ```python
47
+ import asyncio
48
+
49
+ from bleak import BleakScanner
50
+
51
+ from gatt_link_ble import GattLinkClient
52
+
53
+
54
+ async def main() -> None:
55
+ address = "AA:BB:CC:DD:EE:FF"
56
+ device = await BleakScanner.find_device_by_address(address, timeout=10)
57
+ if device is None:
58
+ raise RuntimeError(f"BLE device not found: {address}")
59
+
60
+ client = GattLinkClient(device, state_callback=print)
61
+ await client.start()
62
+ try:
63
+ await asyncio.Event().wait()
64
+ finally:
65
+ await client.stop()
66
+
67
+
68
+ asyncio.run(main())
69
+ ```
70
+
71
+ `PUSH` keeps the connection open, sends an initial state after connecting, and
72
+ reconnects after unexpected disconnects. `POLL_AND_DISCONNECT` reads available
73
+ characteristics once and disconnects before returning.
74
+
75
+ ## Supported profiles
76
+
77
+ | Profile | Service UUID | Characteristics |
78
+ | --- | --- | --- |
79
+ | Battery Service | `0x180F` | Battery Level `0x2A19` |
80
+ | Device Information | `0x180A` | Manufacturer `0x2A29`, Model `0x2A24`, Firmware `0x2A26`, Hardware `0x2A27` |
81
+ | Environmental Sensing | `0x181A` | Temperature `0x2A6E`, Humidity `0x2A6F`, Pressure `0x2A6D` |
82
+ | Nordic Thingy Environment | `ef680200-9b35-4933-9b10-52ffa9740042` | Temperature, pressure, humidity, eCO2, TVOC |
83
+
84
+ Parsed values are available through the immutable `GattDeviceState`,
85
+ `DeviceMetadata`, and `GattSensorData` models.
86
+
87
+ ## Inspect a device
88
+
89
+ The hardware diagnostic script lists nearby devices and, when passed an
90
+ address, dumps raw bytes from readable characteristics:
91
+
92
+ ```shell
93
+ uv run python scripts/live_scan.py
94
+ uv run python scripts/live_scan.py AA:BB:CC:DD:EE:FF --timeout 10
95
+ ```
96
+
97
+ ## Development
98
+
99
+ ```shell
100
+ uv sync --locked --all-groups
101
+ uv run ruff check .
102
+ uv run mypy src tests
103
+ uv run pytest
104
+ uv build
105
+ ```
106
+
107
+ Tests use mocked BLE clients and GATT services; no hardware is needed.
108
+ `ARCHITECTURE.md` is a local implementation guide and is intentionally not
109
+ included in the public repository.
110
+
111
+ ## Releases
112
+
113
+ After CI succeeds on `main`, Python Semantic Release uses Conventional Commits
114
+ (`feat:` for minor, `fix:` or `perf:` for patch, and `!` for breaking changes)
115
+ to update the version, create a GitHub release, and push a tag. Configure the
116
+ repository Actions secret `RELEASE_PAT` with a GitHub token that can write to
117
+ this repository and whose account is allowed to bypass the `main` branch rules.
118
+
119
+ The PAT is only for GitHub release operations; it is not a PyPI token. PyPI
120
+ publishing uses OIDC Trusted Publishing. Configure the PyPI publisher for this
121
+ repository's `publish.yml` workflow before the first upload.
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gatt-link-ble"
7
+ version = "0.1.1"
8
+ description = "Async BLE GATT discovery, parsing, and state updates."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [{ name = "infoyouth" }]
13
+ dependencies = [
14
+ "bleak>=0.22.0",
15
+ "bleak-retry-connector>=3.6.0",
16
+ ]
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "mypy>=1.11.0",
21
+ "pytest>=8.3.0",
22
+ "pytest-asyncio>=0.24.0",
23
+ "pytest-cov>=5.0.0",
24
+ "ruff>=0.6.0",
25
+ ]
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/gatt_link_ble"]
29
+
30
+ [tool.ruff]
31
+ line-length = 88
32
+ target-version = "py312"
33
+
34
+ [tool.ruff.lint]
35
+ select = ["E", "F", "I", "UP", "B"]
36
+
37
+ [tool.mypy]
38
+ python_version = "3.12"
39
+ strict = true
40
+ warn_return_any = true
41
+ mypy_path = "src"
42
+ packages = ["gatt_link_ble"]
43
+
44
+ [tool.pytest.ini_options]
45
+ asyncio_mode = "auto"
46
+ asyncio_default_fixture_loop_scope = "function"
47
+ testpaths = ["tests"]
48
+ addopts = [
49
+ "--strict-config",
50
+ "--strict-markers",
51
+ "--cov=gatt_link_ble",
52
+ "--cov-report=term-missing",
53
+ "--cov-fail-under=95",
54
+ ]
55
+
56
+ [tool.semantic_release]
57
+ version_toml = ["pyproject.toml:project.version"]
58
+ commit_parser = "conventional"
59
+ tag_format = "v{version}"
60
+ commit_message = "chore(release): {version}"
61
+ build_command = "uv lock --no-upgrade && uv build"
62
+ allow_zero_version = true
63
+
64
+ [tool.semantic_release.commit_parser_options]
65
+ parse_squash_commits = true
66
+ ignore_merge_commits = true
67
+
68
+ [tool.semantic_release.branches.main]
69
+ match = "^main$"
70
+
71
+ [tool.semantic_release.changelog]
72
+ default_templates = { changelog_file = "CHANGELOG.md" }
@@ -0,0 +1,99 @@
1
+ """Discover nearby BLE devices and dump readable GATT characteristic values."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import logging
6
+
7
+ from bleak import BleakScanner
8
+ from bleak.backends.device import BLEDevice
9
+ from bleak.exc import BleakError
10
+ from bleak_retry_connector import (
11
+ BleakClientWithServiceCache,
12
+ establish_connection,
13
+ )
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+
18
+ def _parse_args() -> argparse.Namespace:
19
+ parser = argparse.ArgumentParser(description=__doc__)
20
+ parser.add_argument(
21
+ "address",
22
+ nargs="?",
23
+ help="BLE address to inspect; omit to list discovered devices",
24
+ )
25
+ parser.add_argument(
26
+ "--timeout",
27
+ type=float,
28
+ default=5.0,
29
+ help="scan duration in seconds (default: 5)",
30
+ )
31
+ return parser.parse_args()
32
+
33
+
34
+ async def _inspect_device(device: BLEDevice) -> None:
35
+ client = await establish_connection(
36
+ BleakClientWithServiceCache,
37
+ device,
38
+ device.name or device.address,
39
+ max_attempts=3,
40
+ )
41
+ try:
42
+ for service in client.services:
43
+ print(f"\nService {service.uuid}")
44
+ for characteristic in service.characteristics:
45
+ properties = ", ".join(characteristic.properties)
46
+ print(f" Characteristic {characteristic.uuid} [{properties}]")
47
+ if "read" not in characteristic.properties:
48
+ continue
49
+ try:
50
+ value = await client.read_gatt_char(characteristic)
51
+ except BleakError as err:
52
+ _LOGGER.warning(
53
+ "Could not read characteristic %s: %s",
54
+ characteristic.uuid,
55
+ err,
56
+ )
57
+ continue
58
+ print(f" Raw: {value.hex(' ')}")
59
+ finally:
60
+ await client.disconnect()
61
+
62
+
63
+ async def _run(args: argparse.Namespace) -> None:
64
+ try:
65
+ devices = await BleakScanner.discover(timeout=args.timeout)
66
+ except BleakError as err:
67
+ raise SystemExit(f"BLE scan failed: {err}") from err
68
+
69
+ for device in devices:
70
+ print(f"{device.address} {device.name or '(unnamed)'}")
71
+
72
+ if args.address is None:
73
+ return
74
+
75
+ match = next(
76
+ (
77
+ candidate
78
+ for candidate in devices
79
+ if candidate.address.casefold() == args.address.casefold()
80
+ ),
81
+ None,
82
+ )
83
+ if match is None:
84
+ raise SystemExit(f"No discovered BLE device matches {args.address}")
85
+
86
+ try:
87
+ await _inspect_device(match)
88
+ except BleakError as err:
89
+ raise SystemExit(f"Unable to inspect {match.address}: {err}") from err
90
+
91
+
92
+ def main() -> None:
93
+ """Run the BLE scanner CLI."""
94
+ logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
95
+ asyncio.run(_run(_parse_args()))
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
@@ -0,0 +1,21 @@
1
+ """Asynchronous BLE GATT discovery and parsing."""
2
+
3
+ from .client import GattLinkClient
4
+ from .exceptions import GattConnectionError, GattLinkError, GattParseError
5
+ from .models import (
6
+ ConnectionMode,
7
+ DeviceMetadata,
8
+ GattDeviceState,
9
+ GattSensorData,
10
+ )
11
+
12
+ __all__ = [
13
+ "ConnectionMode",
14
+ "DeviceMetadata",
15
+ "GattConnectionError",
16
+ "GattDeviceState",
17
+ "GattLinkClient",
18
+ "GattLinkError",
19
+ "GattParseError",
20
+ "GattSensorData",
21
+ ]