Skip to main content

Frontend lazy context

· 3 min read

What is a context?

In the Home Assistant frontend, a Context is a way to share data across the component tree without explicitly passing it through every level as a property. Instead of threading the hass object down through multiple layers of components, you can provide specific pieces of data via context and consume them only where needed.

The key benefits of using context over passing the entire hass object are:

  • Easier usage: Components can directly consume the data they need without requiring parent components to pass it down. This reduces prop drilling and makes components more self-contained and reusable.
  • Reducing unnecessary component re-renders: When a component receives hass as a property, any change to hass triggers a re-render of that component and all its children—even if the component only cares about a small subset of the data. By using context to provide only the specific data a component needs, you ensure that components only re-render when their actual dependencies change, leading to better performance and a more responsive UI.

Introducing LazyContext

We've introduced a new LazyContext pattern that should replace the traditional subscription-based approach and the usage of the SubscribeMixin. Previously, components would subscribe to data sources and manage subscription lifecycles manually, often leading to boilerplate code and potential memory leaks if subscriptions weren't properly cleaned up.

LazyContext simplifies this by:

  • Lazy loading: Data is only fetched when a component actually consumes the context
  • Automatic cleanup: Subscriptions are managed automatically
  • Shared state: Multiple components consuming the same context share a single subscription
  • Optimized re-renders: Only components that consume the context re-render when data changes

This approach centralizes data-fetching logic and makes it easier to reason about when and how data flows through your application.

Examples

Defining a LazyContext

To define a lazy context, use LazyContextProvider and provide a fetch function:

new LazyContextProvider(this, {
context: labelsContext,
subscribeFn: (connection, setValue) => subscribeLabelRegistry(connection, setValue),
})

Consuming a context with lit

To consume a context in a component, use the @consume decorator:

@state()
@consume({ context: labelsContext, subscribe: true })
private _labels?: LabelRegistryEntry[];

Check out the updated custom card example to use it in vanilla JS: Custom card example.

Using @transform for derived Data

Only available within the home-assistant frontend codebase

The @transform decorator allows you to derive data from a context value, ensuring your component only re-renders when the transformed value actually changes.

@state()
@consume({ context: statesContext, subscribe: true })
@transform({
transformer: function (this: HuiButtonCard, entityStates: HassEntities) {
return this._config?.entity ? entityStates?.[this._config?.entity] : undefined;
},
watch: ["_config"],
})
private _stateObj?: HassEntity;

With @transform, even if the full states object updates, your component will only re-render if the transformed result (_stateObj) actually changes. The watch option allows you to specify additional properties that should trigger re-evaluation of the transformer function—in this case, when _config changes, the transformer runs again to extract the correct entity state.

Backup agents can now report upload progress

· One min read

The BackupAgent.async_upload_backup method now receives a new on_progress callback parameter. Backup agents can call this callback periodically during upload to report the number of bytes uploaded so far:

class ExampleBackupAgent(BackupAgent):

async def async_upload_backup(
self,
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
on_progress: OnProgressCallback,
**kwargs: Any,
) -> None:
"""Upload a backup."""
...
bytes_uploaded = 0
async for chunk in await open_stream():
await do_upload(chunk)
bytes_uploaded += len(chunk)
on_progress(bytes_uploaded=bytes_uploaded)
...

The backup manager uses these progress reports to fire UploadBackupEvent events, enabling the frontend to display real-time upload progress to the user.

Check the backup agent documentation for more details.

Custom integrations can now ship their own brand images

· 2 min read

Starting with Home Assistant 2026.3, custom integrations can include their own brand images (icons and logos) directly in the integration directory. No more submitting to a separate repository — just drop your images in a brand/ folder and they show up in the UI.

Local brand images for custom integrations

Add a brand/ directory to your custom integration with your icon and logo files:

custom_components/my_integration/
├── __init__.py
├── manifest.json
└── brand/
├── icon.png
└── logo.png

The following image filenames are supported:

  • icon.png / dark_icon.png
  • logo.png / dark_logo.png
  • icon@2x.png / dark_icon@2x.png
  • logo@2x.png / dark_logo@2x.png

Local brand images automatically take priority over images from the brands CDN. That's it — no extra configuration needed.

For more details, see the integration file structure documentation.

Brand images now served through a local API

To make local brand images possible, all brand images are now served through the Home Assistant local API instead of being fetched directly from the CDN by the browser.

A new brands system integration proxies brand images through two endpoints:

  • /api/brands/integration/{domain}/{image} — Integration icons and logos
  • /api/brands/hardware/{category}/{image} — Hardware images

Images are cached locally on disk and served with a stale-while-revalidate strategy, so they remain available during internet outages.

Impact on the frontend

The brandsUrl() and hardwareBrandsUrl() helpers in src/util/brands-url.ts now return local API paths instead of CDN URLs. If your custom card or panel uses these helpers, no changes are needed.

