aosmith-ble 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aosmith_ble-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
- aosmith_ble-0.1.0/.github/ISSUE_TEMPLATE/device-support.yml +44 -0
- aosmith_ble-0.1.0/.github/workflows/ci.yml +33 -0
- aosmith_ble-0.1.0/.github/workflows/docs.yml +29 -0
- aosmith_ble-0.1.0/.github/workflows/release.yml +44 -0
- aosmith_ble-0.1.0/.gitignore +8 -0
- aosmith_ble-0.1.0/.pre-commit-config.yaml +39 -0
- aosmith_ble-0.1.0/CHANGELOG.md +47 -0
- aosmith_ble-0.1.0/CLAUDE.md +64 -0
- aosmith_ble-0.1.0/LICENSE +21 -0
- aosmith_ble-0.1.0/PKG-INFO +89 -0
- aosmith_ble-0.1.0/README.md +73 -0
- aosmith_ble-0.1.0/docs/contributing/does-my-heater-work.md +51 -0
- aosmith_ble-0.1.0/docs/development/design-principles.md +14 -0
- aosmith_ble-0.1.0/docs/development/handoff.md +30 -0
- aosmith_ble-0.1.0/docs/development/index.md +8 -0
- aosmith_ble-0.1.0/docs/development/releasing.md +16 -0
- aosmith_ble-0.1.0/docs/development/test-vectors.md +19 -0
- aosmith_ble-0.1.0/docs/hardware/hot-water-availability.md +11 -0
- aosmith_ble-0.1.0/docs/hardware/maintenance-display.md +48 -0
- aosmith_ble-0.1.0/docs/hardware/operating-limits.md +21 -0
- aosmith_ble-0.1.0/docs/index.md +36 -0
- aosmith_ble-0.1.0/docs/protocol/block-11.md +104 -0
- aosmith_ble-0.1.0/docs/protocol/blocks.md +171 -0
- aosmith_ble-0.1.0/docs/protocol/commands.md +152 -0
- aosmith_ble-0.1.0/docs/protocol/connection.md +83 -0
- aosmith_ble-0.1.0/docs/protocol/crc.md +34 -0
- aosmith_ble-0.1.0/docs/protocol/faults.md +68 -0
- aosmith_ble-0.1.0/docs/protocol/handshake.md +155 -0
- aosmith_ble-0.1.0/docs/protocol/reference.md +56 -0
- aosmith_ble-0.1.0/docs/research/captures.md +74 -0
- aosmith_ble-0.1.0/docs/research/index.md +21 -0
- aosmith_ble-0.1.0/docs/research/methodology.md +8 -0
- aosmith_ble-0.1.0/docs/research/open-questions.md +25 -0
- aosmith_ble-0.1.0/docs/research/sources.md +32 -0
- aosmith_ble-0.1.0/docs/research/timed-modes-experiment.md +54 -0
- aosmith_ble-0.1.0/docs/tested-devices.md +30 -0
- aosmith_ble-0.1.0/pyproject.toml +36 -0
- aosmith_ble-0.1.0/scripts/check_identifiers.py +80 -0
- aosmith_ble-0.1.0/src/aosmith_ble/__init__.py +59 -0
- aosmith_ble-0.1.0/src/aosmith_ble/client.py +775 -0
- aosmith_ble-0.1.0/src/aosmith_ble/codecs.py +128 -0
- aosmith_ble-0.1.0/src/aosmith_ble/const.py +123 -0
- aosmith_ble-0.1.0/src/aosmith_ble/crc.py +283 -0
- aosmith_ble-0.1.0/src/aosmith_ble/diagnostics.py +113 -0
- aosmith_ble-0.1.0/src/aosmith_ble/exceptions.py +76 -0
- aosmith_ble-0.1.0/src/aosmith_ble/models.py +64 -0
- aosmith_ble-0.1.0/src/aosmith_ble/profiles/__init__.py +316 -0
- aosmith_ble-0.1.0/src/aosmith_ble/profiles/bundled.py +105 -0
- aosmith_ble-0.1.0/src/aosmith_ble/protocol.py +230 -0
- aosmith_ble-0.1.0/src/aosmith_ble/py.typed +0 -0
- aosmith_ble-0.1.0/tests/fake_transport.py +43 -0
- aosmith_ble-0.1.0/tests/fixtures/frames.json +50 -0
- aosmith_ble-0.1.0/tests/test_client_diagnose.py +460 -0
- aosmith_ble-0.1.0/tests/test_client_hooks.py +260 -0
- aosmith_ble-0.1.0/tests/test_client_lifecycle.py +65 -0
- aosmith_ble-0.1.0/tests/test_client_session.py +445 -0
- aosmith_ble-0.1.0/tests/test_client_state.py +289 -0
- aosmith_ble-0.1.0/tests/test_client_writes.py +523 -0
- aosmith_ble-0.1.0/tests/test_codecs.py +82 -0
- aosmith_ble-0.1.0/tests/test_const_and_models.py +92 -0
- aosmith_ble-0.1.0/tests/test_crc.py +41 -0
- aosmith_ble-0.1.0/tests/test_diagnostics.py +122 -0
- aosmith_ble-0.1.0/tests/test_docs_tested_devices.py +59 -0
- aosmith_ble-0.1.0/tests/test_exceptions.py +37 -0
- aosmith_ble-0.1.0/tests/test_models.py +31 -0
- aosmith_ble-0.1.0/tests/test_profiles.py +673 -0
- aosmith_ble-0.1.0/tests/test_profiles_bundled.py +99 -0
- aosmith_ble-0.1.0/tests/test_protocol_ascii_and_at.py +32 -0
- aosmith_ble-0.1.0/tests/test_protocol_mode.py +40 -0
- aosmith_ble-0.1.0/tests/test_protocol_read_failure_forms.py +63 -0
- aosmith_ble-0.1.0/tests/test_protocol_read_write.py +106 -0
- aosmith_ble-0.1.0/tests/test_protocol_session.py +106 -0
- aosmith_ble-0.1.0/tests/test_protocol_temperature.py +30 -0
- aosmith_ble-0.1.0/tests/test_scaffold.py +70 -0
- aosmith_ble-0.1.0/uv.lock +846 -0
- aosmith_ble-0.1.0/zensical.toml +132 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: true
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Device support request
|
|
2
|
+
description: Ask for support for a heater that connect() refuses, using the diagnostic report.
|
|
3
|
+
title: "Device support: <model> firmware <x.y>"
|
|
4
|
+
labels: ["device-support"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for helping. This only covers **read-only** support to start with. Nothing here
|
|
10
|
+
promises support for a model, and write support needs an observed write for the exact
|
|
11
|
+
model and firmware.
|
|
12
|
+
|
|
13
|
+
Run `AOSmithBLEClient.async_diagnose()` and paste `report.to_markdown()` below (it is
|
|
14
|
+
redacted by default; never use `redact=False`). See
|
|
15
|
+
"Does my heater work?" in the documentation for the steps.
|
|
16
|
+
- type: input
|
|
17
|
+
id: model
|
|
18
|
+
attributes:
|
|
19
|
+
label: Model on the rating plate
|
|
20
|
+
description: For example the model line, not the serial number.
|
|
21
|
+
validations:
|
|
22
|
+
required: true
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: report
|
|
25
|
+
attributes:
|
|
26
|
+
label: Diagnostic report
|
|
27
|
+
description: Paste the output of `report.to_markdown()`.
|
|
28
|
+
render: markdown
|
|
29
|
+
validations:
|
|
30
|
+
required: true
|
|
31
|
+
- type: textarea
|
|
32
|
+
id: notes
|
|
33
|
+
attributes:
|
|
34
|
+
label: Anything else
|
|
35
|
+
description: Does the official app work with this heater? Did the Bluetooth button press change anything?
|
|
36
|
+
- type: checkboxes
|
|
37
|
+
id: checks
|
|
38
|
+
attributes:
|
|
39
|
+
label: Before submitting
|
|
40
|
+
options:
|
|
41
|
+
- label: My report has no Bluetooth address, no 18-character `02iQk...` value and no full serial number.
|
|
42
|
+
required: true
|
|
43
|
+
- label: I understand the diagnostic call only reads, and that support is not guaranteed.
|
|
44
|
+
required: true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
- develop
|
|
7
|
+
pull_request:
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
20
|
+
- run: uv sync --python ${{ matrix.python-version }}
|
|
21
|
+
- run: uv run pytest -q
|
|
22
|
+
lint:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
27
|
+
- run: uvx prek run --all-files
|
|
28
|
+
docs:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v7
|
|
32
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
33
|
+
- run: uvx zensical build --clean --strict
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Documentation
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- master
|
|
6
|
+
- main
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
pages: write
|
|
10
|
+
id-token: write
|
|
11
|
+
jobs:
|
|
12
|
+
deploy:
|
|
13
|
+
environment:
|
|
14
|
+
name: github-pages
|
|
15
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/configure-pages@v6
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
- uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: 3.x
|
|
23
|
+
- run: pip install zensical
|
|
24
|
+
- run: zensical build --clean --strict
|
|
25
|
+
- uses: actions/upload-pages-artifact@v5
|
|
26
|
+
with:
|
|
27
|
+
path: site
|
|
28
|
+
- uses: actions/deploy-pages@v5
|
|
29
|
+
id: deployment
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "v*"
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v7
|
|
13
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
14
|
+
- name: Tag must match the pyproject version
|
|
15
|
+
run: |
|
|
16
|
+
version="$(uv version --short)"
|
|
17
|
+
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
|
|
18
|
+
echo "tag ${GITHUB_REF_NAME} does not match pyproject version ${version}" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
- run: uv sync
|
|
22
|
+
- run: uv run pytest -q
|
|
23
|
+
- run: uv build
|
|
24
|
+
- name: Smoke test the built wheel
|
|
25
|
+
run: uv run --isolated --no-project --with dist/*.whl python -I -c "import aosmith_ble; print(aosmith_ble.__version__)"
|
|
26
|
+
- uses: actions/upload-artifact@v7.0.2
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
if-no-files-found: error
|
|
31
|
+
publish:
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment:
|
|
35
|
+
name: pypi
|
|
36
|
+
url: https://pypi.org/p/aosmith-ble
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@v8.0.2
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@v1.14.2
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Run with prek (https://github.com/j178/prek): `prek install`, then `prek run --all-files`.
|
|
2
|
+
# prek reads this file and is compatible with the pre-commit config format.
|
|
3
|
+
repos:
|
|
4
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
5
|
+
rev: v6.0.0
|
|
6
|
+
hooks:
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-json
|
|
10
|
+
- id: check-case-conflict
|
|
11
|
+
- id: detect-private-key
|
|
12
|
+
- id: end-of-file-fixer
|
|
13
|
+
exclude: "^LICENSE$"
|
|
14
|
+
- id: mixed-line-ending
|
|
15
|
+
args: ["--fix=lf"]
|
|
16
|
+
- id: trailing-whitespace
|
|
17
|
+
- repo: https://github.com/codespell-project/codespell
|
|
18
|
+
rev: v2.4.2
|
|
19
|
+
hooks:
|
|
20
|
+
- id: codespell
|
|
21
|
+
additional_dependencies: [tomli]
|
|
22
|
+
exclude: "^(uv.lock|tests/fixtures/.*)$"
|
|
23
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
24
|
+
rev: v0.15.6
|
|
25
|
+
hooks:
|
|
26
|
+
- id: ruff-check
|
|
27
|
+
- id: ruff-format
|
|
28
|
+
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
29
|
+
rev: v2.18.1
|
|
30
|
+
hooks:
|
|
31
|
+
- id: pyproject-fmt
|
|
32
|
+
- repo: local
|
|
33
|
+
hooks:
|
|
34
|
+
- id: check-identifiers
|
|
35
|
+
name: block real device identifiers
|
|
36
|
+
entry: python scripts/check_identifiers.py
|
|
37
|
+
language: system
|
|
38
|
+
pass_filenames: true
|
|
39
|
+
exclude: "^uv.lock$"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- `connect()` after a link lost under the client now closes the dead link
|
|
6
|
+
before opening a new one. Without this the heater rejected the new session
|
|
7
|
+
(hardware-observed), so a reused client could not reconnect after an
|
|
8
|
+
unexpected drop.
|
|
9
|
+
- `AOSmithBLEClient` takes three optional keyword-only hooks for embedding in a
|
|
10
|
+
long-running application: `client_class` (the bleak client class to connect
|
|
11
|
+
with), `device_resolver` (a callback returning the freshest `BLEDevice`, used
|
|
12
|
+
on every connect and every connection retry), and `on_disconnect` (a callback
|
|
13
|
+
for unexpected link loss). All default to the previous behavior.
|
|
14
|
+
- `bleak-retry-connector` is now imported unconditionally; the unreachable
|
|
15
|
+
fallback to a bare `BleakClient` is gone.
|
|
16
|
+
- `HeaterState` is removed. `AOSmithBLEClient.async_get_state()` now returns a
|
|
17
|
+
`FeatureSet` (`Mapping[FieldName, Feature]`) -- a `Feature` per profile
|
|
18
|
+
field graded at least `min_trust` (default `Trust.OK`), each carrying its
|
|
19
|
+
own `value`, `trust`, and `writable`. A field graded below `min_trust` is
|
|
20
|
+
absent from the mapping, never present with a placeholder.
|
|
21
|
+
- `FieldName` (a `StrEnum`) and `FieldScope` (`STATE`/`IDENTITY`) replace bare
|
|
22
|
+
string field names and the implicit assumption that every field lives in
|
|
23
|
+
Block 11.
|
|
24
|
+
- `DeviceInfo.serial` is now populated, once, at connect time.
|
|
25
|
+
- New `AOSmithBLEClient.async_diagnose()`: an independent connect/probe/
|
|
26
|
+
disconnect sequence that works even for a device `connect()` would refuse
|
|
27
|
+
outright, producing a `DiagnosticReport` with the existing `to_markdown()`
|
|
28
|
+
plus a new `to_fixture_entries()` -- redacted, `tests/fixtures/frames.json`-
|
|
29
|
+
shaped output suitable for a new-profile contribution PR.
|
|
30
|
+
- `Profile`/`FieldSpec`/`Match` device-profile system, with `__post_init__` structural
|
|
31
|
+
validation and inheritance (write trust demoted unless re-asserted).
|
|
32
|
+
- One bundled profile (`hpts50-6.3`).
|
|
33
|
+
- The connecting, writing async client (`AOSmithBLEClient`): session establishment,
|
|
34
|
+
profile matching, MTU and UNITS validation, state reads, and a single profile-gated
|
|
35
|
+
write choke point with fault-active read-only gating and write-then-read-back
|
|
36
|
+
verification.
|
|
37
|
+
- Release-on-demand and `BleakOutOfConnectionSlotsError` wrapping.
|
|
38
|
+
- Presence probing and the unknown-device diagnostic report.
|
|
39
|
+
- Distinguishes the two 7-byte short-response forms (`ReadRefusedError` vs.
|
|
40
|
+
`SessionError`) that the 0.0.1 release's `parse_read` treated identically.
|
|
41
|
+
|
|
42
|
+
## 0.0.1 - Unreleased
|
|
43
|
+
|
|
44
|
+
- Initial protocol layer: frame construction/parsing, CRC-8, the session
|
|
45
|
+
handshake (pairing-slot init, challenge/response), setpoint and mode
|
|
46
|
+
codecs, fault code table.
|
|
47
|
+
- No Bluetooth I/O in this release; frame building and parsing only.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guidance for working in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`aosmith-ble` is a Python library plus a documentation site for local Bluetooth LE
|
|
8
|
+
control of A.O. Smith iCOMM heat pump water heaters (no cloud). The library is in
|
|
9
|
+
`src/aosmith_ble/` (client, protocol, profiles, codecs, diagnostics). The protocol
|
|
10
|
+
knowledge and research record are in `docs/`, built with Zensical. Python only.
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv sync
|
|
16
|
+
uv run pytest -q # the test suite, no hardware needed
|
|
17
|
+
uvx zensical serve # live docs preview
|
|
18
|
+
uvx zensical build --clean --strict # must pass with zero warnings
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Use `uv`/`uvx` for all Python work, `rg` for searching, `fd` for finding files.
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
- `src/aosmith_ble/protocol.py`, `crc.py`: frame building and parsing, CRC-8.
|
|
26
|
+
- `src/aosmith_ble/profiles/`: per-model field maps and trust grades.
|
|
27
|
+
- `src/aosmith_ble/client.py`: the connecting client. All writes go through one profile-gated path.
|
|
28
|
+
- `tests/`: unit tests with fixtures in `tests/fixtures/frames.json`.
|
|
29
|
+
- `docs/protocol/`, `docs/hardware/`, `docs/research/`, `docs/development/`: the site.
|
|
30
|
+
The nav lives in `zensical.toml`; every page must be listed there.
|
|
31
|
+
- `docs/superpowers/`: working plans and specs, if present. Excluded from the published
|
|
32
|
+
site by the exclude plugin in `zensical.toml`, so never link to it as a site page.
|
|
33
|
+
|
|
34
|
+
## Safety rules
|
|
35
|
+
|
|
36
|
+
- Research is read-only. Never send a write whose encoding is not proven (byte-for-byte match with a captured frame, or a verified round trip).
|
|
37
|
+
- No speculative writes, no raw-write API, no blind write retries.
|
|
38
|
+
- Never raise the setpoint casually. Writes are capped at 140F absolute and 130F by default; the handbook range is 95-150F.
|
|
39
|
+
- Timed-mode writes are stateful: the device counts days down and reverts at zero. The setpoint is a scalar, not per-mode.
|
|
40
|
+
- GUEST mode has never been observed written; do not add it to any profile's `writable_modes` until a write is observed.
|
|
41
|
+
- Exploratory reads at non-zero start offsets coincided with a change in Block 11 params 9 and 14 (causation unproven). Keep reads to what the app does.
|
|
42
|
+
|
|
43
|
+
## Hardware facts that cost time
|
|
44
|
+
|
|
45
|
+
- Only one central can connect at a time. Stop other clients first.
|
|
46
|
+
- A stale BlueZ connection blocks discovery (the heater stops advertising while connected): `bluetoothctl disconnect <mac>`.
|
|
47
|
+
- After about 10 minutes with no traffic the heater sleeps and needs a Bluetooth button
|
|
48
|
+
press to wake.
|
|
49
|
+
- The central must answer the heater's ATT MTU request or no notifications arrive.
|
|
50
|
+
- The proprietary service UUID is not advertised. Discovery matches on the `iCOMM-*`
|
|
51
|
+
local name.
|
|
52
|
+
|
|
53
|
+
## Docs rules
|
|
54
|
+
|
|
55
|
+
- Tag every protocol claim as verified on hardware, from the handbook, or unverified. Never upgrade confidence while moving text.
|
|
56
|
+
- Never put a real MAC, serial, assetID, pairing code or advertised name in docs,
|
|
57
|
+
fixtures or tests. Use `AA:BB:CC:DD:EE:FF`, `iCOMM-AC000W037XXXXXX`,
|
|
58
|
+
`02iQk000000EXAMPLE`, `XXXXXX`, `<plate serial>`. Part number `100350404` and model
|
|
59
|
+
`HPTS-50` are model-level and fine.
|
|
60
|
+
- Test vectors are synthetic. See `docs/development/test-vectors.md` before changing
|
|
61
|
+
`ASSET_ID` or `frames.json`.
|
|
62
|
+
- docs/research and docs/hardware are language-neutral: no code snippets or library
|
|
63
|
+
references. State current facts only; no revision history or dates.
|
|
64
|
+
- Use `!!! warning` admonitions, not GitHub-style alerts, and relative links only.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trenton Holmes
|
|
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,89 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aosmith-ble
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local Bluetooth LE client for A.O. Smith iCOMM heat pump water heaters
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: bleak-retry-connector>=3.4
|
|
14
|
+
Requires-Dist: bleak>=0.21
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# aosmith-ble
|
|
18
|
+
|
|
19
|
+
Local Bluetooth LE control of A.O. Smith iCOMM heat pump water heaters -- no cloud
|
|
20
|
+
account, no vendor credentials, no internet connection involved. This project is
|
|
21
|
+
independent and unaffiliated with A.O. Smith.
|
|
22
|
+
|
|
23
|
+
Documentation: see the `docs/` folder or the project site.
|
|
24
|
+
|
|
25
|
+
Looking for the cloud API instead? See [py-aosmith](https://github.com/bdr99/py-aosmith).
|
|
26
|
+
|
|
27
|
+
## What this is
|
|
28
|
+
|
|
29
|
+
An async client that connects directly to a water heater's BLE interface, matches it against a bundled device profile, and reads or writes only what that profile has verified. An unrecognized device is refused outright. To help add support for a new device, call `AOSmithBLEClient.async_diagnose()`. It connects, probes and disconnects on its own (so it works even where `connect()` refuses) and returns a `DiagnosticReport`: `to_markdown()` gives a human-readable report and `to_fixture_entries()` gives redacted, fixture-shaped captures for a profile-contribution PR.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
pip install git+https://github.com/stumpylog/aosmith-ble
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from aosmith_ble import AOSmithBLEClient, FieldName, Mode, async_discover, pairing_code_from_name
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
devices = await async_discover()
|
|
43
|
+
device = devices[0]
|
|
44
|
+
code = pairing_code_from_name(device.name)
|
|
45
|
+
async with AOSmithBLEClient(device, pairing_code=code) as client:
|
|
46
|
+
features = await client.async_get_state()
|
|
47
|
+
setpoint = features[FieldName.SETPOINT].value
|
|
48
|
+
mode = features[FieldName.MODE].value
|
|
49
|
+
fault = features[FieldName.FAULT].value
|
|
50
|
+
print(f"{setpoint}F, mode={Mode(mode.mode).name}, fault={fault.name}")
|
|
51
|
+
await client.async_set_setpoint(124.0)
|
|
52
|
+
|
|
53
|
+
asyncio.run(main())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Pairing
|
|
57
|
+
|
|
58
|
+
Press the Bluetooth button on the control panel once before first use. The device hands out the "assetID" it authenticates sessions against only while its pairing slot is populated. Cache `AOSmithBLEClient.asset_id` (for example with the device address) and pass it back as `asset_id=` so the button press is a one-time step.
|
|
59
|
+
|
|
60
|
+
## Embedding in a long-running application
|
|
61
|
+
|
|
62
|
+
A host that owns Bluetooth scanning and wants the client to survive reconnects can pass three optional, keyword-only hooks:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
client = AOSmithBLEClient(
|
|
66
|
+
device,
|
|
67
|
+
pairing_code=code,
|
|
68
|
+
asset_id=cached_asset_id,
|
|
69
|
+
device_resolver=lambda: my_scanner.latest_device(address),
|
|
70
|
+
client_class=MyBleakClientSubclass,
|
|
71
|
+
on_disconnect=lambda: schedule_reconnect(),
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `device_resolver` returns the freshest `BLEDevice` for the heater, or `None` if there is nothing newer. It is called on every `connect()` and on every connection retry, so a handle that changes between connects (a different adapter or relay) is picked up. Passing a `BLEDevice` as `device` and no resolver keeps the current behavior. Passing an address string makes the library run its own scan, so a host that owns scanning should pass a `BLEDevice` or a resolver instead.
|
|
76
|
+
- `client_class` is the bleak client class used to connect, `BleakClient` by default. Pass a subclass, such as `bleak_retry_connector.BleakClientWithServiceCache`, to change how the link behaves.
|
|
77
|
+
- `on_disconnect` is called with no arguments when the link is lost unexpectedly. It is not called for `disconnect()`, `async_release()` or a failed `connect()`. It runs on the event loop, must not block, and an exception it raises is logged and ignored. A drop while `connect()` is still setting up the session calls it as well, and `connect()` then raises its own error.
|
|
78
|
+
|
|
79
|
+
## Safety model
|
|
80
|
+
|
|
81
|
+
Every write goes through one profile-gated path: setpoint and mode are checked against the profile's verified limits (never above 140F, 130F by default), a fault-active device is read-only, and every write is read back and compared before the call succeeds. The public API cannot write an arbitrary block or parameter.
|
|
82
|
+
|
|
83
|
+
## What this doesn't do (yet)
|
|
84
|
+
|
|
85
|
+
No current-tank-temperature reading (the manufacturer's app never reads it either). No Home Assistant integration; that is a separate, HACS-distributed package. No support for devices other than the bundled profile: `connect()` refuses an unmatched device. Diagnostics are an explicit, separate call, never part of the automatic connection flow.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# aosmith-ble
|
|
2
|
+
|
|
3
|
+
Local Bluetooth LE control of A.O. Smith iCOMM heat pump water heaters -- no cloud
|
|
4
|
+
account, no vendor credentials, no internet connection involved. This project is
|
|
5
|
+
independent and unaffiliated with A.O. Smith.
|
|
6
|
+
|
|
7
|
+
Documentation: see the `docs/` folder or the project site.
|
|
8
|
+
|
|
9
|
+
Looking for the cloud API instead? See [py-aosmith](https://github.com/bdr99/py-aosmith).
|
|
10
|
+
|
|
11
|
+
## What this is
|
|
12
|
+
|
|
13
|
+
An async client that connects directly to a water heater's BLE interface, matches it against a bundled device profile, and reads or writes only what that profile has verified. An unrecognized device is refused outright. To help add support for a new device, call `AOSmithBLEClient.async_diagnose()`. It connects, probes and disconnects on its own (so it works even where `connect()` refuses) and returns a `DiagnosticReport`: `to_markdown()` gives a human-readable report and `to_fixture_entries()` gives redacted, fixture-shaped captures for a profile-contribution PR.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
pip install git+https://github.com/stumpylog/aosmith-ble
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from aosmith_ble import AOSmithBLEClient, FieldName, Mode, async_discover, pairing_code_from_name
|
|
24
|
+
|
|
25
|
+
async def main():
|
|
26
|
+
devices = await async_discover()
|
|
27
|
+
device = devices[0]
|
|
28
|
+
code = pairing_code_from_name(device.name)
|
|
29
|
+
async with AOSmithBLEClient(device, pairing_code=code) as client:
|
|
30
|
+
features = await client.async_get_state()
|
|
31
|
+
setpoint = features[FieldName.SETPOINT].value
|
|
32
|
+
mode = features[FieldName.MODE].value
|
|
33
|
+
fault = features[FieldName.FAULT].value
|
|
34
|
+
print(f"{setpoint}F, mode={Mode(mode.mode).name}, fault={fault.name}")
|
|
35
|
+
await client.async_set_setpoint(124.0)
|
|
36
|
+
|
|
37
|
+
asyncio.run(main())
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Pairing
|
|
41
|
+
|
|
42
|
+
Press the Bluetooth button on the control panel once before first use. The device hands out the "assetID" it authenticates sessions against only while its pairing slot is populated. Cache `AOSmithBLEClient.asset_id` (for example with the device address) and pass it back as `asset_id=` so the button press is a one-time step.
|
|
43
|
+
|
|
44
|
+
## Embedding in a long-running application
|
|
45
|
+
|
|
46
|
+
A host that owns Bluetooth scanning and wants the client to survive reconnects can pass three optional, keyword-only hooks:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
client = AOSmithBLEClient(
|
|
50
|
+
device,
|
|
51
|
+
pairing_code=code,
|
|
52
|
+
asset_id=cached_asset_id,
|
|
53
|
+
device_resolver=lambda: my_scanner.latest_device(address),
|
|
54
|
+
client_class=MyBleakClientSubclass,
|
|
55
|
+
on_disconnect=lambda: schedule_reconnect(),
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `device_resolver` returns the freshest `BLEDevice` for the heater, or `None` if there is nothing newer. It is called on every `connect()` and on every connection retry, so a handle that changes between connects (a different adapter or relay) is picked up. Passing a `BLEDevice` as `device` and no resolver keeps the current behavior. Passing an address string makes the library run its own scan, so a host that owns scanning should pass a `BLEDevice` or a resolver instead.
|
|
60
|
+
- `client_class` is the bleak client class used to connect, `BleakClient` by default. Pass a subclass, such as `bleak_retry_connector.BleakClientWithServiceCache`, to change how the link behaves.
|
|
61
|
+
- `on_disconnect` is called with no arguments when the link is lost unexpectedly. It is not called for `disconnect()`, `async_release()` or a failed `connect()`. It runs on the event loop, must not block, and an exception it raises is logged and ignored. A drop while `connect()` is still setting up the session calls it as well, and `connect()` then raises its own error.
|
|
62
|
+
|
|
63
|
+
## Safety model
|
|
64
|
+
|
|
65
|
+
Every write goes through one profile-gated path: setpoint and mode are checked against the profile's verified limits (never above 140F, 130F by default), a fault-active device is read-only, and every write is read back and compared before the call succeeds. The public API cannot write an arbitrary block or parameter.
|
|
66
|
+
|
|
67
|
+
## What this doesn't do (yet)
|
|
68
|
+
|
|
69
|
+
No current-tank-temperature reading (the manufacturer's app never reads it either). No Home Assistant integration; that is a separate, HACS-distributed package. No support for devices other than the bundled profile: `connect()` refuses an unmatched device. Diagnostics are an explicit, separate call, never part of the automatic connection flow.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Does my heater work?
|
|
2
|
+
|
|
3
|
+
`connect()` refuses any heater without a verified profile, because the library writes to a real appliance and the firmware is not trusted to validate input. The one bundled profile is `hpts50-6.3` ([tested devices](../tested-devices.md)).
|
|
4
|
+
|
|
5
|
+
If your heater is refused, you can still help. `async_diagnose()` is a separate, explicit, read-only call that connects, probes and disconnects on its own, and produces a report you can paste into an issue.
|
|
6
|
+
|
|
7
|
+
## What it does and does not do
|
|
8
|
+
|
|
9
|
+
- It reads only, and never writes.
|
|
10
|
+
- It asks each block id from 0 to 40 for a single word starting at offset 0, with a short
|
|
11
|
+
pause between reads, to record which blocks answer.
|
|
12
|
+
- It reads the identity block to record the model bytes and firmware.
|
|
13
|
+
- It disconnects when finished, whether or not anything matched.
|
|
14
|
+
|
|
15
|
+
## Run it
|
|
16
|
+
|
|
17
|
+
Press the Bluetooth button on the control panel once first, so the heater hands out the session value the library needs ([Handshake](../protocol/handshake.md#the-assetid)). Close the official app and any other client first, since only one can connect.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
|
|
22
|
+
from aosmith_ble import AOSmithBLEClient, async_discover, pairing_code_from_name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def main() -> None:
|
|
26
|
+
device = (await async_discover())[0]
|
|
27
|
+
client = AOSmithBLEClient(device, pairing_code=pairing_code_from_name(device.name))
|
|
28
|
+
report = await client.async_diagnose()
|
|
29
|
+
print(report.to_markdown()) # redacted by default
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`to_markdown()` redacts the session value, serial, Bluetooth address and any Wi-Fi name
|
|
36
|
+
by default. Do not pass `redact=False` when posting publicly.
|
|
37
|
+
|
|
38
|
+
## File a report
|
|
39
|
+
|
|
40
|
+
Open a "Device support request" issue and paste the report. Before you
|
|
41
|
+
post, check it contains none of these:
|
|
42
|
+
|
|
43
|
+
- your heater's Bluetooth address
|
|
44
|
+
- the 18-character session value (it starts `02iQk`)
|
|
45
|
+
- the full serial number from the rating plate or the advertised name's last digits
|
|
46
|
+
|
|
47
|
+
To contribute fixtures, use `report.to_fixture_entries()` (redacted frames shaped like `tests/fixtures/frames.json`), never raw captures. Why: [Test vectors](../development/test-vectors.md).
|
|
48
|
+
|
|
49
|
+
## What happens next
|
|
50
|
+
|
|
51
|
+
A maintainer compares your block presence and model bytes with the [block map](../protocol/blocks.md) and the evidence rules in [Research](../research/index.md). A report does not promise support for any model. A new profile starts read-only, and write support is added only after an observed write exists for that exact model and firmware.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Design principles
|
|
2
|
+
|
|
3
|
+
1. **Audience.** Published for the community, aware of multiple models and firmware versions.
|
|
4
|
+
2. **No speculative writes, ever.** Confidence comes from read-side validation, limits from the verified profile, and frames from range-checked values. No public raw-write API, no blind write retries.
|
|
5
|
+
3. **Strict validation.** An unrecognized device is refused outright, and the refusal produces a complete diagnostic report suitable for pasting into an issue.
|
|
6
|
+
4. **One connection.** Hold a single connection open and poll. Surface the sleep timeout as a repair ("press the Bluetooth button") in the Home Assistant integration.
|
|
7
|
+
5. **Writes.** Setpoint, mode, and timed modes.
|
|
8
|
+
6. **Two packages.** `aosmith-ble` is the library. A separate Home Assistant integration (`ha-aosmith-ble`, distributed through HACS) builds on it.
|
|
9
|
+
|
|
10
|
+
## Compatibility table
|
|
11
|
+
|
|
12
|
+
Compatibility table: contributors run `async_diagnose()` on an unknown device, and `DiagnosticReport.to_fixture_entries()` plus `draft_profile_stanza` become the pull request artifact for a new bundled profile.
|
|
13
|
+
|
|
14
|
+
Frame formats: [Commands](../protocol/commands.md). Low-level frame builders exist for testing and cannot transmit; the [roadmap](handoff.md) tracks whether they stay public. None is a supported write API.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
What is still open. Read `README.md` and `CHANGELOG.md` for what the library does today and [Design principles](design-principles.md) for why.
|
|
4
|
+
|
|
5
|
+
## Smaller library items
|
|
6
|
+
|
|
7
|
+
| Item | Detail |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `Profile.__post_init__` message ("unless this profile is only ever used as a parent via `inherits`") promises a case that later fails in `resolve_inherits` | Fix the wording, or add an `abstract` flag. |
|
|
10
|
+
| Every bundled profile is directly matchable, including future parents | `match_profile` iterates all profiles with a `match`. Harmless with one profile; needs an `abstract` concept before a second is added via `inherits`. |
|
|
11
|
+
| `CODEC_REGISTRY` is a plain mutable `dict` | Wrap in `types.MappingProxyType`. |
|
|
12
|
+
| `duration_days` type checking | The codec range-checks but does not type-check, so `1.5` raises `TypeError` instead of `ValidationError`, and `bool` is accepted as `0` or `1`. Neither reaches the wire. |
|
|
13
|
+
| `assert self._profile is not None` in the write path | In both write methods and in session and read helpers. Stripped under `python -O`; use an explicit `if ... raise`. |
|
|
14
|
+
| `protocol.set_setpoint_request` and `set_mode_request` are public but unused by the client | Only tests use them (to build expected frames), `set_setpoint_request` has no ceiling check, and neither is re-exported at top level. Keep as documented low-level helpers or move out of the public surface. |
|
|
15
|
+
| `pyproject.toml` pins `bleak>=0.21` with no upper bound | Consider `bleak>=0.21,<4`. |
|
|
16
|
+
|
|
17
|
+
## Home Assistant integration
|
|
18
|
+
|
|
19
|
+
Needs a specification first: domain `aosmith_ble`, entity table, operation-mode mapping, config flow, coordinator, error and repair handling, diagnostics redaction. Not started. It belongs in its own repository (`ha-aosmith-ble`, via HACS). The diagnostics-only mode for unmatched devices can build on `async_diagnose()`.
|
|
20
|
+
|
|
21
|
+
## Lower priority
|
|
22
|
+
|
|
23
|
+
- **Stale days-remaining counters.** The Vacation counter keeps its last value after an early switch to another mode (hardware-observed), so `vacation_days_remaining` can read 5 in Hybrid. Decide whether the library should report 0 or hide a counter unless its mode is active.
|
|
24
|
+
|
|
25
|
+
- **Publish to PyPI.** The package builds, but nothing has been uploaded. Do it deliberately, when someone wants it installable from an index.
|
|
26
|
+
- **Fingerprint-based matching.** The third matching outcome: fingerprint matches but firmware differs, so inherit addressing and demote to read-only. Blocked on a block-length detection tool that does not exist yet. Do not start before the Home Assistant integration.
|
|
27
|
+
- **GUEST mode.** No profile may add mode 3 to `writable_modes` until an observed write exists ([Open questions](../research/open-questions.md)).
|
|
28
|
+
- **Current tank temperature and hot-water availability.** Open leads ([Hot water availability](../hardware/hot-water-availability.md)). The official app reads neither over BLE. The [panel experiment](../hardware/maintenance-display.md) could extend to tank temperature, otherwise a new empirical approach is needed.
|
|
29
|
+
|
|
30
|
+
See also [Open questions](../research/open-questions.md).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
For contributors: where the project stands and how it is built.
|
|
4
|
+
|
|
5
|
+
- [Roadmap](handoff.md): what is still open, item by item.
|
|
6
|
+
- [Design principles](design-principles.md): the settled principles behind the library.
|
|
7
|
+
- [Test vectors](test-vectors.md): why the HMAC vectors in the test suite are synthetic, and how to supply real ones.
|
|
8
|
+
- [Releasing](releasing.md): how a version is tagged and published.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
The version lives in one place, `version` in `pyproject.toml`. `aosmith_ble.__version__` reads it from the installed package metadata.
|
|
4
|
+
|
|
5
|
+
To release:
|
|
6
|
+
|
|
7
|
+
1. Set the new `version` in `pyproject.toml` and move the `CHANGELOG.md` entries under that version.
|
|
8
|
+
2. Merge to `main` with the tests, lint and docs jobs green.
|
|
9
|
+
3. Tag the merge commit `v<version>` (for example `v0.1.0`) and push the tag.
|
|
10
|
+
|
|
11
|
+
The release workflow then checks that the tag matches `pyproject.toml`, runs the tests, builds the sdist and wheel, smoke-tests the wheel in a clean environment, and publishes to PyPI. A tag that does not match the version fails before anything is built.
|
|
12
|
+
|
|
13
|
+
Publishing uses PyPI trusted publishing, so no API token is stored in the repository. The PyPI project trusts the `release.yml` workflow running in the `pypi` GitHub environment.
|
|
14
|
+
|
|
15
|
+
!!! warning
|
|
16
|
+
A version published to PyPI can be yanked but never replaced. Check the built files before tagging, and use a new version number for any fix.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Test vectors
|
|
2
|
+
|
|
3
|
+
The test vectors for the `0xF1` challenge response and the init response are synthetic. The HMAC vectors in `tests/test_protocol_session.py` and the `init_response_slot_populated` frame in `tests/fixtures/frames.json` are computed from a placeholder assetID (`02iQk000000EXAMPLE`, 18 characters like a real one). Tests use a dummy pairing code (`123456`).
|
|
4
|
+
|
|
5
|
+
## What that means
|
|
6
|
+
|
|
7
|
+
A real capture is an independent check: the official app produced those bytes, so a match proves the code builds the same response. A synthetic vector is computed with `hmac` and `hashlib` using the same construction as the code under test, so it catches regressions but not a wrong construction.
|
|
8
|
+
|
|
9
|
+
The construction is in [Handshake](../protocol/handshake.md).
|
|
10
|
+
|
|
11
|
+
## Supplying real vectors privately
|
|
12
|
+
|
|
13
|
+
For example from a capture of the official app, all from one device:
|
|
14
|
+
|
|
15
|
+
1. Take the 18-character ASCII assetID from the `0xF2` init response (`DB 2F 18 01 <assetID> 80 <crc>`).
|
|
16
|
+
2. For several sessions of that same device, take the 2-byte nonce from each `0xF4` response (`DB 4F 07 <nonce> 80 <crc>`) and the 20-byte payload from the `0xF1` write that follows it (`BD F1 19 01 <payload> <crc>`).
|
|
17
|
+
3. In `tests/test_protocol_session.py`, replace `ASSET_ID` and `CHALLENGE_RESPONSE_PAIRS` with those values.
|
|
18
|
+
4. Other tests and `tests/fixtures/frames.json` reuse the placeholder. If you change it everywhere, recompute the CRC of `init_response_slot_populated` in `frames.json`. See [CRC](../protocol/crc.md).
|
|
19
|
+
5. Redact device-identifying values, or keep real vectors private and run them locally.
|