Skip to main content

Changes to device tracker entity models

· 3 min read

Summary

There have been multiple recent changes to the device tracker entity model:

  • The battery_level property has been deprecated
  • The location_name property of TrackerEntity has been deprecated
  • A new entity base class BaseScannerEntity has been introduced
  • Users can associate scanners with other zones than the home zone
  • TrackerEntity has a new property in_zones
  • BaseScannerEntity and ScannerEntity have a new state attribute in_zones
  • A new capability attribute tracking_type has been introduced
  • Zones are now calculated by size, then distance to center when calculating the state of TrackerEntity

Details

Deprecation of battery_level

The battery_level property has been deprecated in all device tracker base classes, and will stop working in Home Assistant Core 2027.7. Integrations should communicate battery level via a battery sensor instead.

More details can be found in architecture proposal #627

Deprecation of location_name

The location_name property of TrackerEntity has been deprecated, and will stop working in Home Assistant Core 2027.7.

Integrations with device trackers which do not know or do not want to report the exact coordinates and today use location_name to report the name of a zone should instead report a list of zone entity IDs through the in_zones property. Device trackers which use location_name to give extra context can instead do that via a separate sensor or an extra state attribute.

More details can be found in architecture proposal #1387

Introduction of the BaseScannerEntity base class

The BaseScannerEntity class should be used by integrations which have scanners which do not track connection to a WLAN or other local network, for example scanners which track connection to a BLE beacon.

Users can associate BaseScannerEntity and ScannerEntity with any zone

BaseScannerEntity and ScannerEntity store the associated zone as an entity registry option. The base class will set the state of the entity to the name of the associated zone when connected, and the in_zones state attribute to all zones which contain the associated zone.

More details can be found in architecture proposal #1389

Introduction of the in_zones state attribute

A new state attribute in_zones is present in the state of device tracker entities. The state attribute is automatically calculated by BaseScannerEntity and ScannerEntity. TrackerEntity will derive the in_zones state attribute from the in_zones property if not None, if it is None it will be calculated from the reported location.

The in_zones state attribute is a list of zone entity IDs sorted by size, with the smallest zone first, then by distance to center.

Introduction of the tracking_type capability attribute

A new capability attribute tracking_type is present in the state of device tracker entities. The state attribute is set to connection by BaseScannerEntity and ScannerEntity and to location by TrackerEntity. Integrations should not override this behavior.

Custom card suggestions in the card picker

· One min read

As of Home Assistant 2026.6, custom cards can show up as suggestions in the card picker. When a user selects an entity, custom cards that opt in are listed under a Community section, below the built-in suggestions.

To opt in, add a getEntitySuggestion function to your window.customCards entry. It receives the hass object and the selected entity id, and returns a suggestion (or null if the entity is not supported):

window.customCards.push({
type: "my-card",
name: "My Card",
getEntitySuggestion: (hass, entityId) => {
if (entityId.split(".")[0] !== "light") {
return null;
}
return {
config: { type: "custom:my-card", entity: entityId },
};
},
});

You can also return an array of suggestions to offer several variants, each with its own label.

Only suggest your card when it makes sense for the entity. Check the domain, device class, or supported features with the hass object, and return null otherwise. Suggesting your card for every entity makes the picker noisy.

See the custom card documentation for the full reference.

Frontend component updates in 2026.6

· 2 min read

Component updates

ha-radio updates

ha-radio was removed from our codebase, we use the webawesome based ha-radio-group with ha-radio-option now. No need for a ha-formfield around a ha-radio anymore and you can use the new CSS properties to customize the radio group and options.

New component specific tokens:

--ha-radio-group-required-marker
--ha-radio-group-required-marker-offset

--ha-radio-option-active-color
--ha-radio-option-heigh
--ha-radio-option-toggle-size
--ha-radio-option-border-width
--ha-radio-option-border-color
--ha-radio-option-border-color-hover
--ha-radio-option-background-color
--ha-radio-option-background-color-hover
--ha-radio-option-checked-background-color
--ha-radio-option-checked-icon-color
--ha-radio-option-checked-icon-scale
--ha-radio-option-control-margin

ha-drawer updates

ha-drawer was updated to use the webawesome drawer component. The API is mostly the same it just uses now --ha-sidebar-width instead of --mdc-drawer-width

