Skip to content

Commit 8a9405f

Browse files
authored
Merge pull request #177 from RonanB96/workstream-3/completeness-missing-features
Workstream 3/completeness missing features
2 parents dc45b0c + b362e69 commit 8a9405f

391 files changed

Lines changed: 22900 additions & 1285 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ai-agent-characteristic-rules.md

Lines changed: 0 additions & 79 deletions
This file was deleted.

.github/instructions/bluetooth-gatt.instructions.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,34 @@ SIG characteristics auto-resolve UUID. Custom require `_info = CharacteristicInf
2626

2727
**Your `_decode_value()` only:** Parse bytes using templates, apply scaling, return typed result
2828

29+
**`_python_type` is auto-resolved** from `BaseCharacteristic[T]` generic parameter. Never set `_python_type` on new characteristics — the generic param and template already provide the type. `dict` is banned.
30+
2931
**Patterns:**
30-
- Simple value → Use template (`Uint8Template`, etc.)
32+
- Simple value → Use template from `templates/` package (`Uint8Template`, `ScaledUint16Template`, etc.)
3133
- Multi-field → Override `_decode_value()`, return `msgspec.Struct`
3234
- Enum/bitfield → Use `IntFlag`
35+
- Parsing pipeline → `pipeline/parse_pipeline.py` for multi-stage decode, `pipeline/encode_pipeline.py` for encode, `pipeline/validation.py` for range/type/length checks
36+
37+
**Templates package** (`gatt/characteristics/templates/`):
38+
- `numeric.py``Uint8Template`, `Uint16Template`, `Sint8Template`, etc.
39+
- `scaled.py``ScaledUint8Template(d=N, b=N)`, `ScaledSint16Template`, etc.
40+
- `string.py``Utf8StringTemplate`
41+
- `enum.py``EnumTemplate`
42+
- `ieee_float.py``IEEE11073FloatTemplate`, `IEEE11073SFloatTemplate`
43+
- `composite.py``CompositeTemplate`
44+
- `domain.py` — domain-specific templates
45+
- `data_structures.py` — structured data templates
46+
- `base.py``CodingTemplate` base class
47+
48+
**Core decomposition** (`core/`):
49+
- `query.py` — characteristic/service lookup
50+
- `parser.py` — raw bytes → Python values
51+
- `encoder.py` — Python values → raw bytes
52+
- `registration.py` — custom characteristic/service registration
53+
- `service_manager.py` — service lifecycle management
54+
- `translator.py` — public facade (`BluetoothSIGTranslator`)
3355

3456
**Standards:**
3557
- Multi-byte: little-endian
3658

37-
**Reference:** See `battery_level.py`, `heart_rate_measurement.py`, `templates.py`
59+
**Reference:** See `battery_level.py`, `heart_rate_measurement.py`, `templates/`

.github/instructions/documentation.instructions.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ applyTo: "docs/**/*.md, docs/*.md"
77
## Code Samples
88

99
- Every code block must be runnable and validated
10-
- Use Sphinx cross-references: `:class:`CharacteristicData``, `:meth:`parse_characteristic``
10+
- Use Sphinx cross-references: `:class:`BluetoothSIGTranslator``, `:meth:`parse_characteristic``
1111
- Pair code with expected output
1212

13+
## Architecture Diagrams
14+
15+
- Use Mermaid for architecture and flow diagrams in Markdown docs
16+
- Keep diagrams close to the code they describe
17+
1318
## Style
1419

1520
- Google-style docstrings (see python-implementation.instructions.md)

.github/instructions/python-implementation.instructions.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ applyTo: "**/*.py,**/pyproject.toml,**/requirements*.txt"
99
- Hardcoded UUIDs (use registry resolution)
1010
- `from typing import Optional` (use `Type | None`)
1111
- `TYPE_CHECKING` blocks
12-
- Lazy/conditional imports in core logic
12+
- Lazy/conditional imports in core logic (deferred imports to break cycles are acceptable with a valid `# NOTE:` comment)
1313
- Untyped public function signatures
1414
- `hasattr`/`getattr` when direct access is possible
1515
- Bare `except:` or silent `pass`
1616
- Returning raw `dict` or `tuple` (use `msgspec.Struct`)
17+
- Setting `_python_type` on new characteristics (`BaseCharacteristic[T]` generic param auto-resolves)
1718
- Magic numbers without named constants
1819

1920
## Type Safety (MANDATORY)
@@ -26,6 +27,19 @@ applyTo: "**/*.py,**/pyproject.toml,**/requirements*.txt"
2627

2728
- Use `msgspec.Struct` (frozen, kw_only)
2829

