Skip to main content

Forwarding setup to config entry platforms

· One min read

Calling hass.config_entries.async_forward_entry_setup is deprecated and will be removed in Home Assistant 2025.6. Instead, await hass.config_entries.async_forward_entry_setups as it can load multiple platforms at once and is more efficient since it does not require a separate import executor job for each platform.

hass.config_entries.async_forward_entry_setups must always be awaited if it's called while the config entry is being set up to ensure that it finishes before the config entry setup is complete. For more details, review this blog post.

Alarm Control Panel Entity code validation

· One min read

The AlarmControlPanelEntity is now enforcing validation of code for alarm control panel entities, which set code_arm_required to True (default behavior). Service calls fail if no code is provided when a code is required.

Previously this was entirely optional, and a user could skip code entry regardless of it was needed by the integration or not (and as such each integration needed to implement its own check).

As the default behavior is that code is required, custom integrations that don't require a code input need to set code_arm_required to False or the user will always have to input a code regardless of if it's needed by the service calls.

Exposing Home Assistant API to LLMs

· One min read

Since we introduced LLMs in Home Assistant as part of Year of the Voice, we have received the request to allow enabling LLMs to interact with Home Assistant. This is now possible by exposing a Home Assistant API to LLMs.

Home Assistant will come with a built-in Assist API, which follows the capabilities and exposed entities that are also accessible to the built-in conversation agent.

Integrations that interact with LLMs should update their integration to support LLM APIs.

Custom integration authors can create their own LLM APIs to offer LLMs more advanced access to Home Assistant.

See the LLM API documentation for more information and this example pull request on how to integrate the LLM API in your integration.

Handling time zones without blocking the event loop

· One min read

Constructing ZoneInfo objects may do blocking I/O to load the zone info from disk if the timezone passed is not in the cache.

dt_util.async_get_time_zone is now available to replace dt_util.get_time_zone to fetch a time zone in the event loop which is async safe and will not do blocking I/O in the event loop.

hass.config.set_time_zone is deprecated and replaced with hass.config.async_set_time_zone. hass.config.set_time_zone will be removed in 2025.6. Setting the time zone only affects tests, as no integration should be calling this function in production.

Examining dt_util.DEFAULT_TIME_ZONE directly is deprecated and dt_util.get_default_time_zone() should be used instead.

If your integration needs to construct ZoneInfo objects in the event loop, it is recommended to use the aiozoneinfo library.

Changes in setup entity platforms with group integration

· 2 min read

Changes in setup entity platforms with group integration

By default, the group integration allows entities to be grouped. If the default ON/OFF states for an entity default to on and off, then grouping is supported by default. The setup changes, though, for Entity platforms that can be grouped but have alternative states, e.g., cover (open/closed) or person (home/not_home), or platforms that are meant to be excluded, such as sensor. These entity platforms must implement async_describe_on_off_states in the group.py module.

In async_describe_on_off_states, the domain needs to be the first argument passed to the registry methods on_off_states and exclude_domain. When registering alternative ON/OFF states with registry.on_off_state, in addition to the ON states, the default ON state needs to be passed.

Example registering alternative states

New signature for registry.on_off_states:

    @callback
def on_off_states(
self, domain: str, on_states: set[str], default_on_state:str, off_state: str
) -> None:
"""Register on and off states for the current domain."""
...

Example group.py for the vacuum entity platform registering alternative ON/OFF states. Note the the first ON state now is considered to be the default ON state.

"""Describe group states."""

from typing import TYPE_CHECKING

from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, callback

if TYPE_CHECKING:
from homeassistant.components.group import GroupIntegrationRegistry

from .const import DOMAIN, STATE_CLEANING, STATE_ERROR, STATE_RETURNING


@callback
def async_describe_on_off_states(
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
) -> None:
"""Describe group on off states."""
registry.on_off_states(
DOMAIN, # domain
# set with all group ON states
{
STATE_ON,
STATE_CLEANING,
STATE_RETURNING,
STATE_ERROR,
},
STATE_ON, # Default group ON state
STATE_OFF, # Group OFF state
)

Example excluding an entity platform from group entities

New signature for registry.exclude_domain:

    @callback
def exclude_domain(self, domain: str) -> None:
"""Exclude the current domain."""
...

Example group.py for the sensor entity platform to exclude sensor entities from group entities.

"""Describe group states."""

from typing import TYPE_CHECKING

