Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ coordinator.py → polls every scan_interval seconds via each connection,
lock.py → LockEntity backed by the BLE connection
sensor.py → BatterySensor backed by the same poll + push events
binary_sensor.py → connectivity BinarySensorEntity reflecting live BLE link state
event.py → EventEntity that surfaces decoded LockEvent pushes
event.py → EventEntity that surfaces decoded LogEntry records
```

### Entry typing

`data/` is a package, one class per file, re-exported from `data/__init__.py`. `data/__init__.py` defines `TtlockBleConfigEntry = ConfigEntry[TtlockBleData]`; `data/runtime.py` defines the `TtlockBleData(keys, virtual_keys, connections, coordinator, bluetooth_unsubs)` dataclass. State lives on `entry.runtime_data` (auto-discarded on unload), never on `hass.data`.
`data/` is a package, one class per file, re-exported from `data/__init__.py`. `data/__init__.py` defines `TtlockBleConfigEntry = ConfigEntry[TtlockBleData]`; `data/runtime.py` defines the `TtlockBleData(keys, virtual_keys, connections, coordinator, bluetooth_unsubs, first_refresh)` dataclass. State lives on `entry.runtime_data` (auto-discarded on unload), never on `hass.data`.

### Config flow surface

Expand Down
24 changes: 13 additions & 11 deletions CODE_STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ Style conventions for the `ha-ttlock-ble` project. Before committing, run
`TtlockBleCoordinatorData` live in `data/__init__.py`).
- **Helper functions** may live in the same file as the single class that uses
them (e.g. `_classify_cloud_error` in `api.py`).
- **`__init__.py` of the integration package** wires `async_setup_entry`,
`async_unload_entry`, `async_reload_entry` and nothing else.
- **`__init__.py` of the integration package** wires the Home Assistant
config-entry lifecycle hooks and nothing else.

## Entities: encode behaviour directly, no description-callable indirection

Expand All @@ -50,7 +50,7 @@ should be written:
- Don't write an `<DOMAIN><Platform>Description` subclass with a
`value_fn` / `action_fn` field.
- Do write `<DOMAIN><Name><Platform>` (e.g. `TtlockBleBatterySensor`,
`TtlockBleLock`, `TtlockBleOperationEvent`).
`TtlockBleLock`, `TtlockBleLogEvent`).
- The reason: each entity is a discrete contract; mixing them through a
generic class hides the contract behind indirection and discourages
per-entity refinement (icons, state attributes, custom logic).
Expand All @@ -60,8 +60,8 @@ should be written:
- Public classes are prefixed with `TtlockBle` (rename to
`<YourDomain>` when forking).
- Concrete platform entities end with the entity type:
`TtlockBleSensor`, `TtlockBleBinarySensor`,
`TtlockBleSwitch`.
`TtlockBleBatterySensor`, `TtlockBleConnectionBinarySensor`,
`TtlockBleLogEvent`, `TtlockBleLock`.
- Exception classes end with `Error`: `TtlockBleApiClientError`,
`…CommunicationError`, `…AuthenticationError`.
- Private attributes / functions are prefixed with `_`.
Expand Down Expand Up @@ -171,8 +171,8 @@ with a one-line comment explaining the deliberate narrowing — see
- Format: `"Failed to <verb> <object>: <cause>"` where `<cause>` is the
exception or a short reason. Keep them short and grep-able.
- Pre-validate inputs before the network call so user-facing errors point at
the bad input, not a downstream traceback (`config_flow._validate` rejects
malformed credentials before contacting the API).
the bad input, not a downstream traceback (`manual_key.async_validate`
rejects a malformed key before it is stored on the entry).
- Custom exceptions get the same hierarchy:
`TtlockBleApiClientError` (base) → `…CommunicationError` (timeout,
connection, DNS) and `…AuthenticationError` (401/403). Wrap raw upstream
Expand Down Expand Up @@ -215,16 +215,18 @@ with a one-line comment explaining the deliberate narrowing — see
## Pre-commit hooks

`pre-commit` is a dev dependency (`pyproject.toml`) and `.pre-commit-config.yaml`
mirrors the lint commands (ruff format, ruff check). Install once per
clone:
mirrors the lint commands (ruff format, ruff check, mypy) through local
`uv run` hooks, so every commit runs the exact tool versions pinned in
`pyproject.toml`. Install once per clone:

```bash
pre-commit install
```

The hook runs the same ruff gates as CI on every commit. Skip it only on
The hooks run the same gates as CI on every commit. Skip them only on
emergency `git commit --no-verify` and immediately re-run
`uv run ruff format --check .` and `uv run ruff check .`.
`uv run ruff format --check .`, `uv run ruff check .` and
`uv run mypy custom_components/ttlock_ble`.

## Conventional commits

Expand Down
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ Local control of TTLock smart locks over Bluetooth, for [Home Assistant](https:/

## Entities

Each configured lock produces one HA device with three entities:
Each configured lock produces one HA device with four entities:

| Entity | Domain | Purpose |
|---|---|---|
| `lock.<alias>` | `lock` | Locked/unlocked state, with optimistic updates and a post-command settle window. |
| `lock.<alias>` | `lock` | Locked/unlocked state, with optimistic updates, `locking`/`unlocking` transitional states and a post-command settle window. |
| `sensor.<alias>_battery` | `sensor` | Battery percentage (diagnostic). |
| `event.<alias>_operation` | `event` | Fires on every push from the lock, with decoded `lock_state`, `battery`, `uid`, `record_id`, `timestamp` attributes when present. |
| `binary_sensor.<alias>_connection` | `binary_sensor` | Live BLE link state (connectivity, diagnostic). |
| `event.<alias>_log` | `event` | Fires for every new operation-log record read from the lock. |

The event entity classifies each record as `unlock`, `lock`, `unlock_failed`, `password_change` or `other`, and attaches `record_type` and `battery` always, plus `timestamp`, `uid`, `credential`, `key_id` and `accessory_battery` when the record carries them. `credential` is only populated for record types where the value is an identifier (card number, fingerprint id, fob MAC) — record types where it would be a working door code never expose it.

## Installation

Expand Down Expand Up @@ -82,7 +85,7 @@ The lock's TTLock firmware aggressively closes idle BLE sessions (~5 s of silenc
## Useful commands

```bash
scripts/setup # install dependencies (requirements.txt)
scripts/setup # install dependencies (uv sync, dev + lint groups)
scripts/develop # start Home Assistant in debug mode with the integration loaded

