Skip to main content

Store runtime data inside the config entry

· 2 min read

Integrations often need to set up and track custom data, such as coordinators, API connections, or code objects. Previously, those were all stored inside hass.data, which made tracking them difficult.

With recent changes, it's now possible to use entry.runtime_data for that. The config entry is already available when setting up platforms and gets cleaned up automatically. No more deleting the key from hass.data after unloading.

It also better supports type-checking. ConfigEntry is generic now, so passing the data type along is possible. Use a typed data structure like dataclass for that. To simplify the annotation, it's recommended to define a type alias for it.

An example could look like this:

# <integration>/__init__.py

# The type alias needs to be suffixed with 'ConfigEntry'
type MyConfigEntry = ConfigEntry[MyData]

@dataclass
class MyData:
client: MyClient
other_data: dict[str, Any]

async def async_setup_entry(
hass: HomeAssistant,
entry: MyConfigEntry, # use type alias instead of ConfigEntry
) -> bool:
client = MyClient(...)

# Assign the runtime_data
entry.runtime_data = MyData(client, {...})
# <integration>/switch.py

from . import MyConfigEntry

async def async_setup_entry(
hass: HomeAssistant,
entry: MyConfigEntry, # use type alias instead of ConfigEntry
async_add_entities: AddEntitiesCallback,
) -> None:
# Access the runtime data form the config entry
data = entry.runtime_data

async_add_entities([MySwitch(data.client)])

Always reload after a successful re-auth flow

· 2 min read

Always reload after successful reauthentication

To update and reload the entry after a successful reauthentication flow, the helper async_update_reload_and_abort can be used. The default behavior of the helper has been changed. By default the entry will always reload if the helper is called. If an entry needs reauthentication, it is not always needed to update the entry if an account was temporary disabled or an API-key was temporary disallowed.

For cases where reloading is not wanted in case the entry is not changed, the reload_even_if_entry_is_unchanged=False parameter can be passed to the helper.

More about this helper can be found here here.

Example

class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Config flow to handle OAuth2 authentication."""

reauth_entry: ConfigEntry | None = None

async def async_step_reauth(self, user_input=None):
"""Perform reauth upon an API authentication error."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(self, user_input=None):
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({}),
)
return await self.async_step_user()

async def async_oauth_create_entry(self, data: dict) -> dict:
"""Create an oauth config entry or update existing entry for reauth."""
if self.reauth_entry:
# Only reload if the entry was updated
return self.async_update_reload_and_abort(
self.reauth_entry,
data=data,
reload_even_if_entry_is_unchanged=False,
)
return await super().async_oauth_create_entry(data)

Replacing `async_track_state_change` with `async_track_state_change_event`

· One min read

async_track_state_change is deprecated and will be removed in Home Assistant 2025.5. async_track_state_change_event should be used instead.

async_track_state_change always creates a top-level listener for EVENT_STATE_CHANGED, which would have to reject all state changes that did not match the desired entities. This design presented a performance problem when there were many integrations using async_track_state_change. async_track_state_change has been phased out in core since the introduction of async_track_state_change_event, with the last instance being removed in 2024.5.

Example with async_track_state_change:

from homeassistant.core import State, callback
from homeassistant.helper.event import async_track_state_change

@callback
def _async_on_change(entity_id: str, old_state: State | None, new_state: State | None) -> None:
...

unsub = async_track_state_change(hass, "sensor.one", _async_on_change)
unsub()

Example replacement with async_track_state_change_event:

from homeassistant.core import Event, EventStateChangedData, callback
from homeassistant.helper.event import async_track_state_change_event

@callback
def _async_on_change(event: Event[EventStateChangedData]) -> None:
entity_id = event.data["entity_id"]
old_state = event.data["old_state"]
new_state = event.data["new_state"]
...

unsub = async_track_state_change_event(hass, "sensor.one", _async_on_change)
unsub()

New notify entity platform

· One min read

New notify entity platform

The notify platform is now available as an entity platform. The MVP for the new notify platform implements the method and service send_message. It accepts message as a required attribute. Unlike the legacy notify.notify service we have no targets as argument, as it is an entity we can target multiple notify entities when calling send_message.

The architecture discussion is ongoing, and is about the device classes to implement and the implementation of recipient support in the form of contacts via a contact registry.

Existing integrations that implement the legacy notify services will be migrated in phases. The first step is to migrate the integrations than only use message as a parameter to notify.

The integrations identified for migration are:

  • circuit
  • clickatell
  • clicksend
  • command_line
  • demo
  • ecobee
  • flock
  • free_mobile
  • knx
  • mastodon

As soon as we have title and/or recipient support we can migrate more integrations to use the new platform.

When integrations are migrated, users will need to use the new notify.send_message service, so the migration changes will cause automations to break after the deprecation period is over.

Deprecate old backports and typing alias

· One min read

In the past, we've backported features from upstream CPython to use them early and improve user and developers' experience. Home Assistant only supports Python 3.12, so these can be used directly from Python. These backports are now deprecated and will be removed in the future.

DeprecatedReplacementPython version
homeassistant.backports.enum.StrEnumenum.StrEnum>= 3.11
homeassistant.backports.functools.cached_propertyfunctools.cached_property>= 3.8, >= 3.12 (performance improvement)