from homeassistant.core import HomeAssistant, callback

if TYPE_CHECKING:
from homeassistant.components.group import GroupIntegrationRegistry

from .const import DOMAIN


@callback
def async_describe_on_off_states(
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
) -> None:
"""Describe group on off states."""
registry.exclude_domain(DOMAIN)

LockEntity supports open/opening state

· One min read

Recently we have added an open and opening state to LockEntity

This is useful if you have locks which can differentiate between unlocked (not locked but latched) state and open (unlocked and latch withdrawn) state.

LockEntity already supports the open method by implementing the feature flag LockEntityFeature.OPEN

Example (default implementation):

class MyLock(LockEntity):

@property
def is_opening(self) -> bool:
"""Return true if lock is open."""
return self._state == STATE_OPENING

@property
def is_open(self) -> bool:
"""Return true if lock is open."""
return self._state == STATE_OPEN

async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
self._state = STATE_OPEN
self.async_write_ha_state()

How we managed to speed up our CI to save 168+ days of execution time per month

· 2 min read

We got a taste for speed after UV gave us back 215 compute hours a month. Our CI workflow gets triggered on each commit, and due to the high volume of contributions, it was triggered 6647 times in March 2024. The full runs, where the whole test suite is being executed, take a long time.

It turned out that the plugin, we used to split the tests into 10 groups, was inefficient. Each pytest job needed to discover all tests, even when the job intended to only execute a subset of them.

Now we have a separate job to discover all tests and split them into 10 groups. The 10 pytest jobs only need to execute a subset of all tests. Not doing full-discovery in each test runner saves us 3 hours on each full run!

A short analysis of the 6647 CI workflows in March 2024 revealed the following stats:

  • 2406 were canceled before termination
    • 1771 should be full runs
  • 1085 failed
    • 732 where failed full runs
  • 3007 terminated successfully
    • 1629 partial runs (only tests for a given integration where executed)
    • 1378 full runs

Considering the 1378 successful full runs, we would have saved around 4042 hours ~ 168 days of execution time in March 2024 with #114381. Even more, if I had also analyzed the failed/canceled ones.

The more than 168 monthly saved execution days can be used by other jobs and make the CI experience for all developers and our community better. We improved our sustainability by using fewer resources to run our test suite.

A big thank you to GitHub for providing Home Assistant with additional CI runners.

Second phase of notify entity platform implementation

· One min read

Title option for send_message service notify entity platform

Recently we added the notify entity platform. The new notify platform method implements service send_message. This service now also accepts an optional title as an argument. This allows some new integrations that can be migrated now to use the new entity platform:

  • cisco_webex_teams
  • file
  • sendgrid
  • syslog
  • tibber

The architecture discussion is still ongoing.

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.

Improved typing for hass.data

· 2 min read

In the past, one of the challenges with hass.data was to correctly assign type information. Since it was typed as dict[str, Any], the only options were annotation assignments or cast like:

data: MyData = hass.data[SOME_KEY]

This had several disadvantages. Not only was it necessary to annotate every assignment, but type checkers also basically pretended that the annotation would always be correct. Especially during refactoring, it could easily happen that one instance was missed, and while type-checking still succeeded, the actual code would be broken.

To fix that, it's now possible to use two new key types HassKey and HassEntryKey. With a little bit of magic, type checkers are now able to infer the type and make sure it's correct. Even when storing data.

An example could look like this:

# <integration>/__init__.py
from homeassistant.util.hass_dict import HassKey

MY_KEY: HassKey["MyData"] = HassKey(DOMAIN)

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

async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
client = MyClient(...)

hass.data[MY_KEY] = MyData(client, {...})
hass.data[MY_KEY] = 1 # mypy error
# <integration>/switch.py
from . import MY_KEY

async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
data = hass.data[MY_KEY]
reveal_type(data) # MyData

async_add_entities([MySwitch(data.client)])

Storing data in a dict by entry.entry_id? It's often better to just store it inside the ConfigEntry directly. See the recent blog post about it. If that isn't an option, use HassEntryKey.

# <integration>/__init__.py
from homeassistant.util.hass_dict import HassEntryKey

MY_KEY: HassEntryKey["MyData"] = HassEntryKey(DOMAIN)

async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
) -> bool:
client = MyClient(...)

hass.data.setdefault(MY_KEY, {})[entry.entry_id] = MyData(client, {...})

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)])