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/).
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.
- Integration source (the actual caller). Fetch the raw files:
https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/components/hive/<file>where<file>is one of:__init__.py,config_flow.py,entity.py,const.py,climate.py,water_heater.py,light.py,switch.py,binary_sensor.py,sensor.py,manifest.json. Browse: https://github.com/home-assistant/core/tree/dev/homeassistant/components/hive - User docs: https://www.home-assistant.io/integrations/hive/
- Developer docs:
- Entities overview: https://developers.home-assistant.io/docs/core/entity/
- climate: https://developers.home-assistant.io/docs/core/entity/climate/
- water_heater: https://developers.home-assistant.io/docs/core/entity/water-heater/
- light: https://developers.home-assistant.io/docs/core/entity/light/
- switch: https://developers.home-assistant.io/docs/core/entity/switch/
- binary_sensor: https://developers.home-assistant.io/docs/core/entity/binary-sensor/
- sensor: https://developers.home-assistant.io/docs/core/entity/sensor/
- Config flow (login, SMS 2FA, reauth): https://developers.home-assistant.io/docs/config_entries_config_flow_handler/
- Setup failures (
ConfigEntryNotReady/ConfigEntryAuthFailed): https://developers.home-assistant.io/docs/integration_setup_failures/ - Blocking calls in the event loop: https://developers.home-assistant.io/docs/asyncio_blocking_operations/
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.
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:
- https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt (hard pins applied to every integration install, e.g. boto3/botocore)
- https://raw.githubusercontent.com/home-assistant/core/dev/requirements_all.txt (versions of other integrations' packages, e.g. aiobotocore)
A range that can't be satisfied alongside those pins is a bug: Home Assistant
fails with RequirementsNotFound. Quote the pins you checked.
- Edit only the async source in
src/. The syncpyhiveapipackage is generated by unasync; never hand-edit it. - Do not edit the deprecation shims (
src/heating.pyetc.); changes go insrc/devices/,src/session/orsrc/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_tokenstrings inhive_auth_async.pymust 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 insrc/data/*.jsonmust be anonymised. - New behaviour and bug fixes need tests in
tests/unit,tests/moduleortests/e2e; coverage must stay at 99%.
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.
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.
The library exposes two packages from the same source:
apyhiveapi— async package (the actual source insrc/)pyhiveapi— sync package (auto-generated fromsrc/duringsetup.py build_pyusingunasync)
unasync rewrites apyhiveapi → pyhiveapi and asyncio → threading. Never edit generated sync files — edit the async source in src/ only.
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).
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/.
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 |
src/api/hive_async_api.py(HiveApiAsync) — all async HTTP calls tobeekeeper.hivehome.comsrc/api/hive_auth_async.py(HiveAuthAsync) — AWS Cognito SRP authentication flows; storesself.access_token(Cognito AccessToken) afterlogin()orsms_2fa()src/api/device_registration.py(DeviceRegistrationMixin) — mixed intoHiveAuthAsync; device registration, confirm, forget,get_device_data(). Usesself.access_token(notself.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)
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 fromsession.data.products_map_hive_to_ha(mapping_key, value)— translate Hive API value throughHIVETOHA
BoostMixin (devices/boost.py) is composed into HiveHeating and HiveHotwater.
LightColorHandler (devices/color.py) is composed into HiveLight for colour sub-domain.
| 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 |
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.
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)session.data (a Map) holds:
products/devices— dicts keyed by device ID from the Hive APIactions— automation actionsuser— account infominMax— temperature range data
src/helper/const.py—HIVE_TYPES,PRODUCTS,DEVICES,HIVETOHAmappings, HTTP constantssrc/helper/hivedataclasses.py—Device,EntityConfig,SessionTokens,SessionConfigdataclasses.Devicesupports 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 logssrc/helper/hive_exceptions.py— all custom exceptions (HiveReauthRequired,HiveAuthError, etc.)src/helper/map.py—Mapclass: 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 typessrc/helper/device_handler_base.py(BaseDeviceHandler) — shared_execute_state_change/_get_product_stateplumbing inherited by all device handlerssrc/helper/compat_aliases.py— camelCase alias mixins (HeatingCompatMixin,LightCompatMixin,SwitchCompatMixin,SessionCompatMixin) — keep HA integration working without changessrc/data/*.json— fixture files for offline/file-based testing
Two flows:
-
First login —
Auth.login()returnsChallengeName == "SMS_MFA"(use theSMS_REQUIREDconstant). Callauth.sms_2fa(code, result), then save the three device credentials fromawait auth.get_device_data()(device_group_key,device_key,device_password). Device registration happens automatically insidesms_2fa()— no separate registration call needed. -
Subsequent logins — pass saved
device_datain thestart_sessionconfig. The library callsdevice_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.
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.
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 duringlogin()/sms_2fa(); used byDeviceRegistrationMixinHiveSession.tokens.token_data— session-layer token store ("token","accessToken","refreshToken"); used byHiveApiAsyncfor API requests