If you are constructing brand image URLs manually, update them:

// Old
const url = `https://brands.home-assistant.io/_/${domain}/icon.png`;

// New
import { brandsUrl } from "../util/brands-url";
const url = brandsUrl({ domain, type: "icon" });

These endpoints require authentication. The brandsUrl() helper handles this automatically by appending an access token. If you construct URLs manually, obtain a token via the brands/access_token WebSocket command and append it as a token query parameter.

Remove deprecated light features

· 3 min read

Summary of changes

In October 2022, Home Assistant migrated the preferred color temperature unit from mired to kelvin.

In February 2024, Home Assistant requested explicit supported_color_modes and color_mode properties (triggering deprecation of legacy fallback color mode support).

In December 2024, Home Assistant requested explicit Kelvin support (triggering deprecation of mired support).

It is now time to clean up the legacy code and remove the corresponding attributes, constants and properties:

  • Remove deprecated ATTR_COLOR_TEMP, ATTR_MIN_MIREDS, ATTR_MAX_MIREDS, ATTR_KELVIN, COLOR_MODE_***, and SUPPORT_*** constants
  • Remove deprecated state attributes ATTR_COLOR_TEMP, ATTR_MIN_MIREDS and ATTR_MAX_MIREDS
  • Remove deprecated support for ATTR_KELVIN and ATTR_COLOR_TEMP arguments from the light.turn_on service call
  • Remove deprecated support for LightEntity.color_temp, LightEntity.min_mireds and LightEntity.max_mireds properties from the entity
  • Remove deprecated support for LightEntity._attr_color_temp, LightEntity._attr_min_mireds and LightEntity._attr_max_mireds shorthand attributes from the entity

Additionally, failing to provide valid supported_color_modes and color_mode properties no longer works and will raise an error.

Examples

Custom minimum/maximum color temperature

class MyLight(LightEntity):
"""Representation of a light."""

# Old
# _attr_min_mireds = 200 # 5000K
# _attr_max_mireds = 400 # 2500K

# New
_attr_min_color_temp_kelvin = 2500 # 400 mireds
_attr_max_color_temp_kelvin = 5000 # 200 mireds

Default minimum/maximum color temperature

from homeassistant.components.light import DEFAULT_MAX_KELVIN, DEFAULT_MIN_KELVIN

class MyLight(LightEntity):
"""Representation of a light."""

# Old did not need to have _attr_min_mireds / _attr_max_mireds set
# New needs to set the default explicitly
_attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN
_attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN

Dynamic minimum/maximum color temperature

from homeassistant.util import color as color_util

class MyLight(LightEntity):
"""Representation of a light."""

# Old
# def min_mireds(self) -> int:
# """Return the coldest color_temp that this light supports."""
# return device.coldest_temperature
#
# def max_mireds(self) -> int:
# """Return the warmest color_temp that this light supports."""
# return device.warmest_temperature

# New
def min_color_temp_kelvin(self) -> int:
"""Return the warmest color_temp that this light supports."""
return color_util.color_temperature_mired_to_kelvin(device.warmest_temperature)

def max_color_temp_kelvin(self) -> int:
"""Return the coldest color_temp that this light supports."""
return color_util.color_temperature_mired_to_kelvin(device.coldest_temperature)

Checking color temperature in service call

from homeassistant.components.light import ATTR_COLOR_TEMP_KELVIN
from homeassistant.util import color as color_util

class MyLight(LightEntity):
"""Representation of a light."""
def turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
# Old
# if ATTR_COLOR_TEMP in kwargs:
# color_temp_mired = kwargs[ATTR_COLOR_TEMP]
# color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(color_temp_mired)

# Old
# if ATTR_KELVIN in kwargs:
# color_temp_kelvin = kwargs[ATTR_KELVIN]
# color_temp_mired = color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)

# New
if ATTR_COLOR_TEMP_KELVIN in kwargs:
color_temp_kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN]
color_temp_mired = color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)

Background information

Changes in OAuth 2.0 helper error handling

· 2 min read

Summary of changes

Starting as of 2026.3, we're enhancing how the OAuth 2.0 helper handles token request and refresh token failures. This change makes error handling more robust, decoupled from the aiohttp library and helps integrations, that utilize the Data Update Coordinator, to automatically trigger the right error handling.

What changes

When an OAuth 2.0 token request or token refresh failed, Home Assistant would allow the underlying aiohttp.ClientResponseError to propagate directly to the integration. This behavior is being changed and enhanced.

We're introducing three new exceptions that provide clearer semantics:

  • OAuth2TokenRequestTransientError - Recoverable errors, that can be retried.
  • OAuth2TokenRequestReauthError - Non-recoverable errors, that require a reauthentication.
  • OAuth2TokenRequestError - Base exception for when the above two criteria aren't met or to enable the integration to catch all token request exceptions.

Data Update Coordinator

