Skip to content

Latest commit

 

History

History
325 lines (248 loc) · 17.4 KB

CLAUDE.md

File metadata and controls

325 lines (248 loc) · 17.4 KB

CLAUDE.md

Guidance for Claude Code when working in this repository, locally and in GitHub Actions. Both Claude jobs in .github/workflows/claude.yml (the automatic PR review and @claude mentions) load this file, so project knowledge and rules belong here, not in the workflow prompt.

This is pyhive-integration, the Python library (apyhiveapi / pyhiveapi) behind the Home Assistant Hive integration. Async source lives in src/ (api/, session/, devices/, helper/).

Home Assistant is the main consumer

This library exists to power Home Assistant's hive integration, so a change that looks fine here can still be a bug on the Home Assistant side.

Always check the Home Assistant source. Whenever you review, change or answer a question about this library's code, fetch the Home Assistant files that call the code involved and read how they use it before you conclude. Do not rely on memory of the integration: it changes. In what you post, name the Home Assistant files you checked and quote the lines that matter. If nothing in Home Assistant uses the code involved, say so explicitly.

Home Assistant breaks when: a method or alias it calls is renamed or removed, a return shape or device.status value type/unit it reads changes, a new exception is raised that it doesn't catch, or something blocks the event loop. The public API it depends on is Hive, start_session, update_data, device_list, device.status keys and value types, the camelCase aliases, the exceptions raised, and the hive.* device module methods.

Dependency changes

Home Assistant installs this library inside its own environment, so judge version ranges in pyproject.toml dependencies against what Home Assistant pins today, not against the reasons given in older issues or PRs — those constraints change. Before calling a range right or wrong, fetch:

A range that can't be satisfied alongside those pins is a bug: Home Assistant fails with RequirementsNotFound. Quote the pins you checked.

