Skip to main content

Browse all posts by date →

Shared config flow abort reasons are translated centrally

· 2 min read

As of Home Assistant Core 2026.10, the homeassistant integration can translate abort reasons that every integration words the same way. If you're today linking from a local translation key, for an abort reason, to a shared translation key under the homeassistant integration, you can instead just rely on the shared translation key under the homeassistant domain directly. This is done by default in some helpers and can also be done explicitly by setting the translation_domain parameter when aborting the flow.

What to do​

  • Delete the keys below from the abort sections of strings.json, including those under config_subentries, only when the abort uses the homeassistant translation domain.
  • The helpers listed below use the central translation domain automatically.
  • If your code passes one of these reasons to async_abort or AbortFlow, pass the homeassistant translation domain before deleting the local key. Without that domain, retain the local key. See Raise one yourself.

When an abort uses the central translation domain, the frontend resolves the reason from that domain and does not use a local key.

Default reasons​

ReasonRaised by
already_in_progressasync_set_unique_id, discovery without a unique ID
single_instance_allowedsingle_config_entry, DiscoveryFlowHandler, WebhookFlowHandler
no_devices_foundDiscoveryFlowHandler
cloud_not_connectedWebhookFlowHandler
reauth_successfulasync_update_reload_and_abort, async_update_and_abort
reconfigure_successfulasync_update_reload_and_abort, async_update_and_abort, also for subentries
authorize_url_timeoutAbstractOAuth2FlowHandler
missing_credentialsAbstractOAuth2FlowHandler
no_url_availableAbstractOAuth2FlowHandler
oauth_errorAbstractOAuth2FlowHandler
oauth_failedAbstractOAuth2FlowHandler
oauth_implementation_unavailableAbstractOAuth2FlowHandler
oauth_timeoutAbstractOAuth2FlowHandler
oauth_unauthorizedAbstractOAuth2FlowHandler
user_rejected_authorizeAbstractOAuth2FlowHandler

Explicit translation domain​

Pass the domain that owns the string. Both async_abort and AbortFlow accept it.

from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN

return self.async_abort(
reason="no_devices_found",
translation_domain=HOMEASSISTANT_DOMAIN,
)

This works in config flows and subentry flows. Options flows look in the options section, which the homeassistant integration does not have.

Keep your own wording​

Only needed if you word a reason differently on purpose:

  • Pass reason to async_update_reload_and_abort or async_update_and_abort, even with the default name.
  • In an OAuth2 flow, pass translation_domain=DOMAIN.

Aborts that core raises by itself, like already_in_progress, always use the central string, to keep a customized string there, overwrite the steps that produce the strings upstream.

LLM tools return a ToolResult and declare their integration

· 2 min read

As of Home Assistant Core 2026.10, an LLM tool returns an llm.ToolResult instead of a plain JSON object. ToolResult carries the tool's data and an error flag that says whether the call failed. On the chat log side, ToolResultContent.tool_result is replaced by ToolResultContent.result, which holds the ToolResult.

Returning a plain JSON object from a tool, reading ToolResultContent.tool_result, and setting tool_result on a tool result delta are deprecated. They keep working for custom integrations with a warning in the log until Home Assistant Core 2027.11, and stop working after that.

The release also adds metadata to llm.Tool. A tool now has a title for people to read, annotations that describe how it behaves, and an integration that records which integration provides it. The MCP Server integration serves the title and the annotations to MCP clients.

The annotations are an llm.ToolAnnotations with four flags: read_only, destructive, idempotent and open_world. The defaults describe the least safe case, so a tool that declares nothing is taken to write, to be destructive, and to reach outside Home Assistant.

Creating a tool without an integration is deprecated. A core integration raises an error. A custom integration gets a warning in the log until Home Assistant Core 2027.10, and stops working after that.

Both changes together look like this:

from homeassistant.core import HomeAssistant
from homeassistant.helpers import llm

from .const import DOMAIN


class GetItemsTool(llm.Tool):
"""Tool to read the items on a list."""

name = "my_integration__get_items"
title = "Get list items"
description = "Read the items on a list."
integration = DOMAIN
annotations = llm.ToolAnnotations(
read_only=True, destructive=False, idempotent=True, open_world=False
)

