Skip to main content

`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.

Device registry WebSocket API changes

· 6 min read

Summary

This post describes changes to the device registry WebSocket API — the commands and device serialization consumed by the frontend, custom cards, and other WebSocket clients. They stem from two changes to the device registry:

Most clients only read devices through config/device_registry/list, and the new device fields are additive, so no changes are required to keep working — but clients that iterate the device list should be ready to encounter child devices, which are serialized differently. Clients that remove devices, or that inspect a device's config entries, should read on.

New device fields: config_entry_id and config_subentry_id

Every device returned by config/device_registry/list, and every device in an EVENT_DEVICE_REGISTRY_UPDATED payload, now carries two new fields:

  • config_entry_id — the id of the single config entry the device belongs to.
  • config_subentry_id — the id of the single config subentry the device belongs to, or null.

They replace the previous fields, which modelled a device that could span several config entries and subentries:

  • config_entries — a list of config entry ids.
  • config_entries_subentries — a map of config entry id to a list of subentry ids.
  • primary_config_entry — the id of the device's primary config entry.

The old fields are kept for backwards compatibility and are deprecated; they are scheduled for removal in Home Assistant Core 2027.8. During the deprecation period they are derived from the new values: a device reports config_entries as the single-element list [config_entry_id], config_entries_subentries as {config_entry_id: [config_subentry_id]}, and primary_config_entry equal to config_entry_id.

Update your client to read config_entry_id and config_subentry_id.

Child devices in the device list

Home Assistant Core 2026.9 introduces child devices (architecture proposal #1414, core PR #178666). A child device is a lightweight logical part of a parent device: it has no hardware or firmware metadata of its own, and it references its parent through a parent_device_id. The parent must be registered by the same config entry and belong to the same config subentry.

config/device_registry/list now returns child devices alongside regular devices, so its result is a mix of two kinds of entry. A child device is serialized with a smaller set of fields:

{
"id": "child1234",
"parent_device_id": "abcd1234",
"config_entry_id": "wxyz5678",
"config_subentry_id": null,
"area_id": null,
"name": "Left channel",
"name_by_user": null,
"labels": [],
"identifiers": [["demo", "left"]],
"disabled_by": null,
"created_at": 1723987200.0,
"modified_at": 1723987200.0
}

Compared to a regular device, a child device has no connections, via_device_id, configuration_url, entry_type, manufacturer, model, model_id, hw_version, sw_version, serial_number, primary_config_entry, config_entries, or config_entries_subentries. Clients that read the device list must not assume every entry carries these fields.

The reliable way to tell the two apart is the parent_device_id field: it is present and non-null only on child devices. A regular device carries via_device_id instead.

A child device with no area_id of its own inherits its parent's area, so a client resolving a child device's area should fall back to the parent device when the child's area_id is null.

Two related commands also understand child devices:

  • config/device_registry/update accepts a child device id — setting area_id, disabled_by, labels, or name_by_user — and returns the updated child device in the reduced serialization shown above.
  • config/device_registry/list_linked_devices always returns an empty linked_devices list for a child device, since a child shares its parent's per-config-entry identifier namespace and is never linked to devices of other config entries.

New command: config/device_registry/remove

A new WebSocket command removes a device by id:

{
"type": "config/device_registry/remove",
"device_id": "abcd1234"
}

Because a device now belongs to exactly one config entry, removing it from that config entry removes the device. The command requires admin, and it rejects a composite device id with the error Cannot remove a composite device.

It replaces config/device_registry/remove_config_entry, which took both a device_id and a config_entry_id:

{
"type": "config/device_registry/remove_config_entry",
"config_entry_id": "wxyz5678",
"device_id": "abcd1234"
}

That command still works but is deprecated: it logs a warning and will be removed in Home Assistant Core 2027.9. Its config_entry_id parameter is now only used to check that it matches the device's config entry — a mismatch fails with Config entry not in device — and the device is removed regardless of which config entry was passed. Update clients to call config/device_registry/remove and drop the config_entry_id.

Implemented in core PR #178319.

New command: config/device_registry/list_linked_devices

Because connections and identifiers are now unique per config entry, a physical device supported by several integrations is represented by one device per config entry instead of a single shared device. This command returns the other devices that share a connection or identifier with a given device — the sibling devices representing the same physical hardware under different config entries:

{
"type": "config/device_registry/list_linked_devices",
"device_id": "abcd1234"
}

Result:

{
"linked_devices": ["ef567890", "12ab34cd"]
}

The queried device itself is excluded from the result. This lets a client, for example, link from a device page to the other devices that represent the same hardware. Implemented in core PR #177449.

New command: config/device_registry/list_composite_splits

When the device registry is loaded, each pre-migration device that spanned several config entries is split into one device per config entry, and the original ("composite") device id no longer refers to a registered device. Composite device ids are still referenced by automations, scripts, dashboards, and target pickers, so this command maps every composite device id to the devices it was split into:

{
"type": "config/device_registry/list_composite_splits"
}

Result:

{
"old_composite_id": {
"split_ids": ["ef567890", "12ab34cd"],
"primary_id": "ef567890"
}
}

For each composite device id, split_ids lists the replacement device ids and primary_id is the split that inherited the composite's former primary config entry, or null. Use it to resolve a stored composite device id to a current device — for example, to keep a device picker working when the stored id is a composite id. Implemented in core PR #176693.

The Backwards compatibility section of the companion post describes the rest of the deprecation-period behavior, such as actions targeting a composite device id trickling down to the split devices.

Device registry events

Clients that subscribe to device_registry_updated events (EVENT_DEVICE_REGISTRY_UPDATED) see two changes, mirroring the Python API:

  • The changes map of an update event reports a device move with the keys config_entry_id and config_subentry_id, replacing the previous config_entries and config_entries_subentries.
  • A device now belongs to a single config entry, so it can no longer lose one config entry while remaining for another. A device losing its config entry is now a remove event rather than an update.

See Device registry events in the companion post for details.

Frontend component updates in 2026.8

· 3 min read

Component updates

ha-split-panel

We added ha-split-panel, a Home Assistant wrapper around the Web Awesome split panel component.

Use it when a Home Assistant page, dialog, or tool needs a resizable two-pane layout. Custom card authors can use Home Assistant frontend components, but internal Home Assistant UI APIs may change.

<ha-split-panel position="40" snap="50%">
<div slot="start">Editor</div>
<div slot="end">Preview</div>
</ha-split-panel>

New component-specific tokens:

--ha-split-panel-divider-width
--ha-split-panel-divider-hit-area
--ha-split-panel-min
--ha-split-panel-max
--ha-split-panel-grip-display

ha-tile-info updates

ha-tile-info gained more layout controls for custom cards and tile-like surfaces.

New component-specific tokens:

--ha-tile-info-gap
--ha-tile-info-min-height
--ha-tile-info-primary-min-height
--ha-tile-info-primary-line-clamp

Use --ha-tile-info-primary-line-clamp when the primary text should wrap to more than one line, and use the min-height tokens to keep rows aligned when some tiles have secondary text and others do not.

Form and selector updates

Conditional ha-form fields

ha-form schemas now support conditional field visibility with visible.

[
{
name: "advanced",
selector: { boolean: {} },
},
{
name: "advanced_name",
visible: { field: "advanced", value: true },
selector: { text: {} },
},
]

Supported operators are:

"eq"
"not_eq"
"in"
"not_in"
"exists"
"not_exists"

You can also combine conditions with and, or, and not.

Hidden fields are not rendered and are skipped during validation, so use visible instead of custom frontend-only hiding logic when a form field depends on another value.

Selector additions

The text selector now supports HTML pattern validation:

{
text: {
pattern: "[a-z0-9_]+",
validation_message: "Use lowercase letters, numbers, and underscores",
},
}

This works for both single-value and multiple-value text selectors.

Entity selectors can now filter by properties of the entity's device:

{
entity: {
filter: {
domain: "sensor",
device: {
manufacturer: "Home Assistant",
model: "Connect ZBT-1",
},
},
},
}

A new ui_clock_date_format selector was also added for the clock card date format editor.

Lovelace updates

state_color is moving to color

The entities and glance cards now support color as the replacement for state_color.

Before:

type: entities
state_color: true
entities:
- light.kitchen

After:

type: entities
color: state
entities:
- light.kitchen

Use color: state for the old state_color: true behavior, and color: none for state_color: false.

Custom panels and apps

Safe-area handling

Custom panels and add-on app iframes now get safe-area padding by default, so content stays clear of notches, status bars, and home indicators.

Custom panels that already handle safe areas themselves can opt out:

panel_custom:
- name: my-panel
module_url: /local/my-panel.js
handle_safe_area: true

For iframe-based custom panels, Home Assistant forwards the resolved safe-area values into the iframe document as CSS variables:

--safe-area-inset-top
--safe-area-inset-right
--safe-area-inset-bottom
--safe-area-inset-left

Add-on app iframes can also opt into managing the safe area themselves when subscribing to Home Assistant properties:

window.parent.postMessage(
{
type: "home-assistant/subscribe-properties",
handleSafeArea: true,
},
"*"
);

The properties message then includes safeAreaInsets.

Context and editor infrastructure

Global dirty state

DirtyStateProviderMixin now also publishes a global dirty state.

When any connected dirty-state provider has unsaved changes, window.isDirtyState is set and Home Assistant fires a dirty-state-changed event.

window.addEventListener("dirty-state-changed", (ev) => {
console.log(ev.detail.isDirty);
});

This is useful for shared infrastructure that needs to avoid disrupting active editors or dialogs with unsaved changes.