# Lint and test directly (config lives in pyproject.toml):
Expand All @@ -92,8 +95,6 @@ uv run mypy custom_components/ttlock_ble
uv run pytest
```

Each `scripts/*` helper auto-detects `./.venv` and prepends it to `PATH` — no `source .venv/bin/activate` needed.

HA runs with config in `config/` and `PYTHONPATH` pointing at the repo root. To reset entity/device IDs during development:

```bash
Expand All @@ -104,22 +105,23 @@ rm config/.storage/core.entity_registry config/.storage/core.device_registry

```
custom_components/ttlock_ble/
├── __init__.py # async_setup_entry / unload / reload + bluetooth callbacks
├── __init__.py # config-entry lifecycle: setup / unload / reload
├── advertisement.py # TtlockBleAdvertisementTracker: state from advertisements
├── api.py # TtlockBleApiClient: TTLockCloud wrapper (cloud bootstrap only)
├── binary_sensor.py # TtlockBleConnectionBinarySensor: live BLE link state
├── brand/ # icon / logo PNGs (local placeholder for HA brand registry)
├── config_flow.py # menu / cloud / manual / verify_code / reauth / reconfigure
├── manual_key.py # TtlockBleManualKey: key entry for cloud-less locks
├── connection.py # TtlockBleConnection: persistent BLE session per lock
├── const.py # DOMAIN, LOGGER, defaults
├── coordinator.py # DataUpdateCoordinator polling each connection
├── data.py # TypedDicts + TtlockBleData dataclass
├── data/ # one TypedDict/dataclass per file + type aliases in __init__.py
├── diagnostics.py # redacted credentials/keys
├── entity.py # base CoordinatorEntity with DeviceInfo
├── event.py # TtlockBleOperationEvent push surface
├── event.py # TtlockBleLogEvent: operation-log records as HA events
├── exceptions/ # one file per exception class
├── lock.py # TtlockBleLock: LockEntity backed by the connection
├── manifest.json
├── manual_key.py # TtlockBleManualKey: key entry for cloud-less locks
├── options_flow.py # TtlockBleOptionsFlow: scan_interval, reconnect_interval, permanent_connection
├── sensor.py # TtlockBleBatterySensor backed by polls + pushes
└── translations/
Expand All @@ -137,15 +139,14 @@ Install once per clone (after `scripts/setup`):
pre-commit install
```

This wires ruff + basic file hygiene checks (`.pre-commit-config.yaml`) into every commit, mirroring the CI lint job.
This wires ruff, mypy and basic file hygiene checks (`.pre-commit-config.yaml`) into every commit, mirroring the CI lint job.

## CI

- **`lint.yml`** — ruff (check + format) and mypy (Python 3.14)
- **`tests.yml`** — pytest with the 90 % coverage gate
- **`validate.yml`** — `hassfest` + HACS validation; push/PR to `main` and a daily cron
- **`ci.yml`** — lint (ruff check + format, mypy), tests (pytest with the 90 % coverage gate) and validation (`hassfest` + HACS) via the shared reusable workflows
- **`codeql.yml`** — GitHub CodeQL security scan; push/PR to `main` and a weekly cron
- **`release.yml`** — release-please opens a release PR on every push to `main` based on conventional commits
- **`auto-assign.yml`** — assigns the repository owner to new issues and pull requests

## License

Expand Down
Loading