In addition, some typing aliases are also deprecated now.

DeprecatedReplacement
homeassistant.helpers.typing.ContextTypehomeassistant.core.Context
homeassistant.helpers.typing.EventTypehomeassistant.core.Event
homeassistant.helpers.typing.HomeAssistantTypehomeassistant.core.HomeAssistant
homeassistant.helpers.typing.ServiceCallTypehomeassistant.core.ServiceCall

Deprecating `async_add_hass_job`

· One min read

As part of an effort to improve performance and simplify the core job API, async_add_hass_job is deprecated and will be removed from Home Assistant in 2025.5.

Calls should be replaced with async_run_hass_job instead.

How uv saves Home Assistant 215 compute hours per month

· 2 min read

By replacing pip with uv in our production images, our build pipeline (and therefore releasing a new version) is a lot faster. Uv is an extremely fast Python package installer and resolver written in Rust. It is developed by Astral and it's open source. Check it out on GitHub.

In the following table, you can see that we can save around 5 hours of execution time on each build.

ArchPipUVSavings
aarch641h 24m 53s5m 18s~1h 20m
armhf1h 52m 20s6m 2s~1h 46m
armv71h 26m 43s5m 28s~1h 21m
amd6422m 10s3m 20s~19m
i38617m 37s3m 11s~14m

On average, we run the build pipeline 43 times as we create

  • 31 nightlies (one nightly per day)
  • 7 beta releases
  • 5 stable releases (including patch ones)

In total, we save around 215 hours per month. With this massive improvement, we can now ship hotfixes even faster, as the pipeline to ship a new version now takes around 20 minutes instead of 2.5 hours.

The 215 monthly saved execution hours can be used by other jobs and make the CI experience for all developers and our community better. By replacing pip with uv, we improve our sustainability by using fewer resources to build our images.

A big thank you to Astral for developing this amazing tool. Please check out their website and products as they offer, for example, a "lightning" fast linter/formatter for Python too.

Deprecate hass.helpers usage

· One min read

As of Home Assistant 2024.5, we deprecate the use of hass.helpers. Using hass.helpers will issue a warning in the logs. Authors of custom integrations are encouraged to update their code to prevent any issues before Home Assistant 2024.11.

Starting from Home Assistant 2024.11, hass.helpers will be removed and will no longer work.

Integrations that use hass.helpers should be updated to import the functions and classes directly from the integration package and pass the hass object as first parameter.

New example

from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession

async def async_setup(hass: HomeAssistant, config):
"""Set up the component."""
client = async_get_clientsession(hass)

Old example

from homeassistant.core import HomeAssistant

async def async_setup(hass: HomeAssistant, config):
"""Set up the component."""
client = hass.helpers.aiohttp_client.async_get_clientsession()

Config Entries can now provide a reconfigure step

· 2 min read

As of Home Assistant Core 2024.4, config entries can now be reconfigured by adding a reconfigure step in their config flows.

This is not to replace the optional configuration (OptionsFlow) but instead to allow the user to change the setup configuration after a config entry has been created.

Reconfiguration vs. Reauthentication

The reconfigure step does not replace a reauth step and they have different purposes.

Reauthentication should be started automatically by the integration in the case of a login/token/etc. is invalidated, so the user has an option to adjust those settings.

Reconfiguration is started by the user from the config entry options menu and should be implemented to update config entry data which are not optional for the integration to work. Authentication issues are handled with a re-authentication flow. (See reauthentication).

Example

Examples could be changing the latitude and longitude of a WeatherEntity when moving between homes or having a mobile home, changing the communication port of a local device, etc.

To implement the reconfigure step, include it in your config flow as:

import voluptuous as vol

class ExampleConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Example integration."""

async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None):
"""Add reconfigure step to allow to reconfigure a config entry."""
if user_input is not None:
pass # TODO: process user input

return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema({vol.Required("password"): str}),
)

New state timestamp State.last_reported

· 2 min read

The State object is now always updated and an event is always fired when an integration sets the state of an entity, regardless of any change to the state or a state attribute. This is implemented by adding a new timestamp, State.last_reported and a new event state_reported.

The state_reported event is fired in large volume, and must be used with care to avoid detrimental effects on system load:

  • Event listeners which subscribe to all events are not called when state_reported is fired.
  • It's not allowed to listen to state_reported events for all entities.
  • When listening to state_reported, an event_filter must be used to screen out events for other entities.
  • When listening to state_reported, the run_immediately flag must be set.

Background

Home Assistant previously discarded state writes where neither the state nor the state attributes were changed, unless the integration set the force_update flag. This behavior made it very difficult for integrations to correctly do time series analysis of numerical sensor state. It also meant the user didn't know if an integration is updating a sensor or not.

The new timestamp and associated event were discussed in architecure discussion #1062.

Custom integration impact

The following APIs have breaking changes:

  • The time_fired argument for hass.bus.async_fire now accepts a float timestamp instead of a datetime object.
  • The signature of event filters which can be passed to hass.bus.async_listen has changed.
  • The time_fired: datetime | None argument for Event() has been replaced with time_fired_timestamp: float | None.
  • A new argument last_reported: datetime has been added to State() in the middle of the argument list.

For more details, see core PR #113511 and core PR #113798.