Home Assistant Quality & Testing
Use this skill to align changes with Home Assistant best practices and the Integration Quality Scale expectations.
When to Use
- •Planning or reviewing changes for config flows, options, or reauth
- •Deciding how to structure polling, discovery, and entities
- •Ensuring translations, metadata, and versioning are correct
- •Setting up or updating tests and CI
- •Driving toward Bronze/Silver IQS targets (config flow coverage, >95% overall coverage, diagnostics)
Architectural Principles
- •Async-only: Never block the event loop; offload sync work via
hass.async_add_executor_job. - •Coordinator-first: Centralize device I/O in
DataUpdateCoordinator; entities consumecoordinator.data. - •Single source of discovery: Rely on declared discovery (dhcp/zeroconf/ssdp) or dedicated scanner; avoid ad-hoc discovery inside polling.
- •Separation: Keep protocol/library logic outside entities; prefer a library (
requirements) for raw API/UDP/HTTP handling. - •Quality Scale focus: Bronze requires 100% config_flow coverage and connection tests; Silver pushes >95% overall coverage, strict typing, and reauth flows.
Coordinator Error Handling Patterns
python
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import UpdateFailed
async def _async_update_data(self):
try:
return await self.api.fetch_data()
except AuthError as err:
# Triggers reauth flow automatically
raise ConfigEntryAuthFailed("Authentication failed") from err
except RateLimitError:
# Backoff with retry_after (seconds until retry)
raise UpdateFailed(retry_after=60)
except ConnectionError as err:
raise UpdateFailed(f"Connection failed: {err}")
Coordinator Best Practices
- •Use
_async_setup()for one-time initialization during first refresh - •Set
always_update=Falseif your data supports__eq__comparison (avoids unnecessary entity updates) - •Use
async_contexts()to track which entities are actively listening - •Pass
config_entryto coordinator constructor for automatic linking
Config / Options / Reauth Patterns
- •UI-first: No new YAML; all setup via
config_flow.pywith selectors where helpful. - •Duplicate avoidance: Abort if a device is already configured; update IP/host on existing entries when discovery reports changes.
- •Options reload: Register
entry.add_update_listenerto reload on options change. - •Reauth: Trigger
entry.async_start_reauth; ask only for the changed credential and update the existing entry. - •Duplicate enforcement: Set
unique_idearly and call_abort_if_unique_id_configured()for user/discovery steps.
Entities & Registries
- •
_attr_has_entity_name = Trueis MANDATORY for new integrations; entity name should be capability-only (device name is prepended by HA). - •For the "main feature" entity of a device, set
_attr_name = Noneto use only the device name. - •Provide
device_infowith stable identifiers (prefer MAC/serial over IP); all entities for a device must share the same identifiers set. - •Keep
unique_idstable and predictable (e.g.,{ble_mac}_{key}); changing it breaks history and customizations. - •Only create entities for data that actually exists to avoid permanent
unavailablenoise. - •Use
_attr_*class/instance attributes pattern for cleaner code:pythonclass MySensor(SensorEntity): _attr_has_entity_name = True _attr_device_class = SensorDeviceClass.POWER _attr_native_unit_of_measurement = UnitOfPower.WATT
EntityDescription Pattern (Recommended)
For multiple similar entities, use EntityDescription for declarative definitions:
python
@dataclass(kw_only=True)
class MySensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[DeviceData], StateType]
exists_fn: Callable[[DeviceData], bool] = lambda _: True
SENSORS: tuple[MySensorEntityDescription, ...] = (
MySensorEntityDescription(
key="power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
value_fn=lambda data: data.power,
),
)
Entity Best Practices
- •Use constants from
homeassistant.constfor units:- •
UnitOfEnergy.WATT_HOURnot"Wh" - •
UnitOfPower.WATTnot"W" - •
UnitOfTemperature.CELSIUSnot"°C"
- •
- •Use device classes from
homeassistant.components.sensor:- •
SensorDeviceClass.BATTERYfor battery level sensors - •
SensorDeviceClass.POWERfor power sensors - •
SensorDeviceClass.ENERGYfor energy sensors - •
SensorDeviceClass.ENERGY_STORAGEfor stored energy (battery capacity in Wh) - •
SensorDeviceClass.TEMPERATUREfor temperature sensors
- •
- •Use state classes appropriately:
- •
SensorStateClass.MEASUREMENTfor instantaneous values (power, temperature) - •
SensorStateClass.TOTALfor values that can increase/decrease (net energy) - •
SensorStateClass.TOTAL_INCREASINGfor cumulative counters that only increase
- •
- •Use
EntityCategory.DIAGNOSTICfor non-primary sensors (WiFi RSSI, temperatures) - •Set
entity_registry_enabled_default = Falsefor diagnostic or rarely-used sensors - •Use
suggested_display_precisionto control decimal places shown in UI - •Use
_unrecorded_attributesfrozenset to exclude high-frequency attributes from recorder
Restoring Sensor State
Use RestoreSensor (not RestoreEntity) to restore sensor state after restart:
python
from homeassistant.components.sensor import RestoreSensor
class MyEnergySensor(RestoreSensor):
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
if (last := await self.async_get_last_sensor_data()):
self._attr_native_value = last.native_value
Diagnostics (Gold Quality Scale)
- •Create
diagnostics.pywithasync_get_config_entry_diagnosticsfunction - •Return a dict with: entry data, device_info, coordinator_data, last_update_success, last_exception
- •Redact sensitive data before returning:
- •IPs, MACs, SSIDs, tokens, passwords
- •Use a
TO_REDACTlist pattern for consistency:pythonTO_REDACT = {"host", "ip", "mac", "ble_mac", "wifi_mac", "wifi_name", "SSID", "bleMac"} def _redact_dict(data: dict) -> dict: return {k: "**REDACTED**" if k in TO_REDACT else v for k, v in data.items()}
- •Test diagnostics with snapshot tests and verify redaction
Quality Scale Tracking
Integrations working toward Bronze/Silver/Gold should maintain a quality_scale.yaml:
yaml
rules:
config_flow: done
test_before_setup: done
unique_config_entry: done
diagnostics:
status: done
comment: Added in v0.2.0
reauthentication-flow:
status: exempt
comment: Device has no authentication
Statuses: done, todo, exempt (with comment explaining why).
Manifest & Metadata
- •Pin requirements (e.g.,
pymarstek==x.y.z) to avoid breaking upgrades. - •Set
version,config_flow: true,iot_class, andcodeowners; keep documentation and issue tracker URLs current. - •For HACS, keep releases/tagging consistent and add
hacs.jsonif distribution via HACS.
Translations
- •Author strings in
strings.json; mirror totranslations/en.json. - •Use descriptive error keys (
cannot_connect,invalid_auth,already_configured). - •Prefer placeholders for dynamic content (e.g.,
{ip_address}) to keep translations flexible.
Testing & CI
- •Use
pytestwithpytest-homeassistant-custom-component; pin test deps inrequirements_test.txt. - •Test layout:
tests/mirrors component files (test_config_flow.py,test_init.py,test_sensor.py, etc.); put shared fixtures intests/conftest.py(enable custom integrations). - •Config flow: cover success + cannot_connect + invalid_auth/invalid_discovery_info + already_configured; assert unique_id and aborts.
- •Coordinator/entities: mock transport; assert happy path + errors raise
UpdateFailedand surface unavailable states; gate entities on data keys. - •Actions/commands: verify polling is paused, retries fire, verification logic works, and failures bubble.
- •Snapshot/diagnostics (Gold path): use
syrupyHA extension for diagnostics/device registry dumps; redact sensitive fields. - •CI: hassfest + lint (ruff, mypy) + pytest with coverage threshold (e.g.,
--cov-fail-under=95); test latest supported Python versions.
Verification After Changes (MANDATORY)
After every code change, run both checks before considering the work complete:
bash
# 1. Type checking (strict mode) python3 -m mypy --strict custom_components/<domain>/ # 2. Tests with coverage pytest tests/ -q --cov=custom_components/<domain> --cov-fail-under=95
Both must pass. Fix any errors and re-run until clean.
Operational Hygiene
- •Debounce manual refreshes with
coordinator.async_request_refresh(). - •Pause polling when sending control commands that reuse the same transport to avoid concurrent traffic.
- •Log warnings/errors with actionable context (host, method) but avoid noisy debug logs by default.
Quick Checklist
- • No blocking I/O on the event loop
- • Coordinator is the sole reader/writer to the device
- • Config/Options/Reauth flows implemented and reload on options change
- • Stable unique IDs + device identifiers
- • Translations updated (strings + en.json)
- • Requirements pinned; manifest fields valid; hassfest/HACS clean
- • Tests cover config flow, coordinator happy-path and failure, and entity states
- • mypy --strict passes with no errors
- • All tests pass with >95% coverage