top bar

  • ha-top-app-bar was removed entirely.
  • ha-top-app-bar-fixed was migrated from MWC to plain Lit.
  • ha-two-pane-top-app-bar-fixed was rewritten to extend the new implementation instead of Material base code.
  • ha-header-bar was rewritten from a Material top-app-bar styled wrapper to a native Lit component.

The --ha-top-app-bar-width token replaces --mdc-top-app-bar-width.

New decorators

@consumeLocalize

Following up on the context entry decorators introduced last release, we added a shortcut for the most common single-field read off internationalizationContext: the localize function.

Before:

@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, LocalizeFunc>({
transformer: ({ localize }) => localize,
})
private _localize!: LocalizeFunc;

After:

@state()
@consumeLocalize()
private _localize!: LocalizeFunc;

Use @consumeLocalize() whenever a component only needs the localize function. For other single-field reads off internationalizationContext (e.g. locale, language), keep using @consume + @transform.

Deprecation of advanced mode in data entry flow

· One min read

Summary

User profile advanced mode is going away, which means integrations can no longer check if advanced mode is enabled or not in data entry flows.

Integrations authors need to update integrations to use an alternative user friendly way to present additional options in the UI, for example group additional options in a section.

FlowHandler.show_advanced_options

The FlowHandler.show_advanced_options property has been deprecated and will be removed with the release of Home Assistant Core 2027.6. During the deprecation period, FlowHandler.show_advanced_options unconditionally returns True to not make options gated by this flag inaccessible to users.

FlowHandler.context['show_advanced_options']

There is no longer a show_advanced_options key in FlowHandler.context.

Background

The Advanced mode toggle in the user profile is a single binary switch that gates a collection of unrelated features across Home Assistant, from app (add-on) visibility (Terminal & SSH) to configuration options and UI elements, and we've been working on removing it during the past year.

For a more in-depth explanation, see roadmap issue #54.

BrowseMediaSource: domain is now required

· 2 min read

The BrowseMediaSource class in the media_source integration has been tightened up. The domain parameter is now a required str instead of str | None, and the special "list every media source" root node has moved to its own class, RootBrowseMediaSource.

Previously, domain was optional only to represent one edge case: the top-level node returned when browsing media-source:// with no specific source selected. That made the type hint misleading for the 99% case — every actual media source has a domain — and added a None branch that consumers had to think about. Splitting the root into its own class removes that branch.

What changed

  • BrowseMediaSource.__init__ now requires domain: str.
  • A new RootBrowseMediaSource class represents the root browse node listing all available media sources. It hardcodes domain=None and identifier=None and uses media-source:// as its content ID.
  • media_source.async_browse_media() and MediaSourceItem.async_browse() now return BrowseMediaSource | RootBrowseMediaSource.

Impact on custom integrations

Most integrations don't need any changes. If you implement a media_source.py platform, you were already passing your own domain to BrowseMediaSource — that keeps working.

You only need to act if:

  • You pass domain=None to BrowseMediaSource. This is no longer allowed. Set your integration domain instead.

  • You call media_source.async_browse_media() and annotate the result. Update the type hint to BrowseMediaSource | RootBrowseMediaSource, or narrow with isinstance() before using domain-specific attributes:

    from homeassistant.components.media_source import (
    BrowseMediaSource,
    async_browse_media,
    )

    result = await async_browse_media(hass, media_content_id)
    if isinstance(result, BrowseMediaSource):
    # result.domain is guaranteed to be a str here
    ...

See the updated media source platform documentation for the full reference.

Changes to the condition and script APIs

· 2 min read

Summary

The condition and script APIs have been changed.

Conditions are now instances of condition classes, which are evaluated by calling the async_check method and discarded by calling the async_unload method. Also, conditions may optionally implement an _async_setup or _async_unload method. Note that users of conditions don't need to call the condition's async_setup method.

During a deprecation period, which ends with the release of Home Assistant Core 2027.1, it's possible to use the condition object as a callable.

Scripts also have an async_unload method which must be called when the script is no longer needed.

Impact on custom integrations

Custom integrations which create conditions or scripts

Custom integrations which create condition objects should evaluate them by calling the async_check method and call the async_unload method when the condition is no longer needed.

Example:

from homeassistant.helpers.condition import (
async_condition_from_config,
async_validate_condition_config,
)

# Validate condition config
validated_config = await async_validate_condition_config(hass, config)

# Create a condition
condition = await async_condition_from_config(hass, validated_config)

...

# Evaluate the condition
result = condition.async_check(...)
...