Project rules

  • Edit only the async source in src/. The sync pyhiveapi package is generated by unasync; never hand-edit it.
  • Do not edit the deprecation shims (src/heating.py etc.); changes go in src/devices/, src/session/ or src/helper/.
  • Keep the camelCase aliases (startSession, updateData, deviceList, helper/compat_aliases.py) — Home Assistant uses them.
  • The placeholder aws_access_key_id / aws_secret_access_key / aws_session_token strings in hive_auth_async.py must stay; they are not leaked secrets and boto3 Cognito auth breaks without them.
  • Never log tokens or PII; log payloads via HiveHelper.sanitize_payload. Data in src/data/*.json must be anonymised.
  • New behaviour and bug fixes need tests in tests/unit, tests/module or tests/e2e; coverage must stay at 99%.

Commands

All commands run from the repository root.

# Install dependencies
pip install -r requirements.txt -r requirements_test.txt

# Run all linters (via pre-commit)
pre-commit run --all-files

# Run tests (always run the full suite — coverage is measured against all of src/)
pytest tests/

# Run a single test
pytest tests/module/test_hub.py::TestHiveHubSensorStatus

# Generate the sync (pyhiveapi) package from async source
python setup.py build_py

# Individual linters
ruff check --fix src/
ruff format src/
pylint src/
mypy src/
bandit --configfile=tests/bandit.yaml src/

Direct commits to master are blocked by pre-commit hook. Branch flow: dev → master.

A PII check pre-commit hook (scripts/check_data_pii.py) runs on any src/data/*.json changes — ensure fixture data is anonymised before committing.

Test organisation

Tests are split into three subdirectories under tests/:

Directory Purpose
tests/unit/ Pure unit tests — heavily mocked, no I/O
tests/module/ Module-level tests using the file-based session (username="use@file.com")
tests/e2e/ End-to-end / integration tests

pyproject.toml sets asyncio_mode = "auto", so async test functions do not need @pytest.mark.asyncio.

Coverage is enforced at 99% (fail_under = 99 in pyproject.toml). Running only a subset of tests will report very low coverage and fail — always verify with the full pytest tests/ run. Shim re-export files (src/heating.py, src/light.py, etc.) are excluded from coverage measurement.

Architecture

The library exposes two packages from the same source:

  • apyhiveapi — async package (the actual source in src/)
  • pyhiveapi — sync package (auto-generated from src/ during setup.py build_py using unasync)

unasync rewrites apyhiveapi → pyhiveapi and asyncio → threading. Never edit generated sync files — edit the async source in src/ only.

Entry Point

src/hive.py — Hive class is the public API. It inherits HiveSession and composes all device modules:

from apyhiveapi import Hive
hive = Hive(username="user@example.com", password="pass")
device_list = await hive.start_session(config)

start_session() / update_data() / device_list are the preferred names. startSession(), updateData(), deviceList are backwards-compatible aliases retained for the Home Assistant integration (via SessionCompatMixin in src/helper/compat_aliases.py).

Source layout

src/
├── hive.py                      ← Hive — public entry point; inherits HiveSession
├── __init__.py                  ← exports Hive, Auth, SMS_REQUIRED, all exceptions
├── api/
│   ├── hive_async_api.py        ← HiveApiAsync — all HTTP calls to beekeeper API
│   ├── hive_auth_async.py       ← HiveAuthAsync — AWS Cognito SRP login flows
│   ├── device_registration.py   ← DeviceRegistrationMixin — Cognito device lifecycle
│   └── srp_crypto.py            ← pure SRP/HKDF crypto functions and constants
├── session/
│   ├── __init__.py              ← HiveSession — composes the four session mixins
│   ├── auth.py                  ← SessionAuthMixin — login, token refresh, retry backoff
│   ├── polling.py               ← PollingMixin — rate-limited polling, cache, get_devices
│   └── discovery.py             ← DiscoveryMixin — start_session, create_devices, add_list
├── devices/
│   ├── heating.py               ← HiveHeating / Climate
│   ├── hotwater.py              ← HiveHotwater / WaterHeater
│   ├── light.py                 ← HiveLight / Light
│   ├── plug.py                  ← HiveSmartPlug / Switch
│   ├── sensor.py                ← HiveSensor / Sensor
│   ├── hub.py                   ← HiveHub
│   ├── action.py                ← HiveAction
│   ├── boost.py                 ← BoostMixin (shared by heating + hotwater)
│   └── color.py                 ← LightColorHandler (colour sub-domain for lights)
└── helper/
    ├── const.py                 ← HIVE_TYPES, PRODUCTS, DEVICES, HIVETOHA, HTTP_OK …
    ├── hivedataclasses.py       ← Device, EntityConfig, SessionTokens, SessionConfig
    ├── device_attributes.py     ← HiveAttributes — HA state attribute computation
    ├── device_handler_base.py   ← BaseDeviceHandler — shared set_state plumbing
    ├── compat_aliases.py        ← camelCase alias mixins for HA backwards compat
    ├── hive_helper.py           ← HiveHelper — name resolution, schedule parsing
    ├── hive_exceptions.py       ← all custom exceptions
    └── map.py                   ← Map — dict with attribute-style access

Shim files at src/<module>.py (e.g. src/heating.py, src/action.py, src/session_tokens.py) re-export from the new locations with a DeprecationWarning. Do not edit shims — edit the canonical files under src/devices/, src/session/, or src/helper/.

Core Session

src/session/__init__.py — HiveSession inherits four mixins:

Mixin File Responsibility
SessionAuthMixin session/auth.py Token storage, refresh at 90% lifetime, _retry_login backoff
PollingMixin session/polling.py Rate-limited polling, entity cache, update_data, get_devices
DiscoveryMixin session/discovery.py start_session, create_devices, add_list, file-mode loading
SessionCompatMixin helper/compat_aliases.py camelCase backwards-compat aliases

API Layer

  • src/api/hive_async_api.py (HiveApiAsync) — all async HTTP calls to beekeeper.hivehome.com
  • src/api/hive_auth_async.py (HiveAuthAsync) — AWS Cognito SRP authentication flows; stores self.access_token (Cognito AccessToken) after login() or sms_2fa()
  • src/api/device_registration.py (DeviceRegistrationMixin) — mixed into HiveAuthAsync; device registration, confirm, forget, get_device_data(). Uses self.access_token (not self.session.tokens).
  • src/api/srp_crypto.py — pure stateless SRP/HKDF crypto functions (N_HEX, compute_hkdf, etc.)
  • src/__init__.py — selects sync vs async implementations based on module name (pyhiveapi → sync, otherwise → async)

Device Modules (all in src/devices/)

After start_session(), device modules are available as attributes on hive:

hive.* Class File Covers
hive.heating Climate devices/heating.py Thermostats, TRV controls
hive.hotwater WaterHeater devices/hotwater.py Hot water
hive.light Light devices/light.py All Hive light types
hive.switch Switch devices/plug.py Smart plugs, Heat on Demand, actions
hive.sensor Sensor devices/sensor.py Motion and contact sensors
hive.hub HiveHub devices/hub.py Hub / Sense detection
hive.action HiveAction devices/action.py Hive automation actions

All device handlers inherit BaseDeviceHandler (src/helper/device_handler_base.py), which provides:

  • _execute_state_change(device, **state_kwargs) — token refresh → set_state → get_devices
  • _get_product_state(device, *path_keys, default=None) — safe nested read from session.data.products
  • _map_hive_to_ha(mapping_key, value) — translate Hive API value through HIVETOHA

BoostMixin (devices/boost.py) is composed into HiveHeating and HiveHotwater. LightColorHandler (devices/color.py) is composed into HiveLight for colour sub-domain.

Supported Devices

Device HA entity type Hive types
Heating (thermostat, TRV) climate heating, trvcontrol
Hot Water water_heater hotwater
Lights light warmwhitelight, tuneablelight, colourtuneablelight
Smart Plugs switch activeplug
Motion Sensor binary_sensor motionsensor
Contact Sensor binary_sensor contactsensor
Hub / Sense binary_sensor hub, sense
Actions switch action

Device Discovery (create_devices)

PRODUCTS and DEVICES dicts in src/helper/const.py map Hive product/device types to lists of EntityConfig dataclass instances (entity_type, ha_name, hive_type, category, temperature_unit). DiscoveryMixin.create_devices() iterates these configs and calls add_list(...) for each to build device_list for Home Assistant entity creation.

device_list structure

hive.device_list is a dict keyed by HA entity type, populated by start_session():

{
    "parent":        [...],   # physical Hive hub devices
    "binary_sensor": [...],   # motion/contact sensors, hub sensors
    "climate":       [...],   # heating zones, TRVs
    "light":         [...],
    "sensor":        [...],   # diagnostic (battery, connectivity)
    "switch":        [...],   # smart plugs, Heat on Demand, actions
    "water_heater":  [...],
}

Each entry is a Device instance. After calling the relevant get_* module method, device.status holds the current state:

# Heating
device.status["current_temperature"]  # float
device.status["target_temperature"]   # float
device.status["mode"]                 # "SCHEDULE" | "MANUAL" | "OFF"
device.status["boost"]                # "ON" | "OFF"

# Light
device.status["state"]                # bool
device.status["brightness"]           # int 0-255
device.status.get("color_temp")       # int mireds (tunable/colour only)
device.status.get("hs_color")         # tuple (colour lights in COLOUR mode)

# Switch / Smart Plug
device.status["state"]                # bool
device.status.get("power_usage")      # float watts (activeplug only)

Data Model

session.data (a Map) holds:

  • products / devices — dicts keyed by device ID from the Hive API
  • actions — automation actions
  • user — account info
  • minMax — temperature range data

Helpers

  • src/helper/const.py — HIVE_TYPES, PRODUCTS, DEVICES, HIVETOHA mappings, HTTP constants
  • src/helper/hivedataclasses.py — Device, EntityConfig, SessionTokens, SessionConfig dataclasses. Device supports both attribute-style (device.hive_id) and dict-style (device["hiveID"]) access, with automatic translation of legacy camelCase keys to snake_case.
  • src/helper/hive_helper.py (HiveHelper) — device name resolution, error tracking, schedule NNL parsing (get_schedule_nnl), payload sanitisation for logs
  • src/helper/hive_exceptions.py — all custom exceptions (HiveReauthRequired, HiveAuthError, etc.)
  • src/helper/map.py — Map class: dict wrapper allowing attribute-style access (session.config.home_id)
  • src/helper/device_attributes.py (HiveAttributes) — computes HA state attributes (online/offline, battery, mode) for all device types
  • src/helper/device_handler_base.py (BaseDeviceHandler) — shared _execute_state_change / _get_product_state plumbing inherited by all device handlers
  • src/helper/compat_aliases.py — camelCase alias mixins (HeatingCompatMixin, LightCompatMixin, SwitchCompatMixin, SessionCompatMixin) — keep HA integration working without changes
  • src/data/*.json — fixture files for offline/file-based testing

Authentication

Two flows:

  1. First login — Auth.login() returns ChallengeName == "SMS_MFA" (use the SMS_REQUIRED constant). Call auth.sms_2fa(code, result), then save the three device credentials from await auth.get_device_data() (device_group_key, device_key, device_password). Device registration happens automatically inside sms_2fa() — no separate registration call needed.

  2. Subsequent logins — pass saved device_data in the start_session config. The library calls device_login() internally — no SMS required.

get_device_data() is async def — always await it in async code.

Only the Hive account owner is supported. Guest accounts cannot authenticate.

File-Based Testing

Set username="use@file.com" to make the session load data from src/data/data.json instead of calling the API. Used for development without live credentials.

Token Refresh Strategy

Tokens refresh proactively at 90% of their lifetime (_refresh_threshold = 0.90). On HiveRefreshTokenExpired or HiveFailedToRefreshTokens, the session falls back to _retry_login (3 attempts with backoff). A HiveReauthRequired exception propagates up when user interaction is needed (SMS 2FA). This is the only exception application code needs to handle explicitly — all intermediate refresh failures are handled internally.

Token layers are distinct:

  • HiveAuthAsync.access_token — Cognito AccessToken stored on the auth object during login()/sms_2fa(); used by DeviceRegistrationMixin
  • HiveSession.tokens.token_data — session-layer token store ("token", "accessToken", "refreshToken"); used by HiveApiAsync for API requests