Support discovery
Gold rule discovery: devices are discovered, so users do not have to look them up.
This is a Gold rule. In quality_scale.yaml, it is called discovery.
If the devices of the integration can be found on the network or on a bus, the integration supports discovery. Home Assistant can discover devices with Bluetooth, DHCP, HomeKit, mDNS (zeroconf), MQTT, SSDP, and USB, and apps can announce a service through the Supervisor. You declare what to listen for in manifest.json, and handle the discovery in a step of the config flow.
Without discovery, users first have to find out which integration supports their device, and then look up its IP address. With discovery, Home Assistant tells them it found a device, and they confirm. That is the difference between a setup that takes a minute and one that needs a router admin page.
Discovery also keeps working after setup: when the device gets a new IP address, the integration can update its configuration. See discovery-update-info.
The device announces a _mydevice._tcp.local. service over mDNS. Declare it in manifest.json:
{
"zeroconf": ["_mydevice._tcp.local."]
}Then handle it in config_flow.py, next to the user step. The discovery step sets the unique ID, aborts when the device is already configured, and asks the user to confirm:
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for My integration."""
host: str
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
self.host = discovery_info.host
serial = discovery_info.properties["serialno"]
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
try:
await MyClient(self.host).get_data()
except MyClientError:
return self.async_abort(reason="cannot_connect")
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Ask the user to confirm the discovered device."""
if user_input is not None:
return self.async_create_entry(
title="My device",
data={CONF_HOST: self.host},
)
self._set_confirm_only()
return self.async_show_form(step_id="discovery_confirm")ZeroconfServiceInfo comes from homeassistant.helpers.service_info.zeroconf. The other discovery methods have their own service info classes in homeassistant.helpers.service_info, and their own steps, like async_step_dhcp or async_step_bluetooth.
If the user step sets the same unique ID, pass raise_on_progress=False to async_set_unique_id there. Otherwise, a user who sets up the device by hand while a discovery flow for it is open gets an abort.
- Hassfest, when the rule is
done: the integration needs aconfig_flow.py, and either a discovery key inmanifest.json(bluetooth,dhcp,homekit,lorawan,mqtt,ssdp,usb, orzeroconf) or a discovery step in the config flow, likeasync_step_zeroconforasync_step_hassio. - Codecov requires 100% test coverage of the changed lines in
config_flow.py, so the discovery steps need tests. - Whether every way the device can be discovered is used, is checked in review.
Not every device can be discovered. If the devices or service of the integration cannot be discovered, this rule does not apply.
- config-flow: set up from the UI with a config flow.
- test-before-configure: test the connection in the config flow.
- unique-config-entry: prevent duplicate config entries.
- config-flow-test-coverage: test the config flow fully.