Skip to main content

Migrating app builds to Docker BuildKit

· 4 min read

The legacy home-assistant/builder container and the old home-assistant/builder GitHub Action have been retired. We recommend migrating all GitHub workflows and Dockerfiles for apps (formerly add-ons) as described in this post.

What changed and why

The old builder ran every architecture build inside a single privileged Docker-in-Docker container using QEMU emulation. This was slow, required elevated privileges, and those who were already familiar with Docker needed to learn how to use the custom Home Assistant's builder container. The old builder also had unnecessary maintenance overhead. Today, what the builder does can be fully replaced with Docker BuildKit, which is natively supported on GitHub Actions runners and has built-in multi-arch support with QEMU emulation if needed.

For your CI, the replacement is a set of focused composite GitHub Actions that delegate building to the runner's native Docker with Docker BuildKit. Outside the CI, the migration means that your Dockerfile is now the single source of truth for building your app image, and you can use docker build directly to build and test your app locally without needing to use the builder container.

Migration process

The migration has two parts: updating your Dockerfiles and updating your GitHub Actions workflows.

Update Dockerfiles

The new build workflow doesn't use build.yaml anymore. Move the content into your Dockerfile as follows:

  • build_from - replace the build_from key in build.yaml with a FROM statement in your Dockerfile:

    FROM ghcr.io/home-assistant/base:latest

    As the base images are now published as multi-platform manifests, there is usually no need to define per-arch base images anymore. The build-image action still supplies BUILD_ARCH as a build argument though, so you can use that in your Dockerfile if you need to use it in the template for the base image name.

  • labels - move any custom Docker labels directly into your Dockerfile with a LABEL statement:

    LABEL \
    org.opencontainers.image.title="Your awesome app" \
    org.opencontainers.image.description="Description of your app." \
    org.opencontainers.image.source="https://github.com/your/repo" \
    org.opencontainers.image.licenses="Apache License 2.0"

    If you are creating a custom workflow, note that the legacy builder used to add the io.hass.type, io.hass.name, io.hass.description, and io.hass.url labels automatically. The new actions do not infer these values, so add them explicitly via the labels input of the build-image (or similar) action.

  • args - move custom build arguments into your Dockerfile as ARG definitions with default values:

    ARG MY_BUILD_ARG="default-value"

    Default values in ARG replace what was previously supplied via build.yaml's args dictionary. They can still be overridden at build time with --build-arg if needed.

With the content of build.yaml migrated, you can delete the file from your repository.

Update GitHub Actions workflows

Remove any workflow steps using home-assistant/builder@master and replace them with the new composite actions. See the example workflow in our example app repository for a complete working example. Alternatively, use the individual actions in a more custom workflow as needed.

Image naming

The preferred way to reference a published app image is now the generic (multi-arch) name without an architecture prefix:

# config.yaml
image: "ghcr.io/my-org/my-app"

The {arch} placeholder (e.g. ghcr.io/my-org/{arch}-my-app) is still supported as a compatibility fallback, but it's encouraged to use the generic name and let the manifest handle the platform resolution.

Local builds

After updating your Dockerfile, you can use docker build to build the app image directly - you can refer to Local app testing for more details.

Apps built locally by Supervisor

For backward compatibility, Supervisor still reads build.yaml file if it's present and populates the image build arguments with values read from this file. This will produce warnings and eventually be removed in the future, so it's recommended to migrate to the new Dockerfile-based approach as described above.

New infrared entity platform for IR device integrations

· 2 min read

Home Assistant now has an infrared entity platform that decouples IR emitter hardware from the devices they control. Instead of each device integration talking directly to specific IR hardware, emitter integrations (like esphome) expose InfraredEntity instances, and device integrations (like lg_infrared) send commands through them via helper functions.

See the architecture discussion for the full background.

Frontend component updates 2026.4

· One min read
info

We do not officially support or encourage custom card developers to use our built in components. This component APIs can always change and you should build your card as independent component.

ha-input

We keep migrating our Material Design based components to Web Awesome based. This time we migrated the input components, which leads to an API change but the look and feel stays the same for now.

  • ha-input is the successor of ha-textfield
    • ha-textfield API stays but the component is migrated to use ha-input internally and will be removed in 2026.5
    • Also replaces ha-outlined-text-field
  • ha-input-search replaces search-input and search-input-outlined
  • ha-input-multi replaces ha-multi-textfield
  • ha-input-copy replaces copy-textfield

This component also introduces new semantic theme variables for form backgrounds:

--ha-color-form-background: var(--ha-color-neutral-95);
--ha-color-form-background-hover: var(--ha-color-neutral-90);
--ha-color-form-background-disabled: var(--ha-color-neutral-80);

Date picker

We finally removed the Vue 2 dependency by replacing the date and date range picker with Cally.

Frontend new way of dialogs

· One min read

The Problem

Each dialog managed by the dialog manager was only opened once and stayed in the DOM for the lifetime of the application. This causes:

  • More memory usage: Dialogs accumulate in the DOM even when not visible
  • More bugs because of missing state reset: Dialog state persists between opens, leading to stale data or unexpected behavior

The Solution: DialogMixin

We implemented a new way of handling dialogs using DialogMixin. With this approach:

  • Dialogs are created when opened and destroyed when closed: No need to manually reset the state of the dialog when it is closed
  • Closed events are automatically handled: The dialog mixin takes care of cleanup
  • Subscribe mixin can now be used in dialogs: Since dialogs are properly destroyed, subscriptions are cleaned up automatically
  • Use normal Lit lifecycle methods: Use connectedCallback to initialize when the dialog is opened instead of relying on the showDialog method

Example

Check out ha-dialog-date-picker for a reference implementation. DialogMixin adds dialog params to this.params if available.

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.