# Discard the condition
condition.async_unload()

Custom integrations which create scripts should call the async_unload method when the script is no longer needed.

Example:

from homeassistant.helpers.script import (
Script,
async_validate_actions_config,
)

# Validate script config
validated_config = await async_validate_actions_config(hass, config)

# Create a script
script = Script(hass, validated_config, ...)

...

# Execute the script
result = await script.async_run(...)
...

# Discard the script
await script.async_unload()

Custom integrations which provide a condition platform

Integrations which provide a condition platform don't need to change, but may implement _async_setup and _async_unload method if the platform needs to perform async initialization or do tear down.

Example:


from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import (
Condition,
ConditionCheckParams,
ConditionConfig,
)
from homeassistant.helpers.typing import ConfigType

class CustomCondition(Condition):
"""A custom condition."""

@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
...

def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
super().__init__(hass, config)
...

async def _async_setup(self) -> None:
"""Set up the condition checker."""
...

def _async_unload(self) -> None:
"""Clean up any resources held by the checker."""
...

def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
"""Check the condition."""
...

Format entity names in custom cards

· One min read

As of Home Assistant 2026.4, the hass object exposes a formatEntityName helper. It is the same function used by the built-in cards (tile card, entity rows, ...) to compute the display name of an entity from its registry context (entity, device, area, floor). Custom cards can use it to produce names that stay consistent with the rest of the dashboard.

Given a temperature sensor named Temperature on a device named Thermostat:

const stateObj = hass.states["sensor.living_room_thermostat_temperature"];

hass.formatEntityName(
stateObj,
[{ type: "device" }, { type: "entity" }],
{ separator: " · " }
); // "Thermostat · Temperature"

The frontend also ships an entity_name selector. If your card uses the built-in form editor, you can offer users the same name picker the built-in cards use — accepting either a free-form string or a composition of registry items.

Take a look at the updated data documentation for the full reference and more examples.

MQTT publish API changes

· One min read

In the future, the MQTT publish API will require explicit values for qos and retain. Passing None for either argument will no longer be supported. Custom integrations should update their code to accept the defaults, or pass valid typed arguments. The fallbacks of None to a valid value for qos and retain will stop working with HA Core 2027.6.

The new API signatures are:

def publish(
hass: HomeAssistant,
topic: str,
payload: PublishPayloadType,
qos: int = 0,
retain: bool = False,
encoding: str | None = DEFAULT_ENCODING,
) -> None:
"""Publish message to a MQTT topic."""
hass.create_task(async_publish(hass, topic, payload, qos, retain, encoding))

and

async def async_publish(
hass: HomeAssistant,
topic: str,
payload: PublishPayloadType,
qos: int = 0,
retain: bool = False,
encoding: str | None = DEFAULT_ENCODING,
) -> None:
"""Publish message to a MQTT topic."""

MQTT publish API supports message expiry interval

· One min read

The MQTT publish API now supports setting a message expiry interval. Previously, retained messages were stored by the broker until they were replaced or explicitly cleared. With a message_expiry_interval set (in seconds), a published message — including a retained one — will automatically expire after the specified interval. This option is only supported when using MQTT protocol version 5; it is ignored when using earlier protocol versions.

The new API signatures are:

def publish(
hass: HomeAssistant,
topic: str,
payload: PublishPayloadType,
qos: int = 0,
retain: bool = False,
encoding: str | None = DEFAULT_ENCODING,
*,
message_expiry_interval: int | None = None,
) -> None:
"""Publish message to a MQTT topic."""

and

async def async_publish(
hass: HomeAssistant,
topic: str,
payload: PublishPayloadType,
qos: int = 0,
retain: bool = False,
encoding: str | None = DEFAULT_ENCODING,
*,
message_expiry_interval: int | None = None,
) -> None:
"""Publish message to a MQTT topic."""

Deprecating config entry listener with reloading methods in config flow

· One min read

As of Home Assistant Core 2026.6, using a config entry listener together with any reloading methods in a config flow is deprecated and will result in an error from 2026.12.

Background

Using a config entry listener together with any reloading methods in a config flow can cause the integration to reload twice and/or create a race condition.

Possible solutions

  • Remove the config entry listener and rely only on the reloading methods in your config flow.
  • Use async_update_and_abort() instead of async_update_reload_and_abort().
  • Set reload_on_update=False when calling _abort_if_unique_id_configured().

More details can be found in the core PR.