30+
## Characteristic Implementation Patterns
31+
32+
- **Simple scalars:** Use a template from `templates/` package (`Uint8Template`, `ScaledUint16Template`, etc.)
33+
- **Multi-field composites:** Override `_decode_value()`, use `DataParser` for field extraction, return `msgspec.Struct`
34+
- **Parsing pipeline:** Use `pipeline/parse_pipeline.py` for multi-stage decode with validation
35+
- **Core modules:** `core/encoder.py` for Python→bytes, `core/parser.py` for bytes→Python, `core/query.py` for lookups
36+
37+
## Peripheral Device Patterns
38+
39+
- `PeripheralDevice` (in `device/peripheral_device.py`) for server-side BLE
40+
- `PeripheralManagerProtocol` (in `device/peripheral.py`) for adapter abstraction
41+
- Fluent configuration: `device.with_name(...).with_service(...)`
42+
2943
## Docstrings
3044

3145
- Google style (Args, Returns, Raises)

.github/instructions/testing.instructions.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,22 @@ Every new function needs: success + 2 failure cases minimum.
2222
- Boundaries: min, max, just outside
2323
- Sentinels: 0xFF, 0xFFFF, 0x8000
2424

25+
## Test Directory Structure
26+
27+
- `tests/gatt/` — characteristic and service tests (the primary pattern)
28+
- `tests/device/` — device, peripheral, advertising tests
29+
- `tests/core/` — translator, encoder, parser, query tests
30+
- `tests/benchmarks/` — performance benchmarks (`test_performance.py`, `test_comparison.py`)
31+
- `tests/static_analysis/` — registry completeness, code quality checks
32+
2533
## Commands
2634

2735
```bash
2836
python -m pytest tests/ -v
2937
python -m pytest -k "battery" -v
3038
python -m pytest --lf
39+
python -m pytest tests/ --cov=bluetooth_sig --cov-fail-under=85
40+
python -m pytest tests/benchmarks/ --benchmark-only
3141
```
3242

3343
Tests must be deterministic. See `tests/gatt/` for patterns.

.github/workflows/test-coverage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
5656
- name: Run tests with coverage
5757
run: |
58-
python -m pytest tests/ -n auto --ignore=tests/benchmarks/ --junitxml=test-results.xml --cov=src/bluetooth_sig --cov-report=html --cov-report=xml --cov-report=term-missing --cov-fail-under=70
58+
python -m pytest tests/ -n auto --ignore=tests/benchmarks/ --junitxml=test-results.xml --cov=src/bluetooth_sig --cov-report=html --cov-report=xml --cov-report=term-missing --cov-fail-under=85
5959
6060
- name: Publish JUnit test report
6161
if: always()

.vscode/launch.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
8+
{
9+
"name": "Python Debugger: Current File",
10+
"type": "debugpy",
11+
"request": "launch",
12+
"program": "${file}",
13+
"console": "integratedTerminal"
14+
}
15+
,
16+
{
17+
"name": "Pytest: Current Test",
18+
"type": "debugpy",
19+
"request": "launch",
20+
"console": "integratedTerminal",
21+
"justMyCode": false,
22+
"module": "pytest",
23+
"args": [
24+
"-q",
25+
"--rootdir=/root/homeassistant/custom_components/bluetooth-sig-python",
26+
"${file}::${pytestTest}"
27+
]
28+
}
29+
]
30+
}

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"chat.tools.terminal.autoApprove": {
3+
"bluetoothctl": true,
4+
"hciconfig": true
5+
}
6+
}

