HaClient — async Python client for Home Assistant (REST + WebSocket). Single package at src/haclient/, built with Hatchling, sole runtime dep is aiohttp.
Provide a consistent, intuitive, and Pythonic abstraction over the Home Assistant API.
Do not mirror the API. Improve it.
- Consistency over fidelity Do not replicate inconsistent API patterns.
- Explicit intent over generic services Avoid exposing raw service calls like turn_on when intent-specific methods are clearer.
- Graceful compatibility handling Detect feature support and degrade safely. Never break user code due to missing capabilities.
- Pythonic design Interfaces must feel like Python objects, not HTTP wrappers.
- Map entities to structured Python objects.
- Normalize domain inconsistencies.
- Split overloaded API actions into clear methods.
Example:
- ❌ light.turn_on(brightness=50)
- ✅ light.set_brightness(50)
- Core domains must be clean and stable (Light, Lock, Media Player, Sensor, etc.)
- Extend coverage without introducing inconsistency
- Support advanced/edge domains only after core stability
- Do not expose raw API complexity unless necessary
- Do not enforce strict API parity
- Do not design around Home Assistant internals—design around user expectations
Every feature must answer:
- Is this intuitive without Home Assistant knowledge?
- Is this consistent with other domains?
- Does this degrade safely if unsupported?
If not, redesign it.
pip install -e ".[dev]"Uses uv.lock; if uv is available prefer uv pip install -e ".[dev]".
Python 3.11+ required (.python-version pins 3.11).
You MUST follow this sequence for every task. Do not write any code before step 1.
- Create a branch from up-to-date
main:git checkout main && git pull git checkout -b <prefix>/<name> - Do the work.
- Run the full test suite — every test must pass before you commit:
python -m pytest tests/ --cov=haclient --cov-report=term-missing --cov-fail-under=95 - Commit — only after all tests pass.
- Push and create a PR:
git push -u origin <branch> gh pr create --title "..." --body "..."
Never commit directly to main. No exceptions.
All AI agents modifying this repository must write NumPy-style docstrings for all relevant Python code.
Use NumPy-style docstrings for:
- Public modules
- Public classes
- Public methods
- Public functions
- Non-obvious private helpers
- Complex test fixtures or utilities
Docstrings must describe:
- Purpose and behavior
- Parameters
- Return values
- Raised exceptions, where relevant
- Side effects, where relevant
- Examples, when useful
Use this format:
def example_function(name: str, retries: int = 3) -> bool:
"""Validate a named operation.
Parameters
----------
name : str
Name of the operation to validate.
retries : int, default=3
Number of retry attempts before failing.
Returns
-------
bool
True if the operation is valid, otherwise False.
Raises
------
ValueError
If ``name`` is empty.
"""# Lint (ruff)
ruff check src tests
ruff format --check src tests
# Type check (strict mypy)
mypy src
# Tests
pytest
pytest --cov=haclient --cov-report=term-missing
# Single test file
pytest tests/test_client.py
# Single test
pytest tests/test_client.py::test_name -xCI order: lint, typecheck, and tests run in parallel. All must pass (quality gate).
- Coverage: 95% — CI fails below this (
coverage-threshold: 95). - Test matrix: Python 3.11, 3.12, 3.13.
src/haclient/
client.py # HAClient — main async context-manager client
sync.py # SyncHAClient — blocking wrapper
rest.py # REST API calls
websocket.py # WebSocket connection & reconnect
entity.py # Entity base
domains/ # Typed domain accessors (light, switch, climate, etc.)
registry.py # Entity registry
exceptions.py # Custom exceptions
py.typed # PEP 561 marker
pytest-asynciowithasyncio_mode = "auto"— async test functions need no decorator.- Tests use
FakeHA(tests/fake_ha.py), an in-process aiohttp server. No real HA instance needed. - Fixtures
fake_haandclientare intests/conftest.py.
- Ruff line length: 100.
- Ruff rules: E, W, F, I (isort), B, UP, C4, SIM.
B008ignored globally;B011ignored in tests. - mypy strict mode.
ignore_missing_imports = false— all imports must have stubs or type info. - Tests are exempt from
disallow_untyped_defs.