async def async_call(
self,
hass: HomeAssistant,
tool_input: llm.ToolInput,
llm_context: llm.LLMContext,
) -> llm.ToolResult:
"""Call the tool."""
return llm.ToolResult(data={"items": ["Milk", "Bread"]})

Use Config Entry exceptions in integration migration method

· 2 min read

Integration migration async_migrate_entry method can now raise config entry exceptions instead of only being limited to return False. This is preferred to provide more context and being translatable.

Integrations that fetch data from other services during migration can raise ConfigEntryNotReady to have the migration retried automatically later (as during setup), which helps with timeouts and other errors that may resolve on their own.

By returning False or raising any other exception than ConfigEntryNotReady the migration stops and the config entry state is set to migration_error which is a non-recoverable state. This previously required the user to restart Home Assistant to retry the migration. The new correct way to handle it is to create a repair issue. After the user fixes the issue, call hass.config_entries.async_retry_migration(entry_id) to retry the migration.

More info in the config flow documentation.

Example migration function which raises if client has an issue​

The migration function lives in the integration's __init__.py.

from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating configuration from version %s.%s",
config_entry.version, config_entry.minor_version
)

if config_entry.version == 1:
try:
new_info = await client.get_info()
except ConnectionError as exc:
raise ConfigEntryNotReady(
translation_key="key",
translation_domain=DOMAIN,
) from exc
except AuthenticationError as exc:
async_create_issue(
hass,
DOMAIN,
f"migrate_could_not_auth_{config_entry.entry_id}",
is_fixable=True,
is_persistent=False,
severity=IssueSeverity.ERROR,
translation_key="migrate_could_not_auth",
translation_placeholders={"title": config_entry.title},
data={"entry_id": config_entry.entry_id},
)
raise ConfigEntryError(translation_key="migrate_could_not_auth") from exc

new_data = {**config_entry.data, "some_data": new_info["some_data"]}
hass.config_entries.async_update_entry(
config_entry, data=new_data, version=2
)

_LOGGER.debug("Migration to configuration version %s.%s successful",
config_entry.version, config_entry.minor_version,
)

return True

Example repair which will retry the migration​

The repair flow lives in the integration's repairs.py.

class MigrationRepairFlow(RepairsFlow):
"""Handler for a migration repair flow."""

def __init__(self, entry_id: str) -> None:
"""Initialize the flow."""
self.entry_id = entry_id

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> RepairsFlowResult:
"""Fix the issue and retry the migration."""
if user_input is not None:
# TODO: Do whatever is necessary to fix the issue,
# for example update the config entry data
await self.hass.config_entries.async_retry_migration(self.entry_id)
return self.async_create_entry(data={})

return self.async_show_form(
step_id="init",
data_schema=probatio.Schema({}),
)


async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
if issue_id.startswith("migrate_could_not_auth_"):
return MigrationRepairFlow(data["entry_id"])

`DeviceEntry.config_entries` deprecation is now enforced

· 4 min read

Summary​

The device registry's multi-config-entry compatibility properties — DeviceEntry.config_entries, DeviceEntry.config_entries_subentries and DeviceEntry.primary_config_entry — were announced as deprecated in Devices are restricted to a single config entry and at most one subentry, but reading them was still silent. They now report at runtime: core and core integrations raise RuntimeError, custom integrations log a warning.

Use DeviceEntry.config_entry_id and DeviceEntry.config_subentry_id instead. The properties remain available to custom integrations until Home Assistant Core 2027.10, two releases later than the 2027.8 given in the earlier post.

Reading the properties on a synthesized composite device is not deprecated and does not report, because such a device really does span several config entries.

Most integrations don't read these properties and don't need any changes. Read on if your integration inspects a device's config entries, or accesses them on a deleted or child device.

This is implemented in core PR #181949 and lands in Home Assistant Core 2026.10.

New stop action and idle activity for lawn mowers

· One min read

As of Home Assistant Core 2026.10, LawnMowerEntity supports a stop action and an IDLE activity.

stop cancels the current mowing task without returning the mower to the dock. It differs from pause, which keeps the task so it can be resumed, and from dock, which sends the mower home. To support it, add LawnMowerEntityFeature.STOP to the supported features and implement stop or async_stop.

LawnMowerActivity.IDLE describes a mower that is stopped, but neither docked nor paused. Integrations that mapped such a state to PAUSED or ERROR should now use IDLE.