Most integrations that use the OAuth 2.0 helper, also use the Data Update Coordinator. When a token request or refresh token fails, the exceptions will bubble up in the Data Update Coordinator and now triggers the following error handling:

For unrecoverable errors (400+, except 429 (rate limit)):

  • OAuth2TokenRequestReauthError: Data Update Coordinator raises ConfigEntryAuthFailed if exceptions should be raised or starts a reauthentication flow.

For transient errors (500+ and 429):

  • OAuth2TokenRequestTransientError: Data Update Coordinator treats it as an UpdateFailed and the retry mechanism will be triggered.

This means that integrations that use the OAuth 2.0 helper in combination with the Data Update Coordinator don’t need to do any special handling of the new exceptions.

Migration

Integrations that today use the OAuth 2.0 helper and handle aiohttp.ClientResponseError explicitly should adjust their error handling to deal with the new exceptions. To ease this transition, we have added a compatibility layer by having the new OAuth exceptions inherit from aiohttp.ClientResponseError. Existing code that catches this exception type should continue to work. It is however encouraged to refactor the code to use the new exceptions. See the code example for details.

Code example of migration

Update the exception handling and then continue to work out if it's a (non-)recoverable error in the integration. For example:

try:
await auth.async_get_access_token()
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
except (OAuth2TokenRequestError, ClientError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="auth_server_error"
) from err

Reconfiguration support for webhook helper

· One min read

Integrations that use the webhook config flow helper (homeassistant.helpers.config_entry_flow.register_webhook_flow) now support reconfiguration. This allows the integration to retrieve the webhook again, or obtain an updated webhook when the Home Assistant instance URL changes.

Custom integrations using the webhook config flow helper must add translation strings for the reconfiguration flow.

Example translation strings for a reconfiguration flow:

{
"config": {
"abort": {
"reconfigure_successful": "**Reconfiguration was successful**\n\nIn Sleep as Android go to *Settings → Services → Automation → Webhooks* and update the webhook with the following URL:\n\n`{webhook_url}`"
},
"step": {
"reconfigure": {
"description": "Are you sure you want to re-configure the Sleep as Android integration?",
"title": "Reconfigure Sleep as Android"
}
}
}
}

For more details, see core PR #151729.

async_listen in Labs is deprecated

· One min read

The async_listen helper in the labs integration has been deprecated in favor of async_subscribe_preview_feature.

The new async_subscribe_preview_feature function provides a more consistent API, where the listener callback receives an EventLabsUpdatedData parameter containing the updated feature state. This eliminates the need to separately call async_is_preview_feature_enabled inside the listener to check the current value.

Old usage

from homeassistant.components.labs import async_is_preview_feature_enabled, async_listen

def my_listener() -> None:
if async_is_preview_feature_enabled(hass, DOMAIN, "my_feature"):
# feature enabled
...

async_listen(
hass,
domain=DOMAIN,
preview_feature="my_feature",
listener=my_listener,
)

New usage

from homeassistant.components.labs import EventLabsUpdatedData, async_subscribe_preview_feature

async def my_listener(event_data: EventLabsUpdatedData) -> None:
if event_data["enabled"]:
# feature enabled
...

async_subscribe_preview_feature(
hass,
domain=DOMAIN,
preview_feature="my_feature",
listener=my_listener,
)

Note that the new listener is a coroutine function and receives EventLabsUpdatedData as a parameter.

async_listen will be removed in Home Assistant 2027.3.

For more details, see core PR #162648.

Replacing pre-commit with prek

· One min read

By replacing pre-commit with prek we can increase the performance of our checks. Prek uses the same .pre-commit-config.yaml as pre-commit and is a complete replacement. Due to the fact that prek is written in Rust and allows the execution of different jobs in parallel, we can check our code even faster.

New development environments will automatically install prek and for existing ones please just update the test requirements by running uv pip install requirements_test.txt

Solving pyserial-asyncio blocking the event loop

· One min read

Summary of changes

Starting in 2026.7, installation of pyserial-asyncio will be blocked in Home Assistant.

Library maintainers and custom integrations are advised to migrate to pyserial-asyncio-fast.

Background

pyserial-asyncio blocks the event loop because it does a blocking sleep. The library is also not maintained so efforts to improve the situation haven't been released.

pyserial-asyncio-fast was created as a drop-in replacement (see the repository), and all core integrations have now been migrated.

Migration

pyserial-asyncio-fast was designed as a drop-in replacement of pyserial-asyncio, and the necessary changes are trivial.

Requirements

# Old
install_requires=[
"pyserial-asyncio"
]

# New
install_requires=[
"pyserial-asyncio-fast"
]

Usage

# Old
import serial_asyncio

async def connect():
conn = await serial_asyncio.open_serial_connection(**self.serial_settings)

# New
import serial_asyncio_fast

async def connect():
conn = await serial_asyncio_fast.open_serial_connection(**self.serial_settings)

More examples are available in the tracking pull request.