DECOMPOSITION_PLAN.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Workstream 1: God Class Decomposition Plan
2+
3+
**Goal**: Decompose 4 God classes using **composition + delegation**. Preserve all public APIs. No logic duplication. Single source of truth per concern.
4+
5+
**Execution order**: Step 3 → Step 1 → Step 4 → Step 2 (simplest first, most complex last).
6+
7+
---
8+
9+
## Step 1: Split `BluetoothSIGTranslator` (1,359 lines → ~682 line facade) — ✅ COMPLETE
10+
11+
The class is nearly stateless — only `_services: dict` is mutable. All other methods delegate to static registries. Pattern: **Composition + Delegation Facade** (like `requests.Session`).
12+
13+
### 1.1 New delegate modules under `src/bluetooth_sig/core/`
14+
15+
| New file | Class | Methods moved from translator.py | Mutable state |
16+
|---|---|---|---|
17+
| `core/query.py` | `CharacteristicQueryEngine` | `supports`, `get_value_type`, all 8 `get_*_info_*`, `get_characteristics_info_by_uuids`, `list_supported_*`, `get_service_characteristics`, `get_sig_info_by_name`, `get_sig_info_by_uuid` | None |
18+
| `core/parser.py` | `CharacteristicParser` | `parse_characteristic` (+overloads), `parse_characteristics` (batch), 5 `_*` batch helpers | None |
19+
| `core/encoder.py` | `CharacteristicEncoder` | `encode_characteristic` (+overloads), `create_value`, `validate_characteristic_data`, `_get_characteristic_value_type_class` | None |
20+
| `core/registration.py` | `RegistrationManager` | `register_custom_characteristic_class`, `register_custom_service_class` | None (writes to registries) |
21+
| `core/service_manager.py` | `ServiceManager` | `process_services`, `get_service_by_uuid`, `discovered_services`, `clear_services` | `_services: dict`**only mutable state** |
22+
23+
### 1.2 Facade pattern
24+
25+
`BluetoothSIGTranslator.__init__` creates 5 delegate instances eagerly. Every public method becomes a one-line delegation with identical signature and `@overload` decorators. Async wrappers stay on facade. Singleton `__new__`/`get_instance()`/global `BluetoothSIG` stay.
26+
27+
### 1.3 Update `core/__init__.py`
28+
29+
Re-export delegate classes alongside `BluetoothSIGTranslator` and `AsyncParsingSession`.
30+
31+
---
32+
33+
## Step 2: Split `BaseCharacteristic` (1,761 lines → 1,258 lines) — ✅ COMPLETE
34+
35+
Keeps Template Method contract (`_decode_value`/`_encode_value`). Internal composition invisible to ~150 subclasses. Pattern: **Internal Composition with back-reference**.
36+
37+
### 2.1 New `pipeline/` package under `src/bluetooth_sig/gatt/characteristics/`
38+
39+
| New file | Class | Methods extracted | Status |
40+
|---|---|---|---|
41+
| `pipeline/__init__.py` | Re-exports |||
42+
| `pipeline/parse_pipeline.py` | `ParsePipeline` | `parse_value` orchestration, `_perform_parse_validation`, `_extract_and_check_special_value`, `_decode_and_validate_value`, `_extract_raw_int`, `_check_special_value`, `_is_parse_trace_enabled` ||
43+
| `pipeline/encode_pipeline.py` | `EncodePipeline` | `build_value` orchestration, `_pack_raw_int`, `encode_special`, `encode_special_by_meaning` ||
44+
| `pipeline/validation.py` | `CharacteristicValidator` | `_validate_range` (3-level precedence), `_validate_type`, `_validate_length` ||
45+
46+
### 2.2 Additional extractions
47+
48+
| New file | Class | Methods extracted | Status |
49+
|---|---|---|---|
50+
| `role_classifier.py` | `classify_role()` function | `_classify_role`, `_spec_has_unit_fields` ||
51+
| `descriptor_support.py` | `DescriptorSupport` | 11 descriptor methods | Deferred — low value, methods are 1-liner proxies |
52+
| `special_values.py` | `SpecialValueHandler` | special value methods | Deferred — already uses SpecialValueResolver |
53+
54+
### 2.3 What stays on `BaseCharacteristic`
55+
56+
- `__init__`/`__post_init__` (composition wiring)
57+
- Properties: `uuid`, `info`, `spec`, `name`, `description`, `display_name`, `unit`, `size`, `value_type_resolved`, `role`, `get_byte_order_hint`
58+
- Abstract: `_decode_value`, `_encode_value` (Template Method hooks for subclasses)
59+
- Thin delegation: `parse_value``ParsePipeline.run()`, `build_value``EncodePipeline.run()`, `encode_special*``EncodePipeline`
60+
- Class-level UUID resolution (5 classmethods)
61+
- Dependency resolution (5 methods)
62+
- YAML metadata accessors (5 methods)
63+
- Descriptor methods (kept in base, 1-liner proxies to descriptor_utils)
64+
- Special value properties (kept in base, delegate to SpecialValueResolver)
65+
- YAML metadata accessors (5 methods)
66+
- Proxy methods for backward compat (delegate to composed objects)
67+
68+
---
69+
70+
## Step 3: Split `templates.py` (1,488 lines → package) — ✅ COMPLETE
71+
72+
No circular dependencies. Pure file reorganisation + re-export. Pattern: **Module → Package promotion**.
73+
74+
### 3.1 New `templates/` package
75+
76+
| New file | Classes | Approx lines |
77+
|---|---|---|
78+
| `templates/__init__.py` | Re-exports everything via explicit imports + `__all__` | ~60 |
79+
| `templates/base.py` | `CodingTemplate[T_co]` (ABC), resolution constants | ~100 |
80+
| `templates/data_structures.py` | `VectorData`, `Vector2DData`, `TimeData` | ~40 |
81+
| `templates/numeric.py` | `Uint8Template`, `Sint8Template`, `Uint16Template`, `Sint16Template`, `Uint24Template`, `Uint32Template` | ~200 |
82+
| `templates/scaled.py` | `ScaledTemplate` (abstract) + 8 `Scaled*Template` variants + `PercentageTemplate` | ~400 |
83+
| `templates/domain.py` | `TemperatureTemplate`, `ConcentrationTemplate`, `PressureTemplate` | ~200 |
84+
| `templates/ieee_float.py` | `IEEE11073FloatTemplate`, `Float32Template` | ~80 |
85+
| `templates/string.py` | `Utf8StringTemplate`, `Utf16StringTemplate` | ~150 |
86+
| `templates/complex.py` | `TimeDataTemplate`, `VectorTemplate`, `Vector2DTemplate` | ~200 |
87+
| `templates/enum.py` | `EnumTemplate[T]` | ~240 |
88+
89+
### 3.2 Backward compat
90+
91+
Python resolves `from .templates import X` identically whether `templates` is a module or package — as long as `X` is in the package `__init__.py`. Zero characteristic files need changing.
92+
93+
---
94+
95+
## Step 4: Split `Device` (1,172 lines → ~818 lines) — ✅ COMPLETE
96+
97+
13/40 methods are pure delegation. Substantial logic in dependency resolution and characteristic I/O. Pattern: **Composition with explicit dependencies (no back-references)**.
98+
99+
### 4.1 New modules under `src/bluetooth_sig/device/`
100+
101+
| New file | Class | Methods extracted |
102+
|---|---|---|
103+
| `dependency_resolver.py` | `DependencyResolver` + `DependencyResolutionMode` enum | `_resolve_single_dependency`, `_ensure_dependencies_resolved` |
104+
| `characteristic_io.py` | `CharacteristicIO` | `read` (+overloads), `write` (+overloads), `start_notify` (+overloads), `stop_notify`, `read_multiple`, `write_multiple`, `_resolve_characteristic_name` |
105+
106+
### 4.2 Device composes
107+
108+
```python
109+
self._dep_resolver = DependencyResolver(connection_manager, translator, self.connected)
110+
self._char_io = CharacteristicIO(connection_manager, translator, self.connected, self._dep_resolver)
111+
```
112+
113+
Remaining on Device: delegation one-liners, discovery, advertising, properties, service queries.
114+
115+
---
116+
117+
## Verification (after each step)
118+
119+
1. `python -m pytest tests/ -v` — all existing tests pass
120+
2. `./scripts/lint.sh --all` — zero errors
121+
3. `./scripts/format.sh --check` — formatting valid
122+
4. Backward compat imports still work
123+
124+
## Design Principles
125+
126+
| Principle | Application |
127+
|---|---|
128+
| **Single Responsibility** | Each delegate/composed class owns one concern |
129+
| **DRY** | Each method exists in exactly one place; facade only delegates |
130+
| **Composition over Inheritance** | Translator: 5 delegates. Base: internal composition. Templates: domain grouping |
131+
| **Single Source of Truth** | Registry access per delegate. Validation in one validator. Pipeline in one orchestrator |
132+
| **Open/Closed** | BaseCharacteristic open for extension (override hooks), closed for modification (pipeline internal) |
133+
| **Dependency Inversion** | Delegates take abstractions (registries, protocols), not concretions |
134+
| **Interface Segregation** | QueryEngine separate from Parser — consumers depend only on what they use |

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ for char in client.services.characteristics:
136136
uuid_str = str(char.uuid)
137137
if translator.supports(uuid_str):
138138
raw_data = await client.read_gatt_char(uuid_str) # SKIP: async
139-
result = translator.parse_characteristic(uuid_str, raw_data)
140-
print(f"{result.info.name}: {result.value}") # Returns Any
139+
parsed = translator.parse_characteristic(uuid_str, raw_data)
140+
info = translator.get_characteristic_info_by_uuid(uuid_str)
141+
print(f"{info.name}: {parsed}") # parsed is the value directly (Any)
141142
else:
142143
print(f"Unknown characteristic UUID: {uuid_str}")
143144
```
@@ -215,8 +216,8 @@ battery_uuid = translator.get_characteristic_uuid_by_name(CharacteristicName.BAT
215216
async with BleakClient(address) as client:
216217
# Read: bleak handles connection, bluetooth-sig handles parsing
217218
raw_data = await client.read_gatt_char(str(battery_uuid))
218-
result = translator.parse_characteristic(str(battery_uuid), raw_data)
219-
print(f"Battery: {result.value}%")
219+
level = translator.parse_characteristic(str(battery_uuid), raw_data)
220+
print(f"Battery: {level}%")
220221

221222
# Write: bluetooth-sig handles encoding, bleak handles transmission
222223
data = translator.encode_characteristic(str(battery_uuid), 85)

0 commit comments

Comments
 (0)