More details can be found in the documentation.

Example​

class MyLawnMower(LawnMowerEntity):
_attr_supported_features = (
LawnMowerEntityFeature.START_MOWING
| LawnMowerEntityFeature.DOCK
| LawnMowerEntityFeature.STOP
)

async def async_stop(self) -> None:
"""Stop the mower and cancel the current task."""
await self.mower.stop()
self._attr_activity = LawnMowerActivity.IDLE
self.async_write_ha_state()

OAuth2 error handling moved into the helper

· 2 min read

As of Home Assistant Core 2026.10, the OAuth2 helper raises exceptions that config entry setup already understands, so integrations no longer translate OAuth2 failures themselves.

What changed​

The OAuth2 exceptions now inherit from the config entry exception that describes what should happen, and carry a translated message from the homeassistant integration.

ExceptionAlso aResult when uncaught
ImplementationUnavailableErrorConfigEntryNotReadySetup is retried
UnknownImplementationErrorConfigEntryAuthFailedReauth flow starts
OAuth2TokenRequestErrorConfigEntryNotReadySetup is retried
OAuth2TokenRequestTransientErrorConfigEntryNotReadySetup is retried
OAuth2TokenRequestConnectionErrorConfigEntryNotReadySetup is retried
OAuth2TokenRequestReauthErrorConfigEntryAuthFailedReauth flow starts

All of them are importable from homeassistant.exceptions and carry a translated user-facing message, so the integration does not need a strings.json entry for them.

Migration​

- try:
- implementation = await async_get_config_entry_implementation(hass, entry)
- except ImplementationUnavailableError as err:
- raise ConfigEntryNotReady(
- translation_domain=DOMAIN,
- translation_key="oauth2_implementation_unavailable",
- ) from err
+ implementation = await async_get_config_entry_implementation(hass, entry)

Also remove the now-unused oauth2_implementation_unavailable entry from the exceptions section of strings.json.

Catching ValueError around this call can go as well. UnknownImplementationError subclasses ValueError for backwards compatibility, so a ValueError handler silently downgrades it and discards the translated message.

Token requests​

- try:
- await auth.async_get_access_token()
- except OAuth2TokenRequestReauthError as err:
- raise ConfigEntryAuthFailed from err
- except OAuth2TokenRequestError as err:
- raise ConfigEntryNotReady from err
+ await auth.async_get_access_token()

The same applies to await session.async_ensure_token_valid().

Quality scale​

The test-before-setup rule accepts await session.async_ensure_token_valid() in async_setup_entry as satisfying the rule, alongside await coordinator.async_config_entry_first_refresh(). Both raise the appropriate config entry exception on the integration's behalf.

When to keep catching​

Central handling is a default, not a restriction. Keep an explicit handler when the integration genuinely needs different behavior, for example:

  • Treating a specific status as permanent with ConfigEntryError instead of retrying.
  • Adding context to the message that only the integration knows.
  • Cleaning up integration state before the exception propagates.

In those cases, catch the most specific exception that applies and let the rest propagate.

New device and state class selectors

· 2 min read

New selectors are available to choose a device class or sensor state class.

These selectors can be used in config flows and blueprints when requesting a device or state class from the user.

Device class selector​

The new DeviceClassSelector is available for selecting device classes in config flows and blueprints. It supports device classes for the following platforms:

Platform.BINARY_SENSOR: BinarySensorDeviceClass
Platform.BUTTON: ButtonDeviceClass
Platform.COVER: CoverDeviceClass
Platform.EVENT: EventDeviceClass
Platform.HUMIDIFIER: HumidifierDeviceClass
Platform.INFRARED: InfraredDeviceClass
Platform.MEDIA_PLAYER: MediaPlayerDeviceClass
Platform.NUMBER: NumberDeviceClass
Platform.SENSOR: SensorDeviceClass
Platform.SWITCH: SwitchDeviceClass
Platform.UPDATE: UpdateDeviceClass
Platform.VALVE: ValveDeviceClass

Device class selector examples​

Example of a device class selector that returns a single device class:

vol.Schema(
{
vol.Optional(CONF_DEVICE_CLASS): DeviceClassSelector(
DeviceClassSelectorConfig(domain=Platform.SENSOR)
),
}
)

Example of a device class selector that returns multiple device classes as a list:

vol.Schema(
{
vol.Optional(CONF_DEVICE_CLASS): DeviceClassSelector(
DeviceClassSelectorConfig(
domain=Platform.BINARY_SENSOR,
multiple=True,
)
),
}
)

Sensor state class selector​

The new StateClassSelector is available for selecting sensor state classes in config flows and blueprints.

Sensor state class selector examples​

Example of a sensor state class selector that returns a single state class:

vol.Schema(
{
vol.Optional(CONF_STATE_CLASS): StateClassSelector(),
}
)

Example of a sensor state class selector that returns a single state class with only a filtered subset of available state classes:

vol.Schema(
{
vol.Optional(CONF_STATE_CLASS): StateClassSelector(
StateClassSelectorConfig(
state_classes=[
SensorStateClass.MEASUREMENT,
SensorStateClass.TOTAL_INCREASING,
],
)
),
}
)

Example of a state class selector that returns multiple state classes as a list:

vol.Schema(
{
vol.Optional(CONF_STATE_CLASS): StateClassSelector(
StateClassSelectorConfig(multiple=True),
),
}
)

Migrating existing device and state class selectors​

Unlike using a generic SelectSelector, the DeviceClassSelector allows the frontend to automatically translate device classes into user-friendly names.

Existing implementations that select a device or state class using SelectSelector should be migrated to use DeviceClassSelector or StateClassSelector. After migration, any stale translations related to the old selector values should be removed.

The Selectors documentation has been updated to include the new selectors.

Deprecating modbus.get_hub in favor of async_get_unit

· 2 min read

As of Home Assistant Core 2026.10, modbus.get_hub is deprecated. It will be removed in Home Assistant Core 2027.10. Custom integrations that call it get a warning in the log until then, and stop working after that.

Background​

get_hub attaches an integration to a Modbus hub the user configured in YAML, under a name the integration has to be told. The user has to set up the hub by hand before the integration can work, and two integrations that need the same bus cannot share it.

In July we announced our plan to modernize Modbus in Home Assistant. Home Assistant Core 2026.9 delivered the first piece: async_get_unit. An integration collects the connection details in its own config flow, the same as any other integration, and asks the Modbus integration for a unit on them. Integrations that ask with equal details share one connection. Nothing is configured in YAML and nothing extra is persisted.

What to do​

Replace the call to get_hub with a call to async_get_unit:

from homeassistant.components.modbus import async_get_unit
from modbus_connection import ModbusTcpParams


async def async_setup_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
"""Set up my device from a config entry."""
unit = async_get_unit(
hass,
entry,
ModbusTcpParams(host=entry.data[CONF_HOST], port=entry.data[CONF_PORT]),
entry.data[CONF_UNIT_ID],
)
device = MyDevice(unit)
...

This is not a one-to-one swap. Your config flow has to collect the connection details required by the transport, for example host and port, besides unit ID, that the user used to write in the YAML hub, and the device-specific communication should move into a library built on modbus-connection. The Modbus developer documentation describes both, with example code and a reference device library.

More details can be found in the core PR.

Configurator integration is now deprecated

· One min read

The Configurator integration has been deprecated and will be removed in Home Assistant 2027.10. The integration was originally created to provide a web-based configuration interface for Home Assistant, but it is no longer recommended for use. No core integrations use the Configurator integration anymore, and it is not recommended for custom integrations either.

The modern way to configure integrations in Home Assistant is via config flows and config entries. Config flows provide a user-friendly interface for setting up and configuring integrations, while config entries allow for easy management of integration settings.

More device registry deprecations, new helpers and validation

· 10 min read

Summary​

This is a follow-up to Devices are restricted to a single config entry and at most one subentry, and covers additional device registry deprecations, a few new helper methods, and some stricter validation that landed after that post.

Most custom integrations won't be affected by this set of changes. Read on if your integration sets via_device or default_manufacturer / default_model / default_name in DeviceInfo, looks devices up in the registry, reads the registry's devices, deleted_devices or child_devices containers, calls async_update_device directly, or attaches a device to an entity that has no config entry or unique id.

Unless noted otherwise, deprecated functionality logs a warning at runtime and remains supported until Home Assistant Core 2027.8. As before, deprecations which are only relevant to core and core integrations are enforced more strictly there: those callers raise immediately, while custom integrations keep getting a warning until the removal version.