Skip to main content

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

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

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

· 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()

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

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

· 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, {...})

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

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

· 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()