Compare commits

...
Author SHA1 Message Date
Aidan Timson 382fca108b Run the actions lockfile check on every pull request 2026-10-29 12:25:51 +00:00
Aidan Timson 1c50ae4f0b Lock workflow actions with an actions.lock file 2026-10-29 12:07:09 +00:00
Aidan Timson b79c6350ec Add date format presets to the clock and date cards (#55434) 2026-10-29 11:15:42 +00:00
Paul Bottein 8148041ddc Show multi-select chips in a field with an inline add button (#55416) 2026-10-29 09:59:54 +00:00
WendelinandClaude Opus 5.5 4265624c0e Wrap long text in debug tool and network discovery rows (#55438)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-29 09:40:29 +00:00
Paul Bottein ed4179de8f Use the state color by default in alert cards and badges (#55436) 2026-10-29 09:29:06 +00:00
WendelinandClaude Opus 5.5 e407a1b2dd Migrate the remaining selection lists to ha-list-selectable (#55437)
* Migrate the remaining selection lists from ha-list to ha-list-selectable

Moves the media management dialog, the calendar panel pane, the expose
entities dialog, the user visibility condition and the select view dialog
from the mwc-based ha-list, ha-check-list-item and ha-radio-list-item to
ha-list-selectable and ha-list-item-option. The read-only ZHA endpoint
list now uses ha-list-base, as its rows already were ha-list-item-base.

- Use ha-list-selectable-virtualized for the expose entities dialog,
  replacing the custom click and keyboard handling around lit-virtualizer
- Render the calendar pane as its own list instead of reusing the
  dropdown items, sharing the selection logic with the dropdown
- Make the select view dialog a single-select list and keep the icon
  column aligned for views without an icon
- Drop the media management workaround for spurious mwc select events
- Render the create calendar row as ha-list-item-button
- Remove unused element imports in the touched files

After this, ha-check-list-item is only used by the todo list card and
ha-radio-list-item is unused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ln7qMagr2YDdssKkaBiqf5

* Refactor ha-list components for improved focusability handling and update list items logic

* Enhance focus handling in ha-list-base and update tests for disabled items

* use helper

---------

Co-authored-by: Claude <[email protected]>
2026-10-29 11:16:09 +02:00
Aidan Timson e851101a93 Fix the Marketplace search field overlapping the top bar icons on narrow screens (#55439)
Let the narrow data table search field shrink to fit
2026-10-29 11:13:19 +02:00
Tristan Cartledge d43e1ef4cc Fix duplicated hidden areas when reordering areas in the areas dashboard editor (#55424) 2026-10-29 09:37:33 +01:00
Petar Petrov f699019994 Ignore YAML dashboard update event on first load (#55430) 2026-10-29 09:28:05 +01:00
Daan Vervacke fe90b0339d Show blueprint usage count as text on narrow screens (#55427)
* Show blueprint usage count as text on narrow screens

* Show the blueprint usage count before the path on narrow screens

* Name the blueprint usage column and skip it for broken blueprints
2026-10-29 08:34:42 +02:00
renovate[bot] 3b5ba802f6 Update dependency @rspack/dev-server to v2.2.2 (#55429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 08:30:43 +02:00
JonathanandXyolyp b0c6e7f564 Preselect the only allowed unit also in an optional unit selector (#55417)
Co-authored-by: Xyolyp <[email protected]>
2026-10-29 08:17:31 +02:00
renovate[bot] 658a23724e Update tsparticles to v4.5.0 (#55428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 08:00:52 +02:00
Maarten Lakerveld f859183628 Name maintenance problem tiles after the device and the entity (#55420)
Problem binary sensors were added to the maintenance view using the
battery tile builder, which names every tile after its device. A device
with several problem sensors (overheating, overvoltage, ...) then showed
identical tiles. Problem tiles now use a device plus entity name; the
entity part is dropped automatically when the sensor has no name of its
own, so single-sensor devices still read as just the device name.
Battery tiles keep the device-only name.

Fixes home-assistant/frontend#55413
2026-10-28 20:42:19 +01:00
Paul Bottein 53dc14f413 Keep the section row divider under the entities card header (#55415) 2026-10-28 19:42:41 +01:00
Aidan Timson f2523095ff Remember logs page search, integrations and raw view (#55399)
* Remember logs page search, integrations and raw view

* Keep stored logs choices from links and loading apps

* Keep logs choices when closing dialogs and while apps load

* Restore stored logs choices when linked to the page again
2026-10-28 16:03:00 +00:00
ef5c04b67f Show stop button in the media player more-info dialog (#54794)
* Don't hide stop action on playing media players

* Show stop as a secondary button when play/pause exists

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Add media player entity for "Playing The Music" and update media control card

* Implement logic to remove secondary stop control when play or pause is available in media player

* Test removing the secondary stop control

Runs real computeMediaControls output through removeSecondaryStopControl,
like its callers do, to protect when stop is removed and when it has to
stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-10-28 18:01:26 +02:00
WendelinandClaude Opus 5.5 10ed6f709e Migrate the data table filters to ha-list-selectable (#55412)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-28 16:55:17 +01:00
Bram KragtenandClaude Opus 5.5 c22de3bc7c Keep the device and area page headers fixed while scrolling (#55409)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-28 16:08:12 +01:00
Bram Kragten 37f67a2f45 Bumped version to 20261028.0 2026-10-28 16:04:03 +01:00
Petar Petrov 06df9e3801 Tolerate missing rows and invalid visibility entries (#55408) 2026-10-28 14:58:29 +00:00
renovate[bot] 7f45b38d6f Update dependency home-assistant-js-websocket to v9.8.0 (#55389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-28 14:42:54 +00:00
renovate[bot] 5c0f9b6501 Update dependency maplibre-gl to v6.16.0 (#55406) 2026-10-28 14:40:21 +00:00
Wendelin 645811dfdf Show entity pictures in the add automation element target tree (#55407) 2026-10-28 15:28:25 +01:00
Paul Bottein 45ec2e805a Rename the MCP card URL labels (#55401) 2026-10-28 15:10:56 +01:00
Paul BotteinandBram Kragten 18c93baa70 Add device to dashboard from the device page (#55381)
Co-authored-by: Bram Kragten <[email protected]>
2026-10-28 14:06:15 +00:00
Petar Petrov 2518c6ea77 Keep fitted neighbors clear of the network graph controls (#55405) 2026-10-28 14:48:56 +01:00
Bram Kragten 702381515a Merge branch 'rc' into dev 2026-10-28 14:43:44 +01:00
Przemysław Szypowicz 93efd84fab Load custom modules after the scoped registry polyfill (#54444) 2026-10-28 14:29:23 +01:00
Pradeep Peddineniandpradeeppeddineni be3841ff0d Fit gauge card to the card height in sections view (#55260)
* Fit gauge card to the card height in sections view

* Address review: flex container for gauge, min_rows 2, drop DOM tests

- Wrap ha-gauge in a container that takes the space left after the name
  (flex: 1, min-height: 0, container-type: size) and size the gauge with
  min(250px, 100cqw, 100cqh * 100 / 55) instead of the calc
- Use typeof rows === "number" like the other cards
- Set min_rows: 2 in getGridOptions, one row leaves almost no room
- Remove the DOM tests

---------

Co-authored-by: pradeeppeddineni <[email protected]>
2026-10-28 14:23:28 +01:00
Paul Bottein bfd6ff56a5 Show the helper under the label in checkbox lists (#55400) 2026-10-28 13:21:23 +00:00
Petar Petrov 41ce82ca01 Keep network graph labels when zooming during their fade in (#55397)
With physics off, graph labels fade in after the physics toggle or a
chart rebuild. A roam updates label layout instantly, and ECharts then
stops every label animation, which froze the fade at its current
opacity. Finish the fade before such an instant update.
2026-10-28 14:14:15 +01:00
Petar Petrov f8826fc821 Highlight the device in the ZHA network visualization (#55394) 2026-10-28 14:11:25 +01:00
Petar Petrov c0a98dba54 Keep entity selector data while it refreshes after a reconnect (#55396) 2026-10-28 13:00:50 +00:00
Erwin Douna 839a910a19 Start logs page filters collapsed (#55398) 2026-10-28 12:54:26 +00:00
Petar Petrov 345745ef47 Call the energy power sensor derived, not a helper (#55395) 2026-10-28 12:29:55 +00:00
João Pedro HickmannandPetar Petrov c4c0ae0dde Animate energy distribution flow dots on the compositor (#54872)
Co-authored-by: Petar Petrov <[email protected]>
2026-10-28 13:05:56 +01:00
6297d3e972 Add Zigbee Green Power support to ZHA (#55322)
* Add ZHADeviceType enum for ZHA device types

* Translate ZHA device types

* Support Green Power devices in ZHA panels

* Reword joining text to reflect device-driven joining, ZHA doesn't search

* Use translations for `Source ID`

* Update src/panels/config/integrations/integration-panels/zha/functions.ts

Co-authored-by: Petar Petrov <[email protected]>

---------

Co-authored-by: Simon Lamon <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
2026-10-28 14:04:22 +02:00
Petar Petrov 8985b7d08d Offer gas and water sensors from flow rates (#55360)
* Offer to create an energy sensor for power-only energy devices

An individual device needs an energy sensor, so a device with only a
power sensor could not be saved. Saving now asks to create an Integral
helper from the power sensor, or to reuse one that already integrates
it, and uses it as the device's energy sensor.

* Offer energy sensors from power for solar and home sources

Move the Integral helper flow out of the device dialog into a shared
confirmIntegrals helper. The confirmation now creates the helpers itself
and shows its progress. Solar production and the home meter can be saved
with only a power sensor, like individual devices.

* Offer gas and water sensors from flow rates

Gas and water sources and individual water devices can now be saved with
only a flow rate sensor. Saving offers to create an Integral helper with the
gas or water device class, or to reuse one that already integrates the rate.

* Offer energy sensors from power for grid and battery sources

A grid connection or battery with two power sensors can now create Integral
helpers for its missing energy sensors on save, or reuse existing ones. A
battery needs both, while a grid connection can skip them and save as before.

* Keep a device's children out of its upstream choices without an energy sensor

Clearing the energy sensor while editing a device dropped its children from the
check, so a child could be picked as the upstream device. Saving then reuses
the original Integral helper, which made the hierarchy circular.

* Save the submitted device when its dialog closes during helper creation

Snapshot the draft before creating the Integral helper, and only update the
dialog while the same dialog session is open. Always pass the energy device
class, as the helper only infers it from a power device class.

* Save the submitted water device when its dialog closes during helper creation

Mirror the electricity device dialog: snapshot the draft before creating the
Integral helper, and only update the dialog while the same session is open.

* Price gas in the unit of a reused helper

A reused Integral helper can display another unit than a new one would, so
take the price unit from that helper while only a flow rate is set.

* Keep grid and battery helpers apart and wait for all of them

Neither direction of a source may reuse the statistic picked for the other.
When one of several helpers fails, wait for the others to finish, so a retry
reuses them instead of creating duplicates.

* Lock the device form while it saves

The helper is created after the confirmation closes, so edits made meanwhile
would be replaced by the submitted device.

* Lock the water device form while it saves

Mirror the electricity device dialog, so edits can't be replaced by the
submitted device while its helper is created.

* Save the submitted solar and home sources

Like the device dialogs: snapshot the source before asking for the helper,
lock the inputs while saving, and only touch the dialog while the same
session is open.

* Save the submitted grid and battery sources

Like the other energy dialogs: snapshot the source and power settings before
asking for helpers, lock the energy inputs while saving, and only touch the
dialog while the same session is open.

* Save the submitted gas and water sources

Like the other energy dialogs: snapshot the source and cost choice before
asking for the helper, lock the inputs while saving, and only touch the dialog
while the same session is open.

* Keep a device's name and upstream device when its energy sensor changes

A device can now be named and given an upstream device before it has an energy
sensor, so picking or clearing the sensor must not drop them. The upstream
device is only dropped when the new sensor rules it out.

* Keep a water device's name and upstream device when its sensor changes

Mirror the electricity device dialog: picking or clearing the water sensor
keeps the name, and only drops the upstream device when the sensor rules it out.

* Lock the power settings while a grid or battery saves

Changing a power sensor during the save could pair the new sensor with helpers
for the old one after a failed save. Only the new energy sensors are written
back to the form, so other edits are kept.

* Keep solar and home edits when saving fails

Only write the new energy sensor back to the form, so changes made while the
source was saving survive a failed save.

* Keep gas and water edits when saving fails

Only write the new sensor back to the form, so changes made while the source
was saving survive a failed save.

* Lock the grid pricing controls while saving

Pricing edits made while the grid saved were lost, as the save uses the
submitted source.

* Leave gas and water prices in the form when saving fails

The no-cost reset only applies to the saved source, so a failed save no longer
clears the price text while keeping the hidden price.

* Reset the device dialog's saving state per session

A save that finishes after the dialog closed or reopened no longer touches the
new session's saving state.

* Reset the solar and home saving state per session

A save that finishes after the dialog closed or reopened no longer touches the
new session's saving state.

* Ask grid connections to create or cancel, without skipping

The grid confirmation now offers Cancel and Create like the other sources, so
it always has a way back to the editor. Grid and battery also reset their saving
state per session.

* Reset the gas and water saving state per session

A save that finishes after the dialog closed or reopened no longer touches the
new session's saving state.

* Lock the gas and water pricing controls while saving

Pricing edits made while the source saved were lost, as the save uses the
submitted source.

* Lock the solar forecast controls while saving

Forecast changes made while the source saved were lost, as the save uses the
submitted source.

* Lock the battery state of charge and capacity while saving

Like the other inputs, so edits made while the battery saves aren't lost.

* Drop grid export compensation without an export meter

Compensation set while an export was pending could stay hidden in the source
after the power sensors changed, and come back once an export meter is added.

* Explain why a flow rate can't provide the gas or water sensor

When the flow rate can't become a meter, its picker now says so instead of
leaving Save disabled without a reason. The flow rate help text says
"Optionally" again, since it stays optional next to a meter.

* Find existing helpers in the live states

The home dialog passed the hass from when it opened, so a retry after a
failure didn't see a helper created by the first try and made a second one.
The helper now takes the live states and entities from each dialog. The
counting note also fits solar production now.

* Pass the live states to the grid and battery helpers

* Update Save when the power sensor's unit changes
2026-10-28 13:57:36 +02:00
WendelinandClaude Opus 5.5 9f6de3166f Show entity pictures in the target picker (#55387)
Show entity pictures in the target picker rows

Target picker rows for entities with an entity picture now show it, e.g.
the photo of a person, like the automation targets do since the entity
pictures fix.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-28 11:57:30 +00:00
WendelinandClaude Opus 5.5 64c6f2a133 Remove unused ha-list imports and migrate gallery off ha-list (#55393)
* Remove unused ha-list imports and migrate gallery off ha-list

Prepares the removal of the mwc-based ha-list components:

- Drop unused ha-list / ha-list-item element imports
- Drop leftover @material/mwc-list imports, typing the click handlers
  in ha-domain-integrations as MouseEvent
- Replace the ha-list wrapper in the date and time gallery pages with
  a plain div, as it only wrapped layout divs
- Update the ha-control-select-menu gallery page to pass options via
  the options property, as the component no longer renders slotted
  ha-list-item children, and keep the selected value in state

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ln7qMagr2YDdssKkaBiqf5

* Add HASSDomCurrentTargetEvent type to select menu and integration handlers

---------

Co-authored-by: Claude <[email protected]>
2026-10-28 11:52:22 +00:00
Petar Petrov d1c1d9a05d Offer energy sensors from power for grid and battery sources (#55359)
* Offer energy sensors from power for grid and battery sources

A grid connection or battery with two power sensors can now create Integral
helpers for its missing energy sensors on save, or reuse existing ones. A
battery needs both, while a grid connection can skip them and save as before.

* Keep grid and battery helpers apart and wait for all of them

Neither direction of a source may reuse the statistic picked for the other.
When one of several helpers fails, wait for the others to finish, so a retry
reuses them instead of creating duplicates.

* Save the submitted grid and battery sources

Like the other energy dialogs: snapshot the source and power settings before
asking for helpers, lock the energy inputs while saving, and only touch the
dialog while the same session is open.

* Lock the power settings while a grid or battery saves

Changing a power sensor during the save could pair the new sensor with helpers
for the old one after a failed save. Only the new energy sensors are written
back to the form, so other edits are kept.

* Lock the grid pricing controls while saving

Pricing edits made while the grid saved were lost, as the save uses the
submitted source.

* Ask grid connections to create or cancel, without skipping

The grid confirmation now offers Cancel and Create like the other sources, so
it always has a way back to the editor. Grid and battery also reset their saving
state per session.

* Lock the battery state of charge and capacity while saving

Like the other inputs, so edits made while the battery saves aren't lost.

* Drop grid export compensation without an export meter

Compensation set while an export was pending could stay hidden in the source
after the power sensors changed, and come back once an export meter is added.
2026-10-28 12:44:05 +01:00
Petar Petrov ef531bcb84 Offer energy sensors from power for solar and home sources (#55358)
* Offer to create an energy sensor for power-only energy devices

An individual device needs an energy sensor, so a device with only a
power sensor could not be saved. Saving now asks to create an Integral
helper from the power sensor, or to reuse one that already integrates
it, and uses it as the device's energy sensor.

* Offer energy sensors from power for solar and home sources

Move the Integral helper flow out of the device dialog into a shared
confirmIntegrals helper. The confirmation now creates the helpers itself
and shows its progress. Solar production and the home meter can be saved
with only a power sensor, like individual devices.

* Keep a device's children out of its upstream choices without an energy sensor

Clearing the energy sensor while editing a device dropped its children from the
check, so a child could be picked as the upstream device. Saving then reuses
the original Integral helper, which made the hierarchy circular.

* Save the submitted device when its dialog closes during helper creation

Snapshot the draft before creating the Integral helper, and only update the
dialog while the same dialog session is open. Always pass the energy device
class, as the helper only infers it from a power device class.

* Lock the device form while it saves

The helper is created after the confirmation closes, so edits made meanwhile
would be replaced by the submitted device.

* Save the submitted solar and home sources

Like the device dialogs: snapshot the source before asking for the helper,
lock the inputs while saving, and only touch the dialog while the same
session is open.

* Keep a device's name and upstream device when its energy sensor changes

A device can now be named and given an upstream device before it has an energy
sensor, so picking or clearing the sensor must not drop them. The upstream
device is only dropped when the new sensor rules it out.

* Keep solar and home edits when saving fails

Only write the new energy sensor back to the form, so changes made while the
source was saving survive a failed save.

* Reset the device dialog's saving state per session

A save that finishes after the dialog closed or reopened no longer touches the
new session's saving state.

* Reset the solar and home saving state per session

A save that finishes after the dialog closed or reopened no longer touches the
new session's saving state.

* Lock the solar forecast controls while saving

Forecast changes made while the source saved were lost, as the save uses the
submitted source.

* Find existing helpers in the live states

The home dialog passed the hass from when it opened, so a retry after a
failure didn't see a helper created by the first try and made a second one.
The helper now takes the live states and entities from each dialog. The
counting note also fits solar production now.

* Update Save when the power sensor's unit changes
2026-10-28 12:44:04 +01:00
Petar Petrov 14252d09fb Drop hass from the device pickers (#55384)
* Drop hass from the device pickers

getDevices takes an options object and fetchDeviceCompositeSplits takes connection and callWS.

* Refresh the device picker value when the brands token arrives

Key the memoized value renderer on the brands access token so the selected device's integration icon gets its URL.

* Fetch config entries once in the device selector

Restores the one-time fetch with an empty-list fallback so a failed request no longer hides the selector.
2026-10-28 12:37:46 +01:00
Petar Petrov 12e32ce0ce Drop hass from the target picker item rows (#55383)
filterTargetEntries takes states and the registries it reads as separate values.
2026-10-28 12:36:54 +01:00
Petar Petrov 5c88dc99c2 Drop hass from the entity selector (#55380)
* Drop hass from the entity selector

fetchEntitySourcesWithCache takes callWS and states and caches per connection in module scope.

* Refetch entity selector data on reconnect

Fetch config entries once per connection again and keep overlapping cache callers from overwriting newer results.

* Refetch entity selector data when the connection comes back

Reset entity sources and config entries when the websocket reconnects, and drop an unreachable cache guard.
2026-10-28 12:36:15 +01:00
Petar Petrov 47c3b2f57c Drop hass from the media browser and its dialogs (#55385)
* Drop hass from the media browser and its dialogs

The media browse helpers now take callWS, and the browser, its manual entry form and the browse and manage dialogs read contexts instead.

* Refresh manual media entry labels when the language changes

Key the ha-form label and helper callbacks on localize so they update without a hass change.
2026-10-28 11:35:49 +00:00
Petar Petrov d1c2915c4e Drop hass from the category picker (#55378)
* Drop hass from the category picker

Consume the connection, API and internationalization contexts in ha-category-picker and drop SubscribeMixin.

* Keep categories when the scope returns to the subscribed one

Clear the list only when the new scope differs from the active subscription's scope.
2026-10-28 12:35:07 +01:00
Petar Petrov 7f1aedcb95 Speed up the log viewer tests with a smaller line limit (#55392)
The retention and search tests filled the viewer with 5000 lines,
which takes over a second per test in jsdom and pushed the search
tests past the 5 s timeout on loaded CI runners. Run them against a
50-line limit instead and drop the raised timeouts.
2026-10-28 12:33:25 +01:00
Petar Petrov db63231a5d Drop hass from the area controls picker (#55379)
* Drop hass from the area controls picker

getAreaControlEntities takes states and registries as separate values.

* Keep states out of the area controls editor memo key

Restores the editor's previous caching so state changes do not rescan the area's entities.
2026-10-28 12:32:50 +01:00
Petar Petrov 0aabfa89bc Drop hass from the history charts (#55386)
* Drop hass from the history charts

The history line and timeline charts and state-history-charts consume contexts, and generateStateHistoryChartLineData takes localize and states.

* Rebuild line chart options only on chart updates

Entity state changes are compared when the chart updates, as before, instead of triggering their own update.
2026-10-28 11:32:22 +00:00
Simon Lamon 35601830a8 Rename to child devices (#55391)
Child devices
2026-10-28 12:31:03 +01:00
Paul Bottein fb882dacae Add convert to manual section action (#55377)
* Add convert to manual section action

* Keep the section when its strategy fails to generate during conversion

* Drop strategy disabled state when converting a section

* Skip the conversion save when the section changed meanwhile

* Always drop disabled when converting a section

* Add Oxford comma to convert to manual text
2026-10-28 12:10:00 +01:00
Petar Petrov aaad3f0d3d Offer to create an energy sensor for power-only energy devices (#55353)
* Offer to create an energy sensor for power-only energy devices

An individual device needs an energy sensor, so a device with only a
power sensor could not be saved. Saving now asks to create an Integral
helper from the power sensor, or to reuse one that already integrates
it, and uses it as the device's energy sensor.

* Keep a device's children out of its upstream choices without an energy sensor

Clearing the energy sensor while editing a device dropped its children from the
check, so a child could be picked as the upstream device. Saving then reuses
the original Integral helper, which made the hierarchy circular.

* Save the submitted device when its dialog closes during helper creation

Snapshot the draft before creating the Integral helper, and only update the
dialog while the same dialog session is open. Always pass the energy device
class, as the helper only infers it from a power device class.

* Lock the device form while it saves

The helper is created after the confirmation closes, so edits made meanwhile
would be replaced by the submitted device.

* Keep a device's name and upstream device when its energy sensor changes

A device can now be named and given an upstream device before it has an energy
sensor, so picking or clearing the sensor must not drop them. The upstream
device is only dropped when the new sensor rules it out.

* Reset the device dialog's saving state per session

A save that finishes after the dialog closed or reopened no longer touches the
new session's saving state.
2026-10-28 11:31:26 +01:00
Maarten Lakerveld ce2a9577e5 Refresh the todo card at browser midnight and test the subscription controller (#55356) 2026-10-28 10:48:37 +01:00
Petar Petrov 5bafeb875c Remove unused dynamic-to-generic group move from the automation picker (#55376) 2026-10-28 10:35:15 +01:00
Maarten Lakerveld a24881a54f Fetch the index fresh on navigation and harden the service worker update flow (#54790) 2026-10-28 10:27:49 +01:00
Petar Petrov 4decd87fd6 Drop hass from the navigation picker (#55373)
* Drop hass from the navigation picker

computeNavigationPathInfo and subscribeNavigationPathInfo take an options object of separate values.

* Rerender the navigation picker when related items change

The related section is read in render, and hass updates no longer rerender the picker.
2026-10-28 09:08:52 +00:00
Petar Petrov 7fd2a112bf Drop hass from the TTS media browser (#55370)
fetchCloudStatus now takes callWS, and ha-browse-media-tts reads localize, config and the API from contexts.
2026-10-28 09:08:44 +00:00
Petar Petrov fc22a2af50 Drop hass from the floor selector (#55369)
Read registries and the config entries API from contexts instead of hass.
2026-10-28 09:08:37 +00:00
Petar Petrov bcae4feba9 Drop hass from the entity name picker (#55367)
The entity name picker reads states, registries, localize and formatEntityName from contexts; its selector drops the unused SubscribeMixin.
2026-10-28 09:08:29 +00:00
Petar Petrov d0c81d6d5f Drop hass from the display state cards (#55368)
* Drop hass from the display state cards

The remaining display state cards read formatters, localize and the API from contexts, and the lock and update helpers take the separate values they read.

* Call the entity registry directly in callProtectedLockService

Avoid a hass-shaped literal for getExtendedEntityRegistryEntry.
2026-10-28 09:08:21 +00:00
Raphael Hehl 711ff63983 Select and drag picture elements in the card editor preview (#55263)
* Select and drag picture elements in the card editor preview

* Recreate the element editor on preview selection and moves

* Select a preview element activated by a click alone
2026-10-28 10:07:02 +01:00
Petar Petrov 1bc800d042 Drop hass from the category group header menu (#55374)
Turn the category group menu into a context element

The menu stored hass on its dropdown for its handlers; it now consumes localize and the API context.
2026-10-28 09:00:38 +00:00
Niels 96605b8986 Move manual event trigger into the Event category under Generic (#55348) 2026-10-28 11:00:33 +02:00
Petar Petrov 7d374f9798 Drop hass from the action state cards (#55371) 2026-10-28 09:55:40 +01:00
karwosts 920cca10d1 Update behavior of continue_on_timeout toggle (#54964) 2026-10-28 09:54:39 +01:00
Lackmake cc3334dfce Add color setting to sensor card (#55364) 2026-10-28 09:52:25 +01:00
fcd17e3f2a Translate errors when describing triggers, conditions and actions (#55249)
* Translate errors when describing triggers, conditions and actions

* Delete test/data/describe-error.test.ts

---------

Co-authored-by: Pradeep Peddineni <[email protected]>
Co-authored-by: Simon Lamon <[email protected]>
2026-10-28 10:52:02 +02:00
renovate[bot] da0935c904 Update dependency @home-assistant/webawesome to v3.14.0-ha.2 (#55375) 2026-10-28 08:51:28 +00:00
shazilkhanandJan-Philipp Benecke 38bf0a9ac3 Show custom trigger IDs in the "Triggered by" condition (#55086)
* Show custom trigger IDs in the "Triggered by" condition

Since 2026.10 the trigger list in the "Triggered by" condition only shows the
generated description, and a trigger's ID only appears when it is a duplicate.
Automations that relied on descriptive custom IDs can no longer tell similar
triggers apart.

Show the ID in the same end slot whenever it was set by the user, with a
neutral badge. Generated IDs stay hidden and duplicates keep the warning badge.

Fixes #54721

* Process UX feedback

* Fix accessibility issue

---------

Co-authored-by: Jan-Philipp Benecke <[email protected]>
2026-10-28 09:50:20 +01:00
Jan-Philipp Benecke 01bf2a402b Add bulk expose to voice assistants on the entities table (#55204)
* Add bulk expose to voice assistants on the entities table

* Add exposed entities parameter to voice assistant ID retrieval

* Add toast notifications for exposing and unexposing entities to voice assistants

* Fetch current exposure from core before determining changed entities for assistants

* Adapt to core change

* Remove dropdown fix

* Update toast message count to reflect core changed entities

* Update stub

* Return a fresh copy of stub
2026-10-28 08:48:43 +00:00
NielsandWendelin 4b20e52c29 Add Replace for automation triggers, conditions, and actions (#55131)
Co-authored-by: Wendelin <[email protected]>
2026-10-28 08:46:53 +00:00
Petar Petrov cdc08029e0 Drop hass from the statistic pickers (#55372) 2026-10-28 09:30:27 +01:00
Paul Bottein 5bbe135c53 Add formatNumberWithUnit helper (#55346)
* Add formatNumberWithUnit helper

* Extract formatted values into named variables
2026-10-28 06:05:45 +00:00
Paul Bottein 68445a8f94 Use a non-breaking space between values and units (#55345) 2026-10-28 08:00:17 +02:00
karwosts 316043960c Fix helper text spacing in location selector (#55349)
Fix helper text in location selector
2026-10-27 20:45:17 +01:00
Petar Petrov d8dde4a0bb Refit legends when the More button changes width (#55357)
Refit the legend when the More button changes width
2026-10-27 18:31:59 +01:00
Maarten Lakerveld 525f2c23d6 Drop hass from the shortcut card and the navigation path controller (#55334)
NavigationPathInfoController resolves localize, panels, areas, devices,
config and connection from context and only takes a path, so the
shortcut card, badge, editors and home shortcut dialogs no longer pass
hass to it. It skips the host update when the resolved label and icon
did not change. getShortcutCardDefaults and the panel title helpers take
localize instead of hass. The badge and the editors keep hass for their
actions and forms.
2026-10-27 18:29:45 +01:00
Aidan Timson 41d43fe780 Edit categories from their data table group headers (#55236)
* Edit categories from their data table group headers

* Use an edit and delete menu on category group headers

* Drop a deleted category from the filter and pass callWS

* Name the category in its group header menu label

* Key category group header menus by category ID

* Keep group header menus at normal font weight

* Use the rows' icon size for group header actions, keep marketplace at 20px
2026-10-27 18:26:40 +01:00
Petar Petrov 5b4a9a8967 Fit chart legends to two rows (#55351)
* Fit chart legends to two rows

Use the legend fit controller in ha-chart-base instead of hiding
everything after a fixed count, and resize the chart when the legend
changes height within a fixed height.

* Refit the chart legend when its items change
2026-10-27 18:19:15 +02:00
Maarten Lakerveld 3fe979408a Drop hass from the button, gauge, light, plant, picture and markdown cards (#55329)
* Drop hass from the button, gauge, light, plant, picture and markdown cards

Each card consumes the entity state and registry entry it shows plus the
formatters, localize, config and themes it reads. Actions are dispatched
as hass-action events. The picture card resolves media source images
once per image id, and the markdown card resubscribes its template when
the connection or user changes. The plant status card selects its sensor
states from the configured entity directly, so the selection does not
depend on transform ordering.

* Let the picture card retry a media source that failed to resolve

A rejected resolve left the in-progress marker set, so the same image
was never requested again.

* Subscribe the markdown template once per connection and user

The first context values arrive before connectedCallback() subscribes, so
the first update replaced a subscription that was already live. The card
now tracks the connection and user it subscribed with and only
resubscribes when one of them differs.
2026-10-27 17:13:18 +01:00
Petar Petrov 8b0d03cad2 Fit the distribution card legend to two rows (#55350)
* Fit the distribution card legend to two rows

Hide only the legend items that do not fit in two rows, instead of
everything after a fixed count. Each item is fitted at its widest since
the last resize, so changing values cannot make the More button flicker.

* Create the legend observers when the list renders

* Drop the legend mutation observer
2026-10-27 17:12:09 +01:00
Paul Bottein 76ee92807a Rename "Use device name" to "Main entity of the device" (#55320)
* Rename "Use device name" to "Main entity of the device"

* Explain main entities in the switch description

* Rename device name variables to main entity
2026-10-27 17:41:05 +02:00
Maarten Lakerveld 9f4fd39f3f Drop hass from the clock, glance and other simple cards (#54901)
* Drop hass from the clock, glance and other simple cards

The clock cards consume locale and the server time zone from context,
and the date format helper takes those values instead of hass. The
glance card consumes its entity states and registry entries through the
entity helpers, so it only rerenders when a shown entity changes. The
iframe, empty state, starting and toggle group cards consume localize,
config, states and api, and dispatch actions as hass-action events.

* Act from the element that fired hass-action and keep toggle group order

The shared handler ran handleAction on the app root, so fire-dom-event
dispatched ll-custom above the dashboard listeners and broke the Home
panel's Add device button. The toggle group now walks the configured
entity order, because the states record keeps its identity when only the
order changed.
2026-10-27 15:27:45 +00:00
Maarten Lakerveld 1fe09e6dbd Drop hass from the todo list and shopping list cards (#55331)
* Drop hass from the todo list and shopping list cards

The cards consume their entity state, states, entities, formatters,
localize, config, themes, connection and api from context. The todo
helpers take the connection, callService, callWS or states they use, so
the todo panel and item dialog pass those instead of hass.

* Refresh the todo card at midnight or at the next item deadline

Today labels and the overdue styling depend on the time. They used to
refresh with unrelated state updates, which no longer rerender the card.
The timer now fires at midnight or at the earliest upcoming deadline,
whichever is first, and is rearmed when the items change.
2026-10-27 15:27:41 +00:00
Maarten Lakerveld f74c63f257 Drop hass from the area controls card feature (#55333)
* Drop hass from the area controls card feature

The feature consumes the registries, localize and api it reads and
narrows statesContext to the entities its buttons show, so unrelated
state changes do not rerender it. The area control entities and group
toggle helpers take narrowed hass slices. The hui-card-feature wrappers
keep hass to feed custom features.

* Keep the default area controls stable and key the lookup on states

A fresh default array on every access missed both memoized lookups, which
now run per state notification as well as per render. States join the
cache key because the filter reads device classes from them, so an entity
can only match once its state exists.
2026-10-27 15:27:36 +00:00
Maarten Lakerveld 53a80e3056 Drop hass from the alarm panel, media control and weather forecast cards (#55330)
* Drop hass from the alarm panel, media control and weather forecast cards

The cards consume their entity state, formatters, localize, config,
themes, api and connection from context. callAlarmAction takes
callService and getSecondaryWeatherAttribute takes the formatter and
localize values it uses. The media control card extracts artwork colors
in update(), after @transform has refreshed the entity state for a new
config, and the alarm panel reports the keypad size before its first
update since masonry sizes cards before attaching them.

* Apply the media control theme without waiting for the entity

The theme was skipped when the player was missing on the config or
themes change, and nothing reapplied it once the entity appeared.
2026-10-27 15:27:31 +00:00
Maarten Lakerveld cc424b6a09 Drop hass from the map card and the trend graph card feature (#55332)
* Drop hass from the map card and the trend graph card feature

The map card narrows statesContext to the entities it tracks, including
the entities its visibility conditions read, so unrelated state changes
do not rerender it. The visibility condition checks take a narrowed hass
type that includes the registries the location condition resolves zones
through, the target selector helpers take what they read, and
extractConditionEntityIds visits every condition instead of returning at
the first logical group. The history stream helper takes a connection
and a states slice, which the map card and trend graph feature read live
so a resubscribe after a reconnect sees current states.

* Track core entity_id conditions and single-child groups on the map

The extractor skipped the core entity_id field that the checks prefer,
and threw on a logical group that holds one condition instead of a list.
It now also handles legacy conditions without a condition key.
2026-10-27 15:26:51 +00:00
Maarten Lakerveld c5818c5360 Drop hass from the repairs, updates and discovered devices cards (#55335)
Adds ConnectionSubscriptionController as a context based replacement for
SubscribeMixin. It keeps one websocket subscription alive while the host
is connected and re-evaluates an optional enabled callback after each
update. The repairs and discovered devices cards subscribe through it.
The updates card counts installable updates with a states transform that
waits for the formatters context and evaluates its visibility in
update(), after the transform ran. Its test provides contexts instead of
a hass object.
2026-10-27 15:26:29 +00:00
Petar Petrov 70b86832b2 Drop hass from the device automation pickers (#55302)
The device action, condition and trigger pickers read localization, states, formatters and the API from contexts.
2026-10-27 15:15:06 +00:00
Petar Petrov 2df237975b Drop hass from the object selector (#55315)
formatSelectorValue takes the values it reads, and the object selector consumes them from contexts.
2026-10-27 15:13:32 +00:00
renovate[bot] 60a632f584 Update dependency xss to v1.0.17 (#55354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-27 16:12:25 +01:00
Aidan Timson 4bdf332f30 Add actions to data table group headers (#55235)
* Add an actions slot to data table group headers

* Dismiss new repositories from the Marketplace group header

* Style group header actions like the rows' icon buttons

* Keep group header actions in view and forward ungrouped slots

* Keep the error alert open when dismissing new repositories fails

* Let pages change the colour of group header actions

* Size group header actions like other header icon buttons

* Keep the pinned group header a table row

* Forward only supplied group actions and index the pinned header
2026-10-27 16:01:59 +01:00
Petar Petrov 73416452ef Drop hass from the mount, serial port, push and vacuum segment components (#55308)
getVacuumSegments, getAppKey, listSerialPorts and fetchSupervisorMounts take callWS, so their components read translations, config, registries and the API from contexts.
2026-10-27 14:23:01 +01:00
Petar Petrov fc3d0ee74b Drop hass from the assist pipeline and conversation agent pickers (#55306)
listAssistPipelines, listAgents, getConfigEntry and getSubEntries take callWS, so both pickers and their selectors read translations, the API, the config and the entity registry from contexts.
2026-10-27 14:22:51 +01:00
Petar Petrov 4952db4960 Drop hass from the label pickers (#55303)
The label registry helpers take callWS, so ha-label-picker, ha-labels-picker and ha-selector-label read their data from contexts.
2026-10-27 14:22:48 +01:00
Petar Petrov 2bdcea4bc0 Drop hass from the entity attribute and state content pickers (#55307)
Both pickers read entity state, formatters, translations and registries from contexts, so the attribute and UI state content selectors stop forwarding hass.
2026-10-27 11:38:29 +01:00
Petar Petrov f2bed231ff Drop hass from the media browser thumbnail (#55305)
The thumbnail reads dark mode, the Home Assistant URL and fetchWithAuth from contexts, and re-resolves brand icons when the theme changes.
2026-10-27 11:38:04 +01:00
Petar Petrov e1178bc80b Drop hass from the speech engine pickers (#55300)
The TTS and STT helpers take callWS instead of hass, so the TTS, STT and TTS voice pickers and their selectors read translations, registries, states and the API from contexts.
2026-10-27 11:37:42 +01:00
Petar Petrov d732af588c Drop hass from the floor pickers (#55311)
createFloorRegistryEntry and updateAreaRegistryEntry take callWS instead of hass, so ha-floor-picker and ha-floors-picker read registries, states, translations and the API from contexts.
2026-10-27 11:37:18 +01:00
Petar Petrov d492ecaefd Drop hass from the entity state pickers (#55313)
getStates takes the states and language, so the state pickers read entity states, formatters and translations from contexts.
2026-10-27 11:37:04 +01:00
WendelinandClaude Opus 5.5 bc6d8841eb Refactor add automation element dialog (#55233)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-27 10:29:41 +01:00
c2ff6d5932 UI/YAML switcher keyboard combination (#55035)
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-10-27 09:28:31 +01:00
Petar Petrov 68ab28e3e9 Drop hass from the replace device dialog (#55312)
Read localization, registries, dark mode, the brands URL and callWS from contexts, and load config entries when the dialog params change.
2026-10-27 09:44:18 +02:00
Vu Linh TruongandClaude Opus 5.5 15712821b8 Fix media browser search field overflowing off the left edge (#55316)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-27 07:29:49 +00:00
Petar Petrov 062c08099a Drop hass from the file selector and media upload button (#55304)
Pass fetchWithAuth, callApi and localize to the upload helpers instead of hass.
2026-10-27 08:29:43 +01:00
Petar Petrov cd7c3609c3 Drop hass from the suggest with AI button (#55314)
fetchAITaskPreferences takes callWS and generateDataAITask takes callService, so the button reads translations, config and the API from contexts.
2026-10-27 08:28:19 +01:00
Petar Petrov 7053f34126 Drop hass from state-info (#55301)
state-info reads the entity name formatter and localize from contexts, so callers no longer pass hass.
2026-10-27 08:28:02 +01:00
Petar Petrov 5897eb1fe8 Re-render context-based brand images when the brands token arrives (#55318)
Brand image URLs stay empty until the brands access token is fetched, and
only a hass update announced it. Components that read Lit contexts instead
of hass never re-rendered, so their brand images kept an empty src.

Provide the token as brandsAccessTokenContext, fed from a listener in
brands-url so it also follows the 30-minute rotation, and consume it in
the context-based brand image users.
2026-10-27 08:26:21 +01:00
Petar Petrov 97f290a914 Drop hass from the join media players dialog (#55310)
* Drop hass from the join media players dialog

Read states, entities and localize from contexts and pass callService to the join and unjoin helpers.

* Narrow the join dialog's states to media players

The reused dialog now updates only when a media player state changes.
2026-10-27 08:25:46 +01:00
Petar Petrov ce03327457 Drop hass from the snowflakes overlay (#55309)
* Drop hass from the snowflakes overlay

Read the connection and dark mode from contexts and subscribe to the winter mode lab feature directly.

* Ignore lab feature updates from a replaced subscription
2026-10-27 08:24:17 +01:00
Jump-333andCopilot Autofix powered by AI e3babaaa0c Add home total option to energy dashboard (#54821)
* Add home total option to energy dashboard

* Fix empty card visibility

* Fix missing data

* Add home total consumption and remove individual device toggle

* Fix various styling issues and calculation issues

* Fix various issues

* Fix DialogMixin issue

* Bugfixes

* Fix home and grid issue

* Show energy dashboard with only the home meter

* Fix prettier

* Fix show cards issues

* Fix reconciliation issue

* Fix reconciliation issue, attempt 2

* Fix consumption miscalculation and add calculation for instant power sources

* Remove instant power sources calculation

* Fix issues

* Centralize home grid system

* Fix critical energy dashboard issue

* Bugfix

* Fix power chart and badge

* Fix reconciliation issues

* Fix copilot issues

* Fix grid export issue

* Add more tests and CSV export

* Remove inferred grid

* Fix energy overestimation

* Fix over reported usage and source name

* Fix localization issue

* Remove energy scaling

* Add whitelines back and remove a test

* Change stat_energy_from to stat_energy_to

* Change energy source stat from 'from' to 'to'

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-27 09:08:35 +02:00
Raphael Hehl 94b69033e5 Share a Matter device through the companion app from the share dialog (#54364) 2026-10-26 21:47:32 +01:00
Maarten Lakerveld 8125bf54ee Keep the calendar add button above the safe area (#55298)
The add event button is absolutely positioned, but ha-full-calendar had no
position of its own, so the button resolved against the 100vh app bar layout
and always sat 16px above the physical screen edge. The calendar box already
subtracts the bottom safe-area inset, so anchoring the button to it keeps it
clear of the home indicator and the iOS app's native tab bar.
2026-10-26 20:14:36 +02:00
renovate[bot] 293351bceb Update dependency @lokalise/node-api to v17 (#55321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-26 20:12:08 +02:00
Felix Schneider 0558fa58d4 Add "open in new tab" to automation trace and editor menus (#55264)
Make automation trace and editor toolbar buttons links

Add an href to the "Traces" button in the automation editor and the
"Edit automation" button on the trace page, so they can be opened in a
new tab via middle-click, Ctrl/Cmd+click or the context menu. A plain
click in the editor still runs the unsaved changes check.
2026-10-26 19:09:17 +01:00
Petar Petrov 040630ed8e Drop hass from the categories filter (#55317)
Category registry create, update and delete helpers take callWS.
2026-10-26 18:52:43 +01:00
renovate[bot] a86e7ae955 Update dependency @babel/core to v8.0.9 (#55299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-26 16:53:42 +01:00
renovate[bot] b23fdc40fb Update dependency @babel/runtime to v8.0.8 (#55295) 2026-10-26 15:40:11 +00:00
Paul Bottein b92e761a6c Add an entities section strategy (#55190)
* Add an entities section strategy

* Offer lone device classes and drop empty names in the entities section

* Keep an empty display name in the entities section

* Show the device and entity name by default in the entities section editor

* Name the missing types and targets in the empty entities section

* Require a grouping in the entities section editor

* Drop the unused labels dependency of the entities section

* Add the entities section to the section picker

* Preview the lights of the area with the most lights

* List the best matching types first in the entity type picker search
2026-10-26 16:35:53 +01:00
Paul Bottein cfd05ab129 Add a dialog to choose the type of section to create (#55150)
* Add a dialog to choose the type of section to create

* Add a built-in sections heading when custom section strategies are listed

* Clear the section editor changes on close and fit strategy tiles on small screens

* Add the device section to the section picker
2026-10-26 16:35:52 +01:00
Stefan AgnerandClaude Fable 5.1 745f5490e0 Use dedicated websocket commands for app ingress sessions (#54695)
* Use dedicated websocket commands for app ingress sessions

The ingress panel used the generic supervisor/api websocket command to read
app info, create an ingress session and keep it alive. Core exposed those
three endpoints to non-admin users for apps with panel_admin: false, but
could not check which app was requested, so any user could open any app's
ingress UI.

Core now provides supervisor/ingress/info, supervisor/ingress/session and
supervisor/ingress/validate_session, which take the app slug and authorize
the request per app. Switch the ingress panel to these commands and use a
slim IngressAppInfo type with just the fields the panel needs. The generic
supervisor/api command becomes admin-only in Core.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Handle the session rejection when app info fails

Session creation runs in parallel with the app info request, and the
info checks return early on failure before the session promise is
awaited. With the slug-scoped commands both requests are rejected when a
non-admin opens an admin-only app, which left the session rejection
unhandled. Attach a handler right away and keep the later error
reporting.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-26 17:28:15 +02:00
Petar Petrov 4edd7c5e88 Drop hass from the user pickers (#55294)
fetchUsers takes callWS instead of hass, so ha-user-picker and ha-users-picker read translations and the API from contexts.
2026-10-26 15:27:25 +00:00
Petar Petrov cba0fcf82c Drop hass from the blueprint picker, blueprint filter and related items (#55292)
fetchBlueprints takes callWS instead of hass, so the three components read the API, translations, registries, states and theme from contexts.
2026-10-26 15:21:09 +00:00
Aidan Timson 28ee08a2b9 Keep a filter written while a dialog closes in the URL (#55224)
* Keep a filter written while a dialog closes in the URL

* Replay a dialog write only on the page's own entry

* Share the dialog page baseline across query param controllers

* Keep another controller's dialog replay when one disconnects

* Restore a dialog's URL search in the dialog manager

* Keep a filter written before the dialog's back() lands
2026-10-26 16:25:21 +02:00
Simon LamonandCopilot Autofix powered by AI 0e502f292b Allow to duplicate some helpers (#55286)
* Allow to duplicate some helpers

* Feedback

* Update fetch logic to handle errors gracefully

Handle fetch errors by returning an empty array.

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-26 14:22:53 +00:00
WendelinandClaude Opus 5.5 dffc8bb80e Improve a11y for tile cards and area light buttons (#55192)
* Stop tile cards from being announced twice

- Hide the tile info from assistive technology when the tile is a
  button. The button keeps its name through aria-labelledby, so the
  name and state are no longer read a second time as plain text. This
  applies to every card built on ha-tile-container.
- Name the tile icon button after its action ("Toggle Floor lamp",
  "Press Doorbell", "Activate Movie night") instead of only the entity
  name, so it is distinguishable from the tile itself.
- Group tile and area card features under the card name, so controls
  like "Brightness" or "Increment" have context.
- Give the climate and humidifier badges a text alternative.
- Label the volume and numeric input feature controls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

* Describe what the area light buttons do

The light toggle badges in the home area view and the light panel were
named only "On" or "Off", which reads as a state, not an action. Add an
optional label to the button heading badge for its accessible name and
tooltip, and use it for "Turn on lights in <area>" and "Turn off lights
in <area>".

The toggle group power button was named after its state ("All off") and
then read the same text again. Name it after its action ("Turn all on",
"Turn all off") instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

* Say all lights in the area light button names

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

* Add accessible labels for toggle group card and update translations

* Update area light toggle messages for clarity

* Review

---------

Co-authored-by: Claude <[email protected]>
2026-10-26 14:19:27 +00:00
Joost LekkerkerkerandClaude Opus 5.5 428b9f39a9 Add perform again button to action history detail dialog (#55285)
* Add perform again button to action history detail dialog

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019wZqBGX4CTap14HQB4JQX1

* Make perform again the primary action and drop the footer close button

The dialog header already has a close button, so keep the footer to
view trace and perform again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01G6dWAWvDG94aCuKf7Q29wT

---------

Co-authored-by: Claude <[email protected]>
2026-10-26 15:18:57 +01:00
Matthias AlphartandClaude 14a5c62ae9 Fix see-through corners on ha-label background (#55290)
Co-authored-by: Claude <[email protected]>
2026-10-26 14:09:55 +00:00
Max-src 068eee3d80 Apply a restored data table search before the first render (#55251)
A search set when the table is created, from storage or a link, went through the typing debounce, so the table first showed every row and filtered about 100 ms later. Apply it on the first update and keep the debounce for typing.
2026-10-26 15:55:12 +02:00
Alex Blanck 6a71ac590f Update entity IDs when assigning an area after setting up an integration (#55259) 2026-10-26 15:53:05 +02:00
Paulus SchoutsenandClaude 6e4138628e Remove automatic decision toggle from AI task settings (#55289)
Remove the "Use decisions in Home Assistant features" switch from the
AI suggestions card and its translation strings. The frontend no longer
sets allow_automatic_evaluation. The preference stays in the data
types and the backend.


Claude-Session: https://claude.ai/code/session_012SMsWp4EAMPzb1sLBYBVGJ

Co-authored-by: Claude <[email protected]>
2026-10-26 14:51:41 +01:00
Maarten Lakerveld 712193f98e Highlight the pasted card (#55229)
After a paste it was hard to see which card was new. The pasted card now
fades out a primary-color outline for 1.5 s, the same hint the history
filter pane shows when it is already open. The animation lives in a shared
highlightElement helper in common/dom that the filter pane now uses too.
It animates the full outline shorthand, so callers need no transparent
outline of their own, and holds a steady outline instead of fading when
reduced motion is preferred.

Paste remembers the pasted card object. The wrapper whose slotted card
renders that object claims it once and runs the animation. Keying on the
object rather than the path matters: the view pushes the new config into
the old section elements before rebuilding them, so an old wrapper at the
pasted index would otherwise claim the highlight and be torn down. A
failed save clears it.
2026-10-26 15:44:52 +02:00
Paulus Schoutsen 56e42a37f1 Skip restored update entities when refreshing updates (#55267)
* Skip unavailable entities when refreshing updates

* Distinguish unavailable updates from missing update integrations

* Skip only restored entities when checking for updates
2026-10-26 14:40:58 +01:00
Petar Petrov 94dfccca5b Name individual energy devices after their device (#55287)
Name individual energy devices after their device, without the entity name
2026-10-26 13:39:03 +00:00
Petar Petrov f1bd3bc9d0 Fix extra unit-less statistics chart in history graphs (#55268)
* Merge unit-less statistics into the history chart of each entity

* Append merged statistics without copying the group each time
2026-10-26 13:26:12 +01:00
dschace 7836f8fe23 Add ripple and haptic feedback to control number buttons (#55156) 2026-10-26 12:23:40 +00:00
craig-b 9827f45a8b Show progress and result when checking for app updates (#55205)
* Show progress and result when checking for app updates

* Reuse the app info fetched by the update check

* Remove the app page rendering tests
2026-10-26 12:19:40 +00:00
Petar Petrov 2e5aae8932 Fix cast receiver types for chromecast-caf-receiver 7 (#55288) 2026-10-26 13:09:30 +01:00
Maarten Lakerveld ca36b95170 Share one log viewer between the live log dialog and the Z-Wave logs page (#55057) 2026-10-26 12:05:51 +00:00
f65148ca5c Add Date card (#52994)
* Add Date card

* Use UiClockDateFormatSelector instead of dropdown with preset and rollback format_date.ts formatters

* Rename Clock-branded date-format building blocks to card-agnostic names

* Migrate editor to localize context

* fix date not showing at first init when dateText not yet computed

* fix ui_clock_date_format selector fallback/initial value after rename to ui_date_format_parts

* Apply suggestions from code review

fix midnight timestamp race, forced LTR date text, nowarp

* fix selector in gallery

* fix date card size default and missed server time zone changes

* Remove backdrop-filter from no-background class

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* fix missing midnight refresh after sleep and locale aware default format

* Remove title in stub

* fix date format losing locale grammar when combining fields (e.g. Russian/Finnish month case)

* validate date card time_zone in setConfig instead of crashing later

* Revert shared date format renames and combined formatting

Keep ui_clock_date_format, the clock date-format module and the
card.clock.date.* strings as on dev. Format each date part on its own
again, since combining fields in one Intl call drops CJK unit literals.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Compute date card text in willUpdate and trim time zone tests

Setting _dateText in updated() triggered a second update cycle. Also
drop the leftover direction rule and keep one valid and one invalid
zone for isValidTimeZone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Consume locale and config contexts in date card and guard refresh scheduling

Replace the reactive hass property with internationalizationContext and
configContext consumers so unrelated entity updates no longer rerender
the card. Skip scheduling while detached and when the midnight delay is
not a finite positive number.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-26 13:37:51 +02:00
Joost LekkerkerkerandClaude Opus 5.5 441d8c4897 Add copy as YAML button to action history detail dialog (#55284)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-26 11:04:50 +00:00
Maarten Lakerveld 01824d7c54 Add Paste to the dashboard card menu (#55215)
* Add Paste to the dashboard card menu

The card menu now offers Paste when a card is on the clipboard, inserting a
copy right after the card the menu was opened on, like Paste for sections.
In panel views, which show a single card, Paste replaces that card and
offers Undo in a toast. In sidebar views the pasted card takes the target
card's view_layout so it lands in the same column.

Paste closes the edit toast before saving, because the Cut toast's Undo
undoes the latest change and would otherwise undo the paste.

Paste is hidden in the view header and footer slots and the import-only
section, where adding a card makes no sense.

* Rerender storage-backed state that changed while disconnected

The storage decorator unsubscribes while an element is disconnected and
subscribes again on reconnect, but never asked for a render when the
value changed in between. Cached dashboard views kept stale card menus:
copy a card in another view and the Paste item was missing on return.

The decorator now remembers the value at disconnect and requests an
update on reconnect when it differs.

* Bind the replace toast's Undo to the replaced card

The toast after replacing a panel card used the generic undo-change event,
which undoes the latest change. An edit made before clicking Undo was
reverted instead of the replacement. The action now puts the replaced card
back at its path.

* Make paste generic like copy and cut

performPaste now takes the clipboard item type like performCopy and
performCut, so badges can reuse it. The sidebar rule moved out of the
helper: the view handler adopts the target card's view_layout through
adoptViewLayout before pasting, because only it knows the view type.

* Use the generic undo for the replace toast

Binding Undo to the replaced card captured the pre-paste dashboard, so
clicking it discarded every edit made while the toast was visible, and a
failed restore went unhandled. The toast now fires undo-change like the
cut and delete toasts, sharing their behavior and limitations.

* Drop the cut toast when a paste finished first

A cut whose save was still pending could show its Undo toast after a
later paste. Saves apply optimistically, so Undo would then remove the
pasted card instead of restoring the cut one. The cut still updates the
clipboard when its save succeeds, but skips the toast once a newer
clipboard action has started.
2026-10-26 11:51:49 +01:00
Aidan Timson f5db697af8 Revert "Add translation_key filter to entity selectors" (#55282)
Revert "Add translation_key filter to entity selectors (#55102)"

This reverts commit 2d099dee15.
2026-10-26 10:41:44 +01:00
Christopher Pruijsen d8f3d282bf Scroll the Assist chat to the newest message (#55240)
* Scroll the Assist chat to the newest message

Each message is wrapped in its own .message-container, so .message:last-child matched every message and the query returned the greeting. Select the message inside the last container instead, and skip scrolling when that container renders no message.

* Restore the test shims after the Assist chat scroll test
2026-10-26 10:38:24 +01:00
Joost Lekkerkerker a099c8d27d Add action history page to tools (#55078)
* Add action history page to tools

* Add action history page to tools
2026-10-26 10:04:05 +01:00
Pradeep PeddineniandPradeep Peddineni c395164e1a Fix Enter on Android moving focus away from todo list add item input (#55256)
* Fix Enter on Android moving focus away from todo list add item input

* Remove rendering test for the todo list enter key hint

---------

Co-authored-by: Pradeep Peddineni <[email protected]>
2026-10-26 08:41:23 +02:00
Paulus SchoutsenandClaude a2f101854a Add AI Task evaluate types and method (#55281)
Add evaluate task types and method to AI Task data

Add types that mirror the ai_task.evaluate action in Core: the noul,
choice and score questions, their answers, and the task and result
shapes. Add evaluateAITask to call the action and return its response.


Claude-Session: https://claude.ai/code/session_01QavsnrcREjatsuB7yUDn3n

Co-authored-by: Claude <[email protected]>
2026-10-26 08:31:46 +02:00
renovate[bot] bdb1e95eac Update dependency @types/chromecast-caf-receiver to v7 (#55274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-26 08:11:22 +02:00
renovate[bot] 1286bda99d Update github/codeql-action action to v4.39.0 (#55279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-26 08:11:18 +02:00
renovate[bot] 3b784f6fdb Update release-drafter/release-drafter action to v7.10.0 (#55280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-26 08:11:09 +02:00
renovate[bot] 4c9a05ef5b Update dependency @lezer/common to v1.5.4 (#55265)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-25 11:54:26 +01:00
renovate[bot] eb82a1e997 Update pnpm to v12.15.0 (#55261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-24 22:28:24 +02:00
Petar Petrov f4a073f44f Enlarge the Sendspin mark in the ESPHome setup wizard (#55252)
The mark is wider than it is tall, so at 20 of 24 units it looked smaller than the icons beside it. It now spans 22.
2026-10-24 06:22:45 -04:00
renovate[bot] b8236deb5c Update dependency vite to v8.3.5 (#55253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-24 09:39:11 +00:00
github-actions[bot]andpiitaya 159a60582d Update device class constants (#55250)
Co-authored-by: piitaya <[email protected]>
2026-10-24 08:59:22 +00:00
Petar Petrov adcf554149 Call the Z-Wave adapter a device in the ESPHome setup wizard (#55196) 2026-10-23 21:37:38 +02:00
Paul Bottein 921b73ecbe Use the offset duration mode for sun and calendar offsets (#54703) 2026-10-23 20:50:21 +02:00
Aidan Timsonandcopilot-swe-agent[bot] 6dbf56aae0 Keep the integrations dashboard and integration page search in the URL (#55225)
* Let the query params controller take a fixed page path

* Recreate the integration page per domain and keep params on redirect

* Keep the integrations dashboard search and toggles in the URL

* Keep the integration page search in the URL

* Add search and status params to the integrations My links

* Drop the integrations query params test

* Address review on integrations query params

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-23 20:15:08 +03:00
MichaelandClaude Opus 5.5 784c8f782c Show bridged devices on the Matter network map (#54812)
* Show bridged devices in the Matter network map

A Matter bridge exposes the devices behind it as endpoints of its own
node, so the server topology never lists them and the map showed only the
bridge. Core registers each bridged endpoint as a device whose via_device
is the bridge, so synthesize those devices from the device registry and
hang them off their bridge node with a non-directional "bridged" link.

Bridged devices get their own legend category, a tooltip naming the
bridge, search by manufacturer/model, and click-through to the device page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SdicShTCt2EyiugGZ8GTtZ

* Use a typed key and blue-grey for bridged devices in the Matter map

The bridged link label is a fixed string, so localize it through a typed
visualization key instead of the dynamic network_type lookup, which
mirrors the server's network types. Switch the bridged category to
blue-grey so it no longer reads close to the red offline category.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SdicShTCt2EyiugGZ8GTtZ

* Show Matter bridges as expandable nodes in the network map

* Expand Matter bridges from the node, not the label

* move bridged links after "keep the strongest link"

* skip the "select / hide" hint  when there are no bridged nodes

* search points to the bridge node

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 18:08:42 +03:00
Petar Petrov 78d45ccf76 Use the Sendspin mark and check badges in the ESPHome setup wizard (#55239)
* Use the Sendspin mark and check badges in the ESPHome setup wizard

The audio row and banner show the Sendspin mark instead of a music note, and configured serial ports and the connected Z-Wave network show the check badge used for completed rows instead of a Configured chip.

* Color the ESPHome setup audio capability in Sendspin red
2026-10-23 18:00:29 +03:00
Bram Kragten 901c0a61ad Bumped version to 20260930.5 2026-10-23 15:07:48 +02:00
karwosts f2ad7bbd6a Fix blueprint import redirect links (#55087) 2026-10-23 15:07:04 +02:00
Petar Petrov 25c73b7c0d Fix Sankey labels on thin nodes showing at full size in Safari (#55020) 2026-10-23 15:07:02 +02:00
Raman GuptaandClaude Opus 5.5 41b7c079a8 Fix Z-Wave config parameter status always showing as error (#55002)
The zwave_js/set_config_parameter and set_raw_config_parameter commands
return the zwave-js-server CommandStatus string ("accepted" or
"queued"), not the numeric zwave-js SetValueStatus. #54287 retyped the
status as a number and mapped it through computeSetConfigParamStatus,
which turned every successful update into "error".

Type the status as the string union the backend sends and use it
directly again. The raw variant only returns status, so give it its own
result type without value_id.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-23 15:07:01 +02:00
WendelinandClaude Opus 5.5 e386b091f1 Prevent iOS Safari text inflation on landscape rotation (#54914)
* Prevent iOS Safari text inflation on landscape rotation

Fixes #53060

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CPRiV8TCjj8Z4SX9jX9578

* also add the fix for gallery and demo

---------

Co-authored-by: Claude <[email protected]>
2026-10-23 15:07:00 +02:00
Petar Petrov 52c4f360cc Disable Lit dev mode in the legacy build under pnpm (#54855)
Compile Lit without dev mode in the legacy build again
2026-10-23 15:06:58 +02:00
Aidan Timson 14952d74ac Allow picking several categories in the category filter (#55222)
* Allow picking several categories in the category filter

* Keep every category in script and scene links

* Keep every category in automations links
2026-10-23 15:04:11 +03:00
Paul Bottein 734bc63100 Add a preview and a clearer width setting to the section editor (#54961)
* Add a preview and a clearer width setting to the section editor

* Use early returns in the section editor

* Limit the section preview to the view width and reset a cleared width
2026-10-23 13:49:14 +02:00
Paul Bottein 33e5faac19 Show section backgrounds in the sections view sidebar (#55234) 2026-10-23 13:23:05 +02:00
Marcin BauerandClaude Fable 5.1 11c368a40b Remove "Perform action" prefix from automation action rows (#55228)
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-23 12:28:59 +02:00
Aidan TimsonandSimon Lamon 8b62ec1caf Add query params to the entities table (#55105)
* Keep entities table search, filters, sorting and grouping in the URL

* Pass entities table params through My links

* Name the shared area, floor and category columns for the URL

* Name the domain column for the URL

* Link to the entities page by its main path

* Filter entities by sub-entry across every selected config entry

* Name the domain column entity_domain for the URL

* Refresh the assistants filter when exposed entities load

* Use the data table settings controller on the entities page

* Compare entity filter lists regardless of order

* Ignore filter changes from a page that was left

* Keep filter items found after leaving the entities page

* Keep a cleared status filter when another filter changes

* Keep a cleared status filter after a reload

---------

Co-authored-by: Simon Lamon <[email protected]>
2026-10-23 09:34:22 +00:00
Aidan Timson 5871ec5d04 Open the add integration dialog for brand links (#55227) 2026-10-23 12:29:00 +03:00
Paul Bottein 91af23e254 Refresh the target picker when its filters change (#55186)
* Refresh the target picker when its filters change

* Keep target picker rows on their own target
2026-10-23 12:26:42 +03:00
renovate[bot] 0564cb7e1a Update dependency xss to v1.0.16 (#55226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-23 12:23:25 +03:00
Petar Petrov 684bea20df Find ESPHome setup switches through the device wizard (#55211) 2026-10-23 11:17:00 +02:00
Jan-Philipp BeneckeandCopilot Autofix powered by AI fd8bacb0b8 Apply UX feedback to target picker exclusions (#55198)
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-23 10:57:58 +02:00
Petar Petrov ce8b2cc8b6 Update ESPHome setup wizard audio and serial copy (#55212) 2026-10-23 10:43:39 +02:00
Aidan Timson 7e6c39a30b Add query params to the automations table (#55171)
* Add query params to the automations table

* Keep one category from automations links
2026-10-23 10:42:33 +02:00
Petar Petrov 8c74bf2c04 Show unattributed grid export in energy distribution card (#55213) 2026-10-23 10:37:33 +02:00
Aidan Timson 54e95f3fbd Add query params to the infrared and radio frequency devices tables (#55217) 2026-10-23 11:32:45 +03:00
Aidan Timson 858c8ad7fd Add query params to the Z-Wave provisioned devices table (#55216) 2026-10-23 11:30:26 +03:00
Aidan Timson 3d7308bf1e Drop stale related lookups in the data table filters (#55218) 2026-10-23 11:28:41 +03:00
Aidan Timson 92de25dc04 Add query params to the statistics table (#55219) 2026-10-23 10:11:29 +02:00
Pradeep PeddineniandWendelin 59358ca196 Translate show more items in trace timeline (#55206)
Co-authored-by: Wendelin <[email protected]>
2026-10-23 08:01:40 +00:00
Petar Petrov 563cf92946 Energy usage graph: don't combine grid sources over a negligible value (#55214) 2026-10-23 09:37:07 +02:00
Daan Vervacke 2d099dee15 Add translation_key filter to entity selectors (#55102)
* Add translation_key filter to entity selectors

* Test translation key list without a match

* Forward the entity registry into target selector matching

* Refresh active domain discovery when states and registries change

* Treat empty translation key lists as unrestricted

* Skip helper creation for translation key filters

* Keep active system domain discovery cheap on state changes

* Simplify the automation dialog tests
2026-10-23 08:23:34 +01:00
Aidan TimsonandSimon Lamon 45edadaf7c Add query params to the application credentials table (#55143)
* Add query params to the application credentials table

* Add query params to the application credentials My redirect

* Use the data table settings controller on application credentials

---------

Co-authored-by: Simon Lamon <[email protected]>
2026-10-23 08:27:36 +03:00
Aidan Timson 681ad2f3cd Add query params to the ZHA groups table (#55194)
* Add query params to the ZHA groups table

* Keep the ZHA groups table search in the settings controller
2026-10-23 08:26:40 +03:00
Pradeep PeddineniandPradeep Peddineni dcc704e31d Show device instead of unknown entity for device actions in trace (#55200)
Co-authored-by: Pradeep Peddineni <[email protected]>
2026-10-23 08:26:01 +03:00
renovate[bot] e164971e2c Update Node.js to v24.22.1 (#55210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-23 07:08:10 +02:00
renovate[bot] ed6c36b949 Update formatjs (#55208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-23 07:07:54 +02:00
renovate[bot] f07beb961b Update dependency intl-messageformat to v12.1.4 (#55207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-23 07:07:33 +02:00
7ed6d18b4d Add two way audio support (frontend) (#54660)
* copy from sdb9696

* fixes

* Also log Event when timer is not running

* Find audio transceiver and request restartIce()

* use CameraFeatures instead of CameraCapabilities

* cleanup

* fixes

* lint fixes

* fix lint

* Update ha-camera-stream.ts

* [prettier] Update ha-web-rtc-player.ts

* Review feedback Snake Case

* Use ha-button instead of mwc-button

* fix import

* oeeps, wrong file, fix

* Add iceGatheringStateChanged

* Split signaling path for offer on renegotiation

* Update src/components/ha-web-rtc-player.ts

Co-authored-by: Bram Kragten <[email protected]>

* Start a fresh WebRTC session when enabling the microphone

Renegotiating the existing peer connection fails with go2rtc on Firefox,
since go2rtc answers the re-offer from a new peer with a different DTLS
fingerprint. Instead, tear down the connection and start a new one with
the microphone track as a sendrecv audio transceiver.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Add media_failed message to WebRTC player translations

* Add download snapshot functionality to camera more info dialog

* Add WebRTC microphone support to camera component

- Implement microphone toggle functionality in ha-camera-stream and ha-web-rtc-player.
- Introduce audio level monitoring using watchAudioLevel.
- Update more-info-camera to manage microphone state and UI interactions.
- Enhance English translations for microphone-related messages.

* Add microphone streaming support for non-secure contexts in apps

* Refactor WebRTC microphone handling and update error messages for clarity

* Add has_two_way_audio attribute to CameraEntity and update feature check in MoreInfoCamera

* Update to allign with backend

* Fix translation key for insecure microphone warning

* Refactor camera capabilities to use 'two_way_audio' instead of 'supports_two_way_audio'

* Enhance microphone error handling and localization for camera interactions

* Refactor webrtcCapabilities to webrtc object for microphone streaming support

* Refactor microphone handling and error reporting in WebRTC player; add new translation key for microphone connection failure

* copilot review

---------

Co-authored-by: karwosts <[email protected]>
Co-authored-by: Bram Kragten <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-10-22 21:12:23 +02:00
renovate[bot] 668f17ec34 Update dependency @codemirror/view to v6.43.15 (#55201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-22 18:38:13 +00:00
Aidan Timson 951c445cfd Add query params to the DHCP, SSDP and Zeroconf tables (#55195)
* Add query params to the DHCP, SSDP and Zeroconf tables

* Keep the discovery table search in the settings controller
2026-10-22 20:15:34 +02:00
renovate[bot] 6f04cb918e Update dependency typescript-eslint to v8.72.0 (#55199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-22 18:11:25 +00:00
Yosi Levy 30cd5383bd Rtl automation trace fix (#54841)
* Adding RTL support for trace splitter + additional fixes

* Revert gitignore

* Update .gitignore

* Fixed comments

* Fix comments

* Removed connectedCallback in ha-split-panel

* Remove wrongfully added style

Remove background property from main class styles.
2026-10-22 20:07:07 +02:00
Aidan Timson 5fe9f97948 Add query params to the hardware table (#55193)
* Add query params to the hardware table

* Keep the hardware table search in the settings controller
2026-10-22 20:00:38 +02:00
Aidan Timson c58e7d684a Add query params to the devices table (#55107)
* Keep devices table search, filters, sorting and grouping in the URL

* Pass devices table params through My links

* Filter devices by sub-entry across every selected config entry

* Use the data table settings controller on the devices page

* Compare device filter lists regardless of order

* Keep fresh filter matches when restoring saved device filters

* Ignore filter changes from a page that was left

* Keep filter items found after leaving the devices page
2026-10-22 19:34:53 +02:00
renovate[bot] f9b65b9c2b Update dependency barcode-detector to v3.2.3 (#55197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-22 19:30:17 +02:00
Aidan Timson 9285ef824f Add query params to the tags table (#55133)
* Keep the tags table search and sorting in the URL

* Add query params to the tags My redirect

* Use the data table settings controller on the tags page
2026-10-22 14:45:14 +00:00
WendelinandClaude Opus 5.5 3e99aa1817 Add generic action bar and paste bar components (#55189)
* Make paste bar a generic ha-paste-bar component

Move ha-automation-add-element-paste to src/components/ha-paste-bar and
use it in the add automation element, create card and create badge
dialogs. The caller passes the full localized button label, because
"Paste action", "Paste card" etc. can't be built from one shared string
in every language.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NYDqLWMHjPkhtbWCf9EA1M

* Add generic ha-action-bar and build ha-paste-bar on it

ha-action-bar takes its actions through the default slot and exposes
--ha-action-bar-border-top so the border can match the parent, with the
divider border as default. ha-paste-bar now extends it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NYDqLWMHjPkhtbWCf9EA1M

---------

Co-authored-by: Claude <[email protected]>
2026-10-22 14:37:49 +00:00
Paul Bottein 5ec6f5a08d Make the max sections setting of sections views easier to understand (#54962)
* Make the max sections setting of sections views easier to understand

* Simplify the max sections form handling

* Fall back to 5 sections when the custom max sections input is cleared

* Use a dropdown with a custom number for the max sections setting

* Keep the custom max sections value as typed
2026-10-22 14:37:33 +00:00
Mason Liu 6272ee6eed Allow picture and source elements in markdown (#55155)
* Allow picture and source elements in markdown

The markdown sanitizer escaped <picture> and <source>, so release notes using the light/dark image pattern showed the raw tags as text. Allow them, including <source> inside <video> and <audio>, and apply the same URL protocol check to srcset that xss applies to src.

* Harden srcset validation and rewrite source URLs in READMEs

Split srcset strictly at commas and whitespace, allow only plain width or
density descriptors, check every URL with the same safeAttrValue check as
src, and write the value back with & escaped so the browser reads exactly
what was checked. This closes bypasses with commas without spaces, encoded
commas, nested entities and parentheses in descriptors.

rewriteHtmlUrls now rewrites source[src] and every URL in source[srcset]
like img[src], so README pictures resolve to the repository and cannot
point at the Home Assistant server.

* Simplify srcset rewriting
2026-10-22 16:36:26 +02:00
Paulus SchoutsenandClaude Opus 5.5 debc9685fb Show integration brand icons in the LoRaWAN devices table (#55187)
* Use the default row height for LoRaWAN devices on mobile

The devices table set a 132px row height on screens up to 600px wide.
Narrow screens show only the two-line device name column, and the
group headers also take the row height, so rows and group headers were
much too tall. Narrow screens now use the same 72px row height as wide
screens and the other config tables.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011gT7Yr2jEnpJ3VmKcCV7gy

* Show the integration brand icon of LoRaWAN devices

The devices table now has a first column with the brand icon of the
integration that owns the linked Home Assistant device. The column
also shows on narrow screens. Devices without a linked device show no
icon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011gT7Yr2jEnpJ3VmKcCV7gy

* Update LoRaWAN device brand icons when the theme mode changes

The columns are memoized, so the icon template kept the light or dark
variant that was active on the first render. Dark mode is now an input
of the memoized columns.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011gT7Yr2jEnpJ3VmKcCV7gy

---------

Co-authored-by: Claude <[email protected]>
2026-10-22 16:18:31 +02:00
Aidan Timson fd47b899b4 Add query params to the blueprints table (#55166) 2026-10-22 14:16:51 +00:00
d0e8ff1184 Don't cache signed URLs in the service worker (#55079)
Signed URLs (media files under /media/ carry an authSig) are different on
every request, so the catch-all file-cache route stored a new entry for
each one without ever serving it again. A photo slideshow filled Cache
Storage with one full-size image per slide until the origin quota ran out.
Fetch these from the network only.

Co-authored-by: Gudui <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 16:14:02 +02:00
7aad9aa344 Polish chart zoom on timelines and the zoom slider (#55178)
* Polish chart zoom on timelines and the zoom slider

- Keep timelines from jumping when the zoom slider appears
- Match the slider gap on timelines to line charts
- Double click or double tap the zoom slider to zoom back out
- Show a line at the tapped time on timelines, like on line charts

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Ignore drags for slider double tap and follow theme for timeline line

- Only count a press released where it started as the first tap, so a
  quick drag followed by another press no longer resets the zoom
- Color a time axis's own pointer line in the chart base, so it follows
  theme changes

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Update src/components/chart/ha-chart-base.ts

Co-authored-by: Petar Petrov <[email protected]>

* Color the timeline axis pointer from the chart theme

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
2026-10-22 17:01:08 +03:00
Aidan Timson 993fb3322b Add query params to the scripts table (#55172) 2026-10-22 13:53:31 +00:00
WendelinandClaude Opus 5.5 4922427095 Add paste bar to add card/badges (#55183)
* Add paste bar to create card dialog

Replace the clipboard card tile in the create card dialog with a paste
bar below the entity tree and below the card grid, like the add
automation element dialog. Remove the cancel button footer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ER3ZMdmrCUzqkfaNf1Yv9S

* Add paste bar to create badge dialog

Replace the clipboard badge tile in the create badge dialog with a paste
bar below the entity tree and below the badge grid, like the create card
dialog. Remove the cancel button footer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ER3ZMdmrCUzqkfaNf1Yv9S

* Add CSS properties for padding in badge and card pickers

* Refactor CSS variable names for badge and card pickers to include 'ha-' prefix

* Update styles for badge and card dialogs to use border-top instead of background color

---------

Co-authored-by: Claude <[email protected]>
2026-10-22 13:34:03 +00:00
Aidan Timson b754d16b3e Add query params to the scenes table (#55173) 2026-10-22 15:16:57 +02:00
WendelinandClaude Opus 5.5 323ad45d11 Make data tables usable with keyboard and screen reader (#55181)
* Improve screen reader support in the helpers table

- Make data table group headers real rows and count them in
  aria-rowcount, so row positions no longer exceed the row count.
- Name the row overflow menu after its row ("Actions for <name>")
  instead of a generic "Overflow menu" on every row.
- Label the helper error icon and the "Unmanageable" icon in the
  editable column.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

* Make the row name a button in clickable data tables

Clickable rows could only be opened with a mouse, and nothing told a
screen reader that the row does anything. The name in the main column
is now a native button, so it is reachable with Tab, opens with Enter
and is announced as a button. Its click bubbles to the existing row
click handler.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

* Only make table labels focusable where clicking them does something

Label chips in data tables were always buttons with a tab stop, but only
automations, scenes and scripts handle a click (filter by label). Add a
clickable property and set it on those pages, so labels elsewhere are
plain text and no longer an empty tab stop.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

* Make data table sorting keyboard accessible and fix the row focus ring

- Sortable column headers now contain a button, so sorting works with
  Tab and Enter. The sort arrow shows on focus like on hover, and focus
  stays on the header after a keyboard sort.
- The row name button's focus ring was clipped by its cell. Outline the
  whole row instead while its name is focused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

* Enhance clickable data table accessibility by ensuring main values are rendered in buttons without interactive elements and improve focus styles for sortable header cells.

* Add fallback for scene navigation to show info when ID is missing

---------

Co-authored-by: Claude <[email protected]>
2026-10-22 15:15:25 +02:00
renovate[bot] 27291b7053 Update dependency core-js to v3.51.0 (#55180) 2026-10-22 15:04:46 +02:00
renovate[bot] 41789feb76 Update dependency browserslist to v4.29.4 (#55184) 2026-10-22 15:03:04 +02:00
Marcin BauerandClaude Opus 5.5 9153ce4945 Fix success line through disabled conditions in trace graph (#55182)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-22 15:00:30 +02:00
Aidan Timson b0bdf549d7 Keep URL params on their own page when a page reconnects or redirects (#55176) 2026-10-22 14:48:20 +02:00
WendelinandClaude Opus 5.5 71425dfa5b Tabs subpage improve tabs a11y (#55165)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-22 11:54:24 +00:00
Aidan Timson 7883bee61d Keep the logs page provider, search and raw view in the URL (#55098)
* Keep the logs page provider and search in the URL

* Link to the logs with search instead of filter

* Keep the app logs search in the URL

* Pass the logs search and raw view through My links

* Remount the app view when switching apps so the logs search follows the URL

* Keep the logs provider and edits in the URL while apps load

* Keep the trailing slash when switching log providers

* Format the logs page

* Keep expanded log lines as new lines arrive and show single hidden lines

* Note in the query params skill that search text is search

* Keep the search param note in the skill to the rule

* Only show single hidden lines around search matches
2026-10-22 14:38:30 +03:00
WendelinandClaude Opus 5.5 3bd5e99243 Add badge cut/copy toasts (#55177)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-22 13:38:04 +02:00
Aidan Timson fa72f6545b Add query params to the backups table (#55137)
* Keep backups search, filters, sorting and grouping in the URL

* Add query params to the backups My redirect

* Use the data table settings controller on the backups page

* Compare backup filter lists regardless of order

* Test the backups query params
2026-10-22 14:32:34 +03:00
Aidan Timson a1f54064cf Add query params to the dashboards and dashboard resources tables (#55136)
* Keep the dashboards and dashboard resources table search, sorting and grouping in the URL

* Add query params to the dashboards and resources My redirects

* Wait for dashboards to load before syncing the URL

* Use the data table settings controller on dashboards and resources
2026-10-22 14:30:26 +03:00
Aidan Timson 7ab69e7da0 Add query params to the labels table (#55134)
* Keep the labels table search and sorting in the URL

* Add query params to the labels My redirect

* Use the data table settings controller on the labels page
2026-10-22 14:29:30 +03:00
renovate[bot] 246a88b142 Update dependency core-js-compat to v3.51.0 (#55179) 2026-10-22 11:28:53 +00:00
Pradeep Peddineni 71b7218b15 Detect reordered keys in the automation and script YAML editors (#55161) 2026-10-22 13:27:54 +02:00
Aidan Timson beb089822a Compare helpers and Marketplace filter lists regardless of order (#55169) 2026-10-22 14:21:53 +03:00
Aidan Timson 693940448d Add query params to the Bluetooth monitor tables (#55174)
* Add query params to the Bluetooth monitor tables

* Pass Bluetooth monitor table params through My links

* Keep the Bluetooth monitors' search with the table settings
2026-10-22 14:21:26 +03:00
Aidan Timson 0f628702d8 Add query params to the voice assistants expose table (#55175) 2026-10-22 14:20:59 +03:00
Maarten Lakerveld 86980368a9 Add My links for the LoRaWAN panel (#55168)
config_lorawan opens the overview and lorawan_devices opens the devices
table, forwarding search, sort, direction, group and connection, the
params the devices page keeps in the URL. Both need the lorawan component,
like config_zha and config_zwave_js.
2026-10-22 13:11:50 +02:00
Lucas Mindêllo de AndradeandMaarten Lakerveld d9f7180aa5 Add problems to the Maintenance Dashboard (#54743)
* feat(maintenance): add problems to the dashboard

* test(maintenance): filterProblemEntities

* test(maintenance): additional tests

* Apply batched suggestions from code review

Co-authored-by: Maarten Lakerveld <[email protected]>

---------

Co-authored-by: Maarten Lakerveld <[email protected]>
2026-10-22 11:05:44 +00:00
Paul Bottein 66b084da01 Simplify card resize editor (#55120)
* Add recommended size switch to card layout editor

* Limit the estimated card height to the picker rows
2026-10-22 12:23:02 +02:00
Bram KragtenandClaude Opus 5.5 df7852f2ea Show landing page errors from Supervisor logs and failed updates (#55158)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 11:14:49 +02:00
Jan-Philipp Benecke dbcef18ec4 Allow to exclude from the target details (#55123)
* Allow to exclude from the target details

* Process code review comments

* Process code review comments

* Process code review comments and avoid round tripping
2026-10-22 11:55:28 +03:00
Jan-Philipp Benecke bea6891f87 Add exclusions to the target picker (#55091)
* Add exclusions to the target picker

* Add support for excluded entity targets in automation row

* Move target picker exclusions to own gallery page

* Allow exclude in the perform action editor

The target picker shows exclusions now, so a target with an exclude no
longer has to be edited as YAML.

* Support exclude in the location visibility condition

The target selector offers Exclude, and resolving the zones already
handles it, so accept an exclude when validating the condition and in
its editor.

* Drop unused shared flag from target exclusions

No UI uses it yet; it can come back with the follow-up that needs it.

* Resolve exclusions like the target rows and core

What an exclusion takes out of a target is now worked out from the same
filtered entities the target row counts: the row's area, device and
entity filtering moves to filterTargetEntries, used by both. Before, the
picker only filtered entities, so an exclusion could remove entities of
a device the filters hide.

Excludes are resolved with every entity, not only primary ones, like
core does, so an exclusion of a device also takes out its non-primary
entities.

* Keep child devices of included devices excludable

The exclude picker hid included devices along with their child devices,
so an outlet of a targeted power strip couldn't be excluded. Included
devices are still hidden, but their child devices are offered now.

* Pass the raw exclude to the target details

While the picker is still resolving the exclusions, the details opened
from a target row had nothing to show as excluded. They now get the
target's exclude too and work out the exclusions from it themselves.

* Resolve automation row excludes with every entity
2026-10-22 11:55:28 +03:00
Jan-Philipp Benecke 3faa86d2a2 Support exclude block in targets (#55051)
* Support exclude block in targets

* Process code review

* Keep target exclude when replacing or migrating the sole target
2026-10-22 10:48:13 +02:00
Aidan Timson 45ef6ee7dd Show table params from links without storing them (#55144)
* Show table params from links without storing them

* Update the query params skill for links that aren't stored

* Add a data table settings controller

* Use the data table settings controller on Helpers, Users and Marketplace

* Load uncached keys in StorageClass getValue

* Show saved table settings when a kept page opens without params

* Use the default column sort when a kept page has none stored

* Write restored filters after the table sees the bare URL
2026-10-22 11:46:54 +03:00
Maarten Lakerveld e213935ab6 Forward hass to the card from a setter instead of the update cycle (#55149)
Every state change produces a new hass, which hui-card received as a
reactive property and forwarded to its card from update(). That is one
Lit update cycle per card per event even when the card itself ignores
the change, and with cards consuming contexts instead of hass the
wrapper was the largest remaining per-event cost on a dashboard.

hui-card now stores hass in a plain accessor and forwards it, refreshes
the condition evaluator and re-evaluates visibility right in the setter,
so no update is scheduled. The mixin declares hass abstract so a host
can implement it as an accessor; every other host still declares it as
a reactive property.
2026-10-22 11:41:36 +03:00
Petar Petrov 01aba20e77 Link the ESPHome wizard straight to the ZWA-2 page and call the device a device (#55163) 2026-10-22 10:33:29 +02:00
Erwin DounaandCopilot Autofix powered by AI d7a2690cd5 Filter the logs page by integration (#54815)
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-22 10:31:47 +02:00
WendelinandClaude cbf46a9127 Improve screen reader support on the integrations dashboard (#55164)
- Use h2 for the dashboard sections and h3 for the integration cards
  instead of h1 everywhere, and always render the "Configured" heading
  (visually hidden when it is the only section) so the cards are nested
  under a section heading.
- Turn the secondary integration name on discovered, ignored and
  disabled cards into plain text instead of a second heading.
- Give the search field an accessible name.
- Rename the filter button from "Menu" to "Filters" and announce the
  number of hidden disabled integrations instead of the visual badge.
- Label the settings button on disabled integration cards.
- Show a visible focus ring on integration cards.


Claude-Session: https://claude.ai/code/session_01NPve4q2fqCYUyD1qZnDsWM

Co-authored-by: Claude <[email protected]>
2026-10-22 11:14:41 +03:00
Pradeep Peddineni b9be2bec82 Translate the code editor search and go to line panels (#55162) 2026-10-22 08:00:23 +00:00
aee8fe2c60 Drag to zoom charts and add a zoom slider (#55132)
* Drag to zoom charts without a modifier key

A plain mouse drag now selects a time range to zoom into, instead of
requiring Ctrl/Cmd. Panning a zoomed chart moves to horizontal scrolling
(trackpad swipe or Shift + wheel). Touch input is unchanged.

The history panel's reset zoom button moves from a large floating button
in the bottom corner to a small pill in the top-right of the first chart.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Smooth horizontal scroll panning on charts

Apply wheel pan deltas once per animation frame without an update
animation, so the axis follows the gesture instead of restarting an
animation on every wheel event. Handle page-mode wheel deltas, and
record the zoom range on silently synced charts so panning them starts
from the right window.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Only pan charts on horizontal scroll when the axis is zoomed

A Sankey roam also sets _isZoomed without changing the axis zoom range,
so a horizontal swipe over a zoomed Sankey was swallowed without moving
anything. Check the axis range instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Pan charts against the plot width and require an x-axis for drag zoom

The zoom window spans the x-axis plot area rather than the whole canvas,
so convert horizontal scroll distance against the axis pixel extent.
Only enable drag-to-zoom selection on charts that declare an x-axis.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Only drag to zoom on charts with a visible x-axis

Charts such as the energy devices pie declare hidden axes, which gave
the zoom selection a grid to act on. Require a visible first x-axis, and
re-evaluate when the options change, since that card can switch between
pie and bar without the chart being rebuilt.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Fix chart setup crash on charts without an x-axis

ensureArray(undefined) returns undefined, so reading its first element
threw for charts such as the Sankey and sunburst that declare no x-axis.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Leave modifier and touch wheel gestures to chart zoom

ECharts zooms on the wheel with Ctrl/Cmd held (which is also how a
trackpad pinch is reported) and always on touch devices, so panning on
the same event made one gesture both pan and zoom.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Add a zoom slider and touch-friendly chart interactions

- Show a switch-styled zoom slider below zoomed charts: drag the range
  to pan, drag its ends to resize, tap the track to jump, with keyboard
  support. Space for it is reserved so zooming does not shift layout.
- Put the reset button next to the slider, and move the synced history
  reset into the top app bar.
- On touch, a finger drag moves the tooltip instead of panning, the
  axis pointer handle is gone, and tapping outside hides the tooltip.
- Time double taps from release to the next touch.
- Read the zoom window from the chart after a drag selection, so charts
  without both axis bounds register the zoom.
- Keep chart gestures from dragging the bottom sheet.
- Stop doubling the history and logbook padding in the more-info view.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Try an always-on, muted zoom slider next to the ECharts slider

- Always show the zoom slider on charts with a zoomable x-axis, with the
  reset button only once zoomed.
- Use neutral theme colors for the slider.
- Experiment: add the built-in ECharts slider above ours, with a data
  preview line. Its track, range and grips are drawn as rounded pills in
  CSS behind the canvas, over invisible ECharts handles, so it matches
  our slider.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Move History controls into a floating bar with one shared zoom slider

- Extract the chart zoom slider into ha-chart-zoom-slider, drop the
  ECharts slider experiment, and use neutral colors at 32px.
- Show a chart's own slider only while zoomed, keeping its space
  reserved; synced History charts hide it.
- History: replace the sources side pane and toolbar with a floating
  bar: an energy-style date range with Now, previous and next, the
  menu, a full-width Sources chip with clear, and one zoom slider with
  reset that drives all charts. The date range shows the zoomed span.
- Cap History and Logbook content at 2000px.
- Fix synced charts zooming each other in a loop: ECharts reads silent
  from the dispatchAction options, not the payload.
- Let a plain scroll over a chart scroll the page; only Ctrl/Cmd or a
  pinch zooms, and a Cmd released outside the window no longer sticks.
- Let chips fill the width they are given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Show the chart zoom slider only while zoomed, in room taken from the plot

Charts no longer reserve space below the plot. While zoomed, the plot gives
up room at its bottom for the slider, which fades in. The slider is slimmer,
has hover states, and uses the switch thumb colour so it shows in dark mode.

History keeps its sidebar and custom time span, with one shared zoom slider
in a floating bar below the charts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Move the History zoom reset button next to the shared slider

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Fix touch, zoom state and slider issues in chart zooming

- Keep tap-to-show tooltips on touch; a finger drag moves the tooltip only
  on time-axis charts, and a page scroll hides it
- Switch drag to zoom per input type, so touchscreen laptops zoom with a mouse
- Ignore right-click and sloppy clicks for drag to zoom
- Keep zoom windows inside the axis, in percent, and across chart rebuilds
- Grow content-sized timelines for the slider instead of squeezing their rows
- Slider: grabbable at small ranges, one pointer at a time, reversible arrow
  keys, value text for screen readers, kept while in use
- History: zoom bar is a card without a double safe-area inset; one
  hide-zoom-controls flag replaces the two hide flags

* Keep the zoom slider from staying grabbed after a missed release

When the release of a drag never reached the slider, it stayed grabbed and
moved with the next hover. A move without a pressed button now ends the drag,
and any new primary press starts a fresh one.

* Only listen for outside taps while a chart tooltip is open

Every chart on a touch device listened to all taps on the page and hid its
tooltip on each one, even when none was open. The listener is now added when
a tooltip opens and removed when it closes, as before.

* Finish drag to zoom when the mouse is released outside the chart

zrender only follows a drag outside the chart after a move inside it, so a
quick drag out lost the release and the selection. A press that starts drag
to zoom now captures the pointer on the chart, so the release is always seen
and zooms to the selection up to the chart edge.

* Release a held zoom slider on a chart press and bound thumb values

- Drag to zoom cancels a press on the plot, so a focused slider thumb never
  blurred and the slider stayed at the full range; a press on the chart now
  releases it
- Each thumb reports the range the other thumb leaves it as its ARIA bounds
- A drag selection reads the zoom window from the model instead of cloning
  all options

* Only grow timelines for the zoom slider, not every fixed-height chart

A pixel height does not mean a chart is sized to its content: the statistics
clear range dialog gives its chart a fixed 240px, and zooming grew it 36px
past its host. Timelines now opt in with grow-for-zoom-slider; other charts
give up plot height for the slider.

---------

Co-authored-by: Jelmer Borst <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: marcinbauer85 <[email protected]>
2026-10-22 09:55:25 +02:00
Bram KragtenandClaude Opus 5.5 3b850ed99b Stop the snap bottom sheet from blocking page scrolling (#55108)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 08:52:53 +02:00
3a9366efb3 Automation editor: support for negated state trigger (#27704)
* Support negated state triggers in the automation editor

Add "Is" / "Is not" choices to the from and to fields of the state
trigger, so not_from and not_to can be edited in the UI. Built on the
choose selector, which now forwards context to its inner selector.

Co-Authored-By: Niklas Wagner <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Support negated state triggers in the HaStateTrigger and update tests for description

* Move from/not_from exclusivity check into checkUiSupport

The row and sidebar use the static check for their YAML mode warnings,
so a trigger with both from and not_from (or to and not_to) now shows
the warning there too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep state trigger keys in place and avoid from A to A

Saving a match value no longer moves from/to to the end of the trigger,
so editing an unrelated field doesn't reorder the saved config.

When switching back from "Is not" to "Is", the states the other field
already matches are dropped, since a trigger from A to A never fires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Don't modify ha-form's data in the state trigger editor

The value-changed handler rewrote ha-form's own data object, turning the
match values into plain arrays. When ha-form fired again before fresh data
was passed down, like the attribute selector clearing the attribute after
an entity change, from/to/not_from/not_to were dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Test that the state trigger editor survives a repeated ha-form event

Guards against rewriting ha-form's data again: a second event before new
data is passed down, like the attribute being cleared after an entity
change, must keep from/to and not_from/not_to.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Bram Kragten <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-10-22 08:24:27 +02:00
Joakim PlateandClaude Opus 5.5 3315453f01 Show upcoming events in the calendar more-info dialog (#55013)
* Show upcoming events in the calendar more-info dialog

Add a more-info control for calendar entities that shows the entity's
events in ha-full-calendar, opening on the list view with the same view
options as the calendar card.

The event subscription handling that the calendar card and panel each
implemented is moved into a shared CalendarEventsController, which the
new control also uses.

Also make hass reactive on ha-full-calendar, so its header picks up
translations and language changes, and add gallery pages for the
calendar card and the calendar more-info dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Handle rejected and late calendar subscriptions

Report a calendar whose event subscription is rejected as failed, so
the calendar card does not stay loading. Ignore subscribe calls while
the host is disconnected, so a subscribe that was awaiting
unsubscribeAll() cannot outlive a closed dialog, card or panel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Follow theme changes in calendar more-info and open gallery dialogs

Recompute the calendar colors of the more-info dialog when the theme or
dark mode changes and fetch the events again, so event text keeps a
readable color.

Open the event detail and editor dialogs from the calendar gallery
demos, providing the mocked hass on the demo element so the dialogs
receive it too, and accept event changes in the mocks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Show a spinner while calendar more-info events load

Hide the calendar until every calendar has answered, as the card does, so
the list view does not show an empty state while events are loading.
Consume the internationalization context in ha-full-calendar instead of
making hass reactive, so the header only re-renders on language changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Show a fixed list of the next 7 days in calendar more-info

Drop the view selection and navigation from the dialog, as suggested in
review, and link to the calendar panel for anything beyond the next week.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Title days by relative date in the calendar more-info list

Follow the design review: title days "Today", "Tomorrow" and then by
weekday and date without the year, show only the start time of events to
leave room for their titles on mobile, and explain an empty week.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep the hidden calendar in flow while more-info events load

The spinner overlay shared the loading class with the hidden calendar, so
both were positioned absolutely and the loading area collapsed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Fix the calendar more-info list on FullCalendar 7

FullCalendar 7 shows its default date for a `false` alternative day format,
and its theme gives list times half the width and cuts titles off. Format
the alternative date empty, narrow the time column and wrap titles on
narrow calendars, and show a fetch error as the empty state instead of an
empty box under the warning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018HeUwZuZPyBrC2ggMJLFgD

* Wrap list times on narrow calendars and word the dialog fetch error

Narrowing the list time column cut time ranges off on every narrow
calendar, so let times wrap instead. Use a singular error message for the
one calendar in the more-info dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018HeUwZuZPyBrC2ggMJLFgD

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 08:32:59 +03:00
Bram KragtenandClaude Opus 5.5 c007555044 Guard missing entities in group row and wake word step (#55159)
* Guard missing entities in group row and wake word step

The group entity row threw when a group contained a nested group
that does not exist. The wake word step looks up the mute switch
once, so it threw when that switch was disabled or removed while
the step was open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Guard missing assist satellite state in wake word step

The wake word step threw when the assist satellite had no state, which
can happen briefly while it is being disabled or removed. A missing
state now counts as not idle, so the step shows its spinner. The check
step already reads this state with optional chaining.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-22 08:23:04 +03:00
Niels 17199935f3 Close automation and script editor sidebar with Escape (#55157) 2026-10-21 21:32:13 +02:00
Petar Petrov b0316a046c Add Bluetooth toggling to the ESPHome setup wizard (#55148)
* Add Bluetooth toggling to the ESPHome setup wizard

* Only show the ESPHome wizard for Bluetooth when the device has its switch
2026-10-21 20:37:27 +02:00
Paul Bottein ba07b16b24 Add conditional inputs to blueprints (#55116)
* Hide blueprint inputs and sections based on their visible condition

* Make visibility helpers take the visible option instead of the schema

* Move combineVisible to the form condition helpers

* Keep null defaults when evaluating blueprint input conditions

* Fall back to defaults for cleared blueprint inputs and expand sections with required inputs
2026-10-21 20:17:08 +02:00
Wendelin f0b65853c8 Refactor media player more-info (#55152)
Refactor media player controls to improve source and sound mode handling
2026-10-21 20:13:52 +02:00
Paul Bottein 4066e4190c Add device section strategy (#54930)
* Add device section strategy

* Use the inherited area of child devices in the device section heading

* Keep device entities without a state in the device section

* Add a stub config to the device section strategy
2026-10-21 20:18:09 +03:00
Jan-Philipp Benecke 8517a42959 Restore editing trigger ID from the UI (#55124)
* Restore editing trigger ID from the UI

* Process code review

* sigh .. should commit everything

* Fix trace path details

* Rename
2026-10-21 19:15:57 +02:00
Maarten Lakerveld 92599a8803 Raise legacy browser floors to Chrome 67 and Safari/iOS 15 (#55138)
* Raise legacy browser floors to Chrome 67 and Safari/iOS 15

Every supported browser now has BigInt, which cannot be transpiled.
Chrome 67 keeps Fire tablets on Amazon WebView 70+ (Fire OS 5.6.4+ on
the 2017 models, Fire OS 6.3.1.2+, Fire OS 7) and the Chromecast 1st gen
(Chrome 70); it drops WebView 59 builds (Fire OS 5 on the 2015 and 2016
models, Fire OS 6.3.0.1). iOS/Safari 15 drops the iOS 12 devices (iPad
Air 1 / mini 2 / mini 3, iPhone 5s / 6 / 6 Plus); no Apple device ends on
iOS 13 or 14, so 15 costs the same devices as 13 or 14 would. The Chrome
floor, not Safari, decides what the legacy build transpiles.

Terser legacy ceiling goes to ecma 2019, supportsVectorMaps() no longer
feature-detects BigInt, and bundle-budget.json is re-seeded from a
clean build at dev 45ee91aed2 (legacy app -6.4%, core -15.3%).

* Drop workarounds for browsers below the new legacy floor

With Chrome 67 / Safari 15 as the floor, every supported browser has
ResizeObserver, Element.getAttributeNames, IntersectionObserver,
AbortController, an unprefixed AudioContext, PointerEvent and a
MediaQueryList that is an EventTarget, and iOS has ignored
-webkit-overflow-scrolling since 13. Remove the polyfills, runtime
gates and prefixed fallbacks that only existed for the old floor, and
switch MediaQueryList listeners to addEventListener("change").

* Pin the legacy floor at Safari/iOS 15.4 and Firefox 98

Native <dialog>, which ha-drawer, the popovers and wa-dialog rely on,
landed in Safari 15.4 and Firefox 98. Pinning there keeps the stated
floor honest and costs no device: every iOS 15 device updates to 15.8,
Catalina gets Safari 15.6.1, and Firefox below 98 is not supported by
Mozilla anyway.

* Tell browsers below the legacy floor why nothing loads

A browser that cannot parse the legacy bundles used to sit on the launch
screen forever with no explanation. Probe BigInt, the feature that sets
the Chrome floor, in the inline legacy script and write a message into
the launch-screen info box instead of loading bundles that will not run.
2026-10-21 17:45:23 +03:00
WendelinandClaude Opus 5.5 fdb169254c Redesign integration cards with a compact layout (#55147)
* Redesign integration cards with row layout and attention summary

Rebuild the integration card on ha-row-item: brand icon with a status
badge, wrapping integration name, and a supporting text that shows how
many devices, services, entities or entries need attention. Discovered,
ignored and disabled cards share the same row layout with actions on
their own right-aligned line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Refactor integration card layout and improve attention state handling

* Enhance integration card state handling and accessibility features

* Refactor integration card status handling and extract supporting text logic

* Refactor integration card status messages for improved clarity and consistency

* Enhance integration card styling by improving overflow handling for supporting text

* reduce comments

* Refactor integration card status handling and improve summary logic for attention states

* Refactor supporting text computation and rendering for improved clarity

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-21 17:43:38 +03:00
Petar Petrov 44cdb2d357 Fix FullCalendar layout and time zone on older Chromium (#55139) 2026-10-21 16:43:01 +02:00
fdcda1ec98 Improve paste in automation add Element dialog (#55145)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: marcinbauer85 <[email protected]>
2026-10-21 16:41:18 +02:00
JamesFromIT 8fa72595de Add toast messages when a card is cut or copied (#55126) 2026-10-21 16:37:48 +02:00
renovate[bot] 6b60b98b95 Update dependency maplibre-gl to v6.15.0 (#55146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-21 17:22:22 +03:00
Paulus SchoutsenandClaude Opus 5.5 01b2e6e657 Don't reload the frontend for an update while a flow dialog is open (#55089)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-21 16:22:18 +02:00
Einar Skreslett f5ad2e0cac Scroll picker lists with 12 or fewer items to the selected item when they open (#55128) 2026-10-21 14:41:19 +02:00
Paul Bottein 2b0603c431 Use a single paste action for dashboard sections (#55140) 2026-10-21 14:27:35 +02:00
Aidan Timson c608ceee21 Add query params to the users table (#55135) 2026-10-21 13:26:40 +02:00
Aidan Timson fb9e9ed01d Add a frontend skill for URL query params (#55115) 2026-10-21 13:14:02 +02:00
Petar Petrov fc8a8702b9 Show narrow month calendar dots side by side again (#55058) 2026-10-21 11:10:47 +00:00
caa15c26d5 Add recently used items to the automation add TCA dialog (#54958)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: marcinbauer85 <[email protected]>
2026-10-21 12:18:19 +02:00
Aidan Timson 45ee91aed2 Add display and action options to the alert card and badge (#54884)
* Share tile action and picture helpers with the entity badge

* Add tile card options to the alert card and badge

* Use contexts for alert card and badge actions and pictures

* Match the alert card and badge content options to the tile card and entity badge

* Drop hass from the alert card and badge

* Keep the alert card and badge focused on alert options

* Show only the state on the alert badge
2026-10-21 10:51:43 +02:00
Alexander Scrimgeour 22164c336d Fix: bulk label selection for devices (#55112) 2026-10-21 08:17:47 +00:00
Petar Petrov 88b2b434b4 Show energy compare banner on the energy Summary view (#55129) 2026-10-21 09:14:45 +02:00
Einar Skreslett 5c80d18f86 Replace dropdowns for media player source and sound mode with pickers (#54870) 2026-10-21 08:47:54 +02:00
Aidan Timson 87f016f451 Add query params to the helpers table (#55106)
* Keep helpers table search, filters, sorting and grouping in the URL

* Pass helpers table params through My links

* Match helpers against every category in the filter

* Keep the table params when leaving the helpers add path

* Open the helpers add dialog once the table has written its params
2026-10-21 08:18:08 +03:00
karwosts 624865b2a9 Add accessibleLabel to date & time entity rows (#55127) 2026-10-21 07:32:58 +03:00
karwosts a321b07bba Fix gulp stops watching for translation changes (#55114) 2026-10-20 19:25:34 +02:00
Aidan Timson cfd5b4fd8d Pass the grouping through the Marketplace My redirects (#55113) 2026-10-20 19:17:12 +02:00
Bram KragtenandClaude Opus 5.5 2944efe1b7 Declare Babel plugins the build loads by name (#55110)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-20 14:18:09 +01:00
Bram KragtenandClaude Opus 5.5 9ec5e94c29 Remove the ui.common wildcard from LocalizeKeys (#55109)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-20 14:17:40 +01:00
Bram KragtenandClaude Opus 5.5 bcb123850d Fix the QR scanner on browsers without a native BarcodeDetector (#55111)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-20 14:16:11 +01:00
WendelinandClaude Opus 5.5 af4959b57a Use WA accessible-label for controls without a visible label (#55104)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-20 12:06:22 +00:00
CarcaBotandCarcaBot 00ef6bf8b3 Filter long media browser folders locally when the source cannot search (#54969)
* Filter long media browser folders locally when the source cannot search

Media sources that do not implement search, such as a TV's channel list,
offered no way to narrow a long folder: the Sony Bravia integration lists
over two hundred channels and the only option was to scroll. Folders with
ten or more items from such a source now get a filter field that narrows
the already loaded items by title, ignoring case and accents, with its own
empty state when nothing matches. Sources that can search keep the
existing search row unchanged.

* Reset scroll on filter input, count accepted children, rename guard

Typing a filter while scrolled down left lit-virtualizer rendering its old
range against the shorter list, which threw in grid view and landed on the
last matches instead of the first. Scroll both the virtualizer and the
content to the top on every filter input.

Count children after the accept filter so a picker does not show the
filter on a folder that displays fewer than FILTER_MIN_CHILDREN items.

Rename isBrowsable to allowsSearchOrFilter and fix the comments.

---------

Co-authored-by: CarcaBot <[email protected]>
2026-10-20 11:32:02 +00:00
Aidan Timson b94a4f947c Show context around log search matches and open app update logs filtered to the app (#55097)
* Show context and expandable hidden lines in log search

* Open Supervisor logs filtered to the app after a failed update

* Keep hidden line buttons and focus as new log lines arrive

* Relabel log buttons and check a snapshot for dialog no-match

* Mute context line text instead of fading coloured output

* Keep the logs dialog's log area visible on short screens
2026-10-20 14:21:54 +03:00
Aidan Timson ff91ae42e7 Keep the start of LTR control select labels visible in RTL (#55100) 2026-10-20 12:56:22 +02:00
renovate[bot] 50c354b31f Update dependency @home-assistant/webawesome to v3.14.0-ha.1 (#55101) 2026-10-20 11:56:19 +01:00
Maarten Lakerveld 5c9e16c153 Stop the config panel from stacking cloud status polls (#55054) 2026-10-20 12:45:35 +02:00
Bram KragtenandClaude Opus 5.5 ede771e271 Restore FormatJS Intl polyfills on browsers without BigInt (#55070)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-20 11:59:19 +02:00
renovate[bot] 218bc3e82c Update dependency @versatiles/style to v7 (#55060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-20 11:40:11 +02:00
Aidan Timson 9ad2fe3b47 Use DialogMixin for the logs dialog (#55096) 2026-10-20 11:09:49 +02:00
Aidan Timson 4c4b264f8b Clip control select option labels instead of using an ellipsis (#55095) 2026-10-20 12:08:23 +03:00
Erwin Douna f633622335 Show Supervisor logs in a dialog from app errors (#55059) 2026-10-20 08:13:07 +00:00
Paul Bottein efc6b95e22 Revert "Use the neutral color for the cancel button in confirmation dialogs" (#55094)
* Revert "Use the neutral color for the cancel button in confirmation dialogs (#55075)"

This reverts commit be805da4e2.

* Use the right attribute names in the dialog footer guidance
2026-10-20 08:00:52 +00:00
408888668d Allow themes on all panels (#54801)
* Change hardcoded background colors on multiple panels

* Fix typos

* Fix background coloring issues

* Centralize background system, fix trace, assist and sidebar

* Bugfixes

* Fix color issues

* Fix a typo

* Remove theme controllers, double backgrounds and fix default theme issues

* Fix background attachment issues

* Apply batched suggestions from code review

Co-authored-by: Wendelin <[email protected]>

* Fix a typing error

* fix prettier

---------

Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-10-20 09:47:29 +02:00
Giannis Giannopoulos d34ec423c1 Fix graph drawn outside the card after a resize during its animation (#55014)
* Keep graph viewBox matched to its coordinates while animating

Graph producers compute coordinates for the element size at the time
they set them, but hui-graph-base measured clientWidth/clientHeight on
every render. The coordinate morph renders every frame for 350 ms, so a
resize during it drew the coordinates in a viewBox of another size: the
line was cropped (narrower) or fell short of the edges (wider) until
the next update - up to a minute for the sensor card graph footer.

Store the size with the coordinates and keep it for the viewBox until
new coordinates arrive; a resize in between is handled by SVG scaling.
When the size changed between two updates, scale the line on screen
into the new size before animating, so the morph starts without a jump.

* Add regression test for graph viewBox during a resize while animating

* Remove hui-graph-base component test

The repository testing guidance excludes rendering tests; the scaling
logic stays covered by the scaleCoordinates unit tests.
2026-10-20 07:18:11 +00:00
Marco HillgerandSeoFood 2dbe163279 Fix condition descriptions of if actions in trace step details (#54910)
* Describe conditions of if actions in trace step details

Core traces the conditions of an if action as if/condition/<index>,
while the config keeps them directly in the if list. The step details
stopped at that list and passed it to describeCondition, which failed
with "Cannot read properties of undefined (reading 'includes')".

Skip the condition segment when the current config is a list, and move
the lookup into a helper so it can be tested.

* Resolve template and entity list conditions in trace step details

Stop at the entity_id segment instead of at strings, skip the condition segment after if, and resolve index 0 of a single item to the item. Show the entity of an entity_id step instead of the whole list.

* Read the previous trace path segment from the split path

---------

Co-authored-by: SeoFood <[email protected]>
2026-10-20 05:59:12 +00:00
karwosts a2bc636ac7 Use a fast version of getting backup state for restart-dialog (#55085) 2026-10-20 08:28:40 +03:00
Paulus SchoutsenandClaude 4bf40b7bb2 Hide duplicate integration name on discovery cards (#55090)
Hide duplicate integration name on integration action cards

Discovery cards show the discovered name and the integration name. When
both are the same, the card shows the same text twice. Show the name once.

The shared action card also renders ignored and disabled entries, so
those cards get the same behavior.


Claude-Session: https://claude.ai/code/session_01Qoq4i3tNHjtSuANwHyPYzv

Co-authored-by: Claude <[email protected]>
2026-10-20 08:27:46 +03:00
karwosts ea759b723e Fix blueprint import redirect links (#55087) 2026-10-20 07:20:37 +02:00
Petar Petrov 368b58ad26 Add label position option to the history graph card (#55081)
* Add name position option to the history graph card

* Rename history graph name_position option to label_position

* Hide history graph label position when no entity is shown as bars

* Match history graph label position visibility to the card's timeline rule
2026-10-19 18:14:25 +02:00
shazilkhan 4ba2cb9509 Keep analog clock hands in sync after the page becomes visible again (#54957) 2026-10-19 15:40:25 +00:00
renovate[bot] b849328d71 Update dependency eslint to v10.13.0 (#55084) 2026-10-19 17:12:34 +02:00
Marcin BauerandClaude Fable 5.1 ceef27e9d3 Hide empty related sections and zero-count quick links on device and area pages (#55083)
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-19 16:03:06 +01:00
Raman GuptaandClaude Opus 5.5 31636859cc Fetch the flow after subscribing to progress events (#54866)
* Fetch the flow after subscribing to progress events so a fast progress task can't strand the dialog

Core fires data_entry_flow_progressed when a progress task finishes,
whether or not a client has subscribed yet. The dialog only subscribes
after it has rendered the progress or external step, so a task that
finishes in that window leaves the dialog on the progress screen while
the backend has already moved on.

Once the subscription is established, fetch the flow once and process
the result if the step changed. Events that arrive during that fetch
trigger another fetch afterwards instead of a concurrent one, because
two concurrent fetches can both advance the flow and the second one
fails once the flow has finished.

* Limit flow catch-up fetch to progress steps and handle review feedback

- Only fetch on subscribe for progress steps, since fetching re-runs the
  current step and external steps can treat that as the user returning.
- Treat a progress step with a new progress_action as a new step, since
  flows can move on to another task without changing step_id.
- Retry the catch-up fetch after a failure if a progressed event arrived
  while it was in flight, instead of dropping that event.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-19 17:20:25 +03:00
Paul Bottein be805da4e2 Use the neutral color for the cancel button in confirmation dialogs (#55075) 2026-10-19 15:27:02 +02:00
tzagim dcb26f221a Show Preparing while an update has not started installing yet (#55008) 2026-10-19 15:22:58 +02:00
Aidan Timson 0d9f8b756a Show skipped updates on the Marketplace repository page (#55074) 2026-10-19 15:14:06 +02:00
Paul Bottein 2a51797364 Use warning color in the Marketplace and debug console warnings (#55072) 2026-10-19 14:07:30 +01:00
Paul Bottein 2c8e23e8e1 Add variant to ha-checkbox (#55071) 2026-10-19 14:07:29 +01:00
renovate[bot] 9dec2b362f Update dependency fullcalendar to v7.1.3 (#55068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 15:56:21 +03:00
Paul Bottein aedab4727b Add a triggered by filter to the activity panel (#55064)
* Add a triggered by filter to the activity panel

* Resume live activity on filter change and label the clear button
2026-10-19 14:53:26 +02:00
Marcin BauerandClaude Fable 5.1 a82dc1395f Surface people in the automation target picker (#55066)
* Surface people in the automation target picker

Rename the "Time and sun" section of the "By target" tab to "Other" and
add a People entry next to Time and Sun for triggers and conditions.
People stay listed under unassigned entities as well.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Give the People item its own title on mobile

Tapping People now shows "People" as the dialog title and lists the
people without an "Entities" heading, instead of the integration name.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Use a neutral icon for the People item

Match the Time and Sun entries instead of the blue Person brand icon.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Fix font weight of list rows in the target picker

The rule targeted a non-existent label part, so Time, Sun and label rows
rendered regular while tree rows rendered medium.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Align icons in the Other section

When People is present, render Time and Sun as leaf rows of the same
tree so every row reserves the chevron space and icons line up.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* List every person under People and refresh its selection

Build the People entry from all person entities, including ones assigned
to an area or device, and pass the selected value into the memoized
section so the highlighted person updates.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-19 15:45:40 +03:00
Paul Bottein 68755881b1 Let the history and activity panels compose their source filters (#55063) 2026-10-19 14:41:26 +02:00
renovate[bot] ccf72dee10 Update dependency temporal-polyfill to v1.0.6 (#55069)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 15:40:01 +03:00
49865ec5dd Remove cast row listeners and the Z-Wave S2 subscription on disconnect (#55055)
* Remove cast row listeners and the Z-Wave S2 subscription on disconnect

The cast row registered two listeners on the page-lifetime cast manager
and never removed them, so every rebuild of a dashboard with a cast row
leaked the old row together with its hass snapshot. Keep the disposers,
register on connect and dispose on disconnect.

The Z-Wave dashboard re-subscribed to S2 inclusion after the add-node
dialog closed, but only SubscribeMixin knew about the first subscription,
so the second one outlived the panel and the mixin unsubscribed the first
one twice. Own the subscription lifecycle in the component instead, skip
the re-subscribe when the panel is already gone, and also drop the NVM
backup and restore streams on disconnect.

* Conditionally subscribe to S2 inclusion based on connection

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Keep the NVM backup and restore streams out of the disconnect cleanup

These subscriptions end on their own: the backend sends a final
"finished" event and the message handlers already unsubscribe on it,
so a detached panel is released as soon as the operation completes.
Only the S2 inclusion subscription stays open indefinitely and needs
the disconnect handling.

---------

Co-authored-by: Paul Bottein <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-19 14:33:27 +02:00
Maarten Lakerveld 2f0a3b7eb7 Remove the unreachable scroll-to-top branch from the view tab handler (#55067)
Web Awesome's tab group never emits wa-tab-show for a click or Enter on the
already-active tab, so the same-view branch in _handleViewSelected has not
been reachable by user input since the migration in #26951. It only fires
for the group's own sync events, and once hui-root binds the group's active
property (#54613) those events also fire when the group is re-created after
leaving a subview or edit mode, overriding the restored scroll position.
2026-10-19 14:32:16 +02:00
Petar Petrov b76a3b4c72 Show offline serial ports and skip pairing in the ESPHome serial setup (#55050)
* Show offline ESPHome serial proxy ports and skip pairing for serial setup

A disconnected device still showed its serial ports as configured or
ready to set up, and picking ZHA for a port opened the Zigbee pairing
options first. Show ports missing from the scan as offline, and let the
serial-filtered add-integration dialog start the config flow directly.

* Only show ESPHome serial ports as offline after a USB scan

Without the usb integration nothing is scanned, so a port missing from the
empty usage list is not known to be offline. Keep offering Set up then.

* Rescan ESPHome serial ports when the device connects or drops

A device that reconnected while the wizard was open kept its ports marked
offline until the wizard was reopened, since only config entry changes
triggered a rescan. Rescan when the number of unavailable entities on the
device changes, debounced so one reconnect causes one scan.
2026-10-19 15:29:09 +03:00
Petar Petrovandrenovate[bot] eaa09897ad Update FullCalendar to v7 (#54965)
* Update dependency @fullcalendar/core to v7

* Migrate to FullCalendar 7

FullCalendar 7 replaces the scoped packages with the fullcalendar package,
no longer ships its CSS and handles named time zones through
temporal-polyfill instead of the luxon plugin.

- Add the skeleton and classic theme CSS to the shadow roots, with the
  classic palette mapped to Home Assistant theme variables
- Port the calendar and schedule styles to the v7 class hooks
- Pass event colors as color, contrastColor and className
- Keep the previous event time text, week titles and schedule slot height
- Let rows grow to fit their events with an automatic height
- Load the schedule form only for schedules in the helper settings tab

* Load the schedule form before rendering the helper item

The item was set on the form before its module had loaded, so it shadowed
the form's item setter and left the weekdays undefined.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 15:28:49 +03:00
Aidan Timson 63d1d1b9b8 Drop hass from error-log-card (#55062) 2026-10-19 14:13:25 +02:00
Aidan Timsonandcopilot-swe-agent[bot] bab1485473 Hide skipped updates in the Marketplace by default (#55061)
* Hide skipped updates in the Marketplace by default

* Drop the show skipped updates filter

* Narrow Marketplace skipped update subscriptions

* Work out skipped updates after their states are selected

* Keep the Marketplace table when the same updates are skipped

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-19 14:12:09 +02:00
0x696f6fandClaude Opus 5.5 363a1861e6 Fix view tab ignored after navigating to a hidden view (#54613)
* Fix view tab ignored after navigating to a hidden view

Web Awesome does not activate disabled tabs, so when the dashboard marks
the disabled tab of a hidden view active, the tab group keeps tracking the
previous tab and ignores clicks or Enter/Space on it. Show the picked tab
ourselves when the group ignored it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Drive the view tab group through its active property

Set the tab group's active property from the current view instead of only
marking the tabs, so the group never keeps a stale active tab after
navigating to a hidden view. Also show the tab of a hidden view while it is
the current view. This replaces the fallback in ha-tab-group, as suggested
in review.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-19 12:11:37 +00:00
Maarten Lakerveld 38ac2a01ec Do not start timers or keep subscriptions after an element is removed (#55056) 2026-10-19 10:31:17 +01:00
charishma2k06 7edd0f1c38 Fix entity pictures in automation targets (#54925) 2026-10-19 11:26:31 +02:00
CRYPTONIKAV 313ec9440e Gate welcome banner and dismiss save on loaded config (#54825)
* fix(home): gate welcome banner and dismiss save on loaded config

* chore: retrigger status evaluation

* fix: optional config instead of loaded flag, typed fixtures
2026-10-19 09:14:38 +00:00
5f0fb8ab23 Merge final assistant response into streamed chat text instead of replacing it (#54311)
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-19 11:09:38 +02:00
Lucas DeutschlandandCopilot Autofix powered by AI b9a04ed660 Fix map marker placement for zones with duplicate names (#54895)
* Fix map marker placement for zones with duplicate names

* Ensure entities are spread in ha-map-engine test

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Remove unused zone tracker state import

* Fix legacy map zone grouping and simplify tests

* Fix map zone selection when the active zone is hidden

* Update map markers when referenced zones change

* Align map coordinate fallback with Core zone selection

* Simplify map zone grouping and tests

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-19 09:51:35 +02:00
renovate[bot]andPetar Petrov 60108e1d74 Update dependency js-yaml to v5.5.0 (#55053)
Co-authored-by: Petar Petrov <[email protected]>
2026-10-19 08:45:52 +01:00
karwosts ca685a5c13 Add group role and aria labelling to thermostat card (#55045) 2026-10-19 08:43:23 +01:00
Petar Petrov f008af2751 Fix overlapping horizontal Sankey labels at narrow widths (#55021) 2026-10-19 08:34:09 +01:00
Daan Vervacke 47a0f8863d Fix inconsistent heading spacing on the integrations dashboard (#55037) 2026-10-19 09:32:14 +02:00
renovate[bot] f2bf89ad69 Update actions/setup-node action to v7.1.0 (#55052)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 07:29:57 +00:00
Aidan Timson c2955a8745 Add alert badge (#54881)
* Add alert badge

* Use shared alert domains in the alert badge

* Only alert while the alert badge entity is in an alert state

* Default the alert badge color to red and show its colors side by side

* Use consumeLocalize in the alert badge

* Rename the alert badge colors to alert color and clear color
2026-10-19 09:22:42 +02:00
renovate[bot] b9a5d0af0d Update github/codeql-action action to v4.38.3 (#55048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 07:43:02 +02:00
renovate[bot] 5cfaf7ed40 Update fullcalendar monorepo to v6.1.22 (#55047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 05:31:02 +00:00
karwosts 85a3bcc494 Present hidden select menu label to screen readers (#55032) 2026-10-19 08:11:03 +03:00
karwosts 81f7cbdc22 Apply thermostat accessibility fixes to humidifier and water_heater (#55044)
Apply thermostat localization fixes to humidifier and water_heater
2026-10-19 08:10:30 +03:00
renovate[bot] 8990a42f1b Update actions/upload-artifact action to v7.0.2 (#55046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 08:09:08 +03:00
renovate[bot] 330208a98f Update actions/download-artifact action to v8.0.2 (#55043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 05:05:46 +00:00
renovate[bot] a8a5d8d73a Update home-assistant/actions digest to cd6f363 (#55042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 05:05:27 +00:00
Daan VervackeandCopilot Autofix powered by AI 1801441779 Add selection mode with bulk delete to the labels page (#54987)
* Add selection mode with bulk remove to the labels page

* Seed demo labels and align the registry stub with tags

* Align the label overflow menu with Remove wording

* Trigger event on label removal

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Use Delete wording for label destruction

* Clear deleted labels from entity assignments in the demo

* Clear deleted labels from retired entity entries in the demo

* Copy the seed array in the label registry stub

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-18 19:49:12 +00:00
renovate[bot] 2cdb57b00f Update vitest monorepo to v5.0.5 (#55033)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 15:18:33 +00:00
Aidan Timson a6fe937d45 Add visual editor for the alert card and add clear color (#54880)
* Stop alert card pulse when reduced motion is on

* Add visual editor for the alert card

* Suggest the alert card when adding a card by entity

* Share the alert active states with the alert card

* Only alert while the alert card entity is in an alert state

* Show the alert card colors side by side

* Default the alert card color to red

* Only offer alert entities in the security alerts picker

* Rename the alert card colors to alert color and clear color
2026-10-18 17:10:30 +02:00
ChrisandClaude Opus 5.5 ffb78bd21f Show the local apps folder toggle in backup settings (#54983)
* Show the local apps folder toggle in backup settings

Local apps have the slug local_<slug>, never local, so the toggle for
the addons/local folder was never shown.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Tests for this change

* Remove rendering tests for the local apps toggle

The frontend testing guidelines don't allow rendering tests.

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-18 14:49:19 +00:00
Petar Petrov bb436667d5 Set up Music Assistant through its config flow in the ESPHome wizard (#54973)
* Set up Music Assistant through its config flow in the ESPHome wizard

The Music Assistant config flow now installs, starts and onboards the app
itself, so open it instead of installing the app and waiting for its
discovery flow.

* Don't open the Music Assistant flow after the offer was skipped
2026-10-18 16:44:24 +02:00
Daan Vervacke aca2ba2745 Add selection mode with bulk delete to the blueprints page (#54984)
* Add selection mode with bulk delete to the blueprints page

* Tidy bulk delete state handling and add demo delete stub

* Distinguish failed usage checks from in-use blueprints

* Let fresh usage checks decide which blueprints can be deleted
2026-10-18 16:41:44 +02:00
Daan VervackeandCopilot Autofix powered by AI 807b037650 Add selection mode with bulk uninstall to the Marketplace installed tab (#54996)
* Add selection mode with bulk uninstall to the Marketplace installed tab

* Refactor mock imports in dashboard test

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-18 16:35:12 +02:00
Daan Vervacke 277ae199e5 Add selection mode with bulk delete to the dashboards page (#54993) 2026-10-18 16:30:02 +02:00
renovate[bot] e0aece68aa Update pnpm to v12.14.0 (#55029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 16:24:04 +02:00
Petar Petrov d6449e5707 Fix chart legend squeezed when legend items arrive after setup (#55019)
Resize chart when its legend item count changes
2026-10-18 16:06:12 +02:00
Petar Petrov 54e123c469 Fix Sankey label sizes after zooming (#55023)
The label layout callback gets the node rect in zoomed screen pixels,
so only the slack around the node scales with the zoom. Report the new
zoom before the setOption calls and actions that lay the labels out
again, so a reset or double-click doesn't size them for the old zoom.
2026-10-18 15:35:41 +02:00
Petar Petrov 54f674fd47 Fix Sankey labels on thin nodes showing at full size in Safari (#55020) 2026-10-18 15:34:06 +02:00
renovate[bot] 8a3e7e8e62 Update dependency jsdom to v30.1.3 (#55028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 15:28:43 +02:00
karwosts b8dbf2e419 Improve climate control accessibility (#55024) 2026-10-18 15:28:25 +02:00
Petar Petrov a23759bf63 Handle null cast device capabilities (#55025)
@types/chromecast-caf-receiver 6.0.27 types getDeviceCapabilities() as
nullable, since it returns null until the cast system is ready. This
broke tsc on dev.
2026-10-18 15:10:45 +02:00
karwosts 6f2f62a602 Localize control-number-button aria label (#55026)
* Localize control-number-button aria label

* keep increment

* fix test
2026-10-18 15:10:05 +02:00
renovate[bot] dd1b392b1f Update dependency @types/chromecast-caf-receiver to v6.0.27 (#55015)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 10:20:33 +02:00
Elias LecomteandSeoFood b399dac788 Use server timezone for demo statistics calendar buckets (#54893)
* Use server timezone for demo statistics calendar buckets

* Preserve server timezone in derived energy statistic boundaries

* Clamp demo production day boundaries to the requested end

* Use supported array indexing in demo statistics test

* Align demo sub-day statistics with the UTC bucket grid

* Use server timezone for demo statistics calendar buckets

* Preserve server timezone in derived energy statistic boundaries

* Clamp demo production day boundaries to the requested end

* Use supported array indexing in demo statistics test

---------

Co-authored-by: SeoFood <[email protected]>
2026-10-18 08:30:10 +03:00
Paulus Schoutsen b0f498b5d3 Add LoRaWAN connectivity panel (#54998)
* Add LoRaWAN device overview and connection details

* Add LoRaWAN overview, connections, and topology views

* Match Bluetooth connection list navigation

* Simplify LoRaWAN add-device dialog navigation

* Group LoRaWAN devices by connection and simplify mobile rows

* Fix LoRa icon wave paths

* Use floating Add device action on LoRaWAN overview

* Widen LoRaWAN connection picker and simplify demo connections

* Use public console destinations in LoRaWAN demo

* Show two TTN gateways and one ChirpStack gateway in demo

* Simplify LoRaWAN connection picker heading

* Keep LoRaWAN Add device dialog compact on mobile

* Simplify LoRaWAN device details and widen unsupported-device alert

* Use the brand server for LoRaWAN demo icons

* Revert demo loading screen layout change

* Move LoRaWAN topology helper into the panel

* Select LoRaWAN device in subscription callback

* Only show LoRaWAN info for matching device integrations

* Name LoRaWAN pages by their purpose

* Identify LoRaWAN integrations by their dependency

* Fix LoRaWAN table sorting and column updates

* Refresh LoRaWAN details when navigating between devices

* Fix LoRaWAN device search, grouping, and demo entity IDs

* Treat listed LoRaWAN connections as active

* Use sensecap_lorawan domain in LoRaWAN demo
2026-10-17 14:46:23 -04:00
renovate[bot] 2d71337e12 Update Playwright (#55009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-17 19:51:54 +02:00
renovate[bot] c7064d6dd3 Update dependency @html-eslint/eslint-plugin to v0.67.0 (#55010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-17 19:41:24 +02:00
Raman GuptaandClaude Opus 5.5 7daae8a8cb Fix Z-Wave config parameter status always showing as error (#55002)
The zwave_js/set_config_parameter and set_raw_config_parameter commands
return the zwave-js-server CommandStatus string ("accepted" or
"queued"), not the numeric zwave-js SetValueStatus. #54287 retyped the
status as a number and mapped it through computeSetConfigParamStatus,
which turned every successful update into "error".

Type the status as the string union the backend sends and use it
directly again. The raw variant only returns status, so give it its own
result type without value_id.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 19:55:27 +03:00
renovate[bot] cf2e49aaad Update Node.js to v24.22.0 (#55001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-17 07:34:23 +02:00
Daan Vervacke 545f518a02 Stop numeric data table headers from jumping on hover (#54999)
* Stop numeric data table headers from jumping on hover

* Match the hover pseudo class order in the numeric header rules
2026-10-16 17:12:45 -04:00
Franck Nijhof 9e98e6cdb9 Add Debug console to the Tools panel (#54937) 2026-10-16 19:32:37 +02:00
renovate[bot] dba7a5f238 Update dependency eslint-plugin-lit-a11y to v5.1.3 (#54997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 17:00:17 +00:00
renovate[bot] b453b05e6a Update dependency @rspack/core to v2.2.9 (#54982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 18:55:52 +02:00
Franck Nijhof 73d41a7c6a Update links to the new contributors website (#54995) 2026-10-16 18:55:08 +02:00
renovate[bot] 26a796e308 Update pnpm to v12.13.0 (#54992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 18:53:41 +02:00
renovate[bot] 52a12f08ce Update dependency @versatiles/style to v6.4.1 (#54994) 2026-10-16 17:26:23 +02:00
karwosts a83511ec2f Fix tabs toolbar-icon slot width (#54991) 2026-10-16 06:43:02 -07:00
Øyvind Matheson Wergeland 03447fb4a1 Use separate translation keys for device and service info headings (#54981)
* Use separate translation keys for device and service info headings

The "{type} info" heading cannot be translated correctly in languages
that form compound words.

* Use new translation key for device info heading
2026-10-16 05:42:05 -07:00
Bram Kragten 26690e06b0 Bumped version to 20260930.4 2026-10-16 13:23:16 +02:00
Maarten Lakerveld e7a89e0ac0 Stop the user condition editor from duplicating selected users (#54921)
When the users finish loading, the list picks up the pre-selected items and
fires a selected event for them. The handler appended that user again on
every open of the editor, so a single user condition grew one duplicate id
per edit. Ignore selection events that do not change the configured users.

Fixes #54894
2026-10-16 13:22:57 +02:00
Paulus SchoutsenandClaude eb90a50ca1 Keep hidden columns when reopening data table settings (#54874)
Keep column settings for tables without fixed last columns

The data table settings dialog dropped the stored column order when the
table had no lastFixed columns. With no column order, the dialog showed
every column as visible. Reopening the dialog on the statistics table
then showed hidden columns as enabled again.


Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

Co-authored-by: Claude <[email protected]>
2026-10-16 13:22:56 +02:00
MichaelandClaude Opus 5.5 e221ea2b19 Fix Matter add device button not opening the add device dialog (#54823)
The add device button on the Matter dashboard linked to
/config/matter/add, which navigated to the devices dashboard and opened
the Matter add device dialog without awaiting the navigation. The
navigation closes open dialogs before it completes, so the dialog was
closed again right away and the user ended up on the devices dashboard.

Open the dialog directly from the Matter dashboard, like the Z-Wave
dashboard does, and make the /config/matter/add route (used by the
add_matter_device my link) await the navigation before showing the
dialog from the app root, as its own element is detached by then.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-16 13:22:55 +02:00
Jan-Philipp Benecke abd5f9e985 Fix script graph crash on template shorthand conditions (#54798)
* Fix script graph crash on template shorthand conditions

* Move string to ConditionWithShorthand
2026-10-16 13:22:54 +02:00
Petar Petrov 2f9a4f99be Link Ethernet Matter nodes to Home Assistant in the network map (#54784) 2026-10-16 13:22:53 +02:00
Maarten Lakerveld 0cf9fcef97 Map overview fixes (#54774) 2026-10-16 13:21:22 +02:00
JochenandClaude Opus 5.5 f2210e9081 Fix energy setup wizard skipping the individual devices step (#54977)
The Next button condition was not updated when water was added to the
wizard, so the water step showed Done and the individual devices step
was unreachable. Derive all step checks from one step count.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-16 09:24:07 +03:00
G JohanssonandClaude Opus 5.5 0fdbac3703 Show translated overview labels in ObjectSelector (#54864)
* Show translated overview labels in ObjectSelector

Use the optional `<translation_key>.fields.<field>.overview_label`
translation, with a `{value}` placeholder, to render field values in the
object selector overview. Falls back to the plain value when no
translation exists. Placeholder values are forwarded in config, options
and subentry flows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Forward placeholder values in repair flow selectors

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Forward placeholder values in action, trigger and condition selectors

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-16 08:17:49 +03:00
Bascht74 23d622958d Show the back button when the statistics graph card opens the history (#54976) 2026-10-16 08:10:50 +03:00
Ruslan Sayfutdinov d7e9877c4e Only list the backup's own locations in the upload progress (#54970)
* Only list the backup's own locations in the upload progress

* Show a spinner until a location reports upload progress
2026-10-16 06:43:23 +02:00
renovate[bot] 5f13af579d Update dependency typescript-eslint to v8.71.2 (#54979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 06:39:43 +02:00
Aidan Timson a83d32f7c9 Explain blocked updates in the update dialog during a backup (#54955)
* Explain blocked updates in the update dialog during a backup

* Pause the updates page group buttons during a backup

* Only pause Supervisor updates during a backup

* Show the backup alert for System-only updates
2026-10-15 18:41:51 +02:00
Petar Petrov 7009352285 Show the punctuation warning when opening a conversation trigger (#54950)
Validate conversation trigger sentences on load
2026-10-15 15:58:48 +01:00
renovate[bot] 1c6135efc3 Update dependency marked to v18.1.1 (#54967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-15 14:32:48 +00:00
renovate[bot] 9b9900ef05 Update dependency eslint-plugin-lit-a11y to v5.1.2 (#54966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-15 14:31:14 +00:00
Ruslan Sayfutdinov e401e69d09 Show upload progress in the backup progress bar (#54956)
* Show upload progress in the backup progress bar

* Leave finished locations out of the upload progress fill

* Narrow upload percentages to numbers
2026-10-15 15:30:59 +01:00
Aidan TimsonandClaude Opus 5.5 7c74f45e5e Explain when HTTPS or the microphone blocks a feature (#54948)
* Fix Assist microphone not supported message showing [object Object]

The not supported microphone message was built as a Lit TemplateResult,
but chat messages are rendered through ha-markdown which expects a
string, so it was displayed as "[object Object]". Build the message as
markdown instead and narrow the message text type to string.

Fixes #29769

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jza8VBEqKGoFP35sCcz35H

* Load Assist voice responses from Home Assistant's URL

* Explain when HTTPS or the microphone blocks a feature

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 14:42:26 +02:00
Ruslan Sayfutdinov 05848f0df4 Add gap between backup upload percentage and spinner (#54951) 2026-10-15 14:35:57 +03:00
Marcin BauerandClaude Opus 5.5 bfa14296d7 Show a toast instead of a dialog after reloading an integration entry (#54954)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 14:28:36 +03:00
renovate[bot] ba3722b983 Update vitest monorepo to v5.0.4 (#54953)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-15 14:27:46 +03:00
Lucas Deutschland cbc3b9dbaf Fix tile zone icons for zones with duplicate names (#54909)
* Fix tile zone icons for zones with duplicate names

* Match tile zone icons to the current tracker state

* Simplify person tile zone lookup
2026-10-15 14:11:06 +03:00
Lucas Deutschland 76f5280b0a Fix map activity for zones with duplicate names (#54908)
* Fix map activity for zones with duplicate names

* Preserve legacy zone icons in person activity

* Simplify map activity zone handling

* Fix map demo history zone membership
2026-10-15 14:10:57 +03:00
Petar Petrov 7123790fed Fix number selector validation on blur (#54944) 2026-10-15 11:03:10 +02:00
Petar Petrov ebfc19eff1 Fix validate-on-blur in the change password card and enter code dialog (#54945)
Fix auto-validate on the change password and enter code inputs
2026-10-15 11:02:21 +02:00
Petar Petrov 80b515fb77 Use contexts in small standalone components (#54943) 2026-10-15 10:59:52 +02:00
Aidan Timson 73200e7dc6 Use image select boxes for buttons or slider feature styles (#54947) 2026-10-15 10:59:19 +02:00
Petar Petrov 067aa09222 Use contexts in the add-on, config entry and service pickers (#54942) 2026-10-15 09:21:00 +02:00
Paulus SchoutsenandClaude Opus 5.5 c24f4d29a9 Add dialog to delete statistics in a time range (#54875)
* Add dialog to delete statistics in a time range

A bad measurement ruins the graphs of a statistic. Mean statistics
cannot be adjusted, so the only fix was to delete the complete
statistic. A new action in the statistics table opens a dialog to pick
a time range. The dialog previews the statistics in the range and
deletes them with the recorder/clear_statistics_range command.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Use recorder/clear_statistics with a time range

The backend adds the optional time range to recorder/clear_statistics
instead of a separate command.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Preview the full first period when clearing a statistics range

The recorder also deletes the statistics period in which the range
starts. The preview started at the exact start time, so it did not show
that period. Start the preview at the beginning of the period.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Fall back to hourly statistics in the clear range preview

The recorder purges the 5-minute statistics before the hourly
statistics. For an older short range, the preview showed "No statistics
found" and disabled Delete, although hourly statistics existed. Show the
hourly statistics when no 5-minute statistics are found.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

---------

Co-authored-by: Claude <[email protected]>
2026-10-15 09:17:31 +02:00
Petar Petrov 7597b5ce77 Reject invalid static prices in the energy source dialogs (#54940)
Block saving an invalid static energy price
2026-10-15 08:48:14 +02:00
Aidan TimsonandAlexander Schieweck a491966068 Add buttons or slider style to target temperature card feature (#54923)
* Add buttons or slider style to target temperature card feature

* Always disable the target temperature fallback slider

---------

Co-authored-by: Alexander Schieweck <[email protected]>
2026-10-15 09:06:08 +03:00
Balloob Bot ff76e4e1a0 Add default decision entity preferences to AI Task settings (#54728) 2026-10-15 07:23:54 +02:00
Elias Lecomte 2ad2c38a41 Prevent stale Energy refreshes from changing the selected period (#54741)
* Prevent stale Energy refreshes from overwriting the selected period

* Preserve Energy data when reapplying the current selection

* Handle Energy subscription refresh failures

* Refresh invalidated Energy selections on resubscribe

* Use checked Energy collection construction and a typed cache

* Restore Energy collection access on the connection

* Retain Energy preferences when the selected period changes
2026-10-15 08:16:29 +03:00
Paul Bottein 4277060d6c Hide divider above the first section row of an entities card (#54929) 2026-10-15 07:00:30 +02:00
renovate[bot] 284691592e Update dependency @vvo/tzdb to v6.201.1 (#54938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-15 06:56:08 +02:00
renovate[bot] 736d1f4dec Update dependency maplibre-gl to v6.14.0 (#54928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-14 15:52:01 +00:00
Petar Petrov 6a0a5fb2ef Reduce stale period for pull requests to 60 days (#54926)
* Reduce stale period to 60 days

* Only reduce stale period for pull requests
2026-10-14 17:39:11 +02:00
Marco HillgerandSeoFood a2ad5c4b14 Guard script field descriptions against non-string YAML values (#54924)
A field description of 123 saved through the config API or YAML reaches the editor as a number, and description?.trim() threw while rendering the field row and its sidebar. Reuse getRowNote, like the automation row notes.

Co-authored-by: SeoFood <[email protected]>
2026-10-14 18:01:02 +03:00
Aidan TimsonandCopilot Autofix powered by AI 95afebf538 Explain debug logging banner and make disable the main action (#54922)
* Explain debug logging banner and make disable the main action

* Improve debug logging description clarity

Updated the description for debug logging to clarify log file download availability.

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-14 17:56:47 +03:00
Aidan TimsonandClaude Opus 5.5 07f7bd91de Keep data table search, sorting and grouping in the URL, starting with the Marketplace (#54912)
* Add opt-in URL sync for data table search, sorting and grouping

Adds a shared query params controller and lets hass-tabs-subpage-data-table
keep its search, sorting and grouping in the URL (`search`, `sort`,
`direction`, `group`) when a page sets `sync-url`. Writes replace the current
history entry, search writes are debounced, and empty or default values are
left out. Params in the URL win over stored preferences; a link without table
params keeps what was picked before. Columns can give a short public name
(`urlName`) for the URL, grouping is only written for columns that have one.

No page opts in yet, so behaviour is unchanged.

Part of #54788

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Re1ptLgqtYo2EKj8FezHCp

* Keep links and searches intact in data table URL sync

- Read the URL once the table is opted in and has its columns, and only
  write after that, so a link isn't overwritten when sync is turned on or
  columns arrive after the first render
- Write a pending search as soon as the search field loses focus, so a
  dialog opened right after typing doesn't pick up the write and Back
  doesn't bring an older search back
- Keep other query params exactly as they are written instead of
  re-encoding them
- Note that a column's URL name must not match another sortable column id

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Re1ptLgqtYo2EKj8FezHCp

* Move the Marketplace tables onto the shared URL sync

* Shorten the urlName comment to one line

* Keep statuses picked on another Marketplace tab when applying a link

* Drop the rendering check from the Marketplace link test

* Cancel pending URL writes on navigation

* Write data table URL after update and reset typed search

* Track the host's first update in the URL params controller

* Add tests for the URL params controller

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-14 17:54:41 +03:00
Marco HillgerandSeoFood 9ca8f2e98b Use light color scheme for themes with a light mode (#54898)
* Use light color scheme for themes with an explicit light mode

* Only force the light scheme when a theme defines a light mode

---------

Co-authored-by: SeoFood <[email protected]>
2026-10-14 16:11:32 +02:00
Wendelin 04f46f037d Use ha-section-title for the picker sticky title and share its logic (#54916)
* Refactor section title handling in picker components and add utility function

- Removed unused sectionTitleFunction properties from ha-generic-picker and ha-picker-combo-box.
- Implemented findSectionTitle utility function to determine section titles based on visible items.
- Updated ha-picker-combo-box to use findSectionTitle for section title management.
- Cleaned up related code in ha-target-picker and ha-automation-add-search to enhance readability and maintainability.
- Added tests for findSectionTitle to ensure correct functionality.

* review
2026-10-14 14:41:54 +01:00
a9b25a3180 Guard automation row notes against non-string YAML values (#54911)
* Guard automation row notes against non-string YAML values

A note set in YAML is not guaranteed to be a string (e.g. `note: 123`),
so calling `note?.trim()` in the trigger, condition, action and option
rows and sidebars could throw while rendering. Add a `getRowNote` helper
that only trims string notes and use it everywhere notes are shown.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01W88iUfrBdggmpdR4NUHXUk

* Remove getRowNote comment

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01W88iUfrBdggmpdR4NUHXUk

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-14 13:27:10 +00:00
4b18f1233e Use rounded squares for avatars and entity pictures (#54902)
* Use rounded squares for avatars everywhere

The map draws person and device avatars as rounded squares, while the
user and person badges in the sidebar, people list, profile, pickers
and editors, the picture previews and the badge card's entity picture
were still circles. They now share one --ha-avatar-border-radius
token, defaulting to the large radius the map already used, so themes
have a single knob and the map markers follow the same token. The
large picture previews take the extra-large radius to keep the same
proportion.

* Use shared picture radius across entity images

Apply the theme radius to state badges, picture previews, and the crop overlay while keeping state-badge-specific overrides.

Co-authored-by: Copilot <[email protected]>

* Keep the small radius for media artwork

Media player and camera badges share the has-media-image class; the 8% fallback was replaced by the picture radius, contradicting the PR description.

* Give camera pictures the shared picture radius

Cameras were grouped with media players and kept the small artwork radius; only media artwork keeps it.

* Use a small picture radius for related item thumbnails

The shared 12px picture radius collapses to a circle on the 20px
related item pictures, so add a themeable small variant that keeps
visible straight edges at that size.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-14 14:26:54 +01:00
Joakim PlateandClaude Opus 5.5 424068c4ca Fix media control card cut off for off players without turn on (#54904)
Players that are off with an assumed state but no TURN_ON support are
treated as unavailable, so the controls are not rendered. The controls
were still computed though, which dropped the no-controls class and
removed the bottom padding, clipping the card.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-14 15:15:00 +02:00
Erwin DounaandLucas Deutschland a6f4ab4a7a Add live log dialog to integration page (#54809)
* Add live log dialog to integration page

* Move live log button into the debug banner

* Limit retained integration live log lines to 5000

* Add filters and pause to the live log dialog

* Make integration live log retention configurable

* Align live log line limit input with toolbar controls

* Resubscribe live log after reconnect and use DialogMixin

* Keep reading position while searching live log

* Only show live log unsupported for unknown command

* Simplify the live log dialog

* Handle live log review feedback

* Apply live log review suggestions

---------

Co-authored-by: Lucas Deutschland <[email protected]>
2026-10-14 15:06:17 +02:00
Maarten Lakerveld cfde8cd14b Stop the user condition editor from duplicating selected users (#54921)
When the users finish loading, the list picks up the pre-selected items and
fires a selected event for them. The handler appended that user again on
every open of the editor, so a single user condition grew one duplicate id
per edit. Ignore selection events that do not change the configured users.

Fixes #54894
2026-10-14 15:02:38 +02:00
Paul Bottein 921ac4998a Show the color feature for light group members in color mode (#54918)
* Show the color feature for light group members in color mode

* Show hue and saturation for light group members

* Show only hue for light group members
2026-10-14 15:00:30 +02:00
Petar Petrov a0c7c36403 Follow up on choose option YAML editing (#54900)
* Follow up on choose option YAML editing

Make action and trigger migration safe for malformed YAML, drop the
option-specific sequence migration, type the option sidebar save value
as unknown and wire option YAML mode through the sidebar like the
other sidebars.

* Guard malformed trigger options during migration
2026-10-14 14:55:24 +02:00
WendelinandClaude Opus 5.5 21670e5ae6 Prevent iOS Safari text inflation on landscape rotation (#54914)
* Prevent iOS Safari text inflation on landscape rotation

Fixes #53060

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CPRiV8TCjj8Z4SX9jX9578

* also add the fix for gallery and demo

---------

Co-authored-by: Claude <[email protected]>
2026-10-14 15:53:15 +03:00
Paul Bottein 76e353b36b Fix hue scrubber zoom for features sharing the card width (#54917) 2026-10-14 15:49:44 +03:00
Aidan TimsonandClaude Opus 5.5 ddb33f7d12 Add accessible names to icon-only badges and interactive tile icons (#54907)
* Add accessible names to icon-only badges and interactive tile icons

ha-badge and ha-tile-icon could render a focusable button with no
accessible name. Add an accessibleLabel property to both, set as
aria-label on the button element (for ha-badge only in icon-only mode,
so visible text still names the button), and pass the entity or
shortcut name from the entity badge, shortcut badge, tile card and
toggle group card.

Fixes #54890

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ysguoFdwq848WzsuFHkSn

* Name shortcut badges without an action label and document accessibleLabel

Fall back to the localized shortcut badge name when the shortcut has no
action-derived label, such as hold-only or double-tap-only badges, and
add accessibleLabel to the ha-badge gallery API table.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ysguoFdwq848WzsuFHkSn

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-14 14:49:41 +02:00
Maarten LakerveldandClaude Fable 5.1 85ca758060 Draw the onboarding home marker like the home zone (#54903)
* Show a home icon for the location marker in onboarding

The onboarding location step passed no icon for its marker. On Leaflet that
fell back to the stock pin; since the MapLibre engine it is a bare dot, the
same element that marks a zone's draggable center. Use the home icon for the
editable marker, both before a search and for the highlighted search result,
so the step shows what the marker stands for.

* Draw the onboarding home marker like the home zone on a map

The location step showed a bare home icon, while the map card draws the
home zone as a colored circle with the icon. Let a point marker in the
locations editor opt into that zone marker, and use it in onboarding
with the home zone's color for the editable marker and the highlighted
search result, so the step shows the same marker the user will see later.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Make the home marker color a memoization input

The memoized locations read the primary color implicitly, so a theme change left a stale radius color. Resolve the color in render and pass it in.

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-14 14:42:55 +02:00
Paul Bottein 8401541c87 Fade light color features when the light is off (#54913)
* Fade light color features when the light is off

* Keep light color features faded while focused

* Show a neutral saturation gradient when the hue is unknown
2026-10-14 14:30:13 +02:00
JonathanandXyolyp 2b1b58d482 Link each run in the activity details to its own trace (#54592)
* Link each run in the activity details to its own trace

An automation and the scripts it calls share one context, and the trace contexts the logbook loads keep only one run per context, so every "View trace" link of such a chain opened the same trace. Core now names the run of a run row in its logbook entry: link that run, and keep the link by context for entries recorded before.

* Only link runs with trace contexts

Trace contexts are only loaded for admins and only hold stored traces.
Keep them as the condition for the link, so non-admins get no link to a
trace page they cannot open.

* Use getTraceUrl for the run trace link

---------

Co-authored-by: Xyolyp <[email protected]>
2026-10-14 12:01:42 +02:00
Paul Bottein 374864fcc1 Expand badge shorthand and drop empty entries when loading a dashboard (#54651)
* Expand badge shorthand and drop empty entries when loading a dashboard

* Normalize the generated config when saving a strategy dashboard
2026-10-14 11:58:19 +03:00
oneseventhreeandWendelin 1bcda9fb80 Add Edit in YAML for choose options in the automation editor (#53327)
* Add Edit in YAML for choose options in the automation editor

* Refactor automation sidebar option handling and validation logic

* copilot review

* fix copilot and regression

* Enhance note display logic in automation sidebar option

* copilot fix

* Add sidebarKey property to ha-automation-sidebar-option and update rendering logic

---------

Co-authored-by: Wendelin <[email protected]>
2026-10-14 11:50:04 +03:00
Jan Bouwhuis dca32d7065 Use unit of measurement selector and add device class to input number helper (#54785) 2026-10-14 08:43:55 +00:00
Paul Bottein cfc5add6cf Use the entity name formatter for all entity names (#54891)
* Use the entity name formatter for all entity names

* Guard entity completions until contexts are loaded
2026-10-14 11:36:43 +03:00
Aidan Timson ae6241859f Run tile card icon hold and double tap actions when icon tap is none (#54889) 2026-10-14 10:16:58 +02:00
Muhammad UmairandWendelin 4c190f806d Lower description search weight in automation add-element search (#53751)
* Lower description search weight in automation add-element search

* remove comment

---------

Co-authored-by: Wendelin <[email protected]>
2026-10-14 09:00:18 +03:00
charishma2k06andWendelin 6e698f8c7c Fix todo item input clearing (#54318)
* Fix todo item input clearing

* review

---------

Co-authored-by: Wendelin <[email protected]>
2026-10-14 08:59:33 +03:00
Paul Bottein 63708e9cba Add settings button to AI task entity pickers (#54886)
* Add settings button to AI task entity pickers

* Fix clearing AI task pickers and label settings buttons
2026-10-13 21:23:33 -04:00
Eric Lorenzo Benjamin Jr. f1763068e0 Treat contenteditable elements as text input for keyboard shortcuts (#53491)
canOverrideAlphanumericInput exempts textarea and the text-entry input types,
but has no branch for contenteditable, so an element the user is actively
typing into is still treated as fair game for global shortcuts. For an admin
that means a, c, d, e and m fire in the middle of a word.

The rest of the codebase already treats contenteditable as a typing surface
when it makes this decision, in ha-list-base and in the integrations
dashboard, and the HA-CODE-EDITOR tag name check in this same function is
that omission handled narrowly for CodeMirror, which is contenteditable
itself. Add the general check.

The HA-CODE-EDITOR entry is left in place. It is redundant in the common case
but it matches on the whole composed path rather than the event target, and
narrowing that belongs in its own change.

Adds tests for this function, which had none.
2026-10-13 14:10:59 +00:00
Aidan Timson 2c5970304a Use contexts in state-display (#54883)
* Use contexts in state-display

* Rename computeUpdateStateDisplay hass argument to options
2026-10-13 14:51:57 +01:00
WendelinandClaude Opus 5.5 eef2ad91cd Keep pending target temperature when entity state updates (#54854)
* Keep pending target temperature when entity state updates

Thermostat and water heater +/- taps were lost when the entity updated
during the 1 second debounce: willUpdate copied the entity's setpoint
over the value the user was still adjusting, so the debounced
set_temperature call sent the old value. This commonly happens when the
state update confirming the previous call arrives right after a new tap.

Add PendingValueSync, which skips copying entity state while a change is
scheduled or its service call is in flight, and resyncs from the latest
state once nothing is pending if an update was skipped or the call
failed. Use it in the target temperature card feature and the climate
and water heater more-info controls.

The single-setpoint buttons in the card feature now also display the
pending value instead of the entity attribute.

Fixes #54832

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NecjfzKMkYK4LPY3yxnXgu

* Keep pending target humidity when entity state updates

Apply PendingValueSync to the climate and humidifier humidity controls
and the target humidity card feature, which had the same race as the
temperature controls. The card feature calls the service on every tap,
so state updates from an earlier tap could reset the value before the
next one. It now also displays the pending value.

Always sync from state when the control switches to another entity, so
a pending value is never shown for or sent to a different entity. This
matters because more-info and card features reuse the control element.

When the slider commits a value, cancel any queued button call so it
does not clear the pending state too early.

Don't rethrow failed service calls: callService already shows a toast,
and the control resyncs from the entity state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Implement cancel method in PendingValueSync and update entity state handling in climate controls

* review

* reduce helpers

---------

Co-authored-by: Claude <[email protected]>
2026-10-13 16:26:44 +03:00
f6a9b8e8a0 Location visibility condition: match zones by target - entity, label (#54679)
* Match zones by label in the location visibility condition

Add `target` (zones, or zones with a label) and `away` options to the
location visibility condition. `target` is matched against the person's
`in_zones` attribute, so overlapping zones match too. Existing `locations`
configs keep working and can be converted from the editor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Make resolveLocationZones a private helper

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Cache resolved location zones, validate target, add tests

- Cache resolved zones per target and refresh them only when the entity,
  device or area registry changes, instead of scanning on every update.
- Validate `target` with the shared targetStruct.
- Match the existing helper text wording.
- Add tests for validation, area targets, cache refresh, and the editor's
  value handling and conversion.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Change button size from 'small' to 's'

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Fix button size attribute in ha-card-condition-location

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Validate location target and away shape, keep mixed fields in editor

- Reject a `target` that isn't a mapping and a non-boolean `away` in the
  location validator; evaluation ignores them instead of throwing.
- Keep `target` and `away` when editing a legacy condition, and merge
  into them when converting, so hand-written mixed configs aren't lost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Migrate legacy location conditions when the editor opens

Replace the Convert button with automatic migration, following the
existing pattern for deprecated options. A warning alert explains that
the condition now matches any zone the person is in. Zone names that
match no zone are kept and shown as not found instead of being dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Show legacy location conditions migrated, keep same-named zones

Migrate for display only, like toCoreEditorCondition: the new format is
written once the condition is edited, so opening a card doesn't create
unsaved changes. Zone names aren't unique, so migrate a name to every
zone that has it, as the old condition matched all of them. Mention the
in_zones attribute in the alert.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Don't reuse an existing zone ID for a missing zone name

A renamed zone keeps its old ID, so slugifying a name that matches no
zone could select a zone the condition never matched. Add _2, _3 and so
on until the ID is unused, like the script editor and core do for new
entity IDs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep missing location names that slugify the same apart

Two names that match no zone but slugify to the same ID got the same
generated ID and collapsed into one. Reserve generated IDs while
migrating, and skip repeated names so they don't get a second one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Validate location target IDs, not just that target is a mapping

A target like {entity_id: 5} passed validation and threw while
resolving zones. The entity filter card, entity filter badge and map
card evaluate conditions without catching errors. Check target keys and
ID types the way targetStruct does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep other keys when editing a location condition

Spread the existing condition like the other condition editors, so
keys such as alias aren't dropped. Migrate legacy conditions when the
condition changes instead of on every state update.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Describe Away as the person being away, including passive zones

away matches the not_home state, which passive zones don't change, so
a person only in passive zones is away even though in_zones lists them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Pick up zone changes in the location editor, clarify texts

Redo the legacy migration when states change, so a zone renamed while
the editor is open isn't saved under a stale ID. Keep the same data
object when nothing changed. Drop backticks from the alert, which is
plain text, and mention areas and floors in the Zones helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Potential fix for pull request finding 'Add the required Oxford comma'

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Keep zones named home or not_home when migrating

A zone can be named home or not_home, and the old condition matched a
person in it as well, so collect zones by name for those values too.
Describe away as the not_home state in code comments.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Handle zones without a state in location resolving and migration

Resolving skips entities without a state, so a labeled zone evaluated
while zones reload was cached as missing until a registry change.
Resolve again on state changes while a registered zone has no state.
When migrating, treat registered zone IDs as taken even without a
state, so a missing name can't take over a reloading zone's ID.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Only redo location migration when zones change

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Export the location migration and test it directly

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Watch registered zones without a state in the location editor

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Add match_mode so converted location conditions keep matching the same way

Converted conditions get match_mode: active_zone, which matches the zone
shown as the person state like locations did. any_zone stays the default.
The editor warns when active_zone has passive zones selected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Match the active zone by ID, localize the mixed-config error

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Skip location zones without a state on each check instead of in the cache

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Match the person state when in_zones is empty, for legacy device trackers

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Msmy1J8Q6xPA4i3diLjwbr

* Treat not_home as away in the location fallback, not as a zone name

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Msmy1J8Q6xPA4i3diLjwbr

* Cache location zones on the registries only

Resolve registered zones without a state through a placeholder, so one
missing zone no longer makes every location condition resolve again on
each state change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Msmy1J8Q6xPA4i3diLjwbr

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-13 16:08:41 +03:00
Paul Bottein 25d8c3cadb Use the snap bottom sheet for the automation sidebar (#54664)
* Use the snap bottom sheet for the automation sidebar

* Use a fixed collapsed height for the automation sidebar sheet

* Make room for the half sheet before scrolling to an opened row

* Close the automation sidebar sheet by dragging it below the collapsed state

* Fall back to vh where dvh is not supported in the automation sidebar sheet
2026-10-13 14:49:39 +02:00
renovate[bot] 2034395784 Update dependency prettier to v3.9.10 (#54879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-13 12:12:20 +02:00
Petar Petrov 2ad11a808d Use i18n contexts in the language, country, currency and trace pickers (#54877) 2026-10-13 11:19:45 +02:00
Paulus SchoutsenandClaude Opus 5.5 14866713ba Fix values and time picker in the adjust statistic dialog (#54876)
* Show exact values in the adjust statistic dialog

The dialog rounded each change to the display precision and showed the
raw JavaScript number. This caused three problems:

- The adjustment was calculated from the rounded value. Setting a
  change of 1.2345 to 0 left 0.0045 in the database.
- Changes smaller than the precision showed as 0. They could not be
  found or set to 0, because the new value equaled the shown value.
- The numbers did not use the number format of the user's locale.

The adjustment is now calculated from the stored value. Small changes
get more digits, and the values are formatted with the user's locale.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Wrap the time field below the date field on narrow screens

The date and time fields of the datetime selector are too wide for one
row on a phone. The time field overflowed the dialog and cut off the
AM/PM field in the adjust statistic dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Redraw the adjust statistic dialog when the locale changes

The values use the number format of the user's locale. The dialog only
redrew on hass changes when the translations changed, so a new number
format did not show until another change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Show a missing change as zero in the adjust statistic dialog

A statistic row without a sum has a null change. The digit count
treated it as a very small value and showed 20 decimals.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

---------

Co-authored-by: Claude <[email protected]>
2026-10-13 08:32:47 +03:00
Aidan TimsonandClaude Opus 5.5 d1365b02da Add sticky group headers to data tables (#54848)
* Add sticky group headers to data table

lit-virtualizer positions rows absolutely, so CSS sticky positioning
can't be used. Render an overlay copy of the current group header at the
top of the scroller, pushed up by the next group header, and allow
collapsing the group from it.

With the group header now always visible, leave the grouped column out
of the narrow secondary line (except for auto-height tables, which have
no sticky header).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Keep sticky group header aligned and clear of focused rows

Shift the sticky group header's cell with the scroller's horizontal
scroll, so it lines up with the group header rows it covers, including
in RTL.

Add scroll padding to grouped tables, so a focused control in a row is
not left hidden under the sticky group header. Collapsing a group from
the sticky header scrolls a rendered header to the top directly, as
scrollIntoView would now leave room for the padding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Keep the sticky group header fixed instead of pushing it

Browsers scroll off the main thread, so the push offset set from scroll
events lagged behind the rows. Scrolling fast showed one header taking
over the other late, and scrolling up on mobile left a gap above the
next group header where rows showed through.

The sticky group header now stays at the top of the scroller, and the
next group header slides under it, so it no longer needs to move in
step with the scroll.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Resize the sticky group header with the data table

The sticky group header's width was only measured on scroll, so after
resizing the window it kept its old width until the next scroll, letting
rows show through beside it. Observe the table's size to update it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Count a sticky group header within a pixel of the top as reached

The virtualizer can place rows at fractional positions, so a group
header scrolled to the top could sit a fraction of a pixel below it.
The sticky header then kept showing the previous group over it, for
example after collapsing a group from the sticky header.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Refresh the sticky group after renders and make its whole row clickable

Filtering, sorting or collapsing can replace the virtualizer's items
without changing the visible range, so no visibility event fires and the
sticky group header kept showing the previous group until the next
scroll. Recompute it after each render.

The sticky header's cell shifts with horizontal scrolling, which left a
strip at the end of the header that didn't respond to clicks. Handle
clicks on the whole sticky row instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Explain the sticky group scroll back for unrendered headers

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Memoize the data table row renderer

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

---------

Co-authored-by: Claude <[email protected]>
2026-10-13 08:31:08 +03:00
Erwin Douna 707dc96fb1 Localize live indicator on logs page (#54858) 2026-10-13 08:13:38 +03:00
Paulus SchoutsenandClaude db1a628b57 Keep hidden columns when reopening data table settings (#54874)
Keep column settings for tables without fixed last columns

The data table settings dialog dropped the stored column order when the
table had no lastFixed columns. With no column order, the dialog showed
every column as visible. Reopening the dialog on the statistics table
then showed hidden columns as enabled again.


Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

Co-authored-by: Claude <[email protected]>
2026-10-13 07:55:48 +03:00
renovate[bot] 21c059e9fc Update dependency @types/node to v24.19.2 (#54868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-13 06:28:23 +02:00
renovate[bot] 33c734ea8b Update pnpm to v12.11.2 (#54869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-13 06:28:05 +02:00
Paulus SchoutsenandClaude d8751e5b89 Show Z-Wave device config link for controller node (#54859)
Claude-Session: https://claude.ai/code/session_01Q6GDGNkP4RZT3wg17BEhsD

Co-authored-by: Claude <[email protected]>
2026-10-12 19:18:05 +00:00
renovate[bot]andMaarten Lakerveld edb7a42a7e Update dependency @versatiles/style to v6.4.0 (#54856)
Co-authored-by: Maarten Lakerveld <[email protected]>
2026-10-12 14:33:35 +00:00
WendelinandClaude 65a1f0722f Fix image cropper race conditions (#54850)
* Migrate image cropper dialog to DialogMixin and wait for cropper ready

Closing the cropper dialog with the header close button or Escape did not
run closeDialog(), so the Cropper instance stayed bound to the removed
<img>. The next open called replace() on that stale instance: the new
image never got a source or crop overlay, and clicking Crop before the
hidden reload finished threw because getCroppedCanvas() returned null.

Using DialogMixin gives every open a fresh element, and the cropper is
destroyed on disconnect. The Crop button is now disabled until the
cropper is ready, and _cropImage bails out if no canvas is available.

Refs #54793

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011UTN4HCGBVyXdJ7F7bQnBX

* Add file handling improvements to upload components and cropper dialog

- Reset native input value when cleared externally in ha-file-upload
- Introduce _pickedFile state in ha-picture-upload for better file management
- Ensure cancelCallback is invoked if cropping is not completed in image-cropper-dialog
- Update HaImageCropperDialogParams to include optional cancelCallback

* Handle unsupported file formats in picture upload and ensure dialog connection during blob creation

* Add handling for dialog closing state in image cropper dialog

---------

Co-authored-by: Claude <[email protected]>
2026-10-12 17:32:31 +03:00
Petar Petrov c1981b16f2 Disable Lit dev mode in the legacy build under pnpm (#54855)
Compile Lit without dev mode in the legacy build again
2026-10-12 16:17:28 +02:00
renovate[bot] dfc1971ab4 Update pnpm to v12.11.1 (#54851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 17:08:11 +03:00
768d61e223 Add query strings and My links to the Marketplace (#54787)
* Open the Marketplace with a search from the link

* Add My links for the Marketplace tabs and custom repositories

* Sort ascending when a Marketplace link gives no direction

* Keep the Marketplace link in step with the table

* Apply Installed deep-link settings behind Marketplace warning

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-12 17:07:40 +03:00
Claudio Wunder 04af517a22 Show Thread network information on its own page (#54818)
* Show Thread network information on its own page

The Thread panel showed a network's details behind an unlabeled info
icon that opened a plain-text dialog. Zigbee shows the same kind of
details through a "Network information" row that opens its own page,
so move Thread to that pattern.

Each network card with a stored dataset now ends with a "Network
information" row that opens /config/thread/network-info/<dataset_id>.
A new thread-config-router serves the panel and the page, like the
Zigbee and Bluetooth routers, and the border router lookup moves to
findOTBRInfoForDataset so both match border routers the same way.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Align the Thread network delete button with the network name

The 48px icon button was top-aligned with the shorter header text, so it
sat about 10px low. Center it on the name, and pull its block margins in
so the header keeps the same height as cards without the button.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Show an error when Thread network info fails to load

If thread/list_datasets rejected, the page never finished loading and
stayed empty, with an unhandled rejection. Catch the failure, always
finish loading, and show the error message on the error screen.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Show the Thread dataset before border router details load

otbr/info asks every OpenThread border router over REST, with a 10 s
timeout per call, so the network information page stayed empty while
an unreachable border router timed out. Show the dataset as soon as it
loads, without waiting for otbr/info, and show a Loading row in place
of the border router details until it answers.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Rename the Thread config router to thread-config-dashboard-router

thread-config-router read like a Thread border router. Match
zha-config-dashboard-router and the other config dashboard routers.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Say what the Thread network information page is loading

Use "Loading border router details…" for the row shown while otbr/info
answers, instead of a bare "Loading".

Assisted-by: Claude Opus 5.5 <[email protected]>

* Name each Thread network information landmark after its network

Every network card had a navigation landmark named "Network
information", so screen reader landmark navigation couldn't tell them
apart. Include the network name, or its extended PAN ID when it has none.

Assisted-by: Claude Opus 5.5 <[email protected]>
2026-10-12 17:06:40 +03:00
Petar Petrov 0d74036677 Remove dead .hass bindings (#54853) 2026-10-12 15:46:46 +02:00
Petar Petrov 7a69aac65a Remove unused hass properties from selectors and pickers (#54852) 2026-10-12 15:45:26 +02:00
Josef Zweck 4407404206 Show translated backup agent errors (#54847)
* Show translated backup agent errors

* Add tests for localizing backup agent errors
2026-10-12 16:14:05 +03:00
Markus TuominenandCopilot Autofix powered by AI d6ed305a8b Offer versioned SSL profiles in the HTTP server settings (#54559)
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-12 12:39:42 +00:00
Aidan TimsonandClaude 13ea2643ee Hide last updated time on narrow marketplace dashboard rows (#54846)
Hide Marketplace last updated on narrow rows

A bare relative time like "4 months ago" has no context in the narrow
secondary line. The repository overview already shows it with a label.


Claude-Session: https://claude.ai/code/session_01RYz4pEoJju2EYeHerCi5CG

Co-authored-by: Claude <[email protected]>
2026-10-12 12:25:20 +00:00
Aidan TimsonandClaude cbae291079 Match the tab badge colors to the Apps update tag (#54845)
Match the Marketplace update badge to the Apps update tag

The update badge on the Installed tab and its narrow dot used the
warning color, while Apps marks an available update with a light
primary tag. Use the same primary fill and text colors so both read
the same way.


Claude-Session: https://claude.ai/code/session_012e1fHsMG8ZxDjAzsM5FMvZ

Co-authored-by: Claude <[email protected]>
2026-10-12 15:16:59 +03:00
renovate[bot] 71c061b68b Update dependency @rsdoctor/rspack-plugin to v1.6.5 (#54844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 15:02:04 +03:00
Aidan Timson 26dcbfccfc Fix Marketplace update badge and row numbers on mobile (#54843) 2026-10-12 13:47:42 +02:00
Paul Bottein 3bb04f417c Remove name prefix stripping from generated dashboards (#54727)
* Remove name prefix stripping from generated dashboards

* Keep an empty entity name in generated card rows
2026-10-12 13:45:53 +03:00
Paul Bottein 03e5b8148b Simplify entity name helpers (#54726) 2026-10-12 13:45:53 +03:00
Paul Bottein 49e6a744e2 Finalize the entity naming migration (#54725)
* Finalize the entity naming migration

* Open the name picker in custom mode for a text default name
2026-10-12 13:45:53 +03:00
Petar Petrov 23c75da8f5 Number energy grid colors per direction so export-only connections are not white (#54808) 2026-10-12 10:53:15 +02:00
Petar Petrov fdae33a5ec Keep History and Activity filters usable with many targets (#54786) 2026-10-12 09:06:45 +01:00
Bascht74 ae7ce92a79 Keep adjust statistic dialog open after adjusting and refresh the list (#54628)
* Keep adjust statistics dialog open and refresh list after adjusting

* Guard adjustment against dialog close and handle refresh errors

* Ignore results of outdated statistics fetches

* Reset dirty state on close and guard failed adjustment
2026-10-12 08:04:29 +00:00
renovate[bot] c3ed2635e4 Update pnpm to v12.11.0 (#54842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 11:02:11 +03:00
Daan Vervacke 1f2d5af99f Add selection mode with bulk delete to the tags page (#54819) 2026-10-12 09:46:46 +02:00
Claudio Wunder 1b4ff42821 Show empty state on MQTT panel when MQTT is not set up (#54817)
The MQTT panel rendered nothing when there is no active MQTT config
entry, leaving a blank page. That happens when another integration
loads MQTT as a dependency, so the Connectivity page still links to
the panel. Show an empty state with a button that starts the MQTT
config flow instead.

Assisted-by: Claude Opus 5.5 <[email protected]>
2026-10-12 09:24:01 +03:00
Paulus SchoutsenandClaude Opus 5.5 eec2ff9fa5 Treat unknown media player state as available in media control card (#54835)
* Set no-controls on media control card when entity is unavailable

An entity in the unknown state with assumed_state and turn on/off
support gets control buttons from computeMediaControls. The card hides
the controls for unknown and unavailable entities, but still left out the
no-controls class. That removed the bottom padding and made the card too
short.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wa4wcvqMtHTDDqGChBiGcD

* Treat unknown media player state as available in media control card

A media player in the unknown state is available. The card now shows
its title and controls, such as the turn on and turn off buttons of an
assumed state player, instead of the unavailable layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wa4wcvqMtHTDDqGChBiGcD

---------

Co-authored-by: Claude <[email protected]>
2026-10-12 08:58:33 +03:00
6a9501fa8a Unsubscribe from system health info only once (#54838)
The info page and the system log detail dialog unsubscribe from the
system health callback. The callback runs again for each update that
arrives before the unsubscribe completes, and each run unsubscribes
again. The finish event can unsubscribe once more. The backend rejects
every extra unsubscribe with "Subscription not found.", which ends up
as an unhandled promise rejection.

Make the unsubscribe function from subscribeSystemHealthInfo run only
once, which fixes all callers.

Co-authored-by: Paulus Schoutsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-12 08:18:24 +03:00
bb7308a024 Fix Z-Wave node config page for parameters without an endpoint (#54840)
The backend sends endpoint as null for configuration values on the
root endpoint, because zwave-js-server-python types Value.endpoint as
int | None. The frontend typed it as number and grouped parameters
with item.endpoint.toString(), so the page threw on every render and
stayed on the loading spinner.

Type endpoint as number | null. Treat null as endpoint 0 when grouping
parameters and when setting a parameter, because the backend set
command only accepts an integer endpoint.

Co-authored-by: Paulus Schoutsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-12 08:16:08 +03:00
renovate[bot] 1c151c65f2 Update dependency del to v9 (#54831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 07:04:32 +02:00
439d61966d Fix media control card reading state before config is set (#54834)
The card can update with hass before setConfig is called. The progress
controller reads the state object on every host update, and the
_stateObj getter read _config.entity without a check. Each card then
threw a TypeError as an unhandled promise rejection.

The frontend logger maps the stack of every unhandled rejection with
source maps on the main thread. On a dashboard with 40 media control
cards, the 80 rejections blocked the main thread for about 3 seconds
on load.

Return undefined from _stateObj until the card has a config.

Co-authored-by: Paulus Schoutsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-12 06:44:53 +02:00
renovate[bot] 6dea194706 Update relative-ci/agent-action action to v3.2.4 (#54836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 06:44:35 +02:00
renovate[bot] 31b76346d2 Update dependency vite to v8.3.4 (#54829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 06:44:28 +02:00
renovate[bot] 87c079829a Update release-drafter/release-drafter action to v7.9.0 (#54837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-12 06:42:58 +02:00
Petar Petrov 158c14b6ab Show device area in energy device graph tooltips (#54826) 2026-10-11 17:27:19 +02:00
Paulus SchoutsenandClaude de82c36acb Add bottom padding to AI and entity ID format settings pages (#54828)
The last card on these pages touched the bottom edge when scrolled to
the end. Use the same 24px bottom spacing as the other settings pages.


Claude-Session: https://claude.ai/code/session_013YwRmqQXnkZ3UBQJyBRSS6

Co-authored-by: Claude <[email protected]>
2026-10-11 17:26:16 +02:00
Patrick DecatandClaude Opus 5.5 32801130ba Close the previous ZHA permit subscription on search again (#54811)
* Close the previous ZHA permit subscription on search again

"Search again" called _subscribe() while the subscription from the first
search was still open, since _deactivate() only stops the UI at the end of
the permit window. Each zha/devices/permit subscription keeps ZHA debug
logging enabled in the backend until it is closed, so the leaked one kept
the backend in debug mode until the websocket connection closed.

Close any previous subscription before opening a new one.

Co-authored-by: Claude Opus 5.5 <[email protected]>

* Wait for the previous ZHA permit subscription to close before resubscribing

_unsubscribe() only called the previous unsubscriber from a promise
callback, so _subscribe() sent the new zha/devices/permit subscription
before the old one was closed, and the backend started the next debug
logging session while the previous one was still running.

Chain the new subscription onto the previous unsubscribe instead.
_subscribed always holds the latest chain, so a quick second search waits
for the first one, and leaving the page unsubscribes whatever that chain
ends up subscribing.

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 09:28:35 +03:00
renovate[bot] 0f09f11ac4 Update CodeMirror (#54820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-11 09:27:24 +03:00
MichaelandClaude Opus 5.5 6f3c4b427b Fix Matter add device button not opening the add device dialog (#54823)
The add device button on the Matter dashboard linked to
/config/matter/add, which navigated to the devices dashboard and opened
the Matter add device dialog without awaiting the navigation. The
navigation closes open dialogs before it completes, so the dialog was
closed again right away and the user ended up on the devices dashboard.

Open the dialog directly from the Matter dashboard, like the Z-Wave
dashboard does, and make the /config/matter/add route (used by the
add_matter_device my link) await the navigation before showing the
dialog from the app root, as its own element is detached by then.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 09:26:58 +03:00
renovate[bot] 809ebc5095 Update Playwright (#54824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-11 09:26:06 +03:00
renovate[bot] fbd1de0ec7 Update dependency cropperjs to v1.7.0 (#54810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 17:16:56 +02:00
renovate[bot] 9f50d399a6 Update babel monorepo to v8.0.7 (#54813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 17:16:19 +02:00
renovate[bot] da632926bb Update dependency @versatiles/style to v6.3.1 (#54814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 17:15:37 +02:00
renovate[bot] 0e9b62e7d0 Update dependency @versatiles/style to v6.3.0 (#54804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 05:42:05 +00:00
Elias Lecomte 2f09fb9e5b Improve demo water consumption and cost statistics (#54648)
* Add demo water statistics and long energy source names

* Align demo water statistics scaling and consumption costs

* Preserve non-admin permissions when the test app refreshes its user

* Restore original demo energy meter friendly names

* Remove unrelated authentication scenario fix from water statistics PR

* Derive demo costs generically and scale water by bucket duration
2026-10-10 08:39:06 +03:00
karwosts 1a922fc067 Fix time_format field appearing when it shouldn't in tile editor (#54799)
Fix time_format field appearing when it shouldnt in tile editor.
2026-10-10 08:35:10 +03:00
renovate[bot] 54e15575d2 Update pnpm to v12.10.1 (#54803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 08:32:26 +03:00
renovate[bot] e9bfc29729 Update dependency maplibre-gl to v6.13.0 (#54806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 08:31:33 +03:00
Jan-Philipp Benecke 743755a59f Fix script graph crash on template shorthand conditions (#54798)
* Fix script graph crash on template shorthand conditions

* Move string to ConditionWithShorthand
2026-10-09 16:17:44 -07:00
Aidan TimsonandClaude ea1d3fef73 Add E2E tests for creating an automation (#54791)
Cover creating an automation from the picker through the save dialog,
the save dialog refusing an empty name, and discarding a new automation
when leaving the editor.

The app harness now registers the demo's display registry mock, which
answers the refetch the app makes when a saved automation fires
entity_registry_updated.


Claude-Session: https://claude.ai/code/session_011L15xnuJiRZnioxrmNwDfb

Co-authored-by: Claude <[email protected]>
2026-10-09 19:02:28 +03:00
karwosts 8a83331e94 Fix autocomplete trying to fill username in weird places (#54789) 2026-10-09 16:59:22 +02:00
renovate[bot] b207a486dd Update dependency @versatiles/style to v6.2.0 (#54777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 15:39:04 +02:00
Petar Petrov 00b3dfd43a Link Ethernet Matter nodes to Home Assistant in the network map (#54784) 2026-10-09 14:00:12 +02:00
Aidan Timson 8f38778180 Only offer installed statuses on the Marketplace installed tab (#54662) 2026-10-09 13:51:22 +02:00
Petar Petrovandcopilot-swe-agent[bot] e9ed4c5c66 Add local disk mounts to storage settings (#53643)
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-09 13:04:57 +02:00
Bruno Pantaleão Gonçalves 3ae69b041b Let the tab padding give way before the tab label (#54670) 2026-10-09 13:03:42 +02:00
Paul Bottein f7804a92f4 Use the entity name formatter in voice assistant settings (#54775) 2026-10-09 12:56:39 +02:00
Maarten Lakerveld fb0dcf29cb Map overview fixes (#54774) 2026-10-09 12:55:19 +02:00
tzagim bf564163bf Fix RTL bidi corruption of temperature in weather entity row (#54776) 2026-10-09 12:38:11 +02:00
Bram KragtenandClaude Opus 5 93b04625ac Drop ha-formfield in favour of the switch's own label (#54200)
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 12:26:20 +02:00
Bram KragtenandClaude Opus 5 13c3155dab Remove dead --mdc-typography-* custom properties (#54201)
Co-authored-by: Claude Opus 5 <[email protected]>
2026-10-09 12:18:50 +02:00
WendelinandClaude 9435c5494d Remove @material/web (#54580)
Co-authored-by: Claude <[email protected]>
2026-10-09 12:00:20 +02:00
Paul Bottein 4fbd34ac81 Use the entity name formatter in the entity filter, related items and logbook (#54773) 2026-10-09 12:50:02 +03:00
Paul Bottein 70390d7e43 Simplify how data tables reach the screen edges (#54714) 2026-10-09 11:32:26 +02:00
Bram KragtenandClaude Opus 5.5 3f7f4613d6 Remove invalid ::part() selector that broke CSS minification (#54772)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 11:29:05 +02:00
Bram KragtenandClaude Opus 5.5 43395ab59b Remove invalid ::part() selector that broke CSS minification (#54772)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 09:27:11 +00:00
Bram Kragten 4a8501a6e5 Fix import from ui-support 2026-10-09 11:26:27 +02:00
Bram Kragten 4ae1775644 Bumped version to 20260930.3 2026-10-09 11:16:55 +02:00
Jan-Philipp Benecke 80cd338e77 Fix crash on null trigger entries in the automation editor (#54771) 2026-10-09 11:16:35 +02:00
Jan-Philipp Benecke 4247a1439e Fix crash on null trigger entries in the automation editor (#54771) 2026-10-09 09:15:00 +00:00
Franck Nijhof cd8f785ce6 Don't count the characters of a string target (#54769) 2026-10-09 11:11:58 +02:00
Franck Nijhof 4f4f7bd633 Translate attribute names every entity can have (#54768)
* Translate attribute names every entity can have

* Translate the supported features attribute name
2026-10-09 11:11:57 +02:00
Franck Nijhof c2413fd7fa Greet in the interface language when it is a variant of the pipeline language (#54766) 2026-10-09 11:11:55 +02:00
Franck Nijhof ed596fe700 Hide hidden cards in grid sections without :has() (#54764) 2026-10-09 11:11:53 +02:00
Franck Nijhof e580fb8b5c Keep dropdown menus clear of safe areas (#54762) 2026-10-09 11:11:52 +02:00
Franck Nijhof 5ba7a906d4 Keep icon button labels off the ha-button host (#54761) 2026-10-09 11:11:51 +02:00
Franck Nijhof c6159615d7 Release streaming images when hui-image is detached (#54760) 2026-10-09 11:11:49 +02:00
Franck Nijhof b2f63cf345 Show the speech-to-text duration without VAD (#54759) 2026-10-09 11:11:48 +02:00
Franck Nijhof fbd7bb073e Give dialogs an accessible name (#54758) 2026-10-09 11:11:47 +02:00
Franck Nijhof 42fec4fb9c Expose the real invalid state of inputs to screen readers (#54757) 2026-10-09 11:11:46 +02:00
Franck Nijhof bcd7f0dc88 Show the ZHA config entry state when it isn't loaded (#54756) 2026-10-09 11:11:45 +02:00
Franck Nijhof 44b5dc9446 Fit the add integration list on short screens (#54755) 2026-10-09 11:11:43 +02:00
Franck Nijhof 4fd8732160 Fall back to local voice setup when the cloud status fails (#54754) 2026-10-09 11:11:42 +02:00
Petar Petrov 51675e0cd2 Fix negative upstream device untracked energy in devices detail graph (#54753) 2026-10-09 11:10:17 +02:00
Petar Petrov 36bddbff06 Hide Zigbee from the ESPHome device setup wizard (#54751) 2026-10-09 11:06:57 +02:00
Simon Lamon e8c15f10e2 Restore compatibillity with older chromium devices in ScrollFadeController (#54742)
Restore compatibillity with older chromium devices
2026-10-09 11:06:55 +02:00
Franck Nijhof 8f870146c6 Keep custom logbook messages for automations and scripts (#54739)
* Keep custom logbook messages for automations and scripts

* Only match the domain's own run message
2026-10-09 11:06:54 +02:00
Franck Nijhof 91c0d052df Convert CSS color names to hex when saving a label (#54738) 2026-10-09 11:06:53 +02:00
Franck Nijhof dfb5e7d768 Keep the label of color inputs raised (#54737) 2026-10-09 11:06:51 +02:00
Artur Pragacz 03fb6f5c29 Fix translation key for external config flow step description (#54722) 2026-10-09 11:06:50 +02:00
Maarten Lakerveld 11176a7593 Center the map on a single entity instead of fitting it (#54718)
The vector map's globe projection (new in 2026.10) cannot solve a camera
for a zero-area bound, so fitting a single-entity map warned "Map cannot
fit within canvas" and left the view on the default location. Center a
single point directly with easeTo/flyTo rather than routing it through
fitBounds, carrying the fit's padding as a transient offset and honouring
its fly request.
2026-10-09 11:06:48 +02:00
Paul Bottein 8a7b8ff2ac Add side padding to the statistics search field (#54717) 2026-10-09 11:06:47 +02:00
Franck Nijhof f61e1ab509 Respect custom icons for built-in panels in the sidebar (#54709) 2026-10-09 11:06:46 +02:00
Franck Nijhof 3e75d97b8a Show the right page title for more settings pages (#54708) 2026-10-09 11:06:45 +02:00
Franck Nijhof 8588d34fbe Only cache the frontend's own static files in the service worker (#54707) 2026-10-09 11:06:43 +02:00
Paul Bottein f4952764d4 Replace map overview more info button with a header icon (#54704)
* Replace map overview more info button with a header icon

* Reuse the show more information label

* Show a settings button for admins in the map overview

* Hide the map overview settings button in the demo
2026-10-09 11:06:42 +02:00
Franck Nijhof 52d38105ae Let long dropdown submenus scroll (#54692) 2026-10-09 11:06:41 +02:00
Franck Nijhof 8fa6c1d027 Keep a cleared input value when focusing it again (#54690) 2026-10-09 11:06:39 +02:00
Franck Nijhof 972b999195 Handle map labels without a name (#54689) 2026-10-09 11:06:38 +02:00
Petar Petrov fe6bc5530e Show the newest history states when the browser clock runs behind (#54688) 2026-10-09 11:06:37 +02:00
Franck Nijhof d5d7093ca3 Keep the key order when duplicating a script (#54687) 2026-10-09 11:06:36 +02:00
Franck Nijhof 52d30d5b6e Keep boolean fields inline in the narrow action editor (#54686) 2026-10-09 11:06:35 +02:00
Franck Nijhof 2a25673659 Fix ZHA reconfigure dialog errors after closing and on reconnect (#54684)
* Fix ZHA reconfigure dialog errors after closing and on reconnect

* Only handle reconfigure messages of the current run
2026-10-09 11:06:33 +02:00
Franck Nijhof 94838cf7d1 Load the HLS source once instead of on every media attach (#54683) 2026-10-09 11:06:32 +02:00
Franck Nijhof 9214bcac94 Wrap descriptions in voice assistant settings (#54678) 2026-10-09 11:06:30 +02:00
karwosts 3be6887df6 Fix create floor memoization issue (#54675) 2026-10-09 11:06:29 +02:00
Paul Bottein 33374693c9 Snap the map overview bottom sheet to fixed positions (#54663)
* Snap the map overview bottom sheet to fixed positions

* Include the safe area in the collapsed stop and follow reduced motion
2026-10-09 11:06:28 +02:00
Marcin BauerandClaude Opus 5.5 80516f1f00 Restore left-column timeline names on the history-graph card (#54661)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 11:06:26 +02:00
Petar Petrov 9253af6a19 Align history timeline X axis with line charts (#54657) 2026-10-09 11:06:25 +02:00
Bram KragtenandClaude Opus 5.5 58f1d51c7f Add realistic automations to the demo (#54647)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 11:04:03 +02:00
Bram KragtenandClaude Opus 5.5 05d6f614bc Show the next demo card on the home page demo again (#54646)
* Show the next demo card on the home page demo again

The home overview strategy now generates the favorites section itself
instead of returning a common-controls section strategy, so the demo
strategy no longer found the section to add its card to. Find the
favorites section by its heading instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep at least six favorites next to the demo card

The section strategy used to get a limit of two less, so seven tiles
became six. Only drop as many tiles as needed to keep six.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep the current user when the demo user subscription refreshes

The auth/current_user mock answered with the user from startup, so the
user subscription reset a changed user, like the non-admin scenario of
the e2e tests, to the initial admin user.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 11:00:44 +02:00
Bram KragtenandClaude Opus 5.5 6e2bb1b2f5 Mock editing the own profile in the demo (#54645)
The demo did not answer person/update_own_profile, so saving the profile
dialog showed an error. Also answer auth/current_user, so the user
subscription picks up the new name.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 11:00:43 +02:00
Bram KragtenandClaude Opus 5.5 f3a6e1028e Add focus indicator to entity and sensor cards (#54644)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 11:00:41 +02:00
WendelinandClaude f182b4600d Dismiss reboot/shutdown toast when frontend reconnects (#54640)
The "Rebooting system" toast was shown without an id and with no
timeout, so nothing could ever close it and it stayed on screen after
the host came back. Give it an id, dismiss it when the connection is
re-established, and also when the action fails with an error dialog.

Fixes #54630


Claude-Session: https://claude.ai/code/session_01SkS1AB8w2GPUuRBaDDmsEu

Co-authored-by: Claude <[email protected]>
2026-10-09 11:00:40 +02:00
Jan Čermák b7983e0906 Copy locale data to the landing page build (#54488)
The landing page bundle includes the Intl polyfill. When a browser
needs a polyfill, it loads locale data from /static/locale-data/.
The landing page build never copied that directory, so these requests
returned 404. The language picker then showed raw language codes.

Fixes home-assistant/landingpage#216
2026-10-09 11:00:38 +02:00
Bram KragtenandClaude Opus 5 6c988150e4 Draw the globe against a sky when the map is zoomed out (#54485)
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-09 11:00:37 +02:00
Bram KragtenandClaude Opus 5.5 b88a39e273 Cache map tiles per asset class and stop refetching what still works (#54451)
* Cache map tiles per asset class and stop refetching what still works

The service worker kept glyphs, sprites and tiles in one 500-entry LRU, so
panning around evicted the fonts that every label on every map needs. Glyphs
and sprites move to a cache of their own, on the 30 day age core already sends
them with, and the tile ceiling goes up.

The TileJSON was deliberately left on the network because it is the switching
point for the tile source. Every map waits on it before asking for a tile
though, and at a five minute max-age that is a round trip before the first one.
Served stale-while-revalidate instead: out of the cache immediately, refreshed
behind it, so a moved endpoint still arrives.

The raster fallback redrew on every token rotation, refetching every tile on
screen twice an hour whether or not anything had failed. Later tiles pick up a
new token by themselves, so only a layer that actually had one refused needs
asking again - and since a refused tile is the only sign that path gets that
its token is stale, it now asks for a new one instead of waiting for the
refresh interval to come round.

Style requests that are in flight are shared, so a dashboard of maps, or a
theme toggle switching all of them at once, fetches each style once. Only while
in flight: a settled request is dropped, or a style that failed once would
never be fetched again.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Hand the raster fallback the current map tiles token

A rotation is only announced once, so a vector layer that later falls back
handed the raster layer the token it was created with, and the refresh that
follows a refused tile fetches the already-current token and announces
nothing. The vector layer now tracks rotations itself.

Also covers in-flight style sharing, and passes a map style rather than
`false` in the raster recovery tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-10-09 11:00:35 +02:00
JonathanandXyolyp 0ecf3a0bb3 Link trace steps to the script or automation run they started (#54518)
* Link trace steps to the script or automation run they started

A step that runs a script or triggers an automation already records the started run as child_id in its trace, but the step details only dumped it as raw YAML. Show a link to that run's trace instead, named after the script or automation. A link to another run of the same item, like a script that starts itself, only changes the query string, so the trace pages now pick up a changed run_id themselves. The logbook's trace link and the new link share one URL helper, which encodes the item id.

* Keep the shown trace run in the URL

A link to another run of the same script or automation adds a history entry, but the trace page only followed in-app navigation, so browser back and forward left the URL and the shown run apart. Write the shown run into the current history entry, follow popstate within the same trace page, and let only the latest trace list request update the page. Entries without a run_id are ignored, since closing a dialog with back lands on one with the same URL.

* Let only the latest trace request update the trace page

Going back to the shown run while a linked run was still loading left the older request in charge: it then showed the linked run and wrote it into the history entry the user went back to. Remember which run the latest trace list request is for and drop it when the URL returns to the shown run. Loading the same run twice could also let the older response win, so trace requests are numbered as well.

* Let a URL change replace the first trace list request

While a trace page was loading its first trace list, a link or browser history step to another run of the same script or automation was ignored, so that first request won and wrote its run into the newer history entry. Handle such a change right away, so it replaces the pending request.

* Let a run picked on the trace page win over a pending link

While a link to another run of the same script or automation was loading, a run picked with the trace picker or the arrows was overwritten once the link's trace list arrived, and Refresh dropped the linked run while the URL kept it. A picked run now cancels the pending link, and Refresh keeps the run the link asked for.

* Move the trace run navigation into a shared controller

Both trace pages carried the same logic to follow the URL, number their requests and write the shown run back into the URL. Move it into a TraceRunController with unit tests, build the trace page path in one place, and split the child trace name from its link. No change in behaviour.

* Link trace timeline steps to the run they started

The step details link a step that ran a script or triggered an automation to the run it started. Show the same link under that step in the trace timeline, and share the name lookup and the link between both views.

* Compare trace paths decoded

The route decodes the item id, so a trace page can also be opened by a
link that encodes it differently, like the logbook's links, which do not
encode it. Compare the decoded paths, so that the URL follows the shown
run there too.

---------

Co-authored-by: Xyolyp <[email protected]>
2026-10-09 11:56:00 +03:00
WendelinandClaude aea94f557b Migrate ha-outlined-icon-button to ha-icon-button (#54571)
Co-authored-by: Claude <[email protected]>
2026-10-09 10:55:32 +02:00
Franck Nijhof cd6bdc0344 Don't count the characters of a string target (#54769) 2026-10-09 08:52:30 +00:00
Franck Nijhof 844d7bc0e5 Keep custom logbook messages for automations and scripts (#54739)
* Keep custom logbook messages for automations and scripts

* Only match the domain's own run message
2026-10-09 11:48:14 +03:00
Franck Nijhof b4c6035a75 Convert CSS color names to hex when saving a label (#54738) 2026-10-09 08:45:01 +00:00
Franck Nijhof f77295fcfa Keep dropdown menus clear of safe areas (#54762) 2026-10-09 10:41:39 +02:00
Paul Bottein 951ca9e9f1 Use the entity name formatter in automation descriptions (#54767) 2026-10-09 10:41:23 +02:00
Claudio Wunder e7f3a02d73 Fix thin line flashing under empty sliders while dialogs open (#54716) 2026-10-09 08:40:44 +00:00
Franck Nijhof 9555f6f4d7 Give dialogs an accessible name (#54758) 2026-10-09 10:40:23 +02:00
Franck Nijhof d7f39d3804 Hide hidden cards in grid sections without :has() (#54764) 2026-10-09 11:39:41 +03:00
Franck Nijhof 854f28fc26 Translate attribute names every entity can have (#54768)
* Translate attribute names every entity can have

* Translate the supported features attribute name
2026-10-09 11:38:31 +03:00
Franck Nijhof ef587c6b77 Release streaming images when hui-image is detached (#54760) 2026-10-09 10:36:18 +02:00
Franck Nijhof 70a96e47ca Show the speech-to-text duration without VAD (#54759) 2026-10-09 10:35:26 +02:00
Franck Nijhof 28e7cb39fb Show the ZHA config entry state when it isn't loaded (#54756) 2026-10-09 10:32:10 +02:00
Maarten Lakerveld 49d01d20eb Center the map on a single entity instead of fitting it (#54718)
The vector map's globe projection (new in 2026.10) cannot solve a camera
for a zero-area bound, so fitting a single-entity map warned "Map cannot
fit within canvas" and left the view on the default location. Center a
single point directly with easeTo/flyTo rather than routing it through
fitBounds, carrying the fit's padding as a transient offset and honouring
its fly request.
2026-10-09 11:30:35 +03:00
Franck Nijhof 941b167c82 Keep icon button labels off the ha-button host (#54761) 2026-10-09 11:28:13 +03:00
Franck Nijhof e908ac15d3 Greet in the interface language when it is a variant of the pipeline language (#54766) 2026-10-09 11:27:33 +03:00
Aidan TimsonandPetar Petrov 20cd4683b0 Add area labels to the pull request labeler (#54697)
* Add area labels to the pull request labeler

* Label Labs pull requests from the Labs API as well as paths

* Rename the Landingpage label to Landing Page

* Label Supervisor pull requests that change src/data/hassio

* Label pull requests from area code as well as paths, and only add labels

* Remove stale labels again, owning code-matched ones in the script

* Warn instead of failing the labeler, and summarise changes

* Move the run summary into a shared helper

* Split the Activity label from History

* Add labels for the remaining areas and split broad ones

* Apply the Home Assistant Link label from the content script

* Catch more companion app usage in the content labels

* Add Notifications, Search and Sidebar labels and widen some rules

* Add the AI label

* Label trace changes as Automations

* Match content labels on whole calls against the current labels

* Run the labeler one at a time per pull request

* Label dashboard creation, quick bar and theme loading changes

* Compare Labs and companion app calls with their arguments

* Match full companion app calls and imports in content labels

* Label updates list changes as System

* Match companion app property reads and renamed files in content labels

* Remove

Co-authored-by: Petar Petrov <[email protected]>

* Add ingress panel

Co-authored-by: Petar Petrov <[email protected]>

---------

Co-authored-by: Petar Petrov <[email protected]>
2026-10-09 11:25:29 +03:00
Franck Nijhof c6c85e862d Expose the real invalid state of inputs to screen readers (#54757) 2026-10-09 10:25:05 +02:00
Franck Nijhof c74b9831af Fit the add integration list on short screens (#54755) 2026-10-09 10:18:10 +02:00
Petar Petrov eb4b1e8b3d Fix negative upstream device untracked energy in devices detail graph (#54753) 2026-10-09 10:16:56 +02:00
Petar Petrov 3cd56c4b9c Follow the active row in virtual-focus lists after a removal (#54765) 2026-10-09 10:01:02 +02:00
WendelinandClaude Opus 5.5 e11cdc16ec Migrate remaining ha-md-list usages and remove ha-md-list (#54569)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-09 09:10:04 +02:00
Petar Petrov a42ba01ca2 Hide Zigbee from the ESPHome device setup wizard (#54751) 2026-10-09 08:59:02 +02:00
Franck Nijhof 941dbb5697 Fall back to local voice setup when the cloud status fails (#54754) 2026-10-09 09:51:54 +03:00
renovate[bot] 3d6061e6e0 Update pnpm to v12.10.0 (#54752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 09:33:57 +03:00
Claudio Wunder 21b07240ab Speed up building history line chart data (#54671)
* Allocate less per point when building history line chart data

For a single dataset, a plain numeric value went through
pushData(date, [value]), which allocates a one element array and a
forEach callback for every point. Push [timestamp, value] directly and
reuse one array for prevValues, which is only ever read for its latest
entry. The output is unchanged.

pnpm test:bench state-history-chart-line-data, median of 8 alternating
runs: mixed large (100k states) 8.62 -> 5.83 ms, mixed medium (10k
states) 0.80 -> 0.58 ms. In a production browser build the function took
about 101 ms before and after on 1.34 million points.

Assisted-by: Claude Sonnet 5.5 <[email protected]>

* Tighten the per-point loop in history line chart data

Profiling generateStateHistoryChartLineData on a large history put most
of the remaining time in the per-point path. Iterate the states with an
indexed loop instead of forEach, read the series array once, and track
the y range inline instead of through trackY, since the value is always
finite there. The output is unchanged.

These only help together. On their own, the hoisted array and the inline
min/max measured no change, and for...of loses most of the gain, so
@typescript-eslint/prefer-for-of is disabled for this one loop.

Cold first call in a fresh headless Brave, median of 7 runs, on 1.34
million states: 87.9 -> 77.2 ms on top of the previous commit (98.7 ms
on dev). At 100k states: 5.2 -> 4.1 ms.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Add a test for history line chart states past the end time

The characterization tests never had a state after endTime, so the path
that drops those points was not covered. That path is where the faster
pushValue skips updating the reused prevValues array, and the end point
of the line depends on it.

Add a numeric sensor with a value and a gap that only closes after
endTime. The snapshot was generated on dev before being run against this
branch, so it pins the previous behavior. Updating the reused array
before the endTime check makes only this test fail.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Drop the indexed loop from history line chart data

The previous commit gives nearly all of the gain, so the indexed loop,
the hoisted series array and the inline y range aren't worth disabling
@typescript-eslint/prefer-for-of and keeping a second copy of trackY.

This reverts commit 952022b91.

Assisted-by: Claude Opus 5.5 <[email protected]>
2026-10-09 09:24:52 +03:00
4fa8cf6dc7 Show energy gas in the unit of its sources (#54677)
* Show therm gas in therms on the energy dashboard

Gas sources with an energy unit class were always converted to and shown
in kWh. Now that therm is a native energy unit, show gas in therms when
every gas source reports therms, and request those statistics in therms
for both the current and the compare period. Any other energy unit, or a
mix of units, keeps showing kWh as before.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* Keep kWh when a therm gas statistic is also a device

The device charts read the same statistics map and label values as kWh.
If a therm gas statistic is also configured as an individual device, the
therm values would be shown as kWh there, so keep kWh for gas in that case.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* Use the unit of energy gas sources for any energy unit

Volume gas and water already use the display unit of their sources when
they all share one. Do the same for energy gas instead of always showing
kWh, by adding an ENERGY_UNITS list next to VOLUME_UNITS and requesting
the gas statistics in that unit. Different units, or a gas statistic that
is also an individual device, still fall back to kWh.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* Refactor energy statistics handling in energy.ts

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Test gas statistics shared with grid, solar and battery

Cover the fallback to kWh when a therm gas statistic is also used as a
grid, solar or battery statistic, and update the comment to match.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-09 06:18:46 +00:00
Franck Nijhof c0ed2e1b59 Keep the label of color inputs raised (#54737) 2026-10-09 08:52:39 +03:00
Simon Lamon 5a96e1829f Restore compatibillity with older chromium devices in ScrollFadeController (#54742)
Restore compatibillity with older chromium devices
2026-10-09 08:49:33 +03:00
renovate[bot] 10c91bb9a3 Update dependency vite to v8.3.3 (#54748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 05:08:01 +00:00
renovate[bot] d74804b44f Update dependency zxing-wasm to v3.1.5 (#54747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 06:56:27 +02:00
renovate[bot] fbe209cd0b Update dependency nanoid to v6.0.2 (#54746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 06:24:23 +02:00
renovate[bot] a66e13b0b9 Update dependency js-yaml to v5.4.3 (#54745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 06:24:04 +02:00
Franck Nijhof dca4a8e0a9 Load Marketplace icons through the Marketplace brands view (#54740) 2026-10-08 15:27:54 -04:00
480182c2b3 Make the Marketplace warning a friendlier modal over the Marketplace (#54586)
* Make the Marketplace warning a friendlier modal over the Marketplace

Replace the full-page first-run warning with a calmer dialog shown over
the Marketplace, which stays visible but can't be used until the notice
is accepted. Continue is enabled once the checkbox is ticked, without
the 30 second countdown. Go back, or Escape, leaves the Marketplace.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LZuGZHa3FZ9tNHtuuSy8c3

* Update the Marketplace warning to the new design

Lead with what installing can do, in a highlighted list, followed by
numbered steps to stay safe, and agree to the risks explicitly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LZuGZHa3FZ9tNHtuuSy8c3

* Update the Marketplace warning tests to the modal

Test the warning's accept, failure and go back logic instead of the
countdown, test that the Marketplace is inert behind the warning and
starts over once it is accepted, and drop the countdown strings.

Focus the checkbox when the warning opens, and drop the close handler:
neither Escape nor the scrim close the warning, Go back leaves it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LZuGZHa3FZ9tNHtuuSy8c3

* Keep links from acting behind the Marketplace warning

Until the warning is accepted, the Marketplace behind it shows the
catalog whatever the route is, so a link can't add a repository or open
a dialog over the warning. It starts over on the route once accepted.

Use the animation duration token for the agree row, which respects
reduced motion.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LZuGZHa3FZ9tNHtuuSy8c3

* Add role attribute to marketplace warning list

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Enhance ha-dialog-header and ha-marketplace-warning components with improved structure and styling

* Refactor marketplace panel tests to improve clarity and structure

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-10-08 20:30:54 +02:00
Petar Petrov 2d11eba37f Show only serial integrations when setting up an ESPHome serial port (#54668)
* Show only serial integrations when setting up an ESPHome serial port

The add integration dialog gets an integrationFilter param with a list of
domains and trims the integration descriptions to them. The ESPHome serial
Set up button fetches usb/list_serial_integrations and passes it. If that
fails, it opens the unfiltered dialog as before.

* Check for a newer add integration load after loading translations
2026-10-08 20:23:20 +02:00
Maarten Lakerveld 5821cc6e53 Drop the Mapbox RTL text plugin now that MapLibre shapes RTL text itself (#54706)
Since MapLibre 6.9.0 Arabic and Hebrew labels are shaped and reordered by
MapLibre, and setRTLTextPlugin is deprecated. We were still loading the
0.4.0 plugin, which only replaced that built-in implementation with the
same result. Version 1.0.0 of the plugin drops the UMD bundle we copied
into static assets and no longer works as a plugin at all, so bumping it
breaks the build. Remove the dependency, the static copy and the plugin
registration instead.
2026-10-08 20:21:45 +02:00
renovate[bot] ade5d15fee Update dependency @formatjs/intl-datetimeformat to v7.8.1 (#54735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-08 18:15:34 +00:00
Paul Bottein b8ea4d12b9 Replace map overview more info button with a header icon (#54704)
* Replace map overview more info button with a header icon

* Reuse the show more information label

* Show a settings button for admins in the map overview

* Hide the map overview settings button in the demo
2026-10-08 20:14:37 +02:00
Paul Bottein fcb7efdec2 Use the entity name formatter in dashboards (#54724)
* Use the entity name formatter in dashboards

* Refresh map trail names when the entity name formatter changes

* Show the device only once in the card editor entity search

* Make the name optional in the bootstrap entity name formatter
2026-10-08 20:08:21 +02:00
renovate[bot] 8dfcbe7a74 Update dependency intl-messageformat to v12.1.3 (#54734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-08 18:04:13 +00:00
JonathanandXyolyp 5509c405ad Encode the automation id in activity trace links (#54733)
Encode the item id in the activity trace link

Automation ids are free-form, so an id with a question mark, a hash or a
percent sign broke the "View trace" link of the activity details. Encode
it like the automation editor and the automation list do.

Co-authored-by: Xyolyp <[email protected]>
2026-10-08 19:59:02 +02:00
Paul Bottein bdd67e89e2 Add side padding to the statistics search field (#54717) 2026-10-08 19:56:05 +02:00
renovate[bot] a9f1a24860 Update dependency typescript-eslint to v8.71.1 (#54732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-08 19:53:44 +02:00
renovate[bot] 20062433ec Update dependency marked to v18.1.0 (#54730)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-08 19:38:39 +02:00
Franck Nijhof d3ed92978f Only cache the frontend's own static files in the service worker (#54707) 2026-10-08 18:09:28 +02:00
Franck Nijhof 2ae987504a Show the right page title for more settings pages (#54708) 2026-10-08 17:54:56 +02:00
Franck Nijhof 8662da2e8a Respect custom icons for built-in panels in the sidebar (#54709) 2026-10-08 18:24:05 +03:00
Paul Botteinandcopilot-swe-agent[bot] f2c2b9e472 Add a debug tool to simulate safe areas (#54719)
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-08 15:01:14 +00:00
Petar Petrov 47289adbab Drop hass from ha-network-graph and add a reduced motion context (#54713) 2026-10-08 16:52:15 +02:00
Claudio Wunder 6315f4a9e2 Coalesce live updates on large history charts (#54669)
* Add a delay calculation for live history updates

Rebuilding the History charts for a large history takes tens of
milliseconds, and it grows with the number of states, so applying every
live stream message to one makes the page hitch.

Add countHistoryStates and computeHistoryUpdateDelay. A history under
100,000 states is never delayed. For a larger one the delay is the time
one chart pixel covers (assuming 1000 pixels), between one second and a
maximum the caller passes in.

Assisted-by: Claude Sonnet 5.5 <[email protected]>

* Coalesce live updates on large history charts

When a History range ends in the future, Core keeps streaming states and
every message rebuilds the whole history: computeHistory, the line chart
data and the downsampling all run again over every point. With 1.34
million states (30 days of a sensor updating about every 2 seconds)
that is a 150 to 200 ms long frame per message.

Apply the first message right away, then coalesce later ones with
computeHistoryUpdateDelay, using MIN_TIME_BETWEEN_UPDATES as the maximum.
Every message carries the full history, so the latest one wins. The
timer is cleared when the panel unsubscribes.

A replaced subscription can still deliver until its unsubscribe is
acknowledged. Ignore those messages, so a coalesced one can't land
minutes later on top of the new range.

Over 25 s on a production demo build with a mocked live stream (median
of 5 runs): rebuilds 21 -> 0, long frames 22 -> 0, main thread blocked
2057 -> 0 ms, heap after GC 500 -> 288 MB.

Assisted-by: Claude Sonnet 5.5 <[email protected]>

* Use the measured chart width for the live update delay

The delay was the time one pixel covers on a chart assumed to be 1000
pixels wide. Measure the charts the same way the timeline downsamples
them, so wider or high density charts update as often as a pixel
changes.

Assisted-by: Claude Opus 5.5 <[email protected]>

* Keep the history legends current while live updates wait

The line chart legends show the current state of each entity, but the
charts skip updates where only hass changed. While a live update waited,
the legends could lag behind by up to five minutes. Request an update of
the charts when an update is held back, so they follow the current
states.

Assisted-by: Claude Opus 5.5 <[email protected]>
2026-10-08 17:34:30 +03:00
Artur Pragacz 6269d09b0d Fix translation key for external config flow step description (#54722) 2026-10-08 14:19:51 +00:00
Petar Petrov 98cb371d86 Drop hass from the distribution card (#54711)
* Drop hass from the distribution card

The card consumes the states of its own entities, the formatters,
localize and the core config. It no longer re-renders when an
unrelated entity changes.

* Re-render the distribution card when the theme changes

The card resolves its graph colors from the theme when it renders, so
it has to render again when the theme changes.
2026-10-08 16:07:39 +02:00
Petar Petrov 308b03e432 Pass localize and config to createEntityNotFoundWarning (#54710)
The helper took a hass-shaped object and an unused entity ID. It now
takes the two values it reads. The dead .hass bindings on hui-warning
are removed too; the element has had no hass property since #52926.
2026-10-08 15:54:14 +02:00
Claudio Wunder 24e15e76d9 Avoid recomputing chart series when only options change (#54666)
* Avoid recomputing chart series when only options change

The history line chart downsamples every point of a series before it is
handed to ECharts. ha-chart-base did that again on every options change:
_updateHiddenStatsFromOptions requested an update for _hiddenDatasets
each time it ran, even when no dataset had been hidden, and
_applyChartUpdate rebuilds and downsamples all series whenever
_hiddenDatasets is flagged.

Only the history line chart samples its data, and its x axis bounds
change together with its data, so rebuilding on data or hidden dataset
changes covers every input of _getSeries.

On a 30 day range of a sensor with 1.34 million states, loading the chart
called _getSeries 4 times, and now calls it once. Median of 7 runs on a
production demo build: _getSeries 85 -> 29 ms, time from data delivery to
the first frame 273 -> 209 ms.

Assisted-by: Claude Sonnet 5.5 <[email protected]>

* Drop the chart screen reader connection when the options change

Chart2Music is built with the axis names and value labels of the
current options. It used to be dropped on every options change because
_hiddenDatasets was always flagged, which now only happens when a
dataset gets hidden. Drop it whenever the options change, so the next
keyboard focus rebuilds it with the current ones.

Assisted-by: Claude Opus 5.5 <[email protected]>
2026-10-08 13:30:11 +00:00
XanderandClaude Opus 5.5 77d80bdec4 Add copy, cut and paste for dashboard sections (#54652)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-08 10:46:01 +00:00
Franck Nijhof 8d0d8160f5 Handle map labels without a name (#54689) 2026-10-08 12:16:49 +02:00
Bram KragtenandClaude Opus 5.5 463bda3e7c Add realistic automations to the demo (#54647)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-08 12:05:54 +02:00
Marcin BauerandClaude Opus 5.5 dfdaeee75b Allow running and testing disabled actions and conditions (#54649)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-08 12:01:37 +02:00
Franck Nijhof f0a67727a2 Fix ZHA reconfigure dialog errors after closing and on reconnect (#54684)
* Fix ZHA reconfigure dialog errors after closing and on reconnect

* Only handle reconfigure messages of the current run
2026-10-08 11:54:09 +02:00
Paul Bottein e47a7c3a45 Allow sections to contain badges and sections (#54624) 2026-10-08 09:30:26 +01:00
WendelinandClaude Opus 5.5 da70ab7268 Refactor UI support checks into static methods and add validation helpers (#54493)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-08 10:27:10 +02:00
Franck Nijhof 2f15c7640f Keep boolean fields inline in the narrow action editor (#54686) 2026-10-08 09:18:40 +01:00
Bram KragtenandClaude Opus 5.5 1c8da1329e Add focus indicator to entity and sensor cards (#54644)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-08 09:16:21 +01:00
Franck Nijhof a9c449f502 Load the HLS source once instead of on every media attach (#54683) 2026-10-08 08:55:09 +01:00
Franck Nijhof 568d842245 Let long dropdown submenus scroll (#54692) 2026-10-08 07:48:26 +00:00
Petar Petrov 796795818d Show the newest history states when the browser clock runs behind (#54688) 2026-10-08 08:44:19 +01:00
Franck Nijhof 33c1a3acfd Keep a cleared input value when focusing it again (#54690) 2026-10-08 09:40:27 +02:00
Franck Nijhof 9e0f0931ae Keep the key order when duplicating a script (#54687) 2026-10-08 09:29:30 +02:00
b71c47f2e8 Add unit of measurement selector (#54391)
* WIP

* Implement unit of measurement picker and selector

* Disable units for device classes that do not support units

* Fix no unit

* Follow sensor entity unit rules in unit of measurement selector

Non-numeric device classes only allow no unit, and a device class or
state class without a unit table (such as monetary or measurement)
no longer restricts the units.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Follow up on comments

* Lint error

* Add context info to config

* Only clear the unit when the context filters change

ha-form, the blueprint editor and the gallery create a new context object
on every render, so editing an unrelated field counted as a context
change and could clear a saved unit. Compare the filter values instead.

A filter can be a list of device classes or state classes, so compare
them by value rather than by reference.

Co-authored-by: Petar Petrov <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
2026-10-08 09:25:14 +03:00
karwosts 6afbc6322d Fix unhandled undefined label registry access (#54659) 2026-10-08 09:13:19 +03:00
bd286f2519 Add power values to energy dashboard (#54627)
* Bugfixes

* Remove useless updates

* Fix updating issue

* Remove useless check

* Rename test case for clarity

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Petar Petrov <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-08 05:49:43 +00:00
Jump-333 fb16219c5b Add legend values in the devices details graph (#54631)
* Add legend values in the electricity page

* Fix localization issue and tests

* Add tests and fix localization issues

* Remove energy usage graph legend
2026-10-08 08:25:34 +03:00
Aidan Timson 4852d3c609 Add repository source filter and grouping to Marketplace (#54654)
* Add a source filter and group by source to the marketplace

* Plural form

* Test that marketplace browse links keep the source

* Drop the Marketplace filter pane rendering test
2026-10-08 08:22:14 +03:00
Jump-333 26c332d922 Remove duplicate condition from the automation editor code (#54674)
Remove duplicate condition
2026-10-08 08:13:04 +03:00
renovate[bot]andPetar Petrov b829ba9eca Update dependency jsdom to v30.1.2 (#54642)
* Update dependency jsdom to v30.1.2

* Focus shadow-rendered buttons in dialog-form tests

jsdom 30.1.2 no longer focuses light-DOM children of a shadow host
without a slot. The ha-form mock has no slot, so the opener buttons
were never rendered. Put them in the form's shadow root, where real
ha-form controls live.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
2026-10-08 06:40:44 +02:00
Franck Nijhof 46c65973a0 Wrap descriptions in voice assistant settings (#54678) 2026-10-08 06:39:03 +02:00
karwosts 4bb7908f60 Fix create floor memoization issue (#54675) 2026-10-08 06:38:34 +02:00
Paul Bottein bc21992178 Snap the map overview bottom sheet to fixed positions (#54663)
* Snap the map overview bottom sheet to fixed positions

* Include the safe area in the collapsed stop and follow reduced motion
2026-10-07 17:50:09 +02:00
Bram KragtenandClaude Opus 5.5 28f53d9f5a Show the next demo card on the home page demo again (#54646)
* Show the next demo card on the home page demo again

The home overview strategy now generates the favorites section itself
instead of returning a common-controls section strategy, so the demo
strategy no longer found the section to add its card to. Find the
favorites section by its heading instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep at least six favorites next to the demo card

The section strategy used to get a limit of two less, so seven tiles
became six. Only drop as many tiles as needed to keep six.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep the current user when the demo user subscription refreshes

The auth/current_user mock answered with the user from startup, so the
user subscription reset a changed user, like the non-admin scenario of
the e2e tests, to the initial admin user.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-07 17:33:31 +02:00
Aidan Timson d2fa820035 Add group by to Marketplace tables (#54653)
* Add group by status and type to the marketplace

* Update the marketplace dashboard test for grouping

* Group the marketplace by status by default

* Type the Marketplace grouping events and drop the panel test
2026-10-07 17:05:45 +02:00
Petar Petrov 0e34aa06c1 Align history timeline X axis with line charts (#54657) 2026-10-07 17:53:03 +03:00
Marcin BauerandClaude Opus 5.5 bb168601b8 Restore left-column timeline names on the history-graph card (#54661)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-07 15:47:15 +01:00
Aidan Timson 1823e783ee E2E test fix: Keep the current user in the demo auth stub (#54658) 2026-10-07 15:56:00 +02:00
Marcin BauerandClaude Opus 5.5 03a815bb2e Fix long entity names overflowing the entities picker (#54656)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-07 13:13:50 +00:00
Marcin BauerandClaude Opus 5.5 adde734800 Make disabled automation rows easier to read (#54650)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-07 14:58:02 +02:00
Petar Petrov 8598a35c88 Add reset zoom button to network graphs (#54510) 2026-10-07 14:30:10 +02:00
WendelinandClaude Opus 5.5 7008f2ee38 Keep dropdown menus within the viewport and wrap long items (#54643)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-10-07 14:23:54 +02:00
Paul Bottein 0a8a71049b Use generic paths to address Lovelace dashboard items (#54207)
* Use generic paths to address Lovelace dashboard items

* Return node target for container paths

* Remove dropped card node when creating a section from a drop

* Refuse to write through a missing list item

* Only build card and badge paths in edit mode

* Use optional badge config in edit badge dialog params

* Append cards in a single write in addCards

* Append badges in a single write in addBadges

* Pass a save callback to the badge edit dialog

* Handle ll-create-card from custom layouts that don't bubble it
2026-10-07 14:20:51 +02:00
Bram KragtenandClaude Opus 5.5 79488d0c09 Mock editing the own profile in the demo (#54645)
The demo did not answer person/update_own_profile, so saving the profile
dialog showed an error. Also answer auth/current_user, so the user
subscription picks up the new name.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-07 15:00:39 +03:00
WendelinandClaude d0c09951e6 Dismiss reboot/shutdown toast when frontend reconnects (#54640)
The "Rebooting system" toast was shown without an id and with no
timeout, so nothing could ever close it and it stayed on screen after
the host came back. Give it an id, dismiss it when the connection is
re-established, and also when the action fails with an error dialog.

Fixes #54630


Claude-Session: https://claude.ai/code/session_01SkS1AB8w2GPUuRBaDDmsEu

Co-authored-by: Claude <[email protected]>
2026-10-07 10:37:54 +02:00
Aidan Timsonandcopilot-swe-agent[bot] 3d57d1b949 Remove the blocking-label workflow (#54064)
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-07 10:33:37 +02:00
Maarten Lakerveld ec6b258750 Zone editor: fit without passive zones, hide covered radius handles, name tooltips, and start new zones at the map center (#54597)
* Leave passive zones out of the zones map fit

Opening the zones page fitted the map around every zone, so a few remote
passive zones pushed the view out to the whole globe and adding a zone took
a lot of zooming. Passive zones are still drawn and editable, but no longer
count toward the fit unless they are the only zones there are. Editable
locations gained a fit flag for this.

Fixes #54595

* Hide a zone's radius handle while the circle is under its icon

Zoomed out on the zones page, every zone's radius handle collapsed onto the
zone icon and covered it. The handle now hides while the radius on screen is
shorter than the icon's half width, so it returns as soon as the circle's
edge shows past the icon, and it stays while the radius is being dragged.

* Show a zone's name in a tooltip when hovering its marker on the zones map

The marker only had a native title, which appears late and in the
browser's own style. Every named editable location now gets an ha-tooltip
anchored to its element, so hovering a zone icon on the zones page names
it promptly; the native title is dropped so the two do not stack.

* Start a new zone at the center of the zones map

Create zone placed the new zone at a random spot up to 500 meters from
home, so it landed somewhere unexpected and often off-screen. When the zones
page has its map open, a new zone now starts at the center of the current
view, so panning to the right place first works as expected. A location
handed in, as from a person's Create zone action, still wins; without a map
the random offset around home remains.
2026-10-07 10:12:54 +02:00
Maarten Lakerveld ea1fabdfdd Map: float every marker, keep the selection in the URL, pan-first focus, and fix attribution and zone radius display (#54596)
* Auto-hide compact map attribution and restyle it for dark mode

MapLibre decides compact mode from the map canvas width alone (640px), so
shrinking the window flipped the attribution from compact back to inline
when the sidebar became a drawer and the map got wider. The engine now adds
the control itself and forces compact on a narrow viewport (870px, the app's
own narrow layout), swapping it when the breakpoint changes.

MapLibre only collapsed the opened compact attribution on a drag; it now
also collapses by itself after five seconds, unless the user opened it from
the info button.

MapLibre's stylesheet hardcodes a white attribution with black text, which
Home Assistant never overrode, so it stood out on a dark map. The dark mode
rules now match the zoom control styling, and the inline text color is set
explicitly instead of inherited from a page that may be in the other mode.

* Add show_zone_radius option to the map card

In panel layout the map hides zone radii except on the Zones tab, for the
selected zone, and while editing. A user asked to always see them, so the
new option draws the radius of every visible zone on all tabs. Other layouts
already show radii and are unaffected.

* Regroup map clusters up front for every fit

Clicking a bubble already regrouped its members at the zoom the fit would
land on, so they parted as the animation started. Any other way of zooming
in, such as picking a person in the map overview, waited for the move to end
before the bubble split. The fit itself now does the regrouping, for all
markers, so every animated fit shows the bubbles as they will end up.

* Return the map to its previous view from the overview's back button

Leaving a selected person or zone cleared the selection but left the map
zoomed in on it. The card now remembers where the map was when the overview
drilled in: the exact camera, or the previous entity's fit when one selection
followed another, and the back button eases back there. A camera that was
still auto-fitting resumes auto-fit instead of freezing. Clicking the map to
deselect stays put, since that is the user looking around.

Engines gain getView, and setView can animate.

* Float the selected map marker above a dot at its location

Selecting a person or device marked it with a ring only, and inside a zone
it stayed hidden in the zone's bubble. The selected marker is now excluded
from clustering, drawn a quarter larger, and hangs above a small dot at the
exact coordinate with a tail pointing down to it. The accuracy circle around
the dot carries the entity color, so the ring is dropped; an entity that
reports no accuracy gets a nominal 5 meter circle so the color still shows.
A fix of 10 meters or better draws that circle as a soft disc without an
outline, since a hard edge would claim more precision than is known; map
circles gained an outline option for this. The engine's marker anchor puts
the dot on the coordinate; both engines support it. Bubble avatars keep
their size.

* Open more info from a button in the map overview detail

The selected person, device or zone's name was the only way to the more-info
dialog, which made the title read as a link. The name is plain text now and
a filled More info button sits below the activity, where the user expects an
action. Focus still moves to the name when the detail view opens.

* Always show the selected marker's accuracy circle

The circle only appeared for a GPS fix looser than 100 meters, so most
phones never showed one. The selected marker now floats above a dot at its
coordinate, so even a small circle around that dot reads clearly; it is
never hidden for the selected entity. Focusing still fits the circle into
view only above 100 meters, since a tighter circle fits at the focus zoom.

* Describe every toggle in the map card editor

Only Show all and Show zone radius explained themselves; Scale ruler, Auto
fit, Fit zones and Cluster markers now have helper text too.

* Float every map marker above its location

Only the selected marker hung above a dot with a tail; the rest sat
centered on their coordinate, so a marker's frame always hid the spot it
marked. Every entity marker is now a pin whose tail points just above its
coordinate. Selecting one scales its frame up and shows the dot at the
exact location. Cluster bubble avatars keep their plain frames.

* Keep the map overview selection in browser history

The card remembered a single camera to restore from the overview's back
button, so after selecting one entity and then another the original view
was lost, and back from the second left the map zoomed on the first while
the overview showed the list. The selected entity now lives in the URL as
an entity_id query parameter; selecting pushes a history entry, and so
does deselecting, whether from the overview's back arrow or a click on
the map, so the browser's back and forward step through every selection.
The overview's arrow itself always returns to the list. Selecting focuses
the entity; deselecting leaves the map where it is. Only selectable
domains are accepted from the URL; anything else is dropped from it. The
saved camera, the animated setView and the autoFit view flag had no
other users.

* Pan to a selected map entity instead of zooming in on it

Focusing a person jumped to zoom 19 and a zone fitted its circle, which
often zoomed much further in than the user had been. Selecting now keeps
the current zoom and pans the entity into the free part of the view. It
only zooms in when another marker sits within 80 screen pixels, and then
just far enough to clear it, capped at zoom 17. Markers at the very same
position cannot be separated by zooming and are left alone. A zone is
fitted as its whole circle with a little room around it under that same
zoom cap, so a zone that fits the view keeps the zoom and a bigger one
zooms out just enough. A marker already on screen eases over; one off
screen flies there in an arc. Fits gain a fly option for this, and every
other fit now eases straight to its bounds on both engines, where
MapLibre used to fly. Map-engine gains pixelDistance, the Web Mercator
screen distance at a zoom level, with a test for its scale.

* Float every cluster bubble above its location

Only a bubble attached to a zone had a tail and hung above the zone's
circle; a bubble of nearby markers sat centered on the cluster, hiding the
spot it stood for. Every bubble now has the tail and floats above its
location with the same lift as a single marker's pin, so the two read as
one style. A zone's bubble keeps floating above the zone circle.
2026-10-07 10:12:53 +02:00
Maarten Lakerveld d02b605916 Zone editor: fit without passive zones, hide covered radius handles, name tooltips, and start new zones at the map center (#54597)
* Leave passive zones out of the zones map fit

Opening the zones page fitted the map around every zone, so a few remote
passive zones pushed the view out to the whole globe and adding a zone took
a lot of zooming. Passive zones are still drawn and editable, but no longer
count toward the fit unless they are the only zones there are. Editable
locations gained a fit flag for this.

Fixes #54595

* Hide a zone's radius handle while the circle is under its icon

Zoomed out on the zones page, every zone's radius handle collapsed onto the
zone icon and covered it. The handle now hides while the radius on screen is
shorter than the icon's half width, so it returns as soon as the circle's
edge shows past the icon, and it stays while the radius is being dragged.

* Show a zone's name in a tooltip when hovering its marker on the zones map

The marker only had a native title, which appears late and in the
browser's own style. Every named editable location now gets an ha-tooltip
anchored to its element, so hovering a zone icon on the zones page names
it promptly; the native title is dropped so the two do not stack.

* Start a new zone at the center of the zones map

Create zone placed the new zone at a random spot up to 500 meters from
home, so it landed somewhere unexpected and often off-screen. When the zones
page has its map open, a new zone now starts at the center of the current
view, so panning to the right place first works as expected. A location
handed in, as from a person's Create zone action, still wins; without a map
the random offset around home remains.
2026-10-07 10:12:08 +02:00
Maarten Lakerveld a9c1531e37 Map: float every marker, keep the selection in the URL, pan-first focus, and fix attribution and zone radius display (#54596)
* Auto-hide compact map attribution and restyle it for dark mode

MapLibre decides compact mode from the map canvas width alone (640px), so
shrinking the window flipped the attribution from compact back to inline
when the sidebar became a drawer and the map got wider. The engine now adds
the control itself and forces compact on a narrow viewport (870px, the app's
own narrow layout), swapping it when the breakpoint changes.

MapLibre only collapsed the opened compact attribution on a drag; it now
also collapses by itself after five seconds, unless the user opened it from
the info button.

MapLibre's stylesheet hardcodes a white attribution with black text, which
Home Assistant never overrode, so it stood out on a dark map. The dark mode
rules now match the zoom control styling, and the inline text color is set
explicitly instead of inherited from a page that may be in the other mode.

* Add show_zone_radius option to the map card

In panel layout the map hides zone radii except on the Zones tab, for the
selected zone, and while editing. A user asked to always see them, so the
new option draws the radius of every visible zone on all tabs. Other layouts
already show radii and are unaffected.

* Regroup map clusters up front for every fit

Clicking a bubble already regrouped its members at the zoom the fit would
land on, so they parted as the animation started. Any other way of zooming
in, such as picking a person in the map overview, waited for the move to end
before the bubble split. The fit itself now does the regrouping, for all
markers, so every animated fit shows the bubbles as they will end up.

* Return the map to its previous view from the overview's back button

Leaving a selected person or zone cleared the selection but left the map
zoomed in on it. The card now remembers where the map was when the overview
drilled in: the exact camera, or the previous entity's fit when one selection
followed another, and the back button eases back there. A camera that was
still auto-fitting resumes auto-fit instead of freezing. Clicking the map to
deselect stays put, since that is the user looking around.

Engines gain getView, and setView can animate.

* Float the selected map marker above a dot at its location

Selecting a person or device marked it with a ring only, and inside a zone
it stayed hidden in the zone's bubble. The selected marker is now excluded
from clustering, drawn a quarter larger, and hangs above a small dot at the
exact coordinate with a tail pointing down to it. The accuracy circle around
the dot carries the entity color, so the ring is dropped; an entity that
reports no accuracy gets a nominal 5 meter circle so the color still shows.
A fix of 10 meters or better draws that circle as a soft disc without an
outline, since a hard edge would claim more precision than is known; map
circles gained an outline option for this. The engine's marker anchor puts
the dot on the coordinate; both engines support it. Bubble avatars keep
their size.

* Open more info from a button in the map overview detail

The selected person, device or zone's name was the only way to the more-info
dialog, which made the title read as a link. The name is plain text now and
a filled More info button sits below the activity, where the user expects an
action. Focus still moves to the name when the detail view opens.

* Always show the selected marker's accuracy circle

The circle only appeared for a GPS fix looser than 100 meters, so most
phones never showed one. The selected marker now floats above a dot at its
coordinate, so even a small circle around that dot reads clearly; it is
never hidden for the selected entity. Focusing still fits the circle into
view only above 100 meters, since a tighter circle fits at the focus zoom.

* Describe every toggle in the map card editor

Only Show all and Show zone radius explained themselves; Scale ruler, Auto
fit, Fit zones and Cluster markers now have helper text too.

* Float every map marker above its location

Only the selected marker hung above a dot with a tail; the rest sat
centered on their coordinate, so a marker's frame always hid the spot it
marked. Every entity marker is now a pin whose tail points just above its
coordinate. Selecting one scales its frame up and shows the dot at the
exact location. Cluster bubble avatars keep their plain frames.

* Keep the map overview selection in browser history

The card remembered a single camera to restore from the overview's back
button, so after selecting one entity and then another the original view
was lost, and back from the second left the map zoomed on the first while
the overview showed the list. The selected entity now lives in the URL as
an entity_id query parameter; selecting pushes a history entry, and so
does deselecting, whether from the overview's back arrow or a click on
the map, so the browser's back and forward step through every selection.
The overview's arrow itself always returns to the list. Selecting focuses
the entity; deselecting leaves the map where it is. Only selectable
domains are accepted from the URL; anything else is dropped from it. The
saved camera, the animated setView and the autoFit view flag had no
other users.

* Pan to a selected map entity instead of zooming in on it

Focusing a person jumped to zoom 19 and a zone fitted its circle, which
often zoomed much further in than the user had been. Selecting now keeps
the current zoom and pans the entity into the free part of the view. It
only zooms in when another marker sits within 80 screen pixels, and then
just far enough to clear it, capped at zoom 17. Markers at the very same
position cannot be separated by zooming and are left alone. A zone is
fitted as its whole circle with a little room around it under that same
zoom cap, so a zone that fits the view keeps the zoom and a bigger one
zooms out just enough. A marker already on screen eases over; one off
screen flies there in an arc. Fits gain a fly option for this, and every
other fit now eases straight to its bounds on both engines, where
MapLibre used to fly. Map-engine gains pixelDistance, the Web Mercator
screen distance at a zoom level, with a test for its scale.

* Float every cluster bubble above its location

Only a bubble attached to a zone had a tail and hung above the zone's
circle; a bubble of nearby markers sat centered on the cluster, hiding the
spot it stood for. Every bubble now has the tail and floats above its
location with the same lift as a single marker's pin, so the two read as
one style. A zone's bubble keeps floating above the zone circle.
2026-10-07 10:09:07 +02:00
Bram Kragten af4f86743b Bumped version to 20260930.2 2026-10-07 10:07:02 +02:00
Artur Pragacz ed492d5b94 Consider next name part when unprefixing entity name (#54633)
* Consider next name part when unprefixing entity name

* Adjust
2026-10-07 10:05:34 +02:00
Paul Botteinandcopilot-swe-agent[bot] db00a6b7e7 Fix section editor crash while typing a strategy in YAML (#54622)
* Fix section editor crash while typing a strategy in YAML

* Reject array-valued section strategies in YAML

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-07 10:05:33 +02:00
WendelinandClaude 864f0877f6 Increase touch area of ha-checkbox (#54617)
Co-authored-by: Claude <[email protected]>
2026-10-07 10:05:31 +02:00
Wendelin d7062f3d9d Fix HaChipBase border radius (#54603)
Add border-radius to HaChipBase styles for improved appearance
2026-10-07 10:05:30 +02:00
Jonathan 3051032360 Show the requested run when a trace page switches item (#54591) 2026-10-07 10:05:29 +02:00
Paul Bottein 54c575efd1 Show referenced triggers instead of generated IDs in automation and trace descriptions (#54587) 2026-10-07 10:05:28 +02:00
Paul Bottein e6bc47e6ba Treat an entity name equal to its device name as no own name (#54585)
* Treat an entity name equal to its device name as no own name

* Use the same device name check when computing entity names

* Only check the user-set name against the device name

* Add tests for the device name check

* Clarify why only the user-set name is matched loosely
2026-10-07 10:05:26 +02:00
WendelinandClaude Sonnet 5.5 6244a8db04 Fix config entry state styling (#54584)
* Restore needs attention color on config entry rows

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Restore config entry row state styling

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Highlight config entry state with border color instead of tint

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Add quiet fill background to config entry states

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use semantic colors for config entry state borders

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Drop state border colors and use semantic text colors

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use semantic color for config entry link icon

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Add divider between discovered and attention flow rows

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use semantic border and fill colors for discovered and attention lists

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use warning colors for discovered list too

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use standard divider color for flow list borders

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use border width token for flow list borders

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Restore warning icon color for failed unload state

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* review

---------

Co-authored-by: Claude <[email protected]>
2026-10-07 10:05:25 +02:00
Paul Bottein 3f2c1ea538 Show then branch before else in trace graph (#54583) 2026-10-07 10:05:24 +02:00
Paul Bottein aac947ee4d Fix option labels overflowing horizontal control selects (#54582) 2026-10-07 10:05:23 +02:00
Maarten Lakerveld 594d9be859 Bump home-assistant-js-websocket to 9.7.1 (#54577)
Exclude our own packages from the 3-day minimumReleaseAge rule so
fixes we publish ourselves can be picked up immediately. The
no-downgrade trust policy still rejects unsigned releases.
2026-10-07 10:05:22 +02:00
Paul Bottein 80ae2543e5 Fix map history paths flickering on every update (#54573)
Update map paths and zone circles in place instead of redrawing them
2026-10-07 10:05:21 +02:00
Wendelin 94d176c3d1 Fix styling for config entry device rows (#54570) 2026-10-07 10:05:20 +02:00
Jan-Philipp Benecke 192d9f785f Hide dangling trace graph connectors when nothing follows (#54568) 2026-10-07 10:05:18 +02:00
Paul Bottein 398ffa045b Fix first badges cut off in centered scrolling view header (#54565) 2026-10-07 10:05:17 +02:00
Wendelin e67538fb7c config-area-page fix missing list-item import (#54563) 2026-10-07 10:05:16 +02:00
Matthias AlphartandClaude 87166c98dc Sort selected weekdays in time trigger and condition (#54623)
Co-authored-by: Claude <[email protected]>
2026-10-07 09:16:10 +02:00
Artur Pragacz 89c333894d Consider next name part when unprefixing entity name (#54633)
* Consider next name part when unprefixing entity name

* Adjust
2026-10-07 08:39:45 +03:00
karwosts d994db64cb Fix script duplication (#54637) 2026-10-07 08:20:57 +03:00
renovate[bot] d6a3c6efcc Update dependency @versatiles/style to v6.1.1 (#54629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-07 05:01:37 +00:00
Paul Botteinandcopilot-swe-agent[bot] ce58026788 Fix section editor crash while typing a strategy in YAML (#54622)
* Fix section editor crash while typing a strategy in YAML

* Reject array-valued section strategies in YAML

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-07 04:59:26 +00:00
2e09d1a43b Correct system and diagnostics API types in the frontend (#54290)
* Correct system and diagnostics API types in the frontend

Compare the system health, log, repairs and related data modules with
the handlers that produce them, and fix the mismatches.

The system health failure detail was read from a key that does not
exist, so a failed check rendered blank. Repairs issues carry several
always-present nullable fields and no active flag. Saving the core
config and ignoring a repair return nothing, and the icon types were
missing a nesting level.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

* Surface the HTTP fallback port and the failure reason

Warn separately when the server fell back to the previous default
port, which the saved configuration warning did not cover.

Show the diagnostic message a failed configuration carries, and fall
back to the error code when there is none.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-07 06:52:57 +02:00
WendelinandClaude Opus 5.5 45e2392eeb Fix leaked WebRTC peer connections in ha-web-rtc-player (#54557)
* Abandon superseded WebRTC starts to avoid leaking peer connections

If ha-web-rtc-player was disconnected (or restarted) while awaiting the
WebRTC client configuration, the start continued afterwards and created
an RTCPeerConnection that nothing ever closed.

Track a start id that is bumped on every clean up and bail out when a
start has been superseded. Negotiation now also checks that it still
belongs to the current peer connection.

Fixes #54482

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ZzvX8SAHt1knzzJNULtZx

* Bind WebRTC signaling callbacks to their own peer connection

Events of an offer subscription that belongs to a superseded start could
still reach the current stream before the unsubscribe completed, sending
candidates to the old session or applying the old answer to the new
offer. Ignore events whose peer connection is no longer the current one,
and don't report errors from operations rejected because a superseded
connection was closed.

Also show an error instead of leaving an unhandled rejection when
fetching the WebRTC client configuration fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ZzvX8SAHt1knzzJNULtZx

* Localize WebRTC player errors and handle rejected offer subscriptions

Error messages of ha-web-rtc-player were hardcoded English strings. They
are now stored as an error type plus the backend message and localized
when rendering.

The try/catch around webRtcOffer() never caught anything because the
subscription returns a promise, so a rejected offer left an unhandled
rejection and no error. Handle the rejection, and don't let clean up
re-raise it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ZzvX8SAHt1knzzJNULtZx

* Rename WebRTC start id to clean up count

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ZzvX8SAHt1knzzJNULtZx

* Don't start a WebRTC stream on a disconnected player

Lit still runs a pending update after the element was disconnected, so
a player added and removed in the same task (or given a new entityid
after removal) started a stream after its last clean up, which nothing
closed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ZzvX8SAHt1knzzJNULtZx

---------

Co-authored-by: Claude <[email protected]>
2026-10-07 06:52:10 +02:00
renovate[bot] b0c5ea6296 Update pnpm to v12.9.1 (#54635)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-07 06:49:20 +02:00
renovate[bot] c0204eeb25 Update dependency maplibre-gl to v6.12.0 (#54636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-07 06:49:03 +02:00
59444f72bc Add a gallery page for ha-picker-combo-box (#54588)
* Add a gallery page for ha-picker-combo-box

The picker combo box is only ever seen inside a popover or dialog, which
makes its two render paths and two row highlights hard to inspect. This
page renders it directly in four states: a plain list under the
MAX_PLAIN_LIST_ITEMS threshold, a virtualized list of 200 items, an empty
list, and one with a current value pinned into view.

The component takes no hass, so the page provides internationalizationContext
directly rather than going through a fake hass object.

Co-Authored-By: Claude Opus 5 <[email protected]>

* WIP - combobox gallery

* Tighten the picker combo box gallery text

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Improve documentation for ha-picker-combo-box

Clarified rendering behavior and filtering logic in the documentation. Updated keyboard shortcuts and added information about selected sections.

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Fix Prettier formatting in picker combo box gallery page

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-06 13:52:15 +00:00
Petar Petrov 6856449bd3 Drop hass from the home summary card (#54606)
* Drop hass from the home summary card

The card consumes the contexts it reads and computes its summary text
from statesContext, so it only re-renders when the text changes. It
subscribes to the energy collection through EnergyCollectionController,
which gains a beforeSubscribe hook to set today's period, and fires
hass-action like the button card. isSecurityAlertActive and the battery
filters take the values they need.

* Test isSecurityAlertActive against every alert domain
2026-10-06 15:42:24 +02:00
Petar Petrov 9fa168e22c Pass registries to generateEntityFilter as separate values (#54605)
generateEntityFilter takes states, entities, devices, areas and floors
instead of a hass-shaped object, so callers without hass can use it.
2026-10-06 15:24:18 +02:00
Marcin BauerandClaude Opus 5.5 6d6aaecb39 Remove no-op refresh button from activity panel (#54620)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-06 15:20:22 +02:00
Jump-333 d9f2baeeb4 Add badge for the wait for a template row (#54529)
* Add badge for the wait for a template row

* Removed literal text filter
2026-10-06 15:35:55 +03:00
Aidan Timson 234eb800bb Fetch nightly translations artifact directly with retry backoff (#54574)
* Fetch nightly translations artifact directly with backoff

* Type nightly translations fetch and use shared retry helper

* Retry GitHub API calls in workflow scripts
2026-10-06 15:27:36 +03:00
Wendelin 40c76b3993 Upgrade webawesome to version 3.14 (#54616)
* Upgrade @home-assistant/webawesome to version 3.14.0-ha.0 in package.json and pnpm-lock.yaml

* Update bundle size budget for frontend-modern and frontend-legacy
2026-10-06 15:11:29 +03:00
WendelinandClaude 7ca12940d9 Increase touch area of ha-checkbox (#54617)
Co-authored-by: Claude <[email protected]>
2026-10-06 13:11:20 +01:00
Aidan Timson 19630745d8 Follow webawesome fork releases in Renovate (#54619) 2026-10-06 12:08:17 +00:00
Marcin BauerandClaude Opus 5.5 69f83e0ddc Move dashboard edit mode help button to the overflow menu (#54608)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-06 14:34:55 +03:00
Marcin BauerandClaude Opus 5.5 afac5d6e56 Add undo action to cut toast in automation editor (#54612)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-06 13:19:41 +02:00
Aidan Timson c40846ada7 Skip release delay for first-party packages (#54614) 2026-10-06 13:18:24 +02:00
Bram KragtenandClaude 388c4f856b Simplify CI workflow: make build depend on lint and test (#54609)
Co-authored-by: Claude <[email protected]>
2026-10-06 12:09:47 +01:00
Aidan Timson 7051fd27f7 Add a custom repositories page to the Marketplace (#54604)
Manage custom Marketplace repositories on their own page
2026-10-06 12:27:42 +03:00
Jonathan 6fca7a89b6 Show the requested run when a trace page switches item (#54591) 2026-10-06 11:26:46 +02:00
Wendelin 121b3dade7 Fix HaChipBase border radius (#54603)
Add border-radius to HaChipBase styles for improved appearance
2026-10-06 12:19:56 +03:00
Marcin BauerandClaude Opus 5.5 5a5611df6b Load History and Activity results only after closing the sources sheet on mobile (#54602)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-06 11:05:39 +02:00
Paul Bottein a7c435e1a0 Show referenced triggers instead of generated IDs in automation and trace descriptions (#54587) 2026-10-06 10:52:45 +02:00
Petar Petrov 8bc7ab0e74 Drop hass from the energy total badges and the statistics graph card (#54590)
The badges and the card subscribe to the energy collection through
EnergyCollectionController and consume contexts for everything else.
statistics-chart moves to contexts with them and regenerates on a theme
change. The controller unsubscribes when config() returns undefined, so
turning energy_date_selection off stops the subscription.
2026-10-06 09:40:38 +01:00
Bram KragtenandClaude Opus 5.5 5ca2ac68a3 Use standard Apache 2.0 license text so GitHub detects it (#54599)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-06 10:35:17 +02:00
Petar Petrov eb43b1eab8 Migrate energy, power, gas and water cards to context instead of hass (#54560)
* Migrate energy, power, gas and water cards to context instead of hass

The cards consume only the context slices they read. A new
EnergyCollectionController replaces SubscribeMixin for the energy
collection subscription. ha-chart-base, ha-sankey-chart and
hui-energy-period-selector move to contexts too, and the energy and
recorder helpers take Pick<HomeAssistant, ...> slices.

* Inline the energy collection Pick type

* Pass values instead of hass-shaped objects to energy and recorder helpers

The helpers take states, formatEntityName, callWS, locale or panelUrl
directly. getEnergyDataCollection takes the connection plus an options
object, since it needs several inputs.

* Use consumeContext in EnergyCollectionController

The controller extends ContextController and declares its consumed
values as decorated fields instead of wiring each subscription by hand.

* Consume the internationalization context once in energy cards and charts

ha-chart-base, the period selector and the energy cards read both
localize and locale, so they consume the whole context as _i18n
instead of subscribing to it twice.

* Drop the connected-host guard from EnergyCollectionController

ContextController now defers its setup on connected hosts, so the
controller no longer needs to guard against running before its options
are set.
2026-10-06 10:27:06 +02:00
Petar Petrov 96684da7a7 Add a consumeContext decorator for reactive controllers (#54575)
* Add a consumeContext decorator for reactive controllers

Controllers extend ContextController and decorate their fields with
consumeContext({ context, subscribe, transform }). The base class keeps
each field current and calls contextUpdated() only when the transformed
value changes. ServiceInfoController uses it instead of its own
ContextSubscriptionController wiring.

* Defer context setup on connected hosts and check consumeContext fields

A controller created on a connected host no longer runs subclass code
from super(): Lit calls hostConnected() from addController(), so the
subscriptions are set up after construction instead. consumeContext
also rejects public fields that cannot hold the stored value, like
@consume does.
2026-10-06 10:08:11 +02:00
WendelinandClaude Sonnet 5.5 13e9127bdb Fix config entry state styling (#54584)
* Restore needs attention color on config entry rows

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Restore config entry row state styling

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Highlight config entry state with border color instead of tint

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Add quiet fill background to config entry states

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use semantic colors for config entry state borders

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Drop state border colors and use semantic text colors

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use semantic color for config entry link icon

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Add divider between discovered and attention flow rows

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use semantic border and fill colors for discovered and attention lists

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use warning colors for discovered list too

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use standard divider color for flow list borders

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Use border width token for flow list borders

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* Restore warning icon color for failed unload state

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PMDcr7YWpvXhByiZZqoeAc

* review

---------

Co-authored-by: Claude <[email protected]>
2026-10-06 09:59:54 +02:00
karwosts 9a45b6a200 Cleanup multiple unnecessary usages of entitySources (#54556)
* Cleanup multiple unnecessary usages of entitySources

* performance

* don't need entitySources for device integration

* memoize and filter device integration lookups

* Fix race condition fetching config entries

* Gate deviceIntegratonLookup in selector.ts on integration filter
2026-10-06 09:56:57 +03:00
Einar Skreslett a3f5929ddd Fix invisible inherited label tooltip text colour (#54521)
* fix invisible inherited label tooltip text colour

* fix tooltip while keeping theme text colour
2026-10-06 05:51:42 +00:00
Aidan Timson 0fed256025 Use context in Marketplace, prepare shared layouts for future context migrations (#54566)
* Read config and translations from context in shared layouts

* Use context instead of hass in Marketplace pages

* Provide contexts in Marketplace and tabs subpage tests

* Use provideHass for the Marketplace dialog test host

* Pass entities to tab page filters

* Update tab filters when entities change

* Fall back to hass localize in error screen

* Note when to inline error screen localize
2026-10-06 08:35:04 +03:00
ae3b3eefb3 Forward OAuth authorization parameters to the login flow (#54548)
* Forward OAuth authorization request parameters to the login flow

* Defer OAuth resource forwarding to a separate change

* Test preservation of empty PKCE parameters

* Do not forward state to the login flow

Core does not use state. The frontend appends it to the redirect
after the login flow finishes. The createLoginFlow type excludes
state, because Core rejects unknown fields.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Reduce the login flow change to forwarding response_type

Keep the positional createLoginFlow signature, the existing property
and import order, and the existing tests. Forward response_type and
send empty PKCE values instead of dropping them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Paulus Schoutsen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-06 08:33:40 +03:00
renovate[bot] 1e9d0432b3 Update dependency eslint to v10.12.0 (#54593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-06 06:58:30 +02:00
renovate[bot] 60027ef4b1 Update pnpm to v12.9.0 (#54594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-06 06:48:33 +02:00
Paul Bottein 5b25a220df Treat an entity name equal to its device name as no own name (#54585)
* Treat an entity name equal to its device name as no own name

* Use the same device name check when computing entity names

* Only check the user-set name against the device name

* Add tests for the device name check

* Clarify why only the user-set name is matched loosely
2026-10-05 18:54:27 +02:00
Paul Bottein 74c44e7c71 Fix first badges cut off in centered scrolling view header (#54565) 2026-10-05 18:50:36 +02:00
Paul Bottein 7b6561491a Fix option labels overflowing horizontal control selects (#54582) 2026-10-05 15:38:32 +00:00
Paul Bottein 17e0352ecf Show then branch before else in trace graph (#54583) 2026-10-05 17:03:31 +02:00
Maarten Lakerveld 7b379d587c Bump home-assistant-js-websocket to 9.7.1 (#54577)
Exclude our own packages from the 3-day minimumReleaseAge rule so
fixes we publish ourselves can be picked up immediately. The
no-downgrade trust policy still rejects unsigned releases.
2026-10-05 16:54:43 +02:00
Paul Bottein 6b32c87702 Fix map history paths flickering on every update (#54573)
Update map paths and zone circles in place instead of redrawing them
2026-10-05 15:40:34 +03:00
Jan-Philipp Benecke 2482b47d0b Hide dangling trace graph connectors when nothing follows (#54568) 2026-10-05 13:53:55 +02:00
Wendelin e9507ad201 Fix styling for config entry device rows (#54570) 2026-10-05 12:29:17 +01:00
Paul Bottein c26790e54d Add light color card feature with hue scrubber (#54219)
* Add light color card feature with hue scrubber

* Keep only the hue example in the scrubber gallery page

* Fix cancel event, card suggestion and white light hue in light color feature

* Update saturation gradient live while dragging hue

* Send terminal slider-moved after keyboard changes in scrubber

* Fix End key in wrap mode and keep focus when swapping light color controls

* Center collapsed hue preview on current hue and animate scrubber wrap by shortest path

* Hide the light color indicators when the light has no color
2026-10-05 12:16:16 +02:00
Aidan Timson 7eeca385a8 Shorten Marketplace warning countdown in development (#54564) 2026-10-05 11:55:17 +02:00
0149516743 Highlight the picker row that Enter would pick (#54319)
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-10-05 11:54:25 +02:00
karwosts a270426d98 Show entity diagnostic in a readonly editor w/ copy button (#54536) 2026-10-05 12:26:05 +03:00
Wendelin f8d70893a1 config-area-page fix missing list-item import (#54563) 2026-10-05 09:25:46 +00:00
karwosts 2cc6112d6f Fix stale missing entities in target picker (#54544) 2026-10-05 12:10:13 +03:00
Bram Kragten 025db7ffe7 Bumped version to 20260930.1 2026-10-05 11:02:45 +02:00
Paulus SchoutsenandClaude f9c958c80c Hide MCP URL hint until the server is enabled (#54532)
When the MCP server is not set up, the card has no URLs to show.
The description told the user to add one of the URLs below. Show
only the documentation link until the server is enabled.


Claude-Session: https://claude.ai/code/session_01MeyiXPWWRNRpD2XHVrtPSr

Co-authored-by: Claude <[email protected]>
2026-10-05 11:02:26 +02:00
Jan-Philipp Benecke 652897a3cb Handle undefined trigger condition IDs in rendering (#54527)
* Handle undefined trigger condition IDs in rendering

* Handle trigger conditions without ID in editor and updates
2026-10-05 11:02:25 +02:00
Maarten Lakerveld 3426449315 Keep padded map fits within the poles (#54526)
The MapLibre engine pads a fit by a fraction of its span. A zone whose
circle reaches a pole, or markers far apart, pushed the padded
latitude past 90, and MapLibre throws on such bounds, so the fit never
happened and the console filled with errors. The padded latitude is
now clamped to the poles.

Fixes #54498
2026-10-05 11:02:24 +02:00
Jan-Philipp Benecke 8e19b0249d Fix misplaced trace graph icons on iOS (#54520) 2026-10-05 11:02:23 +02:00
Paul Bottein bba034eeca Show entered and left in map zone activity (#54516)
* Show arrived and left in map zone activity

* Use entered instead of arrived in map zone activity
2026-10-05 11:02:21 +02:00
karwosts 31f2767038 Remove spurious color clear button (#54515)
* Improve number selectors in section settings

* Fix sections editor default color
2026-10-05 11:00:21 +02:00
Petar Petrov e0571044ae Detect the ESPHome Z-Wave discovery flow by the device MAC (#54513) 2026-10-05 10:57:27 +02:00
Maarten Lakerveld 0f00286641 Fix map overview tabs, activity timeline, cluster counts, zone fit and the new-zone route (#54512)
* Show four avatars in a cluster bubble before counting

A bubble capped at three avatars added a +1 chip for the fourth
member, taking a fourth slot to hide one avatar. Four members now show
as four avatars; from five on, three avatars and a count fill the same
four slots.

* Open the new zone dialog on every visit to /config/zone/new

The route was only handled in firstUpdated. The config panel is kept
alive between visits and the router reuses the zone page when it is
already the current one, so "Create zone from current location" did
nothing once the zone list had been opened earlier in the session. The
route is now handled on every route change, like /edit/ already was.

The dialog also opened before the more-info dialog it was launched
from had finished leaving the history, and that departure closed the
new dialog again. The page now awaits the replace navigation, which
waits for closing dialogs, before opening it.

* Hide the map overview while the dashboard is being edited

In panel view edit mode the map card kept rendering its overview. On
phones that is a bottom sheet fixed over the bottom of the screen,
which covered the card's edit bar and the view's add button, so the
map dashboard could not be edited in portrait. The overview is for
using the map, not configuring it, so it is left out while editing.

* Show only the overview tabs that have something to list

People and Zones were always offered, with an empty message when the
map card's entities held none of them; only Devices was hidden when
empty. All three now appear only with entries, the first remaining tab
takes over when the selected one empties, and the overview tells the
card so its marker filter follows. The empty-tab messages are gone
with the empty tabs.

* List the person of the current user first, as "Me"

The overview's People tab sorted everyone alike. The person linked to
the user looking at the map now comes first and is named "Me" in the
list and the detail header, through a localized string. Their initials
still come from their real name.

* Draw the overview activity as a timeline

The activity list reused logbook rows: the person's name on every row,
the state pushed to the right, dividers between rows. It is now a
timeline of its own. Each row names where the person went, with the
clock time and how long ago underneath, hung off a dot in the zone's
color with a rail to the next row. A zone's rows name who came or
left and where they are. Five rows show at first, with a "Show more"
button for the rest. The overview no longer needs the broad hass
object, which only the logbook row took.

* Fit the map to its zones when they are all it shows

Zones only joined the auto-fit with the fit_zones option. The panel
map does not set it, so on the Zones tab, with no people or devices
left, the fit found nothing and jumped straight to the home
coordinates at the default zoom. Zones now stand in whenever nothing
else is on the map, which goes through the animated fit like the
other tabs. With fit_zones they still join a fit of people and devices.
2026-10-05 10:57:26 +02:00
Petar Petrov 9cc166d3cf Fix double side padding around more-info history and activity (#54511) 2026-10-05 10:57:25 +02:00
Bram Kragten 3ad1ed5aa6 Resolve split devices in the logbook card (#54507) 2026-10-05 10:57:24 +02:00
Wendelin 8ee84c2462 Use optional short issue title localization to repairs dialog (#54506)
Add short issue title localization to repairs dialog
2026-10-05 10:57:22 +02:00
Paul Bottein eca108032f Fix area switch for entities without their own name (#54503) 2026-10-05 10:57:21 +02:00
Paul Bottein 3bfe67f784 Give zones their own map colors and prefer white zone icons (#54499)
* Give zones their own map color sequence

* Prefer white zone icons when contrast allows
2026-10-05 10:57:20 +02:00
Paul Bottein c27191de6b Show people and zones on the demo map (#54496) 2026-10-05 10:57:19 +02:00
Bram Kragten 31f626c877 Bump saturation of the default light mode map (#54492)
bump saturation of the default light mode map
2026-10-05 10:57:17 +02:00
Tom Mittelstädtand1358 b081db7020 Give tentative calendar events a striped background (#54483)
* Give tentative calendar events a striped background

A tentative bar gets a light fill in its calendar color with diagonal stripes in the text color, instead of the outline alone. A tentative dot keeps its color and size and gets stripes in the calendar's contrast color. The stripes set a tentative event apart from past days, which are already drawn faded. They repeat on whole pixels, so every stripe looks the same.

* Strengthen tentative dot stripes and add a forced-colors fallback

The stripes on a tentative dot are its only cue, so they now reach at least 3:1 against the dot. In forced colors the stripes and the dot's fill are dropped, so a tentative bar falls back to a dashed border and a tentative dot to a ring of the same size.

---------

Co-authored-by: 1358 <[email protected]>
2026-10-05 10:57:16 +02:00
Jason Hunter bfe09bd2e7 Preserve OAuth callback state through login and onboarding (#54547)
Preserve OAuth callback state when logging in and resuming onboarding
2026-10-05 09:52:41 +03:00
Jump-333 d1370c8577 Add source names for SoC badges (#54370)
* Add device names for SoC badges

* Fixed reload issues

* Removed useless fallback

* Modified badge name source

* Remove useless check
2026-10-05 08:19:33 +03:00
renovate[bot] 430890f401 Update dependency @types/node to v24.19.1 (#54553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-05 07:05:29 +02:00
renovate[bot] ba536e13e7 Update home-assistant/actions digest to 06749dd (#54552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-05 07:05:10 +02:00
renovate[bot] 4278d16a62 Update github/codeql-action action to v4.38.2 (#54555)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-05 07:04:46 +02:00
renovate[bot] 1c28b7fef1 Update dependency globals to v17.13.0 (#54539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-04 21:04:49 +02:00
renovate[bot] f867b6186f Update dependency vite to v8.3.2 (#54541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-04 19:50:00 +02:00
Paulus SchoutsenandClaude Opus 5.5 7432e63c16 Show the Marketplace risks alert full width without an icon (#54540)
* Show the Marketplace risks alert full width without an icon

The risks alert spans the full card width, so it reads as a band in the
card. The text aligns with the card content. The alert icon is hidden
because the heading already shows a large warning icon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wt676c2KAQ1NnNWNRm7dV5

* Scope the full-width alert styles to the risks alert

The error alert for a failed acceptance keeps its icon, corners and
padding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wt676c2KAQ1NnNWNRm7dV5

* Start-align the Marketplace warning heading and intro

The heading and intro line up with the risks and the checkbox below
them. The warning icon stays centered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wt676c2KAQ1NnNWNRm7dV5

---------

Co-authored-by: Claude <[email protected]>
2026-10-04 19:49:40 +02:00
Paulus SchoutsenandClaude 0bda4a13ca Add the Marketplace to the demo (#54546)
The demo registers the Marketplace panel and mocks its read commands.
A list of community repositories shows on the discover, browse and
installed pages, and each repository has its own page. The warning
screen is skipped.

The mocks load with the config panel chunk. A loaded Marketplace config
entry lets the panel start. Commands that change the Marketplace, such
as install and uninstall, are not mocked.


Claude-Session: https://claude.ai/code/session_01Wt676c2KAQ1NnNWNRm7dV5

Co-authored-by: Claude <[email protected]>
2026-10-04 19:48:24 +02:00
karwosts dd0a0df88f Fix max_columns number selector (#54542) 2026-10-04 15:51:41 +02:00
renovate[bot] 6775b1e2bb Update vitest monorepo to v5.0.3 (#54535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-04 08:56:20 +02:00
renovate[bot] 06df4046a9 Update pnpm to v12.8.2 (#54538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-04 08:56:16 +02:00
renovate[bot] 634b6ddfcd Update dependency browserslist to v4.29.3 (#54531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-03 08:06:47 +02:00
Paulus SchoutsenandClaude 970bb12856 Hide MCP URL hint until the server is enabled (#54532)
When the MCP server is not set up, the card has no URLs to show.
The description told the user to add one of the URLs below. Show
only the documentation link until the server is enabled.


Claude-Session: https://claude.ai/code/session_01MeyiXPWWRNRpD2XHVrtPSr

Co-authored-by: Claude <[email protected]>
2026-10-03 08:06:41 +02:00
Jan-Philipp Benecke c03750e605 Handle undefined trigger condition IDs in rendering (#54527)
* Handle undefined trigger condition IDs in rendering

* Handle trigger conditions without ID in editor and updates
2026-10-02 16:41:55 +02:00
Maarten Lakerveld 655e11ac1c Keep padded map fits within the poles (#54526)
The MapLibre engine pads a fit by a fraction of its span. A zone whose
circle reaches a pole, or markers far apart, pushed the padded
latitude past 90, and MapLibre throws on such bounds, so the fit never
happened and the console filled with errors. The padded latitude is
now clamped to the poles.

Fixes #54498
2026-10-02 15:03:02 +02:00
Paul Bottein b25cf13401 Add signed and offset modes to the duration selector (#54111)
* Add signed and offset modes to the duration selector

* Remove translation key from the duration selector

* Address review comments on the duration selector

* Expose the duration selector mode in the selector editor

* Remove allow_negative from the selector editor

* Migrate allow_negative to mode when editing a duration selector

* Derive the magnitude of mixed-sign legacy durations from their total

* Test the duration sign and value helpers

* Merge the duration sign helpers into normalizeDuration

* Omit the negative flag for positive signed durations

* Keep the pending offset type in local state instead of writing negative false

* Keep the offset type when the duration is cleared to zero

* Rename the offset type state

* Address remaining Copilot comments on the duration selector

* Simplify the duration sign handling

* Reset the offset type when the duration changes from outside

* Translate the duration selector mode in the selector editor

* Write negative durations as negative components instead of a negative key

* Simplify duration sign handling around a single normalizeDuration

* Keep the offset type as local state and refresh duration fields with live

* Forget the emitted duration after an external change

* Keep the duration sign at zero and use selector units everywhere

* Simplify duration sign handling and compute typed negative values

* Hide the clear button of offset durations
2026-10-02 09:02:45 +00:00
Wendelin 24c3ab6061 Use optional short issue title localization to repairs dialog (#54506)
Add short issue title localization to repairs dialog
2026-10-02 07:52:22 +00:00
Bram KragtenandClaude Opus 5.5 2c73378e9d Cache map tiles per asset class and stop refetching what still works (#54451)
* Cache map tiles per asset class and stop refetching what still works

The service worker kept glyphs, sprites and tiles in one 500-entry LRU, so
panning around evicted the fonts that every label on every map needs. Glyphs
and sprites move to a cache of their own, on the 30 day age core already sends
them with, and the tile ceiling goes up.

The TileJSON was deliberately left on the network because it is the switching
point for the tile source. Every map waits on it before asking for a tile
though, and at a five minute max-age that is a round trip before the first one.
Served stale-while-revalidate instead: out of the cache immediately, refreshed
behind it, so a moved endpoint still arrives.

The raster fallback redrew on every token rotation, refetching every tile on
screen twice an hour whether or not anything had failed. Later tiles pick up a
new token by themselves, so only a layer that actually had one refused needs
asking again - and since a refused tile is the only sign that path gets that
its token is stale, it now asks for a new one instead of waiting for the
refresh interval to come round.

Style requests that are in flight are shared, so a dashboard of maps, or a
theme toggle switching all of them at once, fetches each style once. Only while
in flight: a settled request is dropped, or a style that failed once would
never be fetched again.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Hand the raster fallback the current map tiles token

A rotation is only announced once, so a vector layer that later falls back
handed the raster layer the token it was created with, and the refresh that
follows a refused tile fetches the already-current token and announces
nothing. The vector layer now tracks rotations itself.

Also covers in-flight style sharing, and passes a map style rather than
`false` in the raster recovery tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-10-02 09:50:33 +02:00
Paul Bottein d8e09dd120 Show entered and left in map zone activity (#54516)
* Show arrived and left in map zone activity

* Use entered instead of arrived in map zone activity
2026-10-02 07:49:26 +00:00
Jan-Philipp Benecke 6cdfafcbf9 Fix misplaced trace graph icons on iOS (#54520) 2026-10-02 09:40:29 +02:00
Elias Lecomte 4819cd6d27 Fix long source name overflow in energy dashboard totals (#54522)
* Fix long source name overflow in energy dashboard totals

* style: format with prettier
2026-10-02 09:39:16 +02:00
karwosts c5f099dab7 Remove spurious color clear button (#54515)
* Improve number selectors in section settings

* Fix sections editor default color
2026-10-02 09:33:42 +02:00
renovate[bot]andSimon Lamon 2da41b6e59 Update dependency zxing-wasm to v3.1.4 (#54480)
* Update dependency zxing-wasm to v3.1.4

* Update type-fest version to 5.10.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <[email protected]>
2026-10-02 09:27:41 +02:00
renovate[bot] b7bb487208 Update pnpm to v12.8.1 (#54523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-02 08:11:53 +03:00
renovate[bot] dfaf2a6f72 Update dependency @types/luxon to v3.7.6 (#54525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-02 08:10:43 +03:00
renovate[bot] 8639ccde06 Update dependency typescript-eslint to v8.71.0 (#54517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-01 20:07:31 +02:00
ee67f6b99f Add dependency review workflow (#54490)
* Add dependency review workflow with npm metadata checks

* Temporarily run dependency review on push

* Remove temporary push trigger from dependency review

* Encode the full package name in npm registry URLs

* Check optional/peer dependencies and fail on npm registry errors

Co-authored-by: timmo001 <[email protected]>

* Convert npm metadata check to TypeScript

* Type workflow scripts with Node 24 types instead of DOM

* Detect npm alias target changes as new dependencies

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-01 20:02:11 +02:00
renovate[bot] e569664724 Update dependency @rspack/core to v2.2.8 (#54514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-01 19:58:07 +02:00
Maarten Lakerveld c113eb1f9a Fix map overview tabs, activity timeline, cluster counts, zone fit and the new-zone route (#54512)
* Show four avatars in a cluster bubble before counting

A bubble capped at three avatars added a +1 chip for the fourth
member, taking a fourth slot to hide one avatar. Four members now show
as four avatars; from five on, three avatars and a count fill the same
four slots.

* Open the new zone dialog on every visit to /config/zone/new

The route was only handled in firstUpdated. The config panel is kept
alive between visits and the router reuses the zone page when it is
already the current one, so "Create zone from current location" did
nothing once the zone list had been opened earlier in the session. The
route is now handled on every route change, like /edit/ already was.

The dialog also opened before the more-info dialog it was launched
from had finished leaving the history, and that departure closed the
new dialog again. The page now awaits the replace navigation, which
waits for closing dialogs, before opening it.

* Hide the map overview while the dashboard is being edited

In panel view edit mode the map card kept rendering its overview. On
phones that is a bottom sheet fixed over the bottom of the screen,
which covered the card's edit bar and the view's add button, so the
map dashboard could not be edited in portrait. The overview is for
using the map, not configuring it, so it is left out while editing.

* Show only the overview tabs that have something to list

People and Zones were always offered, with an empty message when the
map card's entities held none of them; only Devices was hidden when
empty. All three now appear only with entries, the first remaining tab
takes over when the selected one empties, and the overview tells the
card so its marker filter follows. The empty-tab messages are gone
with the empty tabs.

* List the person of the current user first, as "Me"

The overview's People tab sorted everyone alike. The person linked to
the user looking at the map now comes first and is named "Me" in the
list and the detail header, through a localized string. Their initials
still come from their real name.

* Draw the overview activity as a timeline

The activity list reused logbook rows: the person's name on every row,
the state pushed to the right, dividers between rows. It is now a
timeline of its own. Each row names where the person went, with the
clock time and how long ago underneath, hung off a dot in the zone's
color with a rail to the next row. A zone's rows name who came or
left and where they are. Five rows show at first, with a "Show more"
button for the rest. The overview no longer needs the broad hass
object, which only the logbook row took.

* Fit the map to its zones when they are all it shows

Zones only joined the auto-fit with the fit_zones option. The panel
map does not set it, so on the Zones tab, with no people or devices
left, the fit found nothing and jumped straight to the home
coordinates at the default zoom. Zones now stand in whenever nothing
else is on the map, which goes through the animated fit like the
other tabs. With fit_zones they still join a fit of people and devices.
2026-10-01 17:56:01 +02:00
Franck Nijhof 1f98d6ef6b Add Marketplace panel (#54437)
* Add store panel as a copy of the HACS frontend

Verbatim copy of src/ from hacs/frontend at d97dce0 into src/panels/store. Imports still point at the HACS submodule layout and the iframe shell is still present; this is the baseline for the port.

* Rewrite store panel imports and drop the iframe shell

Every import that pointed at the old `homeassistant-frontend` submodule now
resolves inside this repository's `src/` tree. Five components were renamed or
removed upstream, so they point at their current equivalents: `ha-button-menu`
and `ha-menu` to `ha-dropdown`, `ha-md-menu-item` to `ha-dropdown-item`,
`ha-fab` to `ha-button`, and `ha-circular-progress` to `ha-spinner`. The
`@material/mwc-button` and `@material/mwc-linear-progress` side-effect imports
are gone with them; those packages are no longer dependencies here.

`main.ts` becomes `ha-panel-store.ts` and registers `ha-panel-store`. It now
receives `hass`, `narrow`, `route` and `panel` from the panel resolver, so
everything that only made sense inside an iframe is removed: applying themes to
the parent element, forwarding haptic and keydown events to the main window,
bridging `location-changed`, the document-level navigation click handler, the
dialog manager, the injected body stylesheet, and the redirect to the default
route. `entrypoint.ts`, `extra.ts`, `html/` and `version.ts` are deleted for the
same reason.

* Move store panel translations into the frontend translation tree

The panel's own translation bundle is gone. Its keys now live under
`ui.panel.store` in `src/translations/en.json`, and `panel.store` gives the
sidebar its "Community store" title. Every `hacs.localize("some.key")` call
became `hass.localize("ui.panel.store.some.key")`, which means the `localize`
member on the `Hacs` interface, the `HacsLocalizeKeys` type and the localize
bootstrap in `HacsElement` are no longer needed. `TranslationDict` is derived
straight from `en.json`, so the moved keys type-check without any casts.

`dialog_about.frontend_version` did not come along: its only consumer was the
frontend version constant that went away with the iframe shell.

* Hold store panel state in the panel instead of a HACS mixin

The panel element now owns the repository list and the info payload as
internal state, loads both on first update and refreshes them from the
store/subscribe signals through SubscribeMixin. That drops the
HacsElement base class and its logger, so the hacs object handed to the
child components is plain data. Children that used to patch that object
with an update-hacs event now fire store-refresh instead and let the
panel refetch.

Also moves every WebSocket command over to the store/ prefix and trims
the info payload to what the backend still returns.

* Update the store panel to current frontend components

Rebuilds the panel on the components the frontend ships today: the
dialogs get their heading from ha-dialog itself and their buttons from
ha-dialog-footer, the overflow menus become ha-dropdown with
ha-dropdown-item, the download action becomes a fab-slotted ha-button,
and the progress and spinner indicators move to ha-progress-bar and
ha-spinner. brandsUrl loses the dropped useFallback option.

Also clears the eslint backlog the panel came in with: no arrow
functions in templates, no console calls, alt text on the brand images,
and private members named consistently.

* Register the store panel and route old HACS paths

Add the store panel to the panel resolver so the backend can serve it,
and move every internal navigation path in the panel from /hacs to
/store. The /hacsfiles static prefix stays, the backend serves it.

Repoint the hacs_repository my-redirect at the store panel and add a
store_repository alias next to it. Both keep the same target, so the
owner, repository, and category parameters still reach the panel's
redirect handler. Existing README badges keep working.

* Rename HACS to Community store in the store panel

Bring the ported panel in line with the repository conventions: panel
parts are ha-*, dialogs are dialog-* with a matching show-dialog-*
helper, and the typo in the overflow menu filename is fixed.

Rename the code identifiers to match, so the panel talks about a store
instead of HACS: StoreData, StoreInfo, StoreDispatchEvent, a store
property on the child elements, storeStyles and --store-* custom
properties.

Reword every user-facing string that named HACS. Backend names stay put:
the hacs_dispatch_* signal values, the /hacsfiles static prefix, the
hacs_repository my-redirect key, and the hacs.xyz documentation and
catalog infrastructure.

The stale HACS-REPOSITORY-PANEL nodeName check now matches the element
it was always meant to match, so removing a repository from its own page
navigates back again.

* Show a message when the store panel is opened without the integration

The panel is registered on every install while the integration is only set up on some, so an empty store must not try to talk to a backend that is not there.

* Link the Community store from the Settings dashboard

The store panel is hidden from the sidebar, so reach it from the Settings dashboard instead and give its pages a way back.

* Fit the store pages into the main layout

The repository page still sized itself to the viewport and the router recoloured the header with sidebar colours, both leftovers from running inside an iframe. Next to the sidebar that made the page a sidebar-width too wide, pushing the toolbar menu off-screen and un-centering the card.

* Let the quick bar create My links for store repositories

The repository page listened for the "m" key on the document body and opened a My link itself, which fought the quick bar's own "m" shortcut once the panel stopped living in an iframe. The quick bar now resolves a store repository page to the hacs_repository redirect by looking up the repository name, and the panel's handler is gone. The reload after installing a dashboard resource no longer goes through the main window helper, and unused scrollbar styles are dropped.

* Remove the external links from the store menu

The About dialog, the issue tracker link and the link to the HACS GitHub organisation pointed at a project outside Home Assistant. The store is part of Home Assistant now, so they go. The documentation link stays until the documentation moves.

* Fix runtime bugs in the store panel

* Remove external HACS leftovers from the store panel

* Tidy types and conventions in the store panel

* Clean up the store panel translations

* Give the store redirect error screen its hass and narrow properties

Without them the screen's back and refresh buttons render without labels.

* Use the store signal names and local resource URLs

The backend renames these in lockstep: the dispatcher signals lose their
`hacs_dispatch_` prefix, and downloaded dashboard resources are served
from `/local/community/` with a `v` cache-busting parameter instead of
the store-owned `/hacsfiles/` route and `hacstag`.

* Drop the disabled reasons the store backend no longer reports

Setup failures now surface through the config entry state instead of a disabled store, so only the runtime reasons remain.

* Rename the Community store to Marketplace

The panel, its elements, dialogs, data types, WebSocket commands,
dispatcher signals and translations now use marketplace, matching the
renamed backend domain. The hacs_repository My link keeps its slug.

* Finish the Marketplace panel

* Confirm replacing a built-in integration with the download

* Fix README links, older versions and grouping in the Marketplace panel

* Handle list continuations and placeholder characters in Marketplace READMEs

- Indented lines inside a list item are treated as text, so their links and issue references get rewritten.
- A README that already contains the private placeholder characters is left alone.

* Keep indented code inside list items literal in Marketplace READMEs

* Fix the warning title, icon colors and dead subscriptions in the Marketplace panel

- The warning takes its title from the Marketplace translations, it is not empty on a direct visit.
- Icons keep the colors of the menu or chip they are in.
- Dismissing new repositories is offered whatever the filters show.
- The dialogs no longer subscribe to an error signal the backend never sends.
- The custom repositories list names the category in the language of the user.

* Handle the edges of the Marketplace panel more carefully

- The warning can be continued again when the panel did not take over.
- Only a query with an owner and a repository opens a repository as a My link.
- Links to /hacs inside Home Assistant open the Marketplace.
- A rate limit after the download dialog closed opens nothing.
- A removed entry takes its catalog with it.
- The quick bar says why it could not make a My link for a repository.

* Type the Marketplace panel against the backend contract

- The data types match what the backend sends.
- Errors are caught as unknown and read through one helper.
- Events are typed from their declarations.
- Spacing uses the theme tokens.

* Make the wording of Marketplace actions clearer

* Leave the way back from the Marketplace warning to the header

* Show the download progress in the Marketplace download dialog

* Offer to set up a newly downloaded integration right away

* Make the Marketplace download dialog plainer and move other versions to the menu

- One sentence says what the download or update does, no paths or technical details.
- Warnings span the top of the dialog, like in the more-info dialog.
- The progress shows where the update dialog shows it, and the button shows it is busy.
- A restart is only mentioned for updates of integrations.
- Choosing another version moved to the repository menu, it opens on its own when the newest version is too new.

* Use the consume decorator of the frontend in the Marketplace dialogs

* Save the Marketplace search, and keep search and filters per session

* Lay out the Marketplace repository page like the other Settings detail pages

* Bring the Marketplace list page in line with the other Settings lists

* Use the standard Settings dialogs, styles and strings in the Marketplace

* Name the Marketplace data helpers like the rest of the frontend and drop the global refresh event

* Center the Marketplace warning and say how long accepting it lasts

* Call it installing in the Marketplace, and set up right after installing

* Use the uninstall command of the Marketplace

* Fix the row menu, the failed setup screen and README links in the Marketplace panel

* Guide uninstalling an integration that is in use, and offer it after deleting the last entry

* Fix reinstalling, filters, stale pages and README handling in the Marketplace panel

* Explain why an integration in use can not be uninstalled

* Test that accepting the warning and uninstalling in use really happen

* Point HACS links and icons at the Marketplace, and clarify its copy

* Share the error message and menu rendering of the Marketplace panel

* Split the long renders of the Marketplace panel

* Treat Marketplace repository ids as strings

* Guide the user after installing or uninstalling from the Marketplace

* Count down before the Marketplace warning can be accepted, and show it only once

* Show what to do next and who made it on the Marketplace repository page

* Title the Marketplace repository page Marketplace and keep signals one line

* Say which Home Assistant a Marketplace repository requires

"Works with Home Assistant 2025.1 or newer" read as a promise it had been tried there. It is a minimum the repository asks for, so say it that way.

* Explain adding to the Marketplace from a GitHub link

The dialog was called "Custom repositories" and opened on a list of them. Someone adding their first one saw an empty list and two bare fields.

It now says what it is for, and that what is added this way is made by the community like everything else in the Marketplace. Both fields say what they want. The list of what was added from links comes after that, under its own heading, and only once there is any.

* Give the Marketplace Discover, Browse and Installed tabs

The Marketplace was one long table, grouped by status. Finding what you have installed, or what waits for an update, meant scrolling past everything else.

Discover opens with what is popular and what changed lately. Each section links to Browse, sorted and filtered the way the section picked from it. The sorting and filters are in that link, so it works in a new tab or when shared too. Browse is the full table, without the grouping. Installed lists only what is installed, and its tab says how many updates wait.

On the tables, the icon shows a check for what is installed and an arrow for what waits for an update. The search says how many items it looks through. Adding from a GitHub link also got its own button in the toolbar.

Tabs take an optional badge for the update count.

* Find out what is added from a link in the Marketplace

The dialog asks only for the link. What it holds is found out when it is added, and only when that does not settle it, the dialog asks for the type, from what it might be.

* Fix races and lost answers in the Marketplace panel

The add from link dialog could add another link than the one it looked at, a See all link followed a second time kept the filters of the first, Retry did not retry a My link, and a My link that finished late replaced the repository opened meanwhile. A /hacs link from a dashboard showed the Marketplace without its translations. Uninstalling after deleting the last entry could hang when it failed, and a failed removal from the list closed as if it worked. Code inside a quote in a README got issue links.

* Tidy up small things in the Marketplace panel

The README was prepared again for every change of an entity, a screen reader heard the name of a tab but not its badge, and four exports and a type member were left from before.

* Show Marketplace errors in the language of the user and refresh less

Errors from the backend are translated with their key and placeholders in
the panel and when offering to uninstall after deleting an entry, with the
message of Home Assistant as fallback. The repository page reloads after
updating its information, since the list does not carry everything it
shows. Repository signals share one list request, with one more after it
when anything changed meanwhile, and Discover parses each date once.

* Fix the Marketplace strings that showed placeholders and wrong plurals

The add repository question showed {repository} literally, the quotes
around it escaped the placeholder. Seconds, downloads, stars and issues
now say one instead of ones, the status names match across the list and
the repository page, and the GitHub and log messages read the same as
the rest.

* Handle the review of the Marketplace panel

Links to /hacs reach beforeRender again, a route alias was resolved
before it. A refetch on a signal no longer opens the GitHub dialog by
itself, a failed release list is told once, and a failed first load is
translated without logging to the console. Deleting an entry only offers
the uninstall for custom integrations, after the restart and credential
prompts. The YAML resource address carries the version and is encoded.
Applied links mark their history entry instead of listening on the
window, and the last updated column sorts on a number.

* Rewrite Marketplace README addresses once they are rendered

ha-markdown gets an optional rewriteUrl, which runs over every link and
image of the rendered HTML in an inert template, before anything loads.
The README of a repository uses it, so reference links, entities and
unquoted attributes no longer slip through as addresses on Home
Assistant. The regular expressions that set aside links and addresses
for issue references only start where those can start, a README with a
long word or many unclosed links no longer freezes the page.
2026-10-01 11:19:21 -04:00
renovate[bot] e01f5b830f Update dependency @versatiles/style to v6.0.3 (#54509) 2026-10-01 15:45:05 +01:00
Petar Petrov b85cd00cc8 Detect the ESPHome Z-Wave discovery flow by the device MAC (#54513) 2026-10-01 16:44:38 +02:00
Paul Bottein e63fe4d631 Fix area switch for entities without their own name (#54503) 2026-10-01 15:30:53 +02:00
Aidan Timson d24d9d6051 Replace ha-ripple Material base with own implementation (#54508)
* Replace ha-ripple Material base with own implementation

* Remove ha-ripple origin comment

* Restore ha-ripple control setter and CSS zoom handling

* Defer ha-ripple forced-colors check to event handling
2026-10-01 15:10:44 +02:00
Bram Kragten 6c882f47bb Resolve split devices in the logbook card (#54507) 2026-10-01 14:59:12 +02:00
Petar Petrov 3689442f43 Fix double side padding around more-info history and activity (#54511) 2026-10-01 14:16:18 +02:00
Bram KragtenandClaude Opus 5 d19635b6bd Draw the globe against a sky when the map is zoomed out (#54485)
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-01 14:14:40 +02:00
Bram KragtenandClaude Opus 5 6ef7072e08 Add ha/no-unused-element-import lint rule (#54210)
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-01 13:27:03 +02:00
renovate[bot] 36829e0e05 Update dependency browserslist to v4.29.2 (#54504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-01 11:46:51 +02:00
574c89b5fd Update dependency sortablejs to v1.15.7 (#54476)
* Update dependency sortablejs to v1.15.7

* Move sortablejs patch to 1.15.7 and update lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: timmo001 <[email protected]>
2026-10-01 12:34:39 +03:00
4384336476 Migrate combo box item to new lists (#54453)
* Move pickers onto the new list components

Pickers, the quick bar, the automation element search and the card
picker entity tree now use ha-list-base and its item family instead of
Material Web list items.

- Add a virtual-focus mode to ha-list-base and ha-list-virtualized:
  focus stays in a search field and the list marks the active row.
- Render picker rows as ha-list-item-option, with the current value as
  the selected option.
- Make ha-combo-box-item a presentational row layout on ha-row-item.
- Render ha-picker-field as a native button with the clear button next
  to it.
- Let held arrow keys repeat in pickers and the automation search.
- Fix list items not unregistering after they leave the DOM, and an
  unhandled rejection when a virtualized list is removed mid-layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Fix section titles

* fix e2e

* mage entity tree search like picker and automation

* remove comments

* fix picker field aria label

* fix disabled field style selector

* Update src/components/ha-picker-combo-box.ts

Co-authored-by: Petar Petrov <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
2026-10-01 08:46:56 +00:00
Paul Bottein bc5eb1af2d Give zones their own map colors and prefer white zone icons (#54499)
* Give zones their own map color sequence

* Prefer white zone icons when contrast allows
2026-10-01 10:43:10 +02:00
1654 changed files with 114604 additions and 29847 deletions

No files matched your search

+7 -2
View File
@@ -16,7 +16,7 @@ Container components may keep `hass` when they own it and feed providers. Leaf c
## Core Files
- Context definitions: `src/data/context/index.ts`
- `consume` decorator and `ContextSubscriptionController`: `src/common/decorators/consume.ts` (use instead of `@lit/context`'s `consume` and `ContextConsumer`, enforced by ESLint)
- `consume` decorator, `ContextSubscriptionController`, and `consumeContext` / `ContextController` for reactive controllers: `src/common/decorators/consume.ts` (use instead of `@lit/context`'s `consume` and `ContextConsumer`, enforced by ESLint)
- Entity-scoped consume helpers: `src/common/decorators/consume-context-entry.ts`
- Transform decorator: `src/common/decorators/transform.ts`
- Canonical migration example: `src/panels/lovelace/cards/hui-button-card.ts`
@@ -37,6 +37,8 @@ Container components may keep `hass` when they own it and feed providers. Leaf c
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callApiRaw`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
| `uiContext` | themes, selected theme, panels, sidebar, and UI state |
| `narrowViewportContext` | narrow-layout boolean |
| `reducedMotionContext` | `prefers-reduced-motion` media query |
| `brandsAccessTokenContext` | the `hass` rerender that brand images wait for to get their token |
Lazy contexts subscribe on first consumer and tear down after the last consumer: `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, `manifestsContext`, `triggerDescriptionsContext`, and `conditionDescriptionsContext`.
@@ -50,7 +52,9 @@ Import `consume` from `src/common/decorators/consume`, not from `@lit/context`.
- `@state()` plus `@consume` plus `@transform`: rerenders only when the transformed value changes.
- `@consume` alone: the field stays current but never triggers a render. Use this for values only read in event handlers or callbacks, such as `apiContext`, or for data read on demand, such as picker item callbacks.
In controllers or for lazily created subscriptions, use `ContextSubscriptionController` from the same file instead of `@lit/context`'s `ContextConsumer`, which also forces a host update. Its callback must store the value in a reactive field or call `host.requestUpdate()` itself when the host needs to rerender.
In reactive controllers, extend `ContextController` and decorate fields with `consumeContext({ context, subscribe, transform })` from the same file. `transform` stores one part of the context value, and `contextUpdated()` runs only when that part changes. `@state()` and `@transform` do not work on controller fields; call `host.requestUpdate()` when the host needs to rerender. Example: `src/data/service-info-controller.ts`.
For lazily created subscriptions, use `ContextSubscriptionController` from the same file instead of `@lit/context`'s `ContextConsumer`, which also forces a host update. Its callback must store the value in a reactive field or call `host.requestUpdate()` itself when the host needs to rerender.
Use entity-scoped helpers when the component watches an entity ID held on the host:
@@ -90,5 +94,6 @@ To consume a whole group untransformed, omit `@transform` and type the field as
- The component consumes the narrowest context needed for the data it reads.
- A broad `hass` property is kept only when the component is a container or external API requires it.
- Entity-scoped reads use the consume helpers rather than ad hoc context transforms.
- A component that renders brand images consumes `brandsAccessTokenContext` with `@state()`, so the images fill in once the token arrives or rotates.
- Context fields read during render or `willUpdate` are marked `@state()` so updates trigger rendering. Fields only read outside the render cycle are left without `@state()`.
- Tests and mocks only provide the data the component actually consumes.
+4
View File
@@ -54,6 +54,10 @@ http://localhost:8090/?demo=<second slug>#/energy/water
- `demo/src/stubs/`: Mocked WebSocket and REST APIs.
- `demo/script/develop_demo`, `demo/script/build_demo`: Development server and static build wrappers.
## Core Data
The service descriptions in `src/fake_data/demo_services.ts` and the trigger and condition descriptions and icons in `demo/src/stubs/automation_platforms_*.ts` are generated from Home Assistant Core. Do not edit them by hand. Run `script/gen_demo_core_data` to regenerate them from the core `dev` branch, and add a domain to the lists in `build-scripts/gulp/gen-demo-core-data.js` when the demo needs its services, triggers, or conditions. Their translations come from the backend translations merged into the demo build.
## Shared Gallery Stubs
`demo/src/stubs/` is shared, not demo-private: gallery pages import from it directly. Before changing or removing anything a stub does, search for its callers across `demo/src/` and `gallery/src/`, then check the affected gallery pages as well as the demo.
@@ -0,0 +1,91 @@
---
name: ha-frontend-query-params
description: Home Assistant frontend URL query param patterns. Use when keeping page state such as filters, search, sorting or grouping in the URL, adding or reading typed query params, using QueryParamsController or the data table's sync-url and column urlName, building in-app links with a query string, or adding params to My Home Assistant redirects.
---
# HA Frontend Query Params
Use this skill when implementing or reviewing state kept in the URL's query string, or links that set it. Cross-load `ha-frontend-lit` for the controller and lifecycle work, `ha-frontend-testing` for running tests, and `ha-frontend-review` when reviewing.
## Typed Params
`src/common/url/query-params.ts` turns a typed config into values and back:
- `QueryParamConfig` lists keys by type: `string`, `list` (one comma-separated param, `?label=a,b`), `date` (ISO string) and `boolean` (`{ key, trueValue }`, true only when the param equals `trueValue`).
- `QueryParamValues<typeof config>` is the matching partial values type.
- `decodeQueryParams(source, config)` reads a `SearchParamsSource` (a `URLSearchParams`, a plain record or a query string). Empty values are skipped.
- `createQueryString(values, config)` writes the values, leaving out empty strings, empty lists and false booleans.
- `updateQueryString(current, values, config)` replaces only the config's params in an existing query string and keeps every other param exactly as written.
- `hasQueryParams(source, config)` tells whether a URL has any of the config's params, which is how a page knows whether a link asked for anything.
- `serviceTargetFromQueryParams` and `queryParamsFromServiceTarget` map `HassServiceTarget` keys to list params.
Give each feature its own `*-query-params.ts` in `src/common/url/` holding its config (`as const satisfies QueryParamConfig`), its values type, and the helpers that turn params into page state and back. Follow the existing ones: `data-table-query-params.ts`, `marketplace-query-params.ts`, `history-logbook-query-params.ts` and `todo-query-params.ts`. Helpers in `src/data/` that build outside links use a local config the same way, as `src/data/integration.ts` and `src/data/system_log_report.ts` do. In new code, prefer these over hand-built `URLSearchParams`, string concatenation or `extractSearchParam` reads from `src/common/url/search-params.ts`.
## QueryParamsController
`src/common/controllers/query-params-controller.ts` keeps part of a page's state in the URL. Construct it with the host, the feature's config and options:
- `enabled()`: whether the host syncs right now. It can change, as on the Marketplace dashboard, which turns it off on the Discover tab.
- `navigated()`: called when the page's URL changes while the host is connected (`location-changed` or `popstate`), and when the host reconnects after its first update. Read the URL with `searchParams()`, apply it, then `write()` the state back.
Methods:
- `searchParams()` returns the current params, or `undefined` when the controller is disabled or the URL is on another path.
- `write(values, debounced?)` updates only the config's params with `replaceCurrentUrl`, so it replaces the current history entry. Pass `debounced` for values that change while typing; the write lands 300 ms after the last one and any later write replaces it.
- `flush()` writes a pending debounced write now. The data table calls it when the search box loses focus. Call it before anything that adds its own history entry, such as a dialog.
The controller binds to `currentPath()` when the host connects and does nothing while the URL is on another path. Navigating away drops a pending write. When a page is reached through a router `defaultPage` (such as `/marketplace` landing on `/marketplace/browse`), check that a link to the bare path still syncs, since the redirect may land after the host connects.
Read the URL yourself on the first render, after the initial state is set (in `willUpdate` before `hasUpdated`, or `firstUpdated`), and leave later navigations to `navigated()`. `ha-marketplace-dashboard.ts` and `lorawan-devices.ts` show both. When moving a page to the controller, remove its own `location-changed` and `popstate` listeners and any search kept in history state, since the controller replaces them.
When a router reuses one element for different items, such as one app's tab after another's, key it on the item so the controller binds again to the new path.
## URL Rules
- Writes replace the current history entry, so Back leaves the page instead of undoing each change. Push a new entry only for a deliberate navigation, such as picking another log provider.
- Debounce search writes. Write sort, group and filter changes straight away.
- Leave empty and default values out of the URL, such as an empty search or ascending direction.
- A param in the URL wins over stored preferences. A link with none of the page's params keeps what the user picked before; `hasQueryParams` makes that check.
- A link with any of the page's params replaces the search and every filter, since a filter left over from before would hide part of what the link shows. Show what the link applied, but don't store it: links often carry someone else's choices, such as a My Home Assistant link made with `m`. Store only what the user changes on the page, and only that setting.
- The URL is the whole state. Don't add filters while applying a link; a link that needs one, such as showing disabled devices, says so in its params (`status=disabled`).
- A link to a page that is already open must apply the new URL. Handle `navigated()`, not only the first render. The router often keeps a page alive, so this is the common case, not an edge case.
- Use one comma-separated param for several values (`?status=new,installed`), which `list` params do.
- Use the names other pages already use for the same thing: `search`, `sort`, `direction` and `group` for the table, and `floor`, `area`, `device`, `label`, `config_entry`, `sub_entry` and `assistants` for the shared filters.
- A search box's text is always `search`, never `filter` or `q`.
- Keep existing param names so existing links and My Home Assistant links keep working. When renaming, read the old name as well for about six months, with a code comment naming the release it can be removed in, and write only the new name. Keep the old name in the config, use it only as a fallback when reading, and leave it out of the values you write, so the next write removes it from the URL.
- Pages opt in. Some use params for other things, such as `?domain=` starting the add integration flow on the integrations dashboard, so check what a page already reads before syncing it.
- Collapsed groups, column order and hidden columns stay in storage (`@storage`), not the URL.
- Link to a page by its main path, not an alias, so a link to an open page lands on the same path and applies.
## Data Tables
`hass-tabs-subpage-data-table` with the `sync-url` attribute keeps `search`, `sort`, `direction` and `group` in the URL through `dataTableQueryParamsFromState` and `dataTableQueryStateFromUrl`. A linked search replaces the current one, and sorting or grouping a link leaves out keeps what was picked before. The table fires `search-changed`, `sorting-changed` and `grouping-changed` when a link changes them, with `link` set, so the page shows them without storing them. Use `DataTableSettingsController` (`src/common/controllers/data-table-settings-controller.ts`) for the stored search, sorting and grouping: give it the page's storage keys, bind its `search`, `sorting` and `grouping` to the table, and its `searchChanged`, `sortingChanged` and `groupingChanged` to the table's events. Call `showSearch` for a search the page reads from a link itself. A page the router keeps open shows what the user stored when it's opened again without params, like from the sidebar: the controller and table restore the search, sorting and grouping, and a page with filters restores its stored filters when its URL has no params.
- Give sortable and groupable columns a short public `urlName` (for example `type`, not `translated_category`). Sorting falls back to the column id without one; grouping is only written for columns that have one, so internal ids don't end up in links.
- When a column definition is shared between pages, set its `urlName` where the shared column is defined, so every page uses the same name.
- The table reads the URL once, as soon as it has columns. When a column only exists once data has loaded, such as Filename on the dashboards page, turn syncing on only after loading succeeds (`?sync-url=${!this._loading && !this._loadFailed}`), or a link that sorts or groups by that column is dropped.
- The page owns its filters. Give it a filter config in its `*-query-params.ts`, a helper that turns filter values into params, and one that turns a link back into the search and filter values. The link helper returns `undefined` when the URL has none of the filter params and none of the table's (`dataTableQueryParamConfig`), so stored choices stay. Use a second controller on the page for them, as `ha-marketplace-dashboard.ts` and `lorawan-devices.ts` do. `browseUrl` and `browseLinkFromUrl` in `src/panels/marketplace/dashboards/dashboard-repositories.ts` show the pair.
- Filter components also send the value the page gave them once they have looked up their items, so a filter event isn't always the user's. Store a filter only when it changes what the URL holds, comparing the filters' query strings before and after, as `helpersFilterQueryString` does, and store only that filter.
- A memoized filter that also reads async state (anything loaded after the page connects) must take that state as a memoize argument, or a linked filter shows stale results until something else changes.
- A filter component that looks up related items asynchronously should drop a lookup whose value has since changed, or two links landing within one lookup apply the older one.
## Coverage Check
Before calling a page done:
1. List every filter, sortable column, groupable column, the search box and any view toggle (such as a tab, raw view or provider). Confirm each one is written to the URL and read back from a link, both on first render and on a page that is already open.
2. Search the codebase for in-app links and `navigate()` calls into the page with a query string, and move them to the current param names and the page's main path. `lorawan-connections.ts` and `lorawan-overview.ts` link into `lorawan-devices.ts` this way.
## My Home Assistant Links
When a page gains params, add them to its redirect in `getMyRedirects()` in `src/panels/my/ha-panel-my.ts`:
- List them under `params` as `"string?"`, matching what the page reads. For a data table page that includes `search`, `sort`, `direction` and `group` as well as its filters. Keep the list in a const next to the others, such as `marketplaceTableParams`, which several redirects share.
- Point the redirect at the page's main path, the one the page's controller binds to.
- Don't add legacy param names.
- Add the same params to the redirect's entry in `redirect.json` in `home-assistant/my.home-assistant.io`, in a separate pull request that links the frontend pull request. Merge it only once the frontend pull request is in `dev`.
- In the frontend pull request, link the My Home Assistant pull request under "Additional information": `- Link to My Home Assistant pull request: home-assistant/my.home-assistant.io#<n>`.
## Tests
Follow `test/common/url/query-params.test.ts`, `test/common/url/data-table-query-params.test.ts` and `test/common/controllers/query-params-controller.test.ts`. For a feature's params file, cover the encode and decode round trip, defaults being left out, and legacy names still being read. Test the helpers, not the page rendering, and only where the logic could break without anyone noticing.
+1 -1
View File
@@ -58,7 +58,7 @@ Readiness and invalidation:
Repository-owned contracts:
- Consult the public [frontend developer documentation](https://developers.home-assistant.io/docs/frontend/) for documented architecture, data flow, design, and development workflows.
- Consult the public [frontend contributor documentation](https://contributors.home-assistant.io/frontend/) for documented architecture, data flow, design, and development workflows.
- For new leaf components, load `ha-frontend-contexts` and verify they consume narrow contexts instead of introducing a broad `hass` property; containers and external APIs may still require `hass`.
- Verify backend assumptions against the owning Core, Supervisor, or WebSocket implementation, and component assumptions against the exported component contract.
- Prefer canonical repository helpers and test setup over duplicate local implementations.
+29 -22
View File
@@ -15,38 +15,45 @@ not UCAndroid > 0
# Floors are pinned explicitly (not usage-based) so the support policy is
# deliberate and does not drift with global usage statistics, which do not
# represent old tablets and wall displays used as Home Assistant dashboards:
# - iOS/Safari >= 12: iPad Air 1 / mini 2 / mini 3 (last supported iOS).
# Older iPads (iPad 2/3/mini 1 on iOS 9.3, iPad 4 on iOS 10.3) cannot run
# the app: their engines lack custom elements, shadow DOM, and/or CSS grid.
# - Chrome >= 59: Fire OS 5 tablets (Fire 7/HD 8/HD 10 through ~2017) have
# their system WebView pinned at Chromium 59 and commonly run Fully Kiosk;
# also covers old kiosk browsers and no-longer-updating webviews above it.
# - iOS/Safari >= 15.4: where native <dialog> (ha-drawer, popovers,
# wa-dialog) landed. Dropped: the iOS 12 devices, iPad Air 1 / mini 2 /
# mini 3, iPhone 5s / 6 / 6 Plus and iPod touch 6. Nothing ends between
# iOS 12 and 15.8, so no other device is affected. On the Mac, Safari 15.4
# runs on macOS 10.15 Catalina and later. The Chrome floor, not Safari,
# decides what the legacy build has to transpile.
# - Chrome >= 67: the first Chromium with BigInt, which Babel cannot
# transpile. The boundary on Fire tablets is the Amazon WebView build, not
# the Fire OS major: WebView 70+ (Fire OS 5.6.4+ on the 2017 models, Fire
# OS 6.3.1.2+, Fire OS 7) keeps working, WebView 59 (Fire OS 5 on the 2015
# and 2016 models, Fire OS 6.3.0.1) is dropped. Also covers the Chromecast
# 1st gen (frozen at Chrome 70, the next hard ceiling) and old kiosk
# webviews above it.
# - Edge >= 79: all Chromium-based Edge. Costs nothing (above the Chrome
# floor); mainly catches Edge 109 on Windows 7/8.1 and update-frozen
# enterprise installs, whose engines can run the legacy build fine.
# - Firefox >= 94: the zero-cost floor, not a chased population — pinning it
# adds no babel transforms, core-js modules, or Lightning CSS prefixes to
# the output. Mozilla-supported Firefox (incl. current ESR) always matches
# - Firefox >= 98: where native <dialog> landed; otherwise a zero-cost floor,
# not a chased population — pinning it adds no babel transforms, core-js
# modules, or Lightning CSS prefixes to the output. Mozilla-supported Firefox (incl. current ESR) always matches
# [modern]; Firefox on old OSes is not supported (use Chrome 109 instead).
# If Firefox ever becomes the pin forcing extra output, raise it first.
# - Samsung >= 9: Samsung Internet on old Galaxy tablets
Chrome >= 59
ChromeAndroid >= 59
# - Samsung >= 9: Samsung Internet on old Galaxy tablets (9.2 is Chromium 67)
Chrome >= 67
ChromeAndroid >= 67
Edge >= 79
Firefox >= 94
FirefoxAndroid >= 94
iOS >= 12
Safari >= 12
Firefox >= 98
FirefoxAndroid >= 98
iOS >= 15.4
Safari >= 15.4
Samsung >= 9
[legacy-sw]
# Same as legacy, restricted to browsers that support service workers
# (currently resolves to the same set; guards the service worker build if the legacy floor ever drops below them)
Chrome >= 59 and supports serviceworkers
ChromeAndroid >= 59 and supports serviceworkers
Chrome >= 67 and supports serviceworkers
ChromeAndroid >= 67 and supports serviceworkers
Edge >= 79 and supports serviceworkers
Firefox >= 94 and supports serviceworkers
FirefoxAndroid >= 94 and supports serviceworkers
iOS >= 12 and supports serviceworkers
Safari >= 12 and supports serviceworkers
Firefox >= 98 and supports serviceworkers
FirefoxAndroid >= 98 and supports serviceworkers
iOS >= 15.4 and supports serviceworkers
Safari >= 15.4 and supports serviceworkers
Samsung >= 9 and supports serviceworkers
+4
View File
@@ -12,3 +12,7 @@
demo/public/api/camera_proxy_stream/* binary
demo/public/api/media_player_proxy/* binary
# Generated by `gh actions-lock`. Highlight it as YAML and don't auto-merge it:
# on a conflict, restore it from the base branch and run `gh actions-lock`.
.github/workflows/actions.lock linguist-language=YAML -merge
+1 -1
View File
@@ -105,4 +105,4 @@ To help with the load of incoming pull requests:
Below, some useful links you could explore:
-->
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
[perfect-pr]: https://contributors.home-assistant.io/start/pull-requests/#before-you-open-it
+6
View File
@@ -0,0 +1,6 @@
paths:
.github/workflows/**/*.{yml,yaml}:
ignore:
# actionlint doesn't support `$/` self-repository actions yet, remove once it does:
# https://github.com/rhysd/actionlint/issues/711
- 'specifying action "\$/[^"]+" in invalid format because ref is missing'
@@ -24,7 +24,7 @@ runs:
- name: Setup Node and install
if: steps.dependencies.outputs.cache-hit != 'true'
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
cache: false
+1 -1
View File
@@ -34,7 +34,7 @@ runs:
hashFiles('.nvmrc', 'package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'patches/**') }}
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: ".nvmrc"
+226
View File
@@ -1,3 +1,23 @@
# Path rules for .github/workflows/labeler.yaml, which runs actions/labeler on
# every pull request. A label is added when a pull request changes a matching
# file, and removed again when it no longer does, including one added by hand.
#
# Do not add these labels here. .github/scripts/label-content.mts applies and
# removes them from paths and code. A rule here would remove them whenever its
# paths don't match, even when the script's code check does:
#
# - Companion App
# - Home Assistant Link
# - Labs
# - Supervisor
Activity:
- changed-files:
- any-glob-to-any-file:
- src/panels/logbook/**
- src/panels/lovelace/**/*logbook*
- src/data/logbook.ts
Agents:
- changed-files:
- any-glob-to-any-file:
@@ -16,6 +36,36 @@ Agents:
- .github/skills/**
- .github/workflows/copilot-setup-steps.yml
AI:
- changed-files:
- any-glob-to-any-file:
- "src/panels/config/core/{ha-config-section-ai,ai-task-pref,mcp-pref}.ts"
- src/components/ha-suggest-with-ai-button.ts
- "src/data/{ai_task,llm}.ts"
Areas:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/areas/**
- src/data/area/**
- "src/data/{area_floor_picker,floor_registry,ws-area_registry,ws-floor_registry}.ts"
Automations:
- changed-files:
- any-glob-to-any-file:
- "src/panels/config/{automation,script,scene,blueprint}/**"
- src/components/automation/**
- src/components/trace/**
- src/data/trace*.ts
- "src/data/{automation,automation_i18n,script,script_i18n,scene,blueprint,trigger,condition,action}.ts"
Backups:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/backup/**
- src/data/backup*.ts
- src/onboarding/onboarding-restore-backup.ts
Build:
- changed-files:
- any-glob-to-any-file:
@@ -23,12 +73,37 @@ Build:
- .browserslistrc
- gulpfile.js
Calendar:
- changed-files:
- any-glob-to-any-file:
- src/panels/calendar/**
- src/panels/lovelace/**/*calendar*
- src/data/calendar.ts
Cast:
- changed-files:
- any-glob-to-any-file:
- cast/src/**
- src/cast/**
Connectivity:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/connectivity/**
- src/panels/config/tags/**
- "src/panels/config/integrations/integration-panels/{bluetooth,infrared,matter,modbus,mqtt,radio_frequency,serial,thread,zha,zwave_js}/**"
- "src/data/{bluetooth,infrared,matter,matter-lock,modbus,mqtt,otbr,radio_frequency,tag,thread,usb,zha,zwave_js,zwave_js-credentials}.ts"
Dashboards:
- changed-files:
- any-glob-to-any-file:
- src/panels/lovelace/**
- "src/panels/{home,climate,light,security,maintenance,map}/**"
- src/panels/config/lovelace/**
- "src/panels/config/dashboard/{dashboard-card,dialog-new-dashboard,show-dialog-new-dashboard}.ts"
- src/data/lovelace/**
- src/data/lovelace*.ts
Demo:
- changed-files:
- any-glob-to-any-file:
@@ -57,6 +132,29 @@ Dependencies:
- "^renovate/"
- "^dependabot/"
Devices:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/devices/**
- src/data/device/device_registry.ts
- src/data/ws-device_registry.ts
Energy:
- changed-files:
- any-glob-to-any-file:
- src/panels/energy/**
- src/panels/config/energy/**
- src/panels/lovelace/cards/energy/**
- src/panels/lovelace/**/*energy*
- src/data/energy*.ts
Entities:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/entities/**
- src/data/entity/entity_registry*.ts
- src/data/ws-entity_registry_display.ts
GitHub Actions:
- changed-files:
- any-glob-to-any-file:
@@ -64,6 +162,100 @@ GitHub Actions:
- .github/workflows/**
- .github/*.yml
Helpers:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/helpers/**
- src/data/helpers_crud.ts
History:
- changed-files:
- any-glob-to-any-file:
- src/panels/history/**
- src/panels/lovelace/**/*history*
- src/data/history.ts
Integrations:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/integrations/**
- src/panels/config/application_credentials/**
- src/dialogs/config-flow/**
- src/data/application_credential.ts
- src/data/config_entries.ts
- "src/data/{config_flow,data_entry_flow,options_flow,sub_config_flow}.ts"
Labels:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/labels/**
- src/data/label/**
Landing Page:
- changed-files:
- any-glob-to-any-file:
- landing-page/**
Marketplace:
- changed-files:
- any-glob-to-any-file:
- src/panels/marketplace/**
- src/data/marketplace/**
- src/panels/config/integrations/offer-marketplace-uninstall.ts
Media:
- changed-files:
- any-glob-to-any-file:
- src/panels/media-browser/**
- src/components/media-player/**
- src/data/media_source.ts
Notifications:
- changed-files:
- any-glob-to-any-file:
- src/dialogs/notifications/**
- src/data/persistent_notification.ts
Onboarding:
- changed-files:
- any-glob-to-any-file:
- src/onboarding/**
- src/auth/**
- src/data/onboarding.ts
- src/data/auth.ts
People:
- changed-files:
- any-glob-to-any-file:
- "src/panels/config/{person,users}/**"
- "src/data/{person,user,ws-user}.ts"
Profile:
- changed-files:
- any-glob-to-any-file:
- src/panels/profile/**
Search:
- changed-files:
- any-glob-to-any-file:
- src/dialogs/quick-bar/**
- src/state/quick-bar-mixin.ts
- "src/data/{quick_bar,search}.ts"
Sidebar:
- changed-files:
- any-glob-to-any-file:
- src/components/ha-sidebar.ts
- src/dialogs/sidebar/**
System:
- changed-files:
- any-glob-to-any-file:
- "src/panels/config/{core,hardware,info,logs,network,repairs,storage}/**"
- src/panels/config/dashboard/ha-config-updates.ts
- "src/dialogs/{repairs-flow,restart}/**"
- "src/data/{analytics,entity_id_format,error_log,hardware,network,repairs,system_health,system_log,system_log_report,update}.ts"
"Tests: E2E":
- changed-files:
- any-glob-to-any-file:
@@ -93,3 +285,37 @@ GitHub Actions:
- any-glob-to-any-file:
- test/*.ts
- "test/!(e2e)/**"
Theming:
- changed-files:
- any-glob-to-any-file:
- src/resources/theme/**
- src/state/themes-mixin.ts
- src/common/dom/apply_themes_on_element.ts
- "src/data/{theme,ws-themes}.ts"
To-do:
- changed-files:
- any-glob-to-any-file:
- src/panels/todo/**
- src/panels/lovelace/**/*todo*
- src/data/todo.ts
Tools:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/tools/**
Voice:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/voice-assistants/**
- "src/dialogs/{voice-assistant-setup,voice-command-dialog}/**"
- src/components/ha-assist-chat.ts
- "src/data/{assist_pipeline,assist_satellite,conversation,stt,tts,wake_word}.ts"
Zones:
- changed-files:
- any-glob-to-any-file:
- src/panels/config/zone/**
- src/data/zone.ts
-46
View File
@@ -1,46 +0,0 @@
#!/usr/bin/env node
// Fails the check when a pull request carries a label that blocks merging, and
// writes the outcome to the job summary. Invoked from the `check` job in
// .github/workflows/blocking-labels.yaml via actions/github-script:
//
// const { default: checkBlockingLabels } =
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mts`);
// await checkBlockingLabels({ github, context, core });
import type {
GitHubScriptArgs,
PullRequestPayload,
} from "./github-script.d.ts";
export default async function checkBlockingLabels({
context,
core,
}: GitHubScriptArgs<PullRequestPayload>) {
const blockingLabels = [
"wait for backend",
"Needs UX",
"Do Not Review",
"Blocked",
"has-parent",
];
const prLabels = context.payload.pull_request.labels.map((l) => l.name);
const found = blockingLabels.filter((bl) => prLabels.includes(bl));
if (found.length > 0) {
const message = `This Pull Request is blocked by label${found.length > 1 ? "s" : ""}: ${found.join(", ")}`;
await core.summary
.addHeading(":no_entry_sign: Pull Request is blocked", 2)
.addRaw(message)
.write();
core.setFailed(message);
} else {
await core.summary
.addHeading(
":white_check_mark: Pull Request is clear to merge after review",
2
)
.addRaw(
"This Pull Request is not blocked by any labels which prevent it from being merged."
)
.write();
}
}
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env node
// Checks npm registry metadata for the direct dependencies in package.json:
// deprecation, source repository and provenance. On pull requests only newly
// added packages are checked, and a deprecated one fails the check. Scheduled
// and manual runs report on every direct dependency without failing. Invoked
// from the `npm-metadata` job in .github/workflows/dependency-review.yaml via
// actions/github-script:
//
// const { default: checkNpmMetadata } =
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-npm-metadata.mts`);
// await checkNpmMetadata({ github, context, core });
import type { Octokit } from "@octokit/rest";
import type {
Context,
Core,
GitHubScriptArgs,
PullRequestPayload,
} from "./github-script.d.ts";
import { withRetry } from "./github-retry.mts";
// Scheduled and manual runs have no pull request
type Payload = Partial<PullRequestPayload>;
type Section =
| "dependencies"
| "devDependencies"
| "optionalDependencies"
| "peerDependencies";
type PackageJson = Partial<Record<Section, Record<string, string>>>;
interface Dependency {
name: string;
// The package npm installs, which differs from name for npm: aliases
registryName: string;
version: string;
section: Section;
}
// The fields read from a version document on registry.npmjs.org
interface NpmManifest {
deprecated?: string;
repository?: string | { url?: string };
dist?: { attestations?: object };
}
interface Result {
name: string;
version: string;
section: Section;
skipped?: string;
deprecated?: string;
repository?: string;
provenance?: boolean;
}
const SECTIONS: Section[] = [
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
];
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
const NPM_ALIAS = /^npm:((?:@[^/]+\/)?[^@]+)@(.+)$/;
const CONCURRENCY = 8;
// Returns undefined when the registry reports 404, and throws on any other
// failure so a registry outage fails the job instead of skipping the check.
const fetchJson = async <T,>(url: string): Promise<T | undefined> => {
const response = await fetch(url);
if (response.status === 404) {
return undefined;
}
if (!response.ok) {
throw new Error(
`Request to ${url} failed: ${response.status} ${response.statusText}`
);
}
return JSON.parse(await response.text());
};
const readPackageJson = async (
github: Octokit,
context: Context<Payload>,
ref: string
): Promise<PackageJson> => {
const { data } = await withRetry(`package.json lookup at ${ref}`, () =>
github.rest.repos.getContent({
...context.repo,
path: "package.json",
ref,
mediaType: { format: "raw" },
})
);
// The raw media type returns the file contents as a string, but Octokit
// types the response as the JSON form
if (typeof data !== "string") {
throw new Error("Expected raw package.json contents");
}
return JSON.parse(data);
};
const listDependencies = (pkg: PackageJson): Dependency[] =>
SECTIONS.flatMap((section) =>
Object.entries(pkg[section] ?? {}).map(([name, spec]) => {
// npm: aliases install another package, e.g. npm:[email protected]
const alias = spec.match(NPM_ALIAS);
return {
name,
registryName: alias?.[1] ?? name,
version: alias?.[2] ?? spec,
section,
};
})
);
const repositoryUrl = (repository: NpmManifest["repository"]) => {
const url = typeof repository === "string" ? repository : repository?.url;
return url?.replace(/^git\+/, "").replace(/\.git$/, "");
};
const checkPackage = async ({
name,
registryName,
version,
section,
}: Dependency): Promise<Result> => {
const result = {
name: registryName === name ? name : `${name} (npm:${registryName})`,
version,
section,
};
if (!EXACT_VERSION.test(version)) {
return { ...result, skipped: "not an exact version" };
}
const manifest = await fetchJson<NpmManifest>(
`https://registry.npmjs.org/${encodeURIComponent(registryName)}/${version}`
);
if (!manifest) {
return { ...result, skipped: "not found on the npm registry" };
}
return {
...result,
deprecated: manifest.deprecated,
repository: repositoryUrl(manifest.repository),
provenance: Boolean(manifest.dist?.attestations),
};
};
const checkAll = async (dependencies: Dependency[]) => {
const results: Result[] = [];
for (let i = 0; i < dependencies.length; i += CONCURRENCY) {
results.push(
// eslint-disable-next-line no-await-in-loop
...(await Promise.all(
dependencies.slice(i, i + CONCURRENCY).map(checkPackage)
))
);
}
return results;
};
const writeSummary = async (core: Core, heading: string, results: Result[]) => {
await core.summary
.addHeading(heading, 2)
.addTable([
[
"Package",
"Version",
"Section",
"Deprecated",
"Provenance",
"Repository",
].map((data) => ({ data, header: true })),
...results.map((result) =>
result.skipped
? [
result.name,
result.version,
result.section,
`skipped: ${result.skipped}`,
"-",
"-",
]
: [
result.name,
result.version,
result.section,
result.deprecated ? "yes" : "no",
result.provenance ? "yes" : "no",
result.repository ?? "none",
]
),
])
.write();
};
export default async function checkNpmMetadata({
github,
context,
core,
}: GitHubScriptArgs<Payload>) {
const pr = context.payload.pull_request;
if (!pr) {
const pkg = await readPackageJson(github, context, context.sha);
const results = await checkAll(listDependencies(pkg));
const flagged = (result: Result) =>
Number(
Boolean(result.skipped || result.deprecated || !result.repository)
);
results.sort(
(a, b) => flagged(b) - flagged(a) || a.name.localeCompare(b.name)
);
await writeSummary(core, "Direct dependencies", results);
return;
}
const [base, head] = await Promise.all([
readPackageJson(github, context, pr.base.sha),
readPackageJson(github, context, pr.head.sha),
]);
// Compare the packages npm installs, so pointing an npm: alias at another
// package counts as new, but moving a package between sections does not
const existing = new Set(
listDependencies(base).map(({ registryName }) => registryName)
);
const added = listDependencies(head).filter(
({ registryName }) => !existing.has(registryName)
);
if (added.length === 0) {
core.info("No new dependencies");
return;
}
const results = await checkAll(added);
const annotation = { file: "package.json" };
for (const result of results) {
const label = `${result.name}@${result.version}`;
if (result.skipped) {
core.warning(`${label}: ${result.skipped}, not checked`, annotation);
continue;
}
if (result.deprecated) {
core.error(`${label} is deprecated: ${result.deprecated}`, annotation);
}
if (!result.repository) {
core.warning(`${label} has no source repository`, annotation);
}
if (!result.provenance) {
core.notice(`${label} was published without provenance`, annotation);
}
}
await writeSummary(core, "New dependencies", results);
if (results.some((result) => result.deprecated)) {
core.setFailed("New dependencies must not be deprecated on npm");
}
}
@@ -13,6 +13,7 @@ import type {
GitHubScriptArgs,
PullRequestPayload,
} from "./github-script.d.ts";
import { withRetry } from "./github-retry.mts";
export default async function checkPullRequestStandards({
github,
@@ -31,10 +32,12 @@ export default async function checkPullRequestStandards({
return;
}
try {
await github.rest.orgs.checkMembershipForUser({
org: "home-assistant",
username: pr.user.login,
});
await withRetry("organization membership check", () =>
github.rest.orgs.checkMembershipForUser({
org: "home-assistant",
username: pr.user.login,
})
);
core.info(`Skipping organization member: ${pr.user.login}`);
return;
} catch (_error) {
@@ -88,7 +91,9 @@ export default async function checkPullRequestStandards({
}
}`;
try {
await github.graphql(mutation, { id: subjectId });
await withRetry(`comment ${minimized ? "minimize" : "restore"}`, () =>
github.graphql(mutation, { id: subjectId })
);
} catch (error) {
core.info(
`Could not ${minimized ? "minimize" : "restore"} comment: ${error instanceof Error ? error.message : error}`
@@ -132,12 +137,14 @@ export default async function checkPullRequestStandards({
const isValid = problems.length === 0;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const comments = await withRetry("comment list", () =>
github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
})
);
const existing = comments.find((c) => c.body?.includes(marker));
const hasLabel = pr.labels.some((l) => l.name === label);
@@ -147,12 +154,14 @@ export default async function checkPullRequestStandards({
if (hasLabel) {
await ignoreMissing(() =>
github.rest.issues.removeLabel({
owner,
repo,
issue_number,
name: label,
})
withRetry("label removal", () =>
github.rest.issues.removeLabel({
owner,
repo,
issue_number,
name: label,
})
)
);
}
if (existing) {
@@ -164,12 +173,14 @@ export default async function checkPullRequestStandards({
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
if (!hasLabel) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: [label],
});
await withRetry("label addition", () =>
github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: [label],
})
);
}
const message =
@@ -181,18 +192,21 @@ export default async function checkPullRequestStandards({
`\n\n` +
`Please complete the ` +
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
`for more on creating a great pull request (see point 6).`;
`and see the [contributor docs](https://contributors.home-assistant.io/start/pull-requests/#fill-out-the-template) ` +
`for more on creating a great pull request.`;
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: message,
});
await withRetry("comment update", () =>
github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: message,
})
);
await setMinimized(existing.node_id, false);
} else {
// Not retried, as a retry after GitHub created the comment would post it twice
await github.rest.issues.createComment({
owner,
repo,
+25 -17
View File
@@ -9,6 +9,7 @@
// await checkTaskAuthorization({ github, context, core });
import type { GitHubScriptArgs, IssuePayload } from "./github-script.d.ts";
import { withRetry } from "./github-retry.mts";
export default async function checkTaskAuthorization({
github,
@@ -19,17 +20,20 @@ export default async function checkTaskAuthorization({
// Check if user is an organization member
try {
await github.rest.orgs.checkMembershipForUser({
org: "home-assistant",
username: issueAuthor,
});
await withRetry("organization membership check", () =>
github.rest.orgs.checkMembershipForUser({
org: "home-assistant",
username: issueAuthor,
})
);
core.info(`✅ ${issueAuthor} is an organization member`);
return; // Authorized
} catch (_error) {
core.info(`❌ ${issueAuthor} is not authorized to create Task issues`);
}
// Close the issue with a comment
// Close the issue with a comment. Not retried, as a retry after GitHub
// created the comment would post it twice.
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -43,18 +47,22 @@ export default async function checkTaskAuthorization({
`If you believe you should have access to create Task issues, please contact the maintainers.`,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: "closed",
});
await withRetry("issue close", () =>
github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: "closed",
})
);
// Add a label to indicate this was auto-closed
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ["auto-closed"],
});
await withRetry("auto-closed label", () =>
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ["auto-closed"],
})
);
}
+69
View File
@@ -0,0 +1,69 @@
// Retries GitHub API requests with jittered exponential backoff. Shared by the
// workflow scripts in this directory, test/e2e/post-report-comment.mjs and the
// fetch-nightly-translations gulp task. Only wrap requests that are safe to
// repeat: a create that fails after GitHub has applied it would run twice.
//
// This replaces @octokit/plugin-retry, which only retries HTTP error statuses
// on a fixed 1s, 4s, 9s delay with no jitter or logging. GitHub can also
// answer 200 with briefly stale data, such as the nightly translations lookup
// returning an old run with no artifact, and the plugin cannot retry that.
// github-script also leaves its built-in retries off by default.
const RETRIES = 3;
const BASE_DELAY = 1000; // ms
const MAX_DELAY = 10000; // ms
const TRANSIENT_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
// Throw from a request to retry a response GitHub may answer differently on
// the next attempt, such as briefly stale data
export class RetryableError extends Error {}
// Octokit reports network failures as status 500
const isRetryable = (err: unknown) => {
if (err instanceof RetryableError) {
return true;
}
if (!(err instanceof Error) || !("status" in err)) {
return false;
}
const status = Number(err.status);
return (
TRANSIENT_STATUSES.has(status) ||
(status === 403 && /rate limit/i.test(err.message))
);
};
export async function withRetry<T>(
label: string,
request: () => Promise<T>
): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
// eslint-disable-next-line no-await-in-loop
return await request();
} catch (err) {
if (attempt > RETRIES || !isRetryable(err)) {
throw err;
}
const delay =
Math.min(BASE_DELAY * 2 ** (attempt - 1), MAX_DELAY) *
(0.8 + Math.random() * 0.4);
// eslint-disable-next-line no-console
console.log(
`Retrying ${label} after ${(delay / 1000).toFixed(1)}s (attempt ${attempt}/${RETRIES}): ${err instanceof Error ? err.message : err}`
);
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
}
}
+13
View File
@@ -2,14 +2,24 @@
// limited to the parts they use.
import type { Octokit } from "@octokit/rest";
export type SummaryTableCell = string | { data: string; header?: boolean };
export interface Summary {
addHeading(text: string, level?: number): Summary;
addRaw(text: string): Summary;
addTable(rows: SummaryTableCell[][]): Summary;
write(): Promise<Summary>;
}
export interface AnnotationProperties {
file?: string;
}
export interface Core {
info(message: string): void;
notice(message: string, properties?: AnnotationProperties): void;
warning(message: string, properties?: AnnotationProperties): void;
error(message: string, properties?: AnnotationProperties): void;
setFailed(message: string): void;
summary: Summary;
}
@@ -26,6 +36,8 @@ export interface PullRequestPayload {
draft: boolean;
user: User;
labels: { name: string }[];
base: { sha: string };
head: { sha: string };
};
}
@@ -36,6 +48,7 @@ export interface IssuePayload {
}
export interface Context<Payload> {
sha: string;
repo: { owner: string; repo: string };
issue: { owner: string; repo: string; number: number };
payload: Payload;
+56
View File
@@ -0,0 +1,56 @@
/**
* Builds a section of a workflow run's summary: a heading, a table of rows and
* the warnings raised along the way.
*
* Warnings are also logged as annotations, so a script can report problems
* without failing its step.
*/
import type { AnnotationProperties, Core } from "./github-script.d.ts";
interface SummaryOptions {
heading: string;
columns: string[];
// Shown instead of the table when no rows were added
empty: string;
}
export const createSummary = (
core: Core,
{ heading, columns, empty }: SummaryOptions
) => {
const rows: string[][] = [];
const warnings: string[] = [];
return {
addRow: (...cells: string[]) => {
rows.push(cells);
},
warn: (message: string, properties?: AnnotationProperties) => {
core.warning(message, properties);
warnings.push(message);
},
write: async () => {
core.summary.addHeading(heading, 2);
if (rows.length > 0) {
core.summary.addTable([
columns.map((data) => ({ data, header: true })),
...rows,
]);
} else {
core.summary.addRaw(`${empty}\n`);
}
if (warnings.length > 0) {
core.summary
.addHeading("Warnings", 3)
.addRaw(`${warnings.map((warning) => `- ${warning}`).join("\n")}\n`);
}
await core.summary.write();
},
};
};
+324
View File
@@ -0,0 +1,324 @@
#!/usr/bin/env node
/**
* Applies the Companion App, Home Assistant Link, Labs and Supervisor labels
* to pull requests.
*
* These areas are often changed from code outside their folders, which
* actions/labeler can't see, so this script owns them and .github/labeler.yml
* leaves them out.
*
* A label matches when the pull request:
* - changes a file in one of the area's paths, or
* - adds or removes a line that uses the area's API, such as gating a feature
* behind Labs or only showing it on Supervisor installs.
*
* Like actions/labeler, a label is removed once the pull request no longer
* matches, including one added by hand.
*
* Invoked from the `triage` job in .github/workflows/labeler.yaml.
*/
import type {
GitHubScriptArgs,
PullRequestPayload,
} from "./github-script.d.ts";
import { withRetry } from "./github-retry.mts";
import { createSummary } from "./github-summary.mts";
const RULES: { label: string; paths: RegExp[]; pattern: RegExp }[] = [
{
label: "Companion App",
paths: [
/^src\/external_app\//,
/^src\/data\/external\.ts$/,
/^src\/util\/is_ios\.ts$/,
],
// Reads or messages the companion app the frontend is running in, or
// imports its modules. Matches imports of data/external rather than
// isExternal, which is also a common local name.
pattern:
/\bauth\??\.external\b(?:[!?]*\.\w+)*(?:\([^)]*)?|\bfireExternalBusMessage\([^)]*|(?:\bimport\s[^;]*?from\s*|\bimport\(\s*)["'][^"']*\/(?:external_app\/[^"']*|data\/external)["']/g,
},
{
label: "Home Assistant Link",
paths: [
/^src\/panels\/config\/cloud\//,
/^src\/data\/cloud\//,
/^src\/data\/cloud\.ts$/,
],
// Only runs when Home Assistant Link is set up
pattern: /isComponentLoaded\([^)]*["']cloud["']/g,
},
{
label: "Labs",
paths: [/^src\/panels\/config\/labs\//, /^src\/data\/labs\.ts$/],
// Gates a feature behind, or toggles, a Labs preview feature
pattern:
/\b(?:subscribeLabFeatures?|fetchLabFeatures|labsUpdatePreviewFeature)\([^)]*/g,
},
{
label: "Supervisor",
paths: [
/^src\/panels\/config\/apps\//,
/^src\/panels\/app\//,
/^src\/data\/hassio\//,
/^src\/data\/supervisor\//,
],
// Only runs on installs with the Supervisor
pattern: /isComponentLoaded\([^)]*["']hassio["']/g,
},
];
interface HunkSide {
lines: string[];
changed: boolean[];
}
interface CodeChange {
code: string;
filename: string;
}
const parseHunks = (patch: string) => {
const hunks: { before: HunkSide; after: HunkSide }[] = [];
for (const line of patch.split("\n")) {
if (line.startsWith("@@")) {
hunks.push({
before: { lines: [], changed: [] },
after: { lines: [], changed: [] },
});
continue;
}
const hunk = hunks.at(-1);
if (!hunk || line.startsWith("\\")) {
continue;
}
const text = line.slice(1);
if (!line.startsWith("+")) {
hunk.before.lines.push(text);
hunk.before.changed.push(line.startsWith("-"));
}
if (!line.startsWith("-")) {
hunk.after.lines.push(text);
hunk.after.changed.push(line.startsWith("+"));
}
}
return hunks;
};
// Ignores whitespace and trailing commas outside strings, as Prettier changes
// them when wrapping
const normaliseCode = (code: string) => {
const parts = code.split(
/("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`)/
);
return parts
.map((part, i) => {
if (i % 2 === 1) {
return part;
}
const stripped = part.replace(/\s+/g, "").replace(/,(?=[}\])])/g, "");
return i === parts.length - 1 ? stripped.replace(/,$/, "") : stripped;
})
.join("");
};
// Matches whole calls, which can span lines, that touch a changed line
const changedMatches = (side: HunkSide, pattern: RegExp) => {
const text = side.lines.join("\n");
const matches: string[] = [];
for (const match of text.matchAll(pattern)) {
const first = text.slice(0, match.index).split("\n").length - 1;
const last = first + match[0].split("\n").length - 1;
if (side.changed.slice(first, last + 1).some(Boolean)) {
matches.push(normaliseCode(match[0]));
}
}
return matches;
};
// Matches that were only moved, reindented or reformatted cancel out in pairs,
// returning a file with a match that doesn't
const unmatchedChange = (added: CodeChange[], removed: CodeChange[]) => {
const remaining = [...removed];
for (const change of added) {
const index = remaining.findIndex(({ code }) => code === change.code);
if (index === -1) {
return change.filename;
}
remaining.splice(index, 1);
}
return remaining[0]?.filename;
};
const describeError = (cause: unknown) =>
cause instanceof Error ? cause.message : String(cause);
const isNotFound = (cause: unknown) =>
cause instanceof Error && "status" in cause && cause.status === 404;
export default async function labelContent({
github,
context,
core,
}: GitHubScriptArgs<PullRequestPayload>) {
const pr = context.payload.pull_request;
const summary = createSummary(core, {
heading: "Content labels",
columns: ["Label", "Change", "Reason"],
empty: "No content labels changed.",
});
if (process.env.LABELER_OUTCOME === "failure") {
summary.warn(
"Applying labels from .github/labeler.yml failed, see its step"
);
}
// Bot pull requests, such as Prettier bumps, rewrite code they don't change
const checkCode = pr.user.type !== "Bot";
const { owner, repo } = context.repo;
let files;
let existing: Set<string>;
try {
// Read the current labels, as the event's snapshot can be stale by now
const [fileList, labels] = await Promise.all([
withRetry("pull request files", () =>
github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
})
),
withRetry("pull request labels", () =>
github.paginate(github.rest.issues.listLabelsOnIssue, {
owner,
repo,
issue_number: pr.number,
per_page: 100,
})
),
]);
files = fileList;
existing = new Set(labels.map((l) => l.name));
} catch (error) {
summary.warn(
`Could not read the pull request's files and labels: ${describeError(error)}`
);
await summary.write();
return;
}
const add: { label: string; reason: string }[] = [];
const remove: string[] = [];
for (const rule of RULES) {
const added: CodeChange[] = [];
const removed: CodeChange[] = [];
if (checkCode) {
for (const { filename, patch } of files) {
for (const { before, after } of parseHunks(patch ?? "")) {
for (const code of changedMatches(after, rule.pattern)) {
added.push({ code, filename });
}
for (const code of changedMatches(before, rule.pattern)) {
removed.push({ code, filename });
}
}
}
}
// Renames count for both locations, as moving a file out of an area changes it
const match =
files.find(({ filename, previous_filename }) =>
rule.paths.some(
(path) =>
path.test(filename) ||
(previous_filename !== undefined && path.test(previous_filename))
)
)?.filename ?? unmatchedChange(added, removed);
if (match && !existing.has(rule.label)) {
core.info(`Adding ${rule.label} for ${match}`);
add.push({ label: rule.label, reason: match });
} else if (!match && existing.has(rule.label)) {
core.info(`Removing ${rule.label}, no longer matched`);
remove.push(rule.label);
}
}
const removals = await Promise.allSettled(
remove.map((name) =>
withRetry("label removal", () =>
github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr.number,
name,
})
)
)
);
removals.forEach((removal, i) => {
const name = remove[i];
// Already removed, such as by hand or a run that raced this one
if (removal.status === "fulfilled" || isNotFound(removal.reason)) {
summary.addRow(name, "Removed", "No longer matched");
} else {
summary.warn(
`Could not remove ${name}: ${describeError(removal.reason)}`
);
}
});
if (add.length > 0) {
try {
await withRetry("label addition", () =>
github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: add.map(({ label }) => label),
})
);
for (const { label, reason } of add) {
summary.addRow(label, "Added", reason);
}
} catch (error) {
summary.warn(
`Could not add ${add.map(({ label }) => label).join(", ")}: ${describeError(error)}`
);
}
}
await summary.write();
}
+1 -1
View File
@@ -13,7 +13,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"types": []
"types": ["node"]
},
"include": ["*.mts", "*.d.ts"]
}
+41
View File
@@ -0,0 +1,41 @@
name: Verify actions lockfile
on:
push:
branches:
- dev
- master
paths:
- ".github/actions/**"
- ".github/workflows/**"
pull_request:
branches:
- dev
- master
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
verify:
name: Verify actions lockfile
runs-on: ubuntu-latest
env:
GH_ACTIONS_LOCK_VERSION: v0.4.0
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install gh-actions-lock
env:
GH_TOKEN: ${{ github.token }}
run: gh extension install github/gh-actions-lock --pin "${GH_ACTIONS_LOCK_VERSION}"
- name: Verify lockfile
env:
GH_TOKEN: ${{ github.token }}
run: gh actions-lock --verify
+190
View File
@@ -0,0 +1,190 @@
# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
# Docs: https://gh.io/actions-lockfile
version: 'v0.0.3'
workflows:
'.github/workflows/actionlint.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
'.github/workflows/actions-lock.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
'.github/workflows/cast_deployment.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
'.github/workflows/ci.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
'.github/workflows/codeql-analysis.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'github/codeql-action@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee'
'.github/workflows/copilot-setup-steps.yml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
'.github/workflows/demo_deployment.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
'.github/workflows/dependency-review.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294'
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
'.github/workflows/design_deployment.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
'.github/workflows/design_preview.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
'.github/workflows/e2e.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333'
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
'.github/workflows/labeler.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
- 'actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13'
'.github/workflows/lock.yaml':
- 'dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c'
'.github/workflows/nightly.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
- 'actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97'
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
'.github/workflows/pull-request-standards.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
'.github/workflows/relative-ci.yaml':
- 'relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60'
'.github/workflows/release-drafter.yaml':
- 'release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0'
'.github/workflows/release.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
- 'actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97'
- 'home-assistant/actions@cd6f3635a740c84d8a14f6121a90068b8ece6bf6'
- 'home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716'
- 'pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33'
'.github/workflows/restrict-task-creation.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
'.github/workflows/stale.yaml':
- 'actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93'
'.github/workflows/sync-device-classes.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
- 'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1'
'.github/workflows/sync-sensor-entity-constants.yaml':
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
- 'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1'
'.github/workflows/translations.yaml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
dependencies:
'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9':
ref: 'v6.1.0'
commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
owner_id: 44036562
repo_id: 215566462
'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1':
ref: 'v7.0.1'
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
owner_id: 44036562
repo_id: 197814629
'actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294':
ref: 'v5.0.0'
commit: 'sha1-a1d282b36b6f3519aa1f3fc636f609c47dddb294'
owner_id: 44036562
repo_id: 476372928
'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333':
ref: 'v8.0.2'
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
owner_id: 44036562
repo_id: 192626254
'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3':
ref: 'v9.0.0'
commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3'
owner_id: 44036562
repo_id: 205262760
'actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13':
ref: 'v7.0.0'
commit: 'sha1-bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13'
owner_id: 44036562
repo_id: 177139928
'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1':
ref: 'v7.1.0'
commit: 'sha1-949feb2413d6458794dcd2491c4babbbce0c15c1'
owner_id: 44036562
repo_id: 189476904
'actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97':
ref: 'v7.0.0'
commit: 'sha1-5fda3b95a4ea91299a34e894583c3862153e4b97'
owner_id: 44036562
repo_id: 192625525
'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405':
ref: 'v6.2.0'
commit: 'sha1-a309ff8b426b58ec0e2a45f0f869d46889d02405'
owner_id: 44036562
repo_id: 192625525
'actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93':
ref: 'v11.0.0'
commit: 'sha1-4391f3da665fdf50b6810c1a66712fb9ba21aa93'
owner_id: 44036562
repo_id: 200437066
'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9':
ref: 'v7.0.2'
commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9'
owner_id: 44036562
repo_id: 192625955
'dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c':
ref: 'v6.0.2'
commit: 'sha1-89ae32b08ed1a541efecbab17912962a5e38981c'
owner_id: 7513068
repo_id: 113918130
'github/codeql-action@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee':
ref: 'v4.39.0'
commit: 'sha1-6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee'
owner_id: 9919
repo_id: 259445878
'home-assistant/actions@cd6f3635a740c84d8a14f6121a90068b8ece6bf6':
ref: 'master'
commit: 'sha1-cd6f3635a740c84d8a14f6121a90068b8ece6bf6'
owner_id: 13844975
repo_id: 173703505
'home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716':
ref: '2026.07.0'
commit: 'sha1-9e17ab1ed5c4c79d8b61e29fa63de25ca2710716'
owner_id: 13844975
repo_id: 184234812
'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1':
ref: 'v8.1.1'
commit: 'sha1-5f6978faf089d4d20b00c7766989d076bb2fc7f1'
owner_id: 18365890
repo_id: 197176365
'pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33':
ref: 'v1.14.2'
commit: 'sha1-dc37677b2e1c63e2034f94d8a5b11f265b73ba33'
owner_id: 647025
repo_id: 178055147
uses:
- 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405'
'relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60':
ref: 'v3.2.4'
commit: 'sha1-711f3a8fa1b928e6afde280011110c9b0a224a60'
owner_id: 43522883
repo_id: 398376354
'release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0':
ref: 'v7.10.0'
commit: 'sha1-978bb0b30c5ee8cdd332ced253758157150d43f0'
owner_id: 58292376
repo_id: 139022359
-35
View File
@@ -1,35 +0,0 @@
name: Blocking labels
on:
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
- unlabeled
branches:
- dev
- master
permissions:
contents: read
jobs:
check:
name: Check for labels which block the Pull Request from being merged
runs-on: ubuntu-latest
steps:
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/scripts
- name: Check for blocking labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { default: checkBlockingLabels } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mts`
);
await checkBlockingLabels({ github, context, core });
+6 -6
View File
@@ -30,17 +30,17 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Build Cast
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-cast
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: ./.github/actions/netlify-deploy
uses: $/.github/actions/netlify-deploy
with:
dir: cast/dist
alias: dev
@@ -62,17 +62,17 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Build Cast
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-cast
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: ./.github/actions/netlify-deploy
uses: $/.github/actions/netlify-deploy
with:
dir: cast/dist
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+10 -64
View File
@@ -31,7 +31,7 @@ jobs:
with:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
uses: $/.github/actions/prepare-dependencies
lint:
name: Lint and check format
@@ -43,7 +43,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
- name: Check for duplicate dependencies
@@ -89,7 +89,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
- name: Build resources
@@ -100,12 +100,9 @@ jobs:
run: pnpm run test
build:
name: Build frontend
# Runs alongside lint and test rather than after them: the build only needs
# the dependency tree, and with the rspack cache it is no longer expensive
# enough to be worth serialising behind the other checks. The
# cancel-on-failure job below stops the run as soon as a check fails, so a
# broken pull request does not finish building.
needs: prepare-dependencies
needs:
- lint
- test
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
@@ -113,7 +110,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
# Read-only reuse of the rspack cache written by the nightly (see
@@ -130,14 +127,14 @@ jobs:
restore-keys: |
rspack-cache-${{ runner.os }}-
- name: Build Application
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-app
github-token: ${{ secrets.GITHUB_TOKEN }}
is-test: true
rspack-cache: readonly
- name: Upload bundle stats
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -145,60 +142,9 @@ jobs:
- name: Check entrypoint bundle size budget
run: pnpm run check-bundlesize
- name: Upload frontend build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: frontend-build
path: hass_frontend/
if-no-files-found: error
retention-days: 7
# Now that the checks run in parallel, a failing lint or test no longer stops
# the build from finishing on its own, so this watches them and cancels the
# whole run on the first failure.
#
# It is a separate job on purpose. Cancelling needs `actions: write`, and the
# other jobs check out the pull request and run its build scripts — handing
# them that scope would give PR-controlled code (or a compromised dependency)
# write access to Actions. This job never checks out the repository, so the
# elevated token stays away from PR code. It also cannot be a job that
# `needs` the checks: that would only start once they have all finished, which
# is exactly too late to cancel anything.
cancel-on-failure:
name: Cancel run on failure
needs: prepare-dependencies
runs-on: ubuntu-latest
permissions:
actions: write
timeout-minutes: 30
steps:
- name: Cancel the run when a check fails
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
run: |
watched='^(Lint and check format|Run tests|Build frontend)$'
while :; do
jobs=$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
--paginate --jq '.jobs[] | [.name, .status, (.conclusion // "")] | @tsv' \
2>/dev/null || true)
failed=$(printf '%s\n' "$jobs" | awk -F'\t' -v w="$watched" \
'$1 ~ w && ($3 == "failure" || $3 == "timed_out") { print $1 }')
if [ -n "$failed" ]; then
echo "Cancelling the run, these checks failed:"
printf '%s\n' "$failed"
gh run cancel "$RUN_ID" --repo "$REPO" || true
exit 0
fi
found=$(printf '%s\n' "$jobs" | awk -F'\t' -v w="$watched" '$1 ~ w' | wc -l)
running=$(printf '%s\n' "$jobs" | awk -F'\t' -v w="$watched" \
'$1 ~ w && $2 != "completed" { print $1 }')
if [ "$found" -ge 3 ] && [ -z "$running" ]; then
echo "All checks finished without failure"
exit 0
fi
sleep 15
done
+2 -2
View File
@@ -32,12 +32,12 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
languages: javascript-typescript
build-mode: none
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
category: "/language:javascript-typescript"
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
+6 -6
View File
@@ -31,17 +31,17 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Build Demo
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-demo
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: ./.github/actions/netlify-deploy
uses: $/.github/actions/netlify-deploy
with:
dir: demo/dist
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
@@ -62,17 +62,17 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Build Demo
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-demo
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: ./.github/actions/netlify-deploy
uses: $/.github/actions/netlify-deploy
with:
dir: demo/dist
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+55
View File
@@ -0,0 +1,55 @@
name: Dependency review
on:
pull_request:
branches:
- dev
paths:
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
schedule:
- cron: "0 6 1 * *" # Monthly report on all direct dependencies
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
dependency-review:
name: Review dependency changes
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read # To compare the dependency graph between base and head
steps:
- name: Dependency review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
license-check: false # Covered by pnpm lint:licenses
fail-on-severity: high
show-openssf-scorecard: true
warn-on-openssf-scorecard-level: 3
npm-metadata:
name: Check npm metadata
runs-on: ubuntu-latest
permissions:
contents: read # To read package.json
steps:
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/scripts
- name: Check npm metadata
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { default: checkNpmMetadata } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-npm-metadata.mts`
);
await checkNpmMetadata({ github, context, core });
+3 -3
View File
@@ -25,17 +25,17 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Build Gallery
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-gallery
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: ./.github/actions/netlify-deploy
uses: $/.github/actions/netlify-deploy
with:
dir: gallery/dist
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+3 -3
View File
@@ -29,17 +29,17 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Build Gallery
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-gallery
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy preview to Netlify
id: deploy
uses: ./.github/actions/netlify-deploy
uses: $/.github/actions/netlify-deploy
with:
dir: gallery/dist
alias: deploy-preview-${{ github.event.number }}
+29 -29
View File
@@ -32,13 +32,13 @@ jobs:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
uses: $/.github/actions/prepare-dependencies
prepare-container-dependencies:
name: Prepare container dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
options: --user 1001
defaults:
run:
@@ -50,7 +50,7 @@ jobs:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
uses: $/.github/actions/prepare-dependencies
with:
node-modules-cache-key: node-modules-container-v1
@@ -66,19 +66,19 @@ jobs:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
- name: Build demo
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-demo-e2e
github-token: ${{ secrets.GITHUB_TOKEN }}
is-test: true
- name: Upload demo build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: demo-dist
path: demo/dist/
@@ -97,19 +97,19 @@ jobs:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
- name: Build e2e test app
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-e2e-test-app-e2e
github-token: ${{ secrets.GITHUB_TOKEN }}
is-test: true
- name: Upload e2e test app build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: e2e-test-app-dist
path: test/e2e/app/dist/
@@ -128,19 +128,19 @@ jobs:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
- name: Build gallery
uses: ./.github/actions/build
uses: $/.github/actions/build
with:
target: build-gallery
github-token: ${{ secrets.GITHUB_TOKEN }}
is-test: true
- name: Upload gallery build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: gallery-dist
path: gallery/dist/
@@ -155,7 +155,7 @@ jobs:
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
options: --user 1001 --ipc=host
defaults:
run:
@@ -176,13 +176,13 @@ jobs:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
node-modules-cache-key: node-modules-container-v1
- name: Download demo build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: demo-dist
path: demo/dist/
@@ -192,7 +192,7 @@ jobs:
timeout-minutes: 15
- name: Upload demo blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: blob-report-demo-${{ matrix.shardIndex }}
@@ -207,7 +207,7 @@ jobs:
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
options: --user 1001 --ipc=host
defaults:
run:
@@ -230,13 +230,13 @@ jobs:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
node-modules-cache-key: node-modules-container-v1
- name: Download e2e test app build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: e2e-test-app-dist
path: test/e2e/app/dist/
@@ -246,7 +246,7 @@ jobs:
timeout-minutes: 15
- name: Upload app blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: blob-report-app-${{ matrix.shardIndex }}
@@ -261,7 +261,7 @@ jobs:
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
options: --user 1001 --ipc=host
defaults:
run:
@@ -284,13 +284,13 @@ jobs:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
node-modules-cache-key: node-modules-container-v1
- name: Download gallery build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: gallery-dist
path: gallery/dist/
@@ -300,7 +300,7 @@ jobs:
timeout-minutes: 15
- name: Upload gallery blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: blob-report-gallery-${{ matrix.shardIndex }}
@@ -326,26 +326,26 @@ jobs:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
node-modules-cache: true
- name: Download demo blob reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
continue-on-error: true
with:
pattern: blob-report-demo-*
path: test/e2e/reports/demo/
- name: Download app blob reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
continue-on-error: true
with:
pattern: blob-report-app-*
path: test/e2e/reports/app/
- name: Download gallery blob reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
continue-on-error: true
with:
pattern: blob-report-gallery-*
@@ -359,7 +359,7 @@ jobs:
- name: Upload merged HTML report
id: upload-report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: playwright-report
path: test/e2e/reports/combined/
+24 -1
View File
@@ -1,6 +1,11 @@
name: "Pull Request Labeler"
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: only runs actions/labeler, no PR code checkout
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: checks out base repo scripts only and reads PR files through the API, never PR head code
# Runs one at a time per pull request, so an older run can't undo a newer one
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
triage:
@@ -10,6 +15,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply labels
id: labeler
continue-on-error: true
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
with:
sync-labels: true
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/scripts
- name: Apply content labels
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
LABELER_OUTCOME: ${{ steps.labeler.outcome }}
with:
script: |
const { default: labelContent } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/label-content.mts`
);
await labelContent({ github, context, core });
+4 -4
View File
@@ -31,7 +31,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
immutable: false
@@ -104,7 +104,7 @@ jobs:
key: rspack-cache-${{ runner.os }}-${{ github.run_id }}-${{ github.run_attempt }}
- name: Upload build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: wheels
path: dist/home_assistant_frontend*.whl
@@ -124,7 +124,7 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
immutable: false
@@ -137,7 +137,7 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload translations
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: translations
path: translations.tar.gz
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@fcf45416581928e8dd62eded78ce98c78e5149f8 # v3.2.3
uses: relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60 # v3.2.4
with:
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
token: ${{ github.token }}
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@fcf45416581928e8dd62eded78ce98c78e5149f8 # v3.2.3
uses: relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60 # v3.2.4
with:
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
token: ${{ github.token }}
+1 -1
View File
@@ -19,6 +19,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
- uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -36,10 +36,10 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@58bff37c8947f690ace498be413a9b78d6f30f93 # master
uses: home-assistant/actions/helpers/verify-version@cd6f3635a740c84d8a14f6121a90068b8ece6bf6 # master
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
immutable: false
cache: false
@@ -146,7 +146,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
with:
immutable: false
cache: false
+17 -6
View File
@@ -15,22 +15,33 @@ jobs:
name: Add no-stale label
runs-on: ubuntu-latest
permissions:
contents: read # To check out workflow scripts
issues: write # To add labels to issues
if: >-
github.event.issue.type.name == 'Task'
|| github.event.issue.type.name == 'Epic'
|| github.event.issue.type.name == 'Opportunity'
steps:
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/scripts
- name: Add no-stale label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['no-stale']
});
const { withRetry } = await import(
`${process.env.GITHUB_WORKSPACE}/.github/scripts/github-retry.mts`
);
await withRetry("no-stale label", () =>
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['no-stale']
})
);
check-authorization:
name: Check authorization
+3 -2
View File
@@ -15,11 +15,12 @@ jobs:
if: github.repository == 'home-assistant/frontend'
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
- name: Stale policy
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
days-before-issue-stale: 90
days-before-pr-stale: 60
days-before-close: 7
operations-per-run: 25
remove-stale-when-updated: true
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Regenerate device class constants
run: ./script/gen_device_classes
@@ -28,7 +28,7 @@ jobs:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
uses: $/.github/actions/setup
- name: Regenerate sensor entity constants
run: ./script/gen_sensor_entity_constants
-1
View File
@@ -67,4 +67,3 @@ test/e2e/app/dist/
.serena
test/benchmarks/results/
+1 -1
View File
@@ -1 +1 @@
24.21.0
24.22.1
+1
View File
@@ -46,6 +46,7 @@ Detailed guidance lives in `.agents/skills/<name>/SKILL.md`. Load every matching
- `ha-frontend-lit`: reactive fields, DOM queries, lifecycle behavior, and render-derived state.
- `ha-frontend-styling`: theme variables, spacing tokens, responsive layout, RTL, and view transitions.
- `ha-frontend-testing`: lint, typecheck, Vitest, Playwright e2e dev servers, and benchmarks.
- `ha-frontend-query-params`: typed URL query params, `QueryParamsController`, data table URL sync, in-app links, and My Home Assistant redirect params.
- `ha-frontend-user-facing-text`: localization, terminology, sentence case, and Home Assistant text style.
- `ha-frontend-review`: PR template use, existing review feedback, review checklist, recurring issues, and UI/UX evidence including gallery specifications.
- `ha-frontend-ux-readiness`: UI/UX routing that complements frontend review for new user experiences.
+1 -1
View File
@@ -41,5 +41,5 @@ Contributions that do not follow this policy will be closed. Repeated violations
---
The canonical version of this policy is published at
<https://developers.home-assistant.io/docs/ai_policy>. In case of differences,
<https://contributors.home-assistant.io/start/ai-policy/>. In case of differences,
the published version applies.
+169 -162
View File
@@ -1,194 +1,201 @@
Apache License
==============
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
### Terms and Conditions for use, reproduction, and distribution
1. Definitions.
#### 1. Definitions
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
#### 2. Grant of Copyright License
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
#### 3. Grant of Patent License
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
#### 4. Redistribution
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
#### 5. Submission of Contributions
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
#### 6. Trademarks
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
#### 7. Disclaimer of Warranty
END OF TERMS AND CONDITIONS
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
APPENDIX: How to apply the Apache License to your work.
#### 8. Limitation of Liability
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
Copyright [yyyy] [name of copyright owner]
#### 9. Accepting Warranty or Additional Liability
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
http://www.apache.org/licenses/LICENSE-2.0
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+2 -2
View File
@@ -6,12 +6,12 @@ This is the repository for the official [Home Assistant](https://home-assistant.
- [View demo of Home Assistant](https://demo.home-assistant.io/)
- [More information about Home Assistant](https://home-assistant.io)
- [Frontend development instructions](https://developers.home-assistant.io/docs/frontend/development/)
- [Frontend development instructions](https://contributors.home-assistant.io/frontend/development/)
## Development
- Initial setup: `script/setup`
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
- Development: [Instructions](https://contributors.home-assistant.io/frontend/development/)
- Production build: `pnpm build`
- Gallery: `cd gallery && script/develop_gallery`
@@ -18,17 +18,6 @@ const polyfillSupport = {
safari: 17.4,
samsung: 15.0,
},
"element-getattributenames": {
android: 61,
chrome: 61,
edge: 18,
firefox: 45,
ios: 10.3,
opera: 48,
opera_mobile: 45,
safari: 10.1,
samsung: 8.0,
},
"element-toggleattribute": {
android: 69,
chrome: 69,
@@ -67,39 +56,20 @@ const polyfillSupport = {
"intl-other": {
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
},
"resize-observer": {
android: 64,
chrome: 64,
edge: 79,
firefox: 69,
ios: 13.4,
opera: 51,
opera_mobile: 47,
safari: 13.1,
samsung: 9.0,
},
};
// Map of global variables and/or instance and static properties to the
// corresponding polyfill key and actual module to import
// Map of instance and static properties to the corresponding polyfill key
// and actual module to import
const polyfillMap = {
global: {
ResizeObserver: {
key: "resize-observer",
module: join(POLYFILL_DIR, "resize-observer.ts"),
},
},
instance: {
attachInternals: {
key: "element-internals",
module: "element-internals-polyfill",
},
...Object.fromEntries(
["getAttributeNames", "toggleAttribute"].map((prop) => {
const key = `element-${prop.toLowerCase()}`;
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
})
),
toggleAttribute: {
key: "element-toggleattribute",
module: join(POLYFILL_DIR, "element-toggleattribute.ts"),
},
},
static: {
Intl: {
+8 -8
View File
@@ -1,15 +1,15 @@
{
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
"frontend-modern": {
"app": 593517,
"core": 54884,
"authorize": 557490,
"onboarding": 676131
"app": 627654,
"core": 57901,
"authorize": 593862,
"onboarding": 709255
},
"frontend-legacy": {
"app": 738388,
"core": 188119,
"authorize": 716748,
"onboarding": 840680
"app": 724225,
"core": 163115,
"authorize": 693913,
"onboarding": 812415
}
}
+24 -11
View File
@@ -96,8 +96,9 @@ module.exports.htmlMinifierOptions = {
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
// Highest syntax the minifier may emit; it never downlevels. Every browser
// in [modern] is well past ES2020 (universal since spring 2020); the
// [legacy] floors (Chrome 59 / Safari 12) top out at ES2017.
ecma: latestBuild ? 2020 : 2017,
// [legacy] floors (Chrome 67 / Safari 15) top out at ES2019: optional
// catch binding is in, `?.` and `??` (Chrome 80) are not.
ecma: latestBuild ? 2020 : 2019,
module: latestBuild,
format: { comments: false },
sourceMap: !isTestBuild,
@@ -116,6 +117,16 @@ module.exports.swcOptions = () => ({
},
});
const corejsPolyfills = (exclude = []) => [
"babel-plugin-polyfill-corejs3",
{
method: "usage-global",
version: dependencies["core-js"],
shippedProposals: true,
exclude,
},
];
module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
babelrc: false,
compact: false,
@@ -137,14 +148,7 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
// Inject Core-JS polyfills on demand. Babel 8 removed preset-env's
// `useBuiltIns`/`corejs` options, so the equivalent polyfill provider is
// configured directly here (`usage-global` matches the old `useBuiltIns: "usage"`).
[
"babel-plugin-polyfill-corejs3",
{
method: "usage-global",
version: dependencies["core-js"],
shippedProposals: true,
},
],
corejsPolyfills(),
[
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
{
@@ -185,12 +189,21 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
path.join(paths.root_dir, "src/resources/polyfills"),
...[
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
"@lit-labs/virtualizer/polyfills",
"@webcomponents/scoped-custom-element-registry",
"element-internals-polyfill",
].map((p) => new RegExp(`/node_modules/${p}/`)),
],
},
{
// It only constructs `Uint8Array`s, but any reference to one pulls in
// every typed array polyfill, and this lands in the `core` entrypoint.
test: /[\\/]node_modules[\\/]home-assistant-js-websocket[\\/]/,
plugins: [
corejsPolyfills([
/^(es|esnext)\.(array-buffer|typed-array|uint8-array)\./,
]),
],
},
{
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
+10
View File
@@ -0,0 +1,10 @@
// @ts-check
import { noUnusedElementImport } from "./no-unused-element-import.mjs";
export default {
meta: { name: "eslint-plugin-ha" },
rules: {
"no-unused-element-import": noUnusedElementImport,
},
};
@@ -0,0 +1,188 @@
// @ts-check
import fs from "node:fs";
import path from "node:path";
const TAG_PATTERN =
/@customElement\(\s*["'`]([a-z0-9_-]+)["'`]|customElements\.define\(\s*["'`]([a-z0-9_-]+)["'`]/g;
const EXTENSIONS = /\.(ts|js)$/;
// Entry points register the root element that the served HTML renders.
const ENTRY_POINT = /(^|[\\/])(entrypoints[\\/]|entrypoint\.(ts|js)$)/;
// These resolve a tag from config at runtime, so they register every candidate
// they may be asked for.
const RUNTIME_REGISTRY =
/[\\/]create-element[\\/]|createLovelaceElement\(|getLovelaceElementClass\(/;
/** @type {Map<string, string[]>} */
const tagCache = new Map();
/** Tags the module at `file` registers when imported. */
const registeredTags = (file) => {
let tags = tagCache.get(file);
if (!tags) {
try {
const source = fs.readFileSync(file, "utf8");
tags = [...source.matchAll(TAG_PATTERN)].map((m) => m[1] || m[2]);
} catch {
tags = [];
}
tagCache.set(file, tags);
}
return tags;
};
const isFile = (candidate) => {
try {
return fs.statSync(candidate).isFile();
} catch {
return false;
}
};
const resolveModule = (fromFile, request) => {
const base = path.resolve(path.dirname(fromFile), request);
return [
base,
`${base}.ts`,
`${base}.js`,
base.replace(/\.js$/, ".ts"),
path.join(base, "index.ts"),
path.join(base, "index.js"),
].find((candidate) => EXTENSIONS.test(candidate) && isFile(candidate));
};
// `hui-${type}-card` or "state-card-" + domain never spell the tag out.
const buildsTagAtRuntime = (source, tag) => {
const segments = tag.split("-");
for (let end = 1; end < segments.length; end++) {
const prefix = `${segments.slice(0, end).join("-")}-`;
if (
source.includes(`\`${prefix}\${`) ||
source.includes(`"${prefix}" +`) ||
source.includes(`'${prefix}' +`)
) {
return true;
}
}
return false;
};
// Plain substring matching, so <ha-icon-button> counts as a use of ha-icon.
// That direction is deliberate: it misses dead imports rather than asking for
// a removal that breaks registration.
const usesTag = (body, tag) => body.includes(tag);
// A gallery page registers the elements its sibling markdown demos render, so
// that markdown is part of what the module is used by.
const siblingMarkup = (file) => {
const markdown = file.replace(EXTENSIONS, ".markdown");
try {
return fs.readFileSync(markdown, "utf8");
} catch {
return "";
}
};
/** Blanks out the given ranges so an import path cannot count as a usage. */
const withoutRanges = (source, ranges) => {
let result = source;
for (const [start, end] of ranges) {
result =
result.slice(0, start) + " ".repeat(end - start) + result.slice(end);
}
return result;
};
/**
* Side-effect imports register a custom element. ESLint's unused-import rules
* cannot judge them — there is no binding to be unused — so a registration
* outlives the template that needed it and nothing reports the dead import.
*
* Deliberately not fixable: removing the import is only safe once every file
* that renders the element imports it itself, which this rule cannot see.
*
* Off in eslint.config.mjs while a backlog of those paired cases remains;
* `pnpm lint:element-imports` enables it as a warning to list them. Flip it to
* "error" there once the list is empty.
*
* No disable comments while it is off: ESLint reports them as unused
* directives, which fails lint:eslint. Imports that must stay get theirs in
* the change that turns the rule on.
*/
export const noUnusedElementImport = {
meta: {
type: "suggestion",
docs: {
description:
"Disallow side-effect imports of a custom element the importing file never uses",
},
schema: [],
messages: {
unused:
'"{{request}}" only registers <{{tags}}>, which this file never uses. Remove the import, adding it to whichever file renders the element if that file relies on this registration.',
},
},
create(context) {
const filename = context.filename;
if (!EXTENSIONS.test(filename) || ENTRY_POINT.test(filename)) {
return {};
}
return {
Program(program) {
const imports = program.body.filter(
(node) => node.type === "ImportDeclaration"
);
const sideEffectImports = imports.filter(
(node) =>
node.specifiers.length === 0 &&
typeof node.source.value === "string" &&
node.source.value.startsWith(".")
);
if (!sideEffectImports.length) {
return;
}
const source = context.sourceCode.getText();
if (RUNTIME_REGISTRY.test(filename) || RUNTIME_REGISTRY.test(source)) {
return;
}
// Every import declaration, not just the side-effect ones: a module
// path is never a usage, and `import type { HaCard } from "./ha-card"`
// would otherwise pass off its own path as one.
const body =
withoutRanges(
source,
imports.map((node) => node.range)
) + siblingMarkup(filename);
for (const node of sideEffectImports) {
const request = node.source.value;
const target = resolveModule(filename, request);
if (!target) {
continue;
}
// No tags means no registration to judge: styles, polyfills, mocks.
const tags = registeredTags(target);
if (
!tags.length ||
tags.some(
(tag) => usesTag(body, tag) || buildsTagAtRuntime(body, tag)
)
) {
continue;
}
context.report({
node,
messageId: "unused",
data: { request, tags: tags.join(">, <") },
});
}
},
};
},
};
+29 -8
View File
@@ -5,6 +5,7 @@ import mapStream from "map-stream";
import transform from "gulp-json-transform";
import { LokaliseApi } from "@lokalise/node-api";
import JSZip from "jszip";
import { SERVICE_DOMAINS } from "./gen-demo-core-data.js";
const inDir = "translations";
const inDirFrontend = `${inDir}/frontend`;
@@ -51,15 +52,34 @@ function convertBackendTranslations(data, _file) {
if (!data.component) {
return output;
}
// Keep the entity domains and the domains with automation triggers or
// conditions, so the demo can show states and describe automations. Only
// keep the services of the domains whose services the demo mocks.
Object.keys(data.component).forEach((domain) => {
if (!("entity_component" in data.component[domain])) {
const {
entity_component,
services,
triggers,
conditions,
selector,
title,
} = data.component[domain];
if (
!entity_component &&
!triggers &&
!conditions &&
!SERVICE_DOMAINS.includes(domain)
) {
return;
}
output.component[domain] = { entity_component: {} };
Object.keys(data.component[domain].entity_component).forEach((key) => {
output.component[domain].entity_component[key] =
data.component[domain].entity_component[key];
});
output.component[domain] = {
entity_component,
services: SERVICE_DOMAINS.includes(domain) ? services : undefined,
triggers,
conditions,
selector,
title,
};
});
return output;
}
@@ -155,8 +175,9 @@ gulp.task("fetch-lokalise", async function () {
fs.mkdir(inDirBackend, { recursive: true }),
]);
// The backend project only provides entity_component translations, which are
// merged into the demo, gallery, cast and e2e builds. The shipped app fetches
// The backend project only provides the translations the mocked backend
// needs (see convertBackendTranslations), which are merged into the demo,
// gallery, cast and e2e builds. The shipped app fetches
// them live from core, so builds that only produce the app (release, release
// landing-page) can skip this second, whole-project export to save time.
const projects = Object.entries(lokaliseProjects).filter(
@@ -1,7 +1,6 @@
// Task to download the latest Lokalise translations from the nightly workflow artifacts
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest";
import { deleteAsync } from "del";
import { mkdir, readFile, writeFile } from "fs/promises";
@@ -10,11 +9,20 @@ import jszip from "jszip";
import path from "path";
import process from "process";
import { extract } from "tar";
import {
RetryableError,
withRetry,
} from "../../.github/scripts/github-retry.mts";
type Artifact = Awaited<
ReturnType<Octokit["rest"]["actions"]["listArtifactsForRepo"]>
>["data"]["artifacts"][number];
const MAX_AGE = 24; // hours
const OWNER = "home-assistant";
const REPO = "frontend";
const WORKFLOW_NAME = "nightly.yaml";
const BRANCH = "dev";
const ARTIFACT_NAME = "translations";
const CLIENT_ID = "Iv1.3914e28cb27834d1";
const EXTRACT_DIR = "translations";
@@ -36,11 +44,16 @@ gulp.task("fetch-nightly-translations", async function () {
// Read current translations artifact info if it exists,
// and stop if they are not old enough
let currentArtifact;
let currentArtifact: Artifact | null;
try {
currentArtifact = JSON.parse(await readFile(ARTIFACT_FILE, "utf-8"));
const artifact: Artifact = JSON.parse(
await readFile(ARTIFACT_FILE, "utf-8")
);
currentArtifact = artifact;
const currentAge =
(Date.now() - Date.parse(currentArtifact.created_at)) / 3600000;
(Date.now() - Date.parse(artifact.created_at ?? "")) / 3600000;
if (currentAge < MAX_AGE) {
console.log(
"Keeping current translations (only %s hours old)",
@@ -63,19 +76,19 @@ gulp.task("fetch-nightly-translations", async function () {
}
console.warn(
"Failed to fetch nightly translations, continuing with English only:",
err?.message || err
err instanceof Error ? err.message : err
);
}
});
async function fetchTranslations(currentArtifact) {
async function fetchTranslations(currentArtifact: Artifact | null) {
// To store file writing promises
const createExtractDir = mkdir(EXTRACT_DIR, { recursive: true });
const writings = [];
const writings: Promise<void>[] = [];
// Authenticate to GitHub using GitHub action token if it exists,
// otherwise look for a saved user token or generate a new one if none
let tokenAuth;
let tokenAuth: { token: string };
if (process.env.GITHUB_TOKEN) {
tokenAuth = { token: process.env.GITHUB_TOKEN };
} else {
@@ -104,69 +117,86 @@ async function fetchTranslations(currentArtifact) {
});
tokenAuth = await auth({ type: "oauth" });
writings.push(
createExtractDir.then(
createExtractDir.then(() =>
writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2))
)
);
}
}
// Authenticate with token and request workflow runs from GitHub
console.log("Fetching new translations...");
const octokit = new (Octokit.plugin(retry))({
// Authenticate with token and find the newest translations artifact
const octokit = new Octokit({
userAgent: "Fetch Nightly Translations",
auth: tokenAuth.token,
});
const workflowRunsResponse = await octokit.rest.actions.listWorkflowRuns({
owner: OWNER,
repo: REPO,
workflow_id: WORKFLOW_NAME,
status: "success",
event: "schedule",
per_page: 1,
exclude_pull_requests: true,
});
if (workflowRunsResponse.data.total_count === 0) {
throw Error("No successful nightly workflow runs found");
}
const latestNightlyRun = workflowRunsResponse.data.workflow_runs[0];
// Only the nightly workflow uploads this artifact. Requiring a run from this
// repository's own branch excludes artifacts uploaded by fork pull requests.
const latestArtifact = await withRetry(
"translations artifact lookup",
async () => {
const { data } = await octokit.rest.actions.listArtifactsForRepo({
owner: OWNER,
repo: REPO,
name: ARTIFACT_NAME,
per_page: 10,
});
// Stop if current is already the latest, otherwise Find the translations artifact
if (currentArtifact?.workflow_run.id === latestNightlyRun.id) {
const artifact = data.artifacts.find(
({ expired, workflow_run: run }) =>
!expired &&
run?.head_branch === BRANCH &&
run.head_repository_id === run.repository_id
);
if (!artifact) {
throw new RetryableError(
`No ${ARTIFACT_NAME} artifact found from ${BRANCH}`
);
}
return artifact;
}
);
console.log(
"Latest translations artifact is %s from workflow run %s (%s)",
latestArtifact.id,
latestArtifact.workflow_run?.id,
latestArtifact.created_at
);
// Stop if current is already the latest
if (currentArtifact?.id === latestArtifact.id) {
console.log("Stopping because current translations are still the latest");
return;
}
const latestArtifact = (
await octokit.actions.listWorkflowRunArtifacts({
owner: OWNER,
repo: REPO,
run_id: latestNightlyRun.id,
})
).data.artifacts.find((artifact) => artifact.name === ARTIFACT_NAME);
if (!latestArtifact) {
throw Error("Latest nightly workflow run has no translations artifact");
}
// Remove the current translations
const deleteCurrent = Promise.all(writings).then(
const deleteCurrent = Promise.all(writings).then(() =>
deleteAsync([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
);
// Get the download URL and follow the redirect to download (stored as ArrayBuffer)
const downloadResponse = await octokit.actions.downloadArtifact({
owner: OWNER,
repo: REPO,
artifact_id: latestArtifact.id,
archive_format: "zip",
});
if (downloadResponse.status !== 200) {
const downloadResponse = await withRetry("translations download", () =>
octokit.rest.actions.downloadArtifact({
owner: OWNER,
repo: REPO,
artifact_id: latestArtifact.id,
archive_format: "zip",
})
);
// Octokit types the redirect, but fetch follows it to the archive
if ((downloadResponse.status as number) !== 200) {
throw Error("Failure downloading translations artifact");
}
// Artifact is a tarball, but GitHub adds it to a zip file
console.log("Unpacking downloaded translations...");
const zip = await jszip.loadAsync(downloadResponse.data);
const zip = await jszip.loadAsync(downloadResponse.data as ArrayBuffer);
await deleteCurrent;
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract());
await new Promise((resolve, reject) => {
+22 -7
View File
@@ -2,10 +2,14 @@
import fs from "fs-extra";
import gulp from "gulp";
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import path from "path";
import paths from "../paths.cjs";
import { ensureMapAssets, mapAssetsDir } from "./map-assets.js";
const require = createRequire(import.meta.url);
const npmPath = (...parts) =>
path.resolve(paths.root_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(paths.root_dir, ...parts);
@@ -77,19 +81,30 @@ async function copyMapPanel(staticDir) {
// Style, glyphs and sprites for the vector base map
await ensureMapAssets();
fs.copySync(mapAssetsDir, staticPath("map/"));
copyFileDir(
npmPath("@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js"),
staticPath("map/")
);
// Controls and popups of the native MapLibre engine
copyFileDir(npmPath("maplibre-gl/dist/maplibre-gl.css"), staticPath("map/"));
}
function copyZXingWasm(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
staticPath("js")
// Resolve through barcode-detector, the wasm must match the zxing-wasm JS it bundles
const zxingWasm = createRequire(require.resolve("barcode-detector")).resolve(
"zxing-wasm/reader/zxing_reader.wasm"
);
const {
ZXING_WASM_SHA256,
ZXING_WASM_VERSION,
} = require("barcode-detector/pure");
const wasm = fs.readFileSync(zxingWasm);
if (createHash("sha256").update(wasm).digest("hex") !== ZXING_WASM_SHA256) {
throw new Error(
`${zxingWasm} does not match the zxing-wasm ${ZXING_WASM_VERSION} bundled in barcode-detector`
);
}
// Versioned file name, see locateFile in ha-qr-scanner
fs.outputFileSync(
staticPath("js", `zxing_reader-${ZXING_WASM_VERSION}.wasm`),
wasm
);
}
+422
View File
@@ -0,0 +1,422 @@
// Generates the service, trigger and condition descriptions the demo and
// gallery serve instead of a backend, from the files in Home Assistant Core.
import gulp from "gulp";
import { CORE_SCHEMA, load, mergeTag } from "js-yaml";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import process from "node:process";
import prettier from "prettier";
import paths from "../paths.cjs";
const CORE_URL =
process.env.CORE_COMPONENTS_URL ||
"https://raw.githubusercontent.com/home-assistant/core/refs/heads/dev/homeassistant/components";
// Integrations whose services the mocked backend provides
export const SERVICE_DOMAINS = [
"alarm_control_panel",
"automation",
"button",
"calendar",
"camera",
"climate",
"conversation",
"counter",
"cover",
"device_tracker",
"fan",
"frontend",
"group",
"homeassistant",
"humidifier",
"image_processing",
"input_boolean",
"input_button",
"input_datetime",
"input_number",
"input_select",
"input_text",
"lawn_mower",
"light",
"lock",
"logger",
"media_player",
"notify",
"number",
"persistent_notification",
"person",
"recorder",
"remote",
"scene",
"schedule",
"script",
"select",
"siren",
"switch",
"system_log",
"text",
"timer",
"todo",
"tts",
"update",
"vacuum",
"valve",
"water_heater",
"weather",
"zone",
];
// Services that return data. Core sets this when it registers a service, it is
// not part of services.yaml. The value is whether the response is optional.
const RESPONSE_SERVICES = {
"calendar.get_events": false,
"conversation.process": true,
"media_player.browse_media": false,
"media_player.search_media": false,
"recorder.get_statistics": false,
"schedule.get_schedule": false,
"todo.get_items": false,
"weather.get_forecasts": false,
};
// Services that are still in services.yaml, but that core no longer registers
const UNREGISTERED_SERVICES = ["weather.get_forecast"];
// Integrations that provide the new style triggers and conditions
const AUTOMATION_PLATFORM_DOMAINS = [
"air_quality",
"alarm_control_panel",
"assist_satellite",
"battery",
"button",
"calendar",
"climate",
"counter",
"cover",
"door",
"doorbell",
"event",
"fan",
"garage_door",
"gate",
"humidifier",
"humidity",
"illuminance",
"lawn_mower",
"light",
"lock",
"media_player",
"moisture",
"moon",
"motion",
"occupancy",
"power",
"remote",
"scene",
"schedule",
"select",
"siren",
"sun",
"switch",
"temperature",
"text",
"timer",
"todo",
"update",
"vacuum",
"valve",
"vibration",
"water_heater",
"window",
"zone",
];
// YAML 1.2 with merge keys, which matches how core reads these files
const YAML_SCHEMA = CORE_SCHEMA.withTags([mergeTag]);
const HEADER = `// This file is auto-generated from Home Assistant Core. Do not edit by hand.
// Regenerate with \`script/gen_demo_core_data\`.`;
const fetchCoreFile = async (domain, file) => {
const url = `${CORE_URL}/${domain}/${file}`;
const response = await fetch(url);
if (response.status === 404) {
return undefined;
}
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
return response.text();
};
// Fetches a file of every domain, keyed by domain
const fetchCoreFiles = async (domains, file) =>
Object.fromEntries(
await Promise.all(
domains.map(async (domain) => [domain, await fetchCoreFile(domain, file)])
)
);
const parseYaml = (content) => load(content, { schema: YAML_SCHEMA }) ?? {};
// Enum references look like "cover.CoverEntityFeature.OPEN"
const ENUM_REFERENCE = /^([a-z_]+)\.[A-Z]\w*\.[A-Z0-9_]+$/;
const enumDomains = (value, domains = new Set()) => {
if (typeof value === "string") {
const match = value.match(ENUM_REFERENCE);
if (match) {
domains.add(match[1]);
}
} else if (value && typeof value === "object") {
Object.values(value).forEach((val) => enumDomains(val, domains));
}
return domains;
};
// Resolves "<domain>.<Enum>.<MEMBER>" to the value of the member, like core
const enumResolver = async (yamls) => {
const domains = [...enumDomains(yamls)];
const [consts, inits] = await Promise.all([
fetchCoreFiles(domains, "const.py"),
fetchCoreFiles(domains, "__init__.py"),
]);
return (reference) => {
const [domain, enumName, member] = reference.split(".");
for (const source of [consts[domain], inits[domain]]) {
const match = source
?.split(`class ${enumName}(`)[1]
?.match(new RegExp(`\\n\\s+${member} = ("[^"]*"|\\d+)`));
if (match) {
return JSON.parse(match[1]);
}
}
throw new Error(`Unknown enum ${reference}`);
};
};
const createResolvers = (enumValue) => {
// In entity filters, every item of the supported features, a feature or a
// list of features, becomes one bit mask
const featureMask = (features) =>
[].concat(features).reduce(
// eslint-disable-next-line no-bitwise
(mask, feature) => mask | enumValue(feature),
0
);
const resolveSelector = (value) => {
if (Array.isArray(value)) {
return value.map(resolveSelector);
}
if (!value || typeof value !== "object") {
return value;
}
return Object.fromEntries(
Object.entries(value).map(([key, val]) => [
key,
key === "supported_features"
? [].concat(val).map(featureMask)
: resolveSelector(val),
])
);
};
// Like core, an empty target allows any target
const resolveTarget = (target) => resolveSelector(target ?? {});
// Field filters keep their lists, with every enum resolved on its own
const resolveField = (field) => {
const result = { ...field };
if (field.selector) {
result.selector = resolveSelector(field.selector);
}
if (field.filter?.supported_features) {
result.filter = {
...result.filter,
supported_features: field.filter.supported_features.map(enumValue),
};
}
if (field.filter?.attribute) {
result.filter = {
...result.filter,
attribute: Object.fromEntries(
Object.entries(field.filter.attribute).map(([name, options]) => [
name,
options.map(enumValue),
])
),
};
}
return result;
};
// Fields can be grouped in sections, which hold fields themselves
const resolveFields = (fields) =>
Object.fromEntries(
Object.entries(fields ?? {}).map(([key, field]) => [
key,
field && "fields" in field && !("selector" in field)
? { ...field, fields: resolveFields(field.fields) }
: resolveField(field ?? {}),
])
);
return { resolveFields, resolveTarget };
};
// Mirrors how core builds the response of get_services
const serviceDescriptions = (yamls, { resolveFields, resolveTarget }) =>
Object.fromEntries(
SERVICE_DOMAINS.map((domain) => {
if (!yamls[domain]) {
throw new Error(`No services.yaml found for ${domain}`);
}
const services = Object.entries(yamls[domain])
.filter(
([service]) =>
!service.startsWith(".") &&
!UNREGISTERED_SERVICES.includes(`${domain}.${service}`)
)
.map(([service, value]) => {
const description = { fields: resolveFields(value?.fields) };
for (const item of ["description", "name"]) {
if (value && item in value) {
description[item] = value[item];
}
}
if (value && "target" in value) {
description.target = resolveTarget(value.target);
}
const key = `${domain}.${service}`;
if (key in RESPONSE_SERVICES) {
description.response = { optional: RESPONSE_SERVICES[key] };
}
return [service, description];
});
return [domain, Object.fromEntries(services)];
})
);
// Mirrors how core builds the trigger and condition platform descriptions
const platformDescriptions = (yamls, { resolveFields, resolveTarget }) =>
Object.fromEntries(
AUTOMATION_PLATFORM_DOMAINS.flatMap((domain) =>
Object.entries(yamls[domain] ?? {})
// Keys starting with a dot only hold YAML anchors
.filter(([key, value]) => !key.startsWith(".") && value)
.map(([key, value]) => {
const absoluteKey = !key.startsWith("_")
? `${domain}.${key}`
: key.slice(1) || domain;
const description = { fields: resolveFields(value.fields) };
if ("target" in value) {
description.target = resolveTarget(value.target);
}
return [absoluteKey, description];
})
)
);
// The trigger or condition icons of every domain that has them
const platformIcons = (icons, category) =>
Object.fromEntries(
Object.entries(icons)
.map(([domain, content]) => [
domain,
content ? JSON.parse(content)[category] : undefined,
])
.filter(([, categoryIcons]) => categoryIcons)
);
const parseYamls = (files) =>
Object.fromEntries(
Object.entries(files)
.filter(([, content]) => content !== undefined)
.map(([domain, content]) => [domain, parseYaml(content)])
);
const writeTs = async (path, content) => {
const file = join(paths.root_dir, path);
const options = await prettier.resolveConfig(file);
await writeFile(
file,
await prettier.format(`${HEADER}\n${content}`, {
...options,
filepath: file,
})
);
};
gulp.task("gen-demo-core-data", async () => {
const [servicesFiles, triggersFiles, conditionsFiles, iconsFiles] =
await Promise.all([
fetchCoreFiles(SERVICE_DOMAINS, "services.yaml"),
fetchCoreFiles(AUTOMATION_PLATFORM_DOMAINS, "triggers.yaml"),
fetchCoreFiles(AUTOMATION_PLATFORM_DOMAINS, "conditions.yaml"),
fetchCoreFiles(AUTOMATION_PLATFORM_DOMAINS, "icons.json"),
]);
const servicesYamls = parseYamls(servicesFiles);
const triggersYamls = parseYamls(triggersFiles);
const conditionsYamls = parseYamls(conditionsFiles);
const resolvers = createResolvers(
await enumResolver([servicesYamls, triggersYamls, conditionsYamls])
);
for (const key of UNREGISTERED_SERVICES) {
const [domain, service] = key.split(".");
if (!(service in (servicesYamls[domain] ?? {}))) {
throw new Error(`Remove ${key} from UNREGISTERED_SERVICES, it is gone`);
}
}
const services = serviceDescriptions(servicesYamls, resolvers);
for (const key of Object.keys(RESPONSE_SERVICES)) {
const [domain, service] = key.split(".");
if (!services[domain]?.[service]) {
throw new Error(`Service ${key} that returns a response does not exist`);
}
}
const triggers = platformDescriptions(triggersYamls, resolvers);
const conditions = platformDescriptions(conditionsYamls, resolvers);
const icons = {
triggers: platformIcons(iconsFiles, "triggers"),
conditions: platformIcons(iconsFiles, "conditions"),
};
await Promise.all([
writeTs(
"src/fake_data/demo_services.ts",
`import type { HassServices } from "home-assistant-js-websocket";
// Core sends more than the HassServices type covers, like sections and list
// examples.
export const demoServices = ${JSON.stringify(services)} as unknown as HassServices;
`
),
writeTs(
"demo/src/stubs/automation_platform_domains.ts",
`
// Integrations that provide the new style triggers and conditions. Their
// translations are part of the demo build.
export const automationPlatformDomains = ${JSON.stringify(AUTOMATION_PLATFORM_DOMAINS)};
`
),
writeTs(
"demo/src/stubs/automation_platforms_data.ts",
`import type { ConditionDescriptions } from "../../../src/data/condition";
import type { TriggerDescriptions } from "../../../src/data/trigger";
export const triggerDescriptions: TriggerDescriptions = ${JSON.stringify(triggers)};
export const conditionDescriptions: ConditionDescriptions = ${JSON.stringify(conditions)};
`
),
writeTs(
"demo/src/stubs/automation_platforms_icons.ts",
`
export const triggerIcons = ${JSON.stringify(icons.triggers)};
export const conditionIcons = ${JSON.stringify(icons.conditions)};
`
),
]);
});
@@ -69,7 +69,7 @@ export const SENSOR_STATE_CLASSES: string[] = [
${stateClasses.map((stateClass) => ` "${stateClass}",`).join("\n")}
];
export const SENSOR_STATE_CLASS_UNITS: Record<string, string[]> = {
export const SENSOR_STATE_CLASS_UNITS: Record<string, (string | null)[]> = {
${Object.entries(stateClassUnits)
.map(
([stateClass, units]) =>
+2 -1
View File
@@ -6,9 +6,10 @@ import "./demo.js";
import "./download-translations.js";
import "./e2e-test-app.js";
import "./entry-html.js";
import "./fetch-nightly-translations.js";
import "./fetch-nightly-translations.mts";
import "./gallery.js";
import "./gather-static.js";
import "./gen-demo-core-data.js";
import "./gen-device-classes.js";
import "./gen-icons-json.js";
import "./gen-sensor-entity-constants.js";
+1 -1
View File
@@ -29,7 +29,7 @@ const LICENSE_OVERRIDES = [
// type-fest ships two license files (MIT for code, CC0 for types).
// We use the MIT license since that covers the bundled code.
packageName: "type-fest",
version: "5.8.0",
version: "5.10.0",
licenseFile: "license-mit",
},
];
+3 -1
View File
@@ -153,7 +153,9 @@ gulp.task("rspack-watch-app", () => {
: createAppConfig({ isProdBuild: false, latestBuild: true })
).watch({ poll: isWsl }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
// Watch the folder via a glob; git replaces en.json on checkout/stash,
// which can permanently drop a watch on the file itself
path.join(paths.translations_src, "*.json"),
gulp.series("build-translations", "copy-translations-app")
);
});
+1 -1
View File
@@ -10,7 +10,7 @@ import { finished } from "node:stream/promises";
import env from "../env.cjs";
import paths from "../paths.cjs";
import { mergeTranslations } from "./merge-translations.js";
import "./fetch-nightly-translations.js";
import "./fetch-nightly-translations.mts";
const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend";
+45 -6
View File
@@ -3,6 +3,7 @@ const fs = require("fs");
const { existsSync } = fs;
const path = require("path");
const rspack = require("@rspack/core");
const browserslist = require("browserslist");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -40,6 +41,21 @@ const TOOLCHAIN_PACKAGES = [
"caniuse-lite",
];
// Not direct dependencies: read the copy the package that loads them uses.
const TOOLCHAIN_PACKAGE_PARENTS = {
terser: "terser-webpack-plugin",
"caniuse-lite": "browserslist",
};
const toolchainVersion = (pkg) => {
const parent = TOOLCHAIN_PACKAGE_PARENTS[pkg];
const lookup = parent && [
path.dirname(require.resolve(`${parent}/package.json`)),
];
return require(require.resolve(`${pkg}/package.json`, { paths: lookup }))
.version;
};
// Our own build logic — the config, loaders and babel plugins. Their contents
// (not their paths) go into the cache version, so a change invalidates the
// cache the same way `buildDependencies` would, but without tying validity to
@@ -63,9 +79,7 @@ const PATCHES_DIR = path.join(paths.root_dir, "patches");
// stays valid when reused on a different machine or checkout path.
const cacheVersion = () => {
const parts = [
...TOOLCHAIN_PACKAGES.map(
(pkg) => `${pkg}@${require(`${pkg}/package.json`).version}`
),
...TOOLCHAIN_PACKAGES.map((pkg) => `${pkg}@${toolchainVersion(pkg)}`),
...CONFIG_FILES.map(
(file) => `${path.basename(file)}:${fs.readFileSync(file, "utf8")}`
),
@@ -115,7 +129,10 @@ const createRspackConfig = ({
dontHash = new Set();
}
const ignorePackages = bundle.ignorePackages({ latestBuild });
const litHtmlRoot = path.resolve(__dirname, "../node_modules/lit-html");
// pnpm links node_modules/lit-html into its store; rspack compares real paths
const litHtmlRoot = fs.realpathSync(
path.resolve(__dirname, "../node_modules/lit-html")
);
const litHtmlDevelopmentRoot = path.join(litHtmlRoot, "development");
const litDisableDevModeLoader = path.join(
__dirname,
@@ -212,6 +229,22 @@ const createRspackConfig = ({
{
test: /\.css$/,
type: "asset/source",
// Imported stylesheets skip the minifier that compiles css``
// templates, so legacy builds compile logical properties and :is()
// here
use: latestBuild
? []
: [
{
loader: "builtin:lightningcss-loader",
options: {
targets: browserslist.loadConfig({
path: paths.root_dir,
env: "legacy",
}),
},
},
],
},
],
},
@@ -377,6 +410,14 @@ const createRspackConfig = ({
resolve: {
extensions: [".ts", ".js", ".json"],
alias: {
// @formatjs/bigdecimal uses BigInt (Safari 14, Chrome 67), which
// breaks the NumberFormat and PluralRules polyfills
...(!latestBuild && {
"@formatjs/bigdecimal$": path.join(
paths.root_dir,
"src/resources/polyfills/bigdecimal.ts"
),
}),
"lit/static-html$": "lit/static-html.js",
"lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js",
@@ -393,8 +434,6 @@ const createRspackConfig = ({
"lit/directives/keyed$": "lit/directives/keyed.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
"@lit-labs/observers/resize-controller":
"@lit-labs/observers/resize-controller.js",
"@formatjs/intl-durationformat/should-polyfill$":
+12 -17
View File
@@ -4,23 +4,18 @@ const castContext = framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
// type definition is wrong, should be "FMP4" instead of "fmp4"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
media.hlsVideoSegmentFormat =
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
playerManager.setMessageInterceptor("LOAD", (loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = "LIVE";
media.contentType = "application/vnd.apple.mpegurl";
// @ts-expect-error type definition is wrong, should be "FMP4" instead of "fmp4"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
media.hlsVideoSegmentFormat = "FMP4";
}
);
return loadRequestData;
});
castContext.start();
+1 -1
View File
@@ -4,7 +4,7 @@ import { castContext } from "../cast_context";
export const castDemoLovelace: () => LovelaceConfig = () => {
const touchSupported =
castContext.getDeviceCapabilities().touch_input_supported;
castContext.getDeviceCapabilities()?.touch_input_supported;
return {
views: [
{
+34 -47
View File
@@ -19,7 +19,7 @@ const playerStylesAdded = false;
let controls: HTMLElement | null;
const setTouchControlsVisibility = (visible: boolean) => {
if (!castContext.getDeviceCapabilities().touch_input_supported) {
if (!castContext.getDeviceCapabilities()?.touch_input_supported) {
return;
}
controls =
@@ -39,8 +39,7 @@ const playDummyMedia = (viewTitle?: string) => {
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType =
"NONE" as framework.messages.StreamType.NONE;
loadRequestData.media.streamType = "NONE";
const metadata = new framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -51,7 +50,7 @@ const playDummyMedia = (viewTitle?: string) => {
clearTimeout(timeOut);
timeOut = undefined;
}
if (castContext.getDeviceCapabilities().touch_input_supported) {
if (castContext.getDeviceCapabilities()?.touch_input_supported) {
timeOut = window.setTimeout(() => playDummyMedia(viewTitle), 540000); // repeat every 9 minutes to keep it active (gets deactivated after 10 minutes)
}
};
@@ -91,15 +90,12 @@ options.disableIdleTimeout = true;
options.customNamespaces = {
// type definition is wrong, should be "JSON" instead of "json"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
[CAST_NS]: "JSON",
};
castContext.addCustomMessageListener(CAST_NS, (ev) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !==
("IDLE" as framework.messages.PlayerState.IDLE)
) {
if (playerManager.getPlayerState() !== "IDLE") {
playerManager.stop();
} else {
showLovelaceController();
@@ -111,46 +107,37 @@ castContext.addCustomMessageListener(CAST_NS, (ev) => {
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
"https://cast.home-assistant.io/images/google-nest-hub.png"
) {
return loadRequestData;
}
// We received a play media command, hide Lovelace and show media player
showMediaPlayer();
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// type definition is wrong, should be "FMP4" instead of "fmp4"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
media.hlsVideoSegmentFormat =
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
playerManager.setMessageInterceptor("LOAD", (loadRequestData) => {
if (
loadRequestData.media.contentId ===
"https://cast.home-assistant.io/images/google-nest-hub.png"
) {
return loadRequestData;
}
);
playerManager.addEventListener(
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState ===
("IDLE" as framework.messages.PlayerState.IDLE) &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !==
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();
}
// We received a play media command, hide Lovelace and show media player
showMediaPlayer();
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = "LIVE";
media.contentType = "application/vnd.apple.mpegurl";
// @ts-expect-error type definition is wrong, should be "FMP4" instead of "fmp4"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
media.hlsVideoSegmentFormat = "FMP4";
}
);
return loadRequestData;
});
playerManager.addEventListener("MEDIA_STATUS", (event) => {
if (
event.mediaStatus?.playerState === "IDLE" &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !== "INTERRUPTED"
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();
}
});
castContext.start(options);
+2 -2
View File
@@ -27,6 +27,7 @@ import type {
LovelaceConfig,
LovelaceDashboardStrategyConfig,
} from "../../../../src/data/lovelace/config/types";
import { normalizeLovelaceConfig } from "../../../../src/data/lovelace/config/normalize";
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
import { fetchResources } from "../../../../src/data/lovelace/resource";
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
@@ -34,7 +35,6 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
@@ -359,7 +359,7 @@ export class HcMain extends HassElement {
this._urlPath || "lovelace"
);
castContext.setApplicationState(title || "");
this._lovelaceConfig = checkLovelaceConfig(
this._lovelaceConfig = normalizeLovelaceConfig(
lovelaceConfig
) as LovelaceConfig;
}
+96 -2
View File
@@ -1,12 +1,16 @@
import { navigate } from "../../../src/common/navigate";
import { slugify } from "../../../src/common/string/slugify";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import type { Lovelace } from "../../../src/panels/lovelace/types";
import { setDemoAreas } from "../stubs/area_registry";
import { setPlatformEntityRegistryEntries } from "../stubs/entity_registry";
import { energyEntities } from "../stubs/entities";
import { connectivityEntities } from "../stubs/connectivity/fixtures";
import { setDemoFloors } from "../stubs/floor_registry";
import { getDemoTheme } from "../stubs/frontend";
import type { DemoConfig, DemoTheme } from "./types";
import type { EntityInput } from "../../../src/fake_data/entities/types";
import type { DemoAutomation, DemoConfig, DemoTheme } from "./types";
export const applyDemoTheme = (hass: MockHomeAssistant, theme: DemoTheme) => {
if (typeof theme === "function") {
@@ -16,6 +20,95 @@ export const applyDemoTheme = (hass: MockHomeAssistant, theme: DemoTheme) => {
hass.mockTheme(null, getDemoTheme(theme));
};
export const automationEntity = ({
config: automation,
entityId,
state = "on",
icon,
lastTriggered,
}: DemoAutomation): EntityInput => ({
entity_id: entityId ?? `automation.${slugify(automation.alias)}`,
platform: "automation",
state,
attributes: {
id: automation.id,
friendly_name: automation.alias,
last_triggered:
lastTriggered === undefined
? null
: new Date(Date.now() - lastTriggered * 60000).toISOString(),
mode: automation.mode ?? "single",
current: 0,
// Like core, only the modes that can run more than once have a maximum
...((automation.mode === "queued" || automation.mode === "parallel") && {
max: automation.max ?? 10,
}),
icon,
},
});
// Automations saved in the editor during this session, per demo
// Automations saved in the editor during this session, per demo. A deleted
// automation is null.
const savedAutomations: Record<
string,
Record<string, DemoAutomation | null>
> = {};
export const saveDemoAutomation = (
id: string,
automation: DemoAutomation | null
) => {
savedAutomations[selectedDemo] = {
...savedAutomations[selectedDemo],
[id]: automation,
};
};
// The automations of a demo config, with the ones saved in the editor
export const demoAutomations = (
demo: string,
config: DemoConfig
): DemoAutomation[] => {
const saved = savedAutomations[demo] ?? {};
const configured = config.automations ?? [];
const ids = new Set(configured.map((automation) => automation.config.id));
return [
...configured.map((automation) =>
automation.config.id in saved ? saved[automation.config.id] : automation
),
...Object.values(saved).filter(
(automation) => automation && !ids.has(automation.config.id)
),
].filter((automation): automation is DemoAutomation => automation !== null);
};
// The automation entities are derived from the configs the editor shows, so
// their names and IDs always match.
export const demoConfigEntities = (
demo: string,
config: DemoConfig,
localize: LocalizeFunc
): EntityInput[] => [
...config.entities(localize),
...demoAutomations(demo, config).map(automationEntity),
];
// Like core, the automations of a demo config are in the entity registry
export const registerDemoAutomations = (
hass: MockHomeAssistant,
demo: string,
config: DemoConfig
) =>
setPlatformEntityRegistryEntries(
hass,
"automation",
demoAutomations(demo, config).map((automation) => ({
entity_id: automationEntity(automation).entity_id,
unique_id: automation.config.id,
}))
);
export const demoConfigs: Record<string, () => Promise<DemoConfig>> = {
sections: () => import("./sections").then((mod) => mod.demoSections),
home: () => import("./home").then((mod) => mod.demoHome),
@@ -52,7 +145,8 @@ export const setDemoConfig = async (
setDemoFloors(hass, config.floors);
setDemoAreas(hass, config.areas);
hass.addEntities(config.entities(hass.localize), true);
hass.addEntities(demoConfigEntities(demo, config, hass.localize), true);
registerDemoAutomations(hass, demo, config);
hass.addEntities(energyEntities());
// Replaced the whole state map above, so the entities that do not belong to a
// demo config have to be added back.
+27 -23
View File
@@ -7,7 +7,7 @@ import type { HomeDashboardStrategyConfig } from "../../../../src/panels/lovelac
import { HomeDashboardStrategy } from "../../../../src/panels/lovelace/strategies/home/home-dashboard-strategy";
import type { HomeOverviewViewStrategyConfig } from "../../../../src/panels/lovelace/strategies/home/home-overview-view-strategy";
import { HomeOverviewViewStrategy } from "../../../../src/panels/lovelace/strategies/home/home-overview-view-strategy";
import { generateLovelaceSectionStrategy } from "../../../../src/panels/lovelace/strategies/get-strategy";
import type { HeadingCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { HomeAssistant } from "../../../../src/types";
export interface DemoHomeDashboardStrategyConfig extends Omit<
@@ -61,29 +61,33 @@ class DemoHomeOverviewViewStrategy extends ReactiveElement {
{ ...config, type: "home-overview" },
hass
);
// Expand the favorites section so the demo card can be added to it
const sections = await Promise.all(
(view.sections || []).map(async (section) => {
if (
!isStrategySection(section) ||
section.strategy.type !== "common-controls"
) {
return section;
}
// The demo card takes up the space of two tiles
const limit = (section.strategy.limit as number | undefined) ?? 8;
const favorites = await generateLovelaceSectionStrategy(
{ ...section, strategy: { ...section.strategy, limit: limit - 2 } },
hass
);
const [heading, ...cards] = favorites.cards || [];
return {
...favorites,
// Place the demo card first so the tiles fill the rows next to it
cards: [heading, { type: "custom:ha-demo-next-card" }, ...cards],
};
})
// Add the demo card to the favorites section, found by its heading
const favoritesHeading = hass.localize(
"ui.panel.lovelace.strategy.home.favorites"
);
const sections = (view.sections || []).map((section) => {
if (
isStrategySection(section) ||
!section.cards?.some(
(card) =>
card.type === "heading" &&
(card as HeadingCardConfig).heading === favoritesHeading
)
) {
return section;
}
const [heading, ...cards] = section.cards;
return {
...section,
// Place the demo card first so the tiles fill the rows next to it. It
// takes up the space of two tiles, so keep two less, but at least six.
cards: [
heading,
{ type: "custom:ha-demo-next-card" },
...cards.slice(0, Math.max(6, cards.length - 2)),
],
};
});
return {
...view,
+361
View File
@@ -0,0 +1,361 @@
import type { DemoAutomation } from "../types";
export const demoAutomationsSections: DemoAutomation[] = [
{
lastTriggered: 60 * 9,
state: "off",
icon: "mdi:auto-mode",
config: {
id: "1700669321947",
alias: "Home Assistant Auto-update",
description:
"Installs Home Assistant updates at night, so nobody is bothered by the restart.",
triggers: [{ trigger: "time", at: "03:00:00" }],
conditions: [
{
condition: "update.is_available",
target: { entity_id: "update.home_assistant_core_update" },
options: { for: "00:00:00" },
},
],
actions: [
{
action: "update.install",
target: { entity_id: "update.home_assistant_core_update" },
data: { backup: true },
},
],
mode: "queued",
},
},
{
lastTriggered: 25,
config: {
id: "1700669322001",
alias: "Kitchen lights on motion",
description:
"Turns on the kitchen lights when motion is detected and turns them off again once the kitchen has been empty for 2 minutes.",
triggers: [
{
trigger: "motion.detected",
target: { entity_id: "binary_sensor.kitchen_motion" },
options: { for: "00:00:00" },
},
],
conditions: [],
actions: [
{
action: "light.turn_on",
target: {
entity_id: ["light.kitchen_spotlights", "light.worktop_spotlights"],
},
data: { brightness_pct: 80 },
},
{
wait_for_trigger: [
{
trigger: "motion.cleared",
target: { entity_id: "binary_sensor.kitchen_motion" },
options: { for: "00:02:00" },
},
],
},
{
action: "light.turn_off",
target: {
entity_id: ["light.kitchen_spotlights", "light.worktop_spotlights"],
},
},
],
mode: "restart",
},
},
{
lastTriggered: 60 * 3,
config: {
id: "1700669322002",
alias: "Outdoor light from sunset to sunrise",
description:
"Uses trigger IDs to handle both sunset and sunrise in one automation.",
triggers: [
{
trigger: "sun.sunset",
id: "sunset",
options: {
offset: { days: 0, hours: 0, minutes: 15, seconds: 0 },
offset_type: "before",
},
},
{
trigger: "sun.sunrise",
id: "sunrise",
options: {
offset: { days: 0, hours: 0, minutes: 0, seconds: 0 },
offset_type: "before",
},
},
],
conditions: [],
actions: [
{
choose: [
{
conditions: [{ condition: "trigger", id: "sunset" }],
sequence: [
{
action: "light.turn_on",
target: { entity_id: "light.outdoor_light" },
},
],
},
{
conditions: [{ condition: "trigger", id: "sunrise" }],
sequence: [
{
action: "light.turn_off",
target: { entity_id: "light.outdoor_light" },
},
],
},
],
},
],
mode: "single",
},
},
{
lastTriggered: 60 * 14,
config: {
id: "1700669322003",
alias: "Flood light on motion when dark",
description:
"Turns on the flood light for 5 minutes when someone walks by in the dark.",
triggers: [
{
trigger: "motion.detected",
target: { entity_id: "binary_sensor.outdoor_motion_sensor_motion" },
options: { for: "00:00:00" },
},
],
conditions: [
{
condition: "illuminance.is_value",
target: { entity_id: "sensor.outdoor_motion_sensor_illuminance" },
options: {
threshold: {
type: "below",
value: { number: 50, unit_of_measurement: "lx" },
},
for: "00:00:00",
},
},
],
actions: [
{
action: "light.turn_on",
target: { entity_id: "light.flood_light" },
},
{ delay: { hours: 0, minutes: 5, seconds: 0, milliseconds: 0 } },
{
action: "light.turn_off",
target: { entity_id: "light.flood_light" },
},
],
mode: "restart",
},
},
{
lastTriggered: 60 * 26,
config: {
id: "1700669322004",
alias: "Close the shutters when it gets hot",
description:
"Keeps the living room cool by closing the shutters on hot, sunny days.",
triggers: [
{
trigger: "temperature.crossed_threshold",
target: { entity_id: "sensor.outdoor_temperature" },
options: {
threshold: {
type: "above",
value: { number: 25, unit_of_measurement: "°C" },
},
for: "00:10:00",
},
},
],
conditions: [{ condition: "sun.is_up" }],
actions: [
{
action: "cover.close_cover",
target: {
entity_id: [
"cover.living_room_garden_shutter",
"cover.living_room_graveyard_shutter",
"cover.living_room_left_shutter",
"cover.living_room_right_shutter",
],
},
},
],
mode: "single",
},
},
{
lastTriggered: 60 * 50,
config: {
id: "1700669322005",
alias: "Fridge door left open",
description:
"Sends a reminder when the fridge door has been open for 2 minutes.",
triggers: [
{
trigger: "door.opened",
target: { entity_id: "binary_sensor.fridge_door" },
options: { for: "00:02:00" },
},
],
conditions: [],
actions: [
{
action: "notify.notify",
data: {
title: "Fridge door",
message: "The fridge door has been open for 2 minutes.",
},
},
],
mode: "single",
},
},
{
lastTriggered: 90,
config: {
id: "1700669322006",
alias: "Meeting mode",
description:
"Pauses the music in the study and turns on the lights when a meeting starts.",
triggers: [
{
trigger: "switch.turned_on",
target: { entity_id: "switch.in_meeting" },
options: { for: "00:00:00" },
},
],
conditions: [],
actions: [
{
action: "media_player.media_pause",
target: { entity_id: "media_player.study_nest_hub" },
},
{
action: "light.turn_on",
target: { entity_id: "light.study_spotlights" },
data: { brightness_pct: 100, color_temp_kelvin: 4000 },
},
],
mode: "single",
},
},
{
lastTriggered: 60 * 20,
config: {
id: "1700669322009",
alias: "Lights up when the music pauses",
description:
"Turns on the living room spotlights when the music is paused in the evening.",
triggers: [
{
trigger: "media_player.paused_playing",
target: { entity_id: "media_player.living_room_nest_mini" },
options: { for: "00:00:00" },
},
],
conditions: [{ condition: "sun.is_set" }],
actions: [
{
action: "light.turn_on",
target: { entity_id: "light.living_room_spotlights" },
data: { brightness_pct: 40 },
},
],
mode: "single",
},
},
{
lastTriggered: 60 * 7,
config: {
id: "1700669322007",
alias: "Lower heating when the car leaves",
triggers: [
{
trigger: "zone.left",
target: { entity_id: "device_tracker.car" },
options: { zone: "zone.home", for: "00:00:00" },
},
],
conditions: [
{
condition: "time",
weekday: ["mon", "tue", "wed", "thu", "fri"],
},
],
actions: [
{
action: "climate.set_temperature",
target: {
entity_id: ["climate.ground_floor", "climate.first_floor"],
},
data: { temperature: 17 },
},
],
mode: "single",
},
},
{
lastTriggered: 60 * 11,
config: {
id: "1700669322008",
alias: "Good night",
description:
"Turns off all lights and closes the shutters at 23:00 on weeknights.",
triggers: [
{
trigger: "time",
at: "23:00:00",
weekday: ["sun", "mon", "tue", "wed", "thu"],
},
],
conditions: [],
actions: [
{
action: "light.turn_off",
target: {
entity_id: [
"light.floor_lamp",
"light.living_room_spotlights",
"light.bar_lamp",
"light.kitchen_spotlights",
"light.worktop_spotlights",
"light.study_spotlights",
],
},
},
{
action: "cover.close_cover",
target: {
entity_id: [
"cover.living_room_garden_shutter",
"cover.living_room_graveyard_shutter",
"cover.living_room_left_shutter",
"cover.living_room_right_shutter",
"cover.kitchen_shutter",
"cover.study_shutter",
],
},
},
],
mode: "single",
},
},
];
+12 -13
View File
@@ -140,6 +140,18 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Outdoor humidity",
},
},
// Used by the automation for when the car leaves home
"zone.home": {
entity_id: "zone.home",
state: "0",
attributes: {
latitude: 52.3731339,
longitude: 4.8903147,
radius: 100,
friendly_name: "Home",
icon: "mdi:home",
},
},
"device_tracker.car": {
entity_id: "device_tracker.car",
state: "not_home",
@@ -514,19 +526,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Outdoor motion sensor Illuminance",
},
},
"automation.home_assistant_auto_update": {
entity_id: "automation.home_assistant_auto_update",
state: "off",
attributes: {
id: "1700669321947",
last_triggered: "2024-02-29T18:02:05.343139+00:00",
mode: "queued",
current: 0,
max: 50,
icon: "mdi:auto-mode",
friendly_name: "Home Assistant Auto-update",
},
},
"update.home_assistant_operating_system_update": {
entity_id: "update.home_assistant_operating_system_update",
state: "off",
+2
View File
@@ -1,4 +1,5 @@
import type { DemoConfig } from "../types";
import { demoAutomationsSections } from "./automations";
import { demoEntitiesSections } from "./entities";
import { demoLovelaceSections } from "./lovelace";
@@ -8,5 +9,6 @@ export const demoSections: DemoConfig = {
name: "Home Demo",
lovelace: demoLovelaceSections,
entities: demoEntitiesSections,
automations: demoAutomationsSections,
theme: { theme: "default", dark: false },
};
+12
View File
@@ -1,5 +1,6 @@
import type { TemplateResult } from "lit";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { ManualAutomationConfig } from "../../../src/data/automation";
import type { LovelaceRawConfig } from "../../../src/data/lovelace/config/types";
import type { EntityInput } from "../../../src/fake_data/entities/types";
import type { ThemeSettings } from "../../../src/types";
@@ -8,6 +9,16 @@ import type { DemoFloor } from "../stubs/floor_registry";
export type DemoTheme = ThemeSettings | (() => Record<string, string> | null);
export interface DemoAutomation {
config: ManualAutomationConfig & { id: string; alias: string };
/** Defaults to the alias as slug */
entityId?: string;
state?: "on" | "off";
icon?: string;
/** Minutes ago */
lastTriggered?: number;
}
export interface DemoConfig {
index?: number;
name: string;
@@ -17,6 +28,7 @@ export interface DemoConfig {
string | ((localize: LocalizeFunc) => string | TemplateResult<1>);
lovelace: (localize: LocalizeFunc) => LovelaceRawConfig;
entities: (localize: LocalizeFunc) => EntityInput[];
automations?: DemoAutomation[];
floors?: DemoFloor[];
areas?: DemoArea[];
theme: DemoTheme;
+118 -14
View File
@@ -1,11 +1,18 @@
import { customElement } from "lit/decorators";
import type { LocalizeFunc } from "../../src/common/translations/localize";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import type { MockHomeAssistant } from "../../src/fake_data/provide_hass";
import { provideHass } from "../../src/fake_data/provide_hass";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import type { HomeAssistant } from "../../src/types";
import { applyDemoTheme, selectedDemoConfig } from "./configs/demo-configs";
import {
applyDemoTheme,
demoConfigEntities,
registerDemoAutomations,
selectedDemo,
selectedDemoConfig,
} from "./configs/demo-configs";
import { mockAreaRegistry, setDemoAreas } from "./stubs/area_registry";
import {
connectivityCommands,
@@ -18,7 +25,10 @@ import { demoDevices } from "./stubs/devices";
import { mockDeviceRegistry } from "./stubs/device_registry";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import {
mockEntityRegistry,
mockEntityRegistryDisplay,
} from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFloorRegistry, setDemoFloors } from "./stubs/floor_registry";
import { mockFrontend } from "./stubs/frontend";
@@ -26,6 +36,7 @@ import { mockHardware } from "./stubs/hardware";
import { mockHassioSupervisor } from "./stubs/hassio_supervisor";
import { mockIntegration } from "./stubs/integration";
import { mockLabelRegistry } from "./stubs/label_registry";
import { automationPlatformDomains } from "./stubs/automation_platform_domains";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
@@ -58,6 +69,12 @@ const CONFIG_PANEL_COMMANDS = [
"system_health/",
"backup/",
"automation/config",
"trigger_platforms/subscribe",
"condition_platforms/subscribe",
"get_triggers_for_target",
"get_conditions_for_target",
"get_services_for_target",
"extract_from_target",
"script/config",
"config/automation/config",
"config/script/config",
@@ -67,6 +84,7 @@ const CONFIG_PANEL_COMMANDS = [
"assist_pipeline/",
"config/entity_registry/settings/",
"slugify",
"marketplace/",
...connectivityCommands,
];
@@ -88,25 +106,81 @@ export class HaDemo extends HomeAssistantAppEl {
config: {
...hass.config,
components: [
...(hass.config?.components ?? []),
"backup",
"webhook",
"usage_prediction",
"assist_pipeline",
"hassio",
"hardware",
...connectivityComponents,
// Some automation platforms are loaded already
...new Set([
...(hass.config?.components ?? []),
"backup",
"webhook",
"usage_prediction",
"assist_pipeline",
"hassio",
"hardware",
"marketplace",
...connectivityComponents,
...automationPlatformDomains,
]),
],
},
panels: {
...hass.panels,
// Opened from the settings page, not listed in the sidebar
marketplace: {
component_name: "marketplace",
icon: null,
title: null,
config: null,
url_path: "marketplace",
},
},
});
const localizePromise =
// @ts-ignore
this._loadFragmentTranslations(hass.language, "page-demo").then(
() => this.hass!.localize
// hass is not updated yet when another fragment started loading
// meanwhile, so use the localize with these translations directly
(localize?: LocalizeFunc) => localize ?? this.hass!.localize
);
mockLovelace(hass, localizePromise);
mockLovelace(hass, localizePromise, [
{
id: "music",
url_path: "music",
require_admin: false,
show_in_sidebar: true,
icon: "mdi:music",
title: "Music",
mode: "storage",
},
{
id: "cameras",
url_path: "cameras",
require_admin: false,
show_in_sidebar: true,
icon: "mdi:cctv",
title: "Cameras",
mode: "storage",
},
{
id: "garden",
url_path: "garden",
require_admin: false,
show_in_sidebar: true,
icon: "mdi:flower",
title: "Garden",
mode: "storage",
},
{
id: "lovelace-advanced",
url_path: "lovelace-advanced",
require_admin: false,
show_in_sidebar: false,
icon: "mdi:cog",
title: "Advanced",
mode: "yaml",
filename: "lovelace-advanced.yaml",
},
]);
mockAuth(hass);
mockTranslations(hass, localizePromise);
mockHistory(hass);
@@ -136,8 +210,37 @@ export class HaDemo extends HomeAssistantAppEl {
mockAreaRegistry(hass);
mockDeviceRegistry(hass, demoDevices);
mockFloorRegistry(hass);
mockLabelRegistry(hass);
mockLabelRegistry(hass, [
{
label_id: "kitchen",
name: "Kitchen",
icon: null,
color: "red",
description: "Everything in the kitchen",
created_at: 1735689600,
modified_at: 1735689600,
},
{
label_id: "garden",
name: "Garden",
icon: "mdi:flower",
color: "green",
description: null,
created_at: 1735689600,
modified_at: 1735689600,
},
{
label_id: "battery-powered",
name: "Battery powered",
icon: null,
color: "orange",
description: "Runs on batteries",
created_at: 1735689600,
modified_at: 1735689600,
},
]);
mockUsagePrediction(hass);
mockEntityRegistryDisplay(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
@@ -193,7 +296,8 @@ export class HaDemo extends HomeAssistantAppEl {
([conf, localize]) => {
setDemoFloors(hass, conf.floors);
setDemoAreas(hass, conf.areas);
hass.addEntities(conf.entities(localize));
hass.addEntities(demoConfigEntities(selectedDemo, conf, localize));
registerDemoAutomations(hass, selectedDemo, conf);
applyDemoTheme(hass, conf.theme);
}
);
+3
View File
@@ -39,6 +39,9 @@
html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);
/* Prevent iOS Safari from inflating text when rotating to landscape */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
@media (prefers-color-scheme: dark) {
html {
+19
View File
@@ -1,3 +1,4 @@
import type { OwnProfileMutableParams } from "../../../src/data/person";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAuth = (hass: MockHomeAssistant) => {
@@ -6,4 +7,22 @@ export const mockAuth = (hass: MockHomeAssistant) => {
hass.mockWS("auth/sign_path", (msg: { path: string }) => ({
path: msg.path,
}));
let name: string | undefined;
// The profile dialog refreshes the user subscription after saving, which
// writes the updated user back to hass.user. Read the current user on each
// call so later changes to it, such as a non-admin user, are kept.
hass.mockWS("auth/current_user", (_msg, currentHass: MockHomeAssistant) =>
name && currentHass.user ? { ...currentHass.user, name } : currentHass.user
);
hass.mockWS(
"person/update_own_profile",
(msg: Partial<OwnProfileMutableParams>, currentHass: MockHomeAssistant) => {
if (msg.name) {
name = msg.name;
}
return { user_name: name ?? currentHass.user?.name, person: null };
}
);
};
+142 -56
View File
@@ -1,69 +1,155 @@
import type { AutomationConfig } from "../../../src/data/automation";
import type { ScriptConfig } from "../../../src/data/script";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { computeDomain } from "../../../src/common/entity/compute_domain";
import { slugify } from "../../../src/common/string/slugify";
import {
automationEntity,
demoAutomations,
saveDemoAutomation,
selectedDemo,
selectedDemoConfig,
} from "../configs/demo-configs";
import type { DemoAutomation } from "../configs/types";
import { mockAutomationPlatforms } from "./automation_platforms";
import {
addEntityRegistryEntry,
getEntityRegistryEntry,
removeEntityRegistryEntry,
} from "./entity_registry";
const demoAutomationConfig = (entityId: string): AutomationConfig => ({
id: entityId.split(".")[1],
alias: "Demo automation",
description: "An example automation shown in the demo.",
triggers: [
{ trigger: "state", entity_id: "binary_sensor.basement_floor_wet" },
],
conditions: [],
actions: [
{
action: "light.turn_on",
target: { entity_id: "light.bed_light" },
},
],
mode: "single",
});
const automationConfig = async (
id: string | undefined
): Promise<AutomationConfig> => {
const automation = demoAutomations(
selectedDemo,
await selectedDemoConfig
).find((a) => a.config.id === id);
if (!automation) {
throw new Error("Automation not found");
}
return automation.config;
};
const demoScriptConfig = (): ScriptConfig => ({
alias: "Demo script",
description: "An example script shown in the demo.",
sequence: [
{
action: "light.turn_on",
target: { entity_id: "light.bed_light" },
},
],
mode: "single",
});
const scriptConfig = (
currentHass: MockHomeAssistant,
entityId: string
): ScriptConfig => {
const alias =
currentHass.states[entityId]?.attributes.friendly_name ?? entityId;
return {
alias,
sequence: [
{
action: "persistent_notification.create",
data: { message: `${alias} ran` },
},
],
mode: "single",
};
};
export const mockAutomation = (hass: MockHomeAssistant) => {
hass.mockWS("automation/config", (msg: { entity_id: string }) => ({
config: demoAutomationConfig(msg.entity_id),
}));
hass.mockWS("script/config", () => ({ config: demoScriptConfig() }));
hass.mockAPI(/config\/automation\/config\/.+/, () =>
demoAutomationConfig("automation.demo")
);
hass.mockAPI(/config\/script\/config\/.+/, () => demoScriptConfig());
// Trigger/condition type pickers subscribe for integration-provided
// platforms. The demo only uses the built-in ones, so emit empty records.
hass.mockWS(
"trigger_platforms/subscribe",
(
_msg,
_hass,
onChange?: (descriptions: Record<string, unknown>) => void
) => {
onChange?.({});
return () => undefined;
"automation/config",
async (msg: { entity_id: string }, currentHass: MockHomeAssistant) => ({
config: await automationConfig(
currentHass.states[msg.entity_id]?.attributes.id
),
})
);
hass.mockAPI(
/config\/automation\/config\/.+/,
(currentHass, method, path, parameters) => {
const id = decodeURIComponent(path.split("/").pop()!);
const existing = Object.values(currentHass.states).find(
(stateObj) =>
computeDomain(stateObj.entity_id) === "automation" &&
stateObj.attributes.id === id
);
if (method === "DELETE") {
// Like core, remove the automation with its entity
saveDemoAutomation(id, null);
if (existing) {
const { [existing.entity_id]: _state, ...states } =
currentHass.states;
const { [existing.entity_id]: _entity, ...entities } =
currentHass.entities;
currentHass.updateHass({ states, entities });
// Otherwise a service call to it would add its state again
delete currentHass.mockEntities[existing.entity_id];
removeEntityRegistryEntry(currentHass, existing.entity_id);
}
return { result: "ok" };
}
if (method === "POST") {
// Core stores the ID in the config
const config = {
...(parameters as DemoAutomation["config"]),
id,
};
config.alias ||= id;
// Like core, update the automation entity, or add it for a new one
// A new automation gets a free entity ID, like core
const slug = `automation.${slugify(config.alias)}`;
let entityId = existing?.entity_id ?? slug;
for (let i = 2; !existing && entityId in currentHass.states; i++) {
entityId = `${slug}_${i}`;
}
const lastTriggered = existing?.attributes.last_triggered;
const automation: DemoAutomation = {
config,
entityId,
state: existing?.state as DemoAutomation["state"],
icon: existing?.attributes.icon,
lastTriggered: lastTriggered
? (Date.now() - Date.parse(lastTriggered)) / 60000
: undefined,
};
// Kept per demo, so it is still there after switching demos
saveDemoAutomation(id, automation);
// A display entry of a new automation is left over from another demo
const display = existing ? currentHass.entities[entityId] : undefined;
currentHass.addEntities(automationEntity(automation));
// Adding the entity resets its display entry, keep what the registry
// holds
if (display) {
const registryName = getEntityRegistryEntry(entityId)?.name;
currentHass.entities[entityId] = {
...currentHass.entities[entityId],
...(registryName && { name: registryName }),
area_id: display.area_id,
labels: display.labels,
icon: display.icon,
hidden: display.hidden,
};
}
if (!existing) {
addEntityRegistryEntry(currentHass, {
entity_id: entityId,
platform: "automation",
unique_id: id,
});
}
return { result: "ok" };
}
return automationConfig(id);
}
);
// Scripts do not have demo configs yet, show the script under its own name.
hass.mockWS(
"condition_platforms/subscribe",
(
_msg,
_hass,
onChange?: (descriptions: Record<string, unknown>) => void
) => {
onChange?.({});
return () => undefined;
}
"script/config",
(msg: { entity_id: string }, currentHass: MockHomeAssistant) => ({
config: scriptConfig(currentHass, msg.entity_id),
})
);
hass.mockAPI(/config\/script\/config\/.+/, (currentHass, _method, path) =>
scriptConfig(
currentHass,
`script.${decodeURIComponent(path.split("/").pop()!)}`
)
);
mockAutomationPlatforms(hass);
};
@@ -0,0 +1,52 @@
// This file is auto-generated from Home Assistant Core. Do not edit by hand.
// Regenerate with `script/gen_demo_core_data`.
// Integrations that provide the new style triggers and conditions. Their
// translations are part of the demo build.
export const automationPlatformDomains = [
"air_quality",
"alarm_control_panel",
"assist_satellite",
"battery",
"button",
"calendar",
"climate",
"counter",
"cover",
"door",
"doorbell",
"event",
"fan",
"garage_door",
"gate",
"humidifier",
"humidity",
"illuminance",
"lawn_mower",
"light",
"lock",
"media_player",
"moisture",
"moon",
"motion",
"occupancy",
"power",
"remote",
"scene",
"schedule",
"select",
"siren",
"sun",
"switch",
"temperature",
"text",
"timer",
"todo",
"update",
"vacuum",
"valve",
"vibration",
"water_heater",
"window",
"zone",
];
+270
View File
@@ -0,0 +1,270 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { ensureArray } from "../../../src/common/array/ensure-array";
import { computeDomain } from "../../../src/common/entity/compute_domain";
import type { ConditionDescriptions } from "../../../src/data/condition";
import type { ExtractFromTargetResult } from "../../../src/data/target";
import type { TriggerDescriptions } from "../../../src/data/trigger";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { getLabelIds } from "./label_registry";
import {
conditionDescriptions,
triggerDescriptions,
} from "./automation_platforms_data";
type Descriptions = TriggerDescriptions | ConditionDescriptions;
interface EntityFilter {
integration?: string;
domain?: string | string[];
device_class?: string | string[];
supported_features?: number[];
}
// Expands a target to its areas, devices and entities, like core does
const expandTarget = (
hass: MockHomeAssistant,
target: HassServiceTarget,
primaryEntitiesOnly = true
) => {
const missing = {
floors: [] as string[],
areas: [] as string[],
devices: [] as string[],
labels: [] as string[],
};
const knownLabelIds = getLabelIds();
const floorIds = ensureArray(target.floor_id ?? []);
const labelIds = ensureArray(target.label_id ?? []);
missing.floors.push(...floorIds.filter((id) => !(id in hass.floors)));
// Like core, unknown labels are reported, but still expanded
missing.labels.push(...labelIds.filter((id) => !knownLabelIds.includes(id)));
const hasLabel = (labels?: string[]) =>
labelIds.some((labelId) => labels?.includes(labelId));
// Unknown areas and devices are reported, but still referenced
const areaIds = new Set(ensureArray(target.area_id ?? []));
missing.areas.push(...[...areaIds].filter((id) => !(id in hass.areas)));
for (const area of Object.values(hass.areas)) {
if (
(area.floor_id && floorIds.includes(area.floor_id)) ||
hasLabel(area.labels)
) {
areaIds.add(area.area_id);
}
}
// A targeted or labeled device includes its child devices
const deviceIds = new Set<string>();
const addDevice = (deviceId: string) => {
deviceIds.add(deviceId);
for (const device of Object.values(hass.devices)) {
if (device.parent_device_id === deviceId) {
deviceIds.add(device.id);
}
}
};
for (const deviceId of ensureArray(target.device_id ?? [])) {
if (!(deviceId in hass.devices)) {
missing.devices.push(deviceId);
}
addDevice(deviceId);
}
for (const device of Object.values(hass.devices)) {
if (hasLabel(device.labels)) {
addDevice(device.id);
}
}
// A child device without its own area is in the area of its parent device
const deviceArea = (deviceId: string) => {
const device = hass.devices[deviceId];
return (
device?.area_id ??
(device?.parent_device_id
? hass.devices[device.parent_device_id]?.area_id
: undefined)
);
};
const areaDeviceIds = new Set<string>();
for (const device of Object.values(hass.devices)) {
const areaId = deviceArea(device.id);
if (areaId && areaIds.has(areaId)) {
areaDeviceIds.add(device.id);
}
}
const entityIds = new Set(ensureArray(target.entity_id ?? []));
for (const entity of Object.values(hass.entities)) {
// The display registry keeps the entities of a previous demo, and like
// core, hidden entities are never included indirectly
if (!(entity.entity_id in hass.states) || entity.hidden) {
continue;
}
const primary = !primaryEntitiesOnly || !entity.entity_category;
if (
hasLabel(entity.labels) ||
(primary &&
((entity.device_id && deviceIds.has(entity.device_id)) ||
(entity.area_id && areaIds.has(entity.area_id)) ||
// An entity with its own area is not in the area of its device
(!entity.area_id &&
entity.device_id &&
areaDeviceIds.has(entity.device_id))))
) {
entityIds.add(entity.entity_id);
}
}
return {
areas: [...areaIds],
devices: [...new Set([...deviceIds, ...areaDeviceIds])],
entities: [...entityIds],
missing,
};
};
const matchesFilter = (
hass: MockHomeAssistant,
entityId: string,
filter: EntityFilter
) =>
(!filter.integration ||
hass.entities[entityId]?.platform === filter.integration) &&
(!filter.domain ||
ensureArray(filter.domain).includes(computeDomain(entityId))) &&
(!filter.device_class ||
ensureArray(filter.device_class).includes(
hass.states[entityId]?.attributes.device_class ?? ""
)) &&
// Like core, the entity needs all the features of one of the feature sets
(!filter.supported_features ||
filter.supported_features.some(
(features) =>
// eslint-disable-next-line no-bitwise
((hass.states[entityId]?.attributes.supported_features ?? 0) &
features) ===
features
));
type TargetDescriptions = Record<
string,
{
target?: {
entity?: EntityFilter | EntityFilter[];
primary_entities_only?: boolean;
} | null;
}
>;
// Like core, the components with a target that apply to at least one entity
// of the target. A target without entity filters applies to any entity.
const componentsForTarget = (
hass: MockHomeAssistant,
target: HassServiceTarget,
descriptions: TargetDescriptions
) => {
const entityIds = expandTarget(hass, target, false).entities;
const targetedIds = ensureArray(target.entity_id ?? []);
return Object.entries(descriptions)
.filter(([, description]) => {
if (!description.target) {
return false;
}
const filters = ensureArray(description.target.entity ?? []);
const primaryEntitiesOnly =
description.target.primary_entities_only ?? true;
return entityIds.some(
(entityId) =>
// Entities with a category only count when targeted directly,
// unless the component allows them
(!primaryEntitiesOnly ||
targetedIds.includes(entityId) ||
!hass.entities[entityId]?.entity_category) &&
(!filters.length ||
filters.some((filter) => matchesFilter(hass, entityId, filter)))
);
})
.map(([key]) => key);
};
const mockForTarget = (
hass: MockHomeAssistant,
type: string,
descriptions: Descriptions
) =>
hass.mockWS(
type,
(msg: { target: HassServiceTarget }, currentHass: MockHomeAssistant) =>
componentsForTarget(
currentHass,
msg.target,
descriptions as TargetDescriptions
)
);
const mockPlatformSubscription = (
hass: MockHomeAssistant,
type: string,
descriptions: Descriptions
) =>
hass.mockWS(
type,
(_msg, _hass, onChange?: (descriptions: Descriptions) => void) => {
onChange?.(descriptions);
return () => undefined;
}
);
export const mockAutomationPlatforms = (hass: MockHomeAssistant) => {
mockPlatformSubscription(
hass,
"trigger_platforms/subscribe",
triggerDescriptions
);
mockPlatformSubscription(
hass,
"condition_platforms/subscribe",
conditionDescriptions
);
mockForTarget(hass, "get_triggers_for_target", triggerDescriptions);
mockForTarget(hass, "get_conditions_for_target", conditionDescriptions);
hass.mockWS(
"extract_from_target",
(
msg: { target: HassServiceTarget; primary_entities_only?: boolean },
currentHass: MockHomeAssistant
): ExtractFromTargetResult => {
const { areas, devices, entities, missing } = expandTarget(
currentHass,
msg.target,
msg.primary_entities_only ?? true
);
return {
missing_areas: missing.areas,
missing_devices: missing.devices,
missing_floors: missing.floors,
missing_labels: missing.labels,
referenced_areas: areas,
referenced_devices: devices,
referenced_entities: entities,
};
}
);
hass.mockWS(
"get_services_for_target",
(msg: { target: HassServiceTarget }, currentHass: MockHomeAssistant) =>
componentsForTarget(
currentHass,
msg.target,
// Keyed by <domain>.<service>, like the triggers and conditions
Object.fromEntries(
Object.entries(currentHass.services).flatMap(([domain, services]) =>
Object.entries(services).map(([service, description]) => [
`${domain}.${service}`,
description,
])
)
) as TargetDescriptions
)
);
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,495 @@
// This file is auto-generated from Home Assistant Core. Do not edit by hand.
// Regenerate with `script/gen_demo_core_data`.
export const triggerIcons = {
air_quality: {
co2_changed: { trigger: "mdi:molecule-co2" },
co2_crossed_threshold: { trigger: "mdi:molecule-co2" },
co_changed: { trigger: "mdi:molecule-co" },
co_cleared: { trigger: "mdi:check-circle" },
co_crossed_threshold: { trigger: "mdi:molecule-co" },
co_detected: { trigger: "mdi:molecule-co" },
gas_cleared: { trigger: "mdi:check-circle" },
gas_detected: { trigger: "mdi:gas-cylinder" },
n2o_changed: { trigger: "mdi:factory" },
n2o_crossed_threshold: { trigger: "mdi:factory" },
no2_changed: { trigger: "mdi:factory" },
no2_crossed_threshold: { trigger: "mdi:factory" },
no_changed: { trigger: "mdi:factory" },
no_crossed_threshold: { trigger: "mdi:factory" },
ozone_changed: { trigger: "mdi:weather-sunny-alert" },
ozone_crossed_threshold: { trigger: "mdi:weather-sunny-alert" },
pm10_changed: { trigger: "mdi:blur" },
pm10_crossed_threshold: { trigger: "mdi:blur" },
pm1_changed: { trigger: "mdi:blur" },
pm1_crossed_threshold: { trigger: "mdi:blur" },
pm25_changed: { trigger: "mdi:blur" },
pm25_crossed_threshold: { trigger: "mdi:blur" },
pm4_changed: { trigger: "mdi:blur" },
pm4_crossed_threshold: { trigger: "mdi:blur" },
smoke_cleared: { trigger: "mdi:check-circle" },
smoke_detected: { trigger: "mdi:smoke-detector-variant" },
so2_changed: { trigger: "mdi:factory" },
so2_crossed_threshold: { trigger: "mdi:factory" },
voc_changed: { trigger: "mdi:air-filter" },
voc_crossed_threshold: { trigger: "mdi:air-filter" },
voc_ratio_changed: { trigger: "mdi:air-filter" },
voc_ratio_crossed_threshold: { trigger: "mdi:air-filter" },
},
alarm_control_panel: {
armed: { trigger: "mdi:shield" },
armed_away: { trigger: "mdi:shield-lock" },
armed_home: { trigger: "mdi:shield-home" },
armed_night: { trigger: "mdi:shield-moon" },
armed_vacation: { trigger: "mdi:shield-airplane" },
disarmed: { trigger: "mdi:shield-off" },
triggered: { trigger: "mdi:bell-ring" },
},
assist_satellite: {
idle: { trigger: "mdi:chat-sleep" },
listening: { trigger: "mdi:chat-question" },
processing: { trigger: "mdi:chat-processing" },
responding: { trigger: "mdi:chat-alert" },
},
battery: {
became_low: { trigger: "mdi:battery-alert" },
level_changed: { trigger: "mdi:battery-unknown" },
level_crossed_threshold: { trigger: "mdi:battery-alert" },
no_longer_low: { trigger: "mdi:battery" },
started_charging: { trigger: "mdi:battery-charging" },
stopped_charging: { trigger: "mdi:battery" },
},
button: { pressed: { trigger: "mdi:gesture-tap-button" } },
calendar: {
event_ended: { trigger: "mdi:calendar-end" },
event_started: { trigger: "mdi:calendar-start" },
},
climate: {
hvac_mode_changed: { trigger: "mdi:thermostat" },
started_cooling: { trigger: "mdi:snowflake" },
started_drying: { trigger: "mdi:water-percent" },
started_heating: { trigger: "mdi:fire" },
target_humidity_changed: { trigger: "mdi:water-percent" },
target_humidity_crossed_threshold: { trigger: "mdi:water-percent" },
target_temperature_changed: { trigger: "mdi:thermometer" },
target_temperature_crossed_threshold: { trigger: "mdi:thermometer" },
turned_off: { trigger: "mdi:power-off" },
turned_on: { trigger: "mdi:power-on" },
},
counter: {
decremented: { trigger: "mdi:numeric-negative-1" },
incremented: { trigger: "mdi:numeric-positive-1" },
maximum_reached: { trigger: "mdi:sort-numeric-ascending-variant" },
minimum_reached: { trigger: "mdi:sort-numeric-descending-variant" },
reset: { trigger: "mdi:refresh" },
},
cover: {
awning_closed: { trigger: "mdi:storefront-outline" },
awning_opened: { trigger: "mdi:storefront-outline" },
blind_closed: { trigger: "mdi:blinds-horizontal-closed" },
blind_opened: { trigger: "mdi:blinds-horizontal" },
curtain_closed: { trigger: "mdi:curtains-closed" },
curtain_opened: { trigger: "mdi:curtains" },
shade_closed: { trigger: "mdi:roller-shade-closed" },
shade_opened: { trigger: "mdi:roller-shade" },
shutter_closed: { trigger: "mdi:window-shutter" },
shutter_opened: { trigger: "mdi:window-shutter-open" },
},
door: {
closed: { trigger: "mdi:door-closed" },
opened: { trigger: "mdi:door-open" },
},
doorbell: { rang: { trigger: "mdi:doorbell" } },
event: { received: { trigger: "mdi:eye-check" } },
fan: {
turned_off: { trigger: "mdi:fan-off" },
turned_on: { trigger: "mdi:fan" },
},
garage_door: {
closed: { trigger: "mdi:garage" },
opened: { trigger: "mdi:garage-open" },
},
gate: {
closed: { trigger: "mdi:gate" },
opened: { trigger: "mdi:gate-open" },
},
humidifier: {
mode_changed: { trigger: "mdi:air-humidifier" },
started_drying: { trigger: "mdi:arrow-down-bold" },
started_humidifying: { trigger: "mdi:arrow-up-bold" },
turned_off: { trigger: "mdi:air-humidifier-off" },
turned_on: { trigger: "mdi:air-humidifier" },
},
humidity: {
changed: { trigger: "mdi:water-percent" },
crossed_threshold: { trigger: "mdi:water-percent" },
},
illuminance: {
changed: { trigger: "mdi:brightness-6" },
cleared: { trigger: "mdi:brightness-4" },
crossed_threshold: { trigger: "mdi:brightness-6" },
detected: { trigger: "mdi:brightness-7" },
},
lawn_mower: {
became_idle: { trigger: "mdi:stop" },
errored: { trigger: "mdi:alert-circle-outline" },
paused_mowing: { trigger: "mdi:pause" },
returned_to_dock: { trigger: "mdi:home-import-outline" },
started_mowing: { trigger: "mdi:play" },
started_returning: { trigger: "mdi:home-import-outline" },
},
light: {
brightness_changed: { trigger: "mdi:lightbulb-on-50" },
brightness_crossed_threshold: { trigger: "mdi:lightbulb-on-50" },
turned_off: { trigger: "mdi:lightbulb-off" },
turned_on: { trigger: "mdi:lightbulb-on" },
},
lock: {
jammed: { trigger: "mdi:lock-alert" },
locked: { trigger: "mdi:lock" },
opened: { trigger: "mdi:lock-open-variant" },
unlocked: { trigger: "mdi:lock-open-variant" },
},
media_player: {
muted: { trigger: "mdi:volume-mute" },
paused_playing: { trigger: "mdi:pause" },
started_playing: { trigger: "mdi:play" },
stopped_playing: { trigger: "mdi:stop" },
turned_off: { trigger: "mdi:power" },
turned_on: { trigger: "mdi:power" },
unmuted: { trigger: "mdi:volume-high" },
volume_changed: { trigger: "mdi:volume-medium" },
volume_crossed_threshold: { trigger: "mdi:volume-medium" },
},
moisture: {
changed: { trigger: "mdi:water-percent" },
cleared: { trigger: "mdi:water-off" },
crossed_threshold: { trigger: "mdi:water-percent" },
detected: { trigger: "mdi:water" },
},
moon: { phase_changed: { trigger: "mdi:moon-waning-crescent" } },
motion: {
cleared: { trigger: "mdi:motion-sensor-off" },
detected: { trigger: "mdi:motion-sensor" },
},
occupancy: {
cleared: { trigger: "mdi:home-outline" },
detected: { trigger: "mdi:home-account" },
},
power: {
changed: { trigger: "mdi:flash" },
crossed_threshold: { trigger: "mdi:flash" },
},
remote: {
turned_off: { trigger: "mdi:remote-off" },
turned_on: { trigger: "mdi:remote" },
},
scene: { activated: { trigger: "mdi:palette" } },
schedule: {
block_ended: { trigger: "mdi:calendar-blank" },
block_started: { trigger: "mdi:calendar-clock" },
},
select: { selection_changed: { trigger: "mdi:format-list-bulleted" } },
siren: {
turned_off: { trigger: "mdi:bullhorn-outline" },
turned_on: { trigger: "mdi:bullhorn" },
},
sun: {
blue_hour_ended: { trigger: "mdi:weather-sunset" },
blue_hour_started: { trigger: "mdi:weather-sunset" },
dawn: { trigger: "mdi:weather-sunset-up" },
dusk: { trigger: "mdi:weather-sunset-down" },
elevation_changed: { trigger: "mdi:sun-angle" },
elevation_crossed_threshold: { trigger: "mdi:sun-angle" },
golden_hour_ended: { trigger: "mdi:weather-sunset" },
golden_hour_started: { trigger: "mdi:weather-sunset" },
midnight_sun_ended: { trigger: "mdi:weather-sunset" },
midnight_sun_started: { trigger: "mdi:weather-sunny" },
polar_night_ended: { trigger: "mdi:weather-sunset-up" },
polar_night_started: { trigger: "mdi:weather-night" },
solar_midnight: { trigger: "mdi:weather-night" },
solar_noon: { trigger: "mdi:weather-sunny" },
sunrise: { trigger: "mdi:weather-sunset-up" },
sunset: { trigger: "mdi:weather-sunset-down" },
},
switch: {
turned_off: { trigger: "mdi:toggle-switch-variant-off" },
turned_on: { trigger: "mdi:toggle-switch-variant" },
},
temperature: {
changed: { trigger: "mdi:thermometer" },
crossed_threshold: { trigger: "mdi:thermometer" },
},
text: { changed: { trigger: "mdi:form-textbox" } },
timer: {
cancelled: { trigger: "mdi:timer-cancel" },
finished: { trigger: "mdi:timer-check" },
paused: { trigger: "mdi:timer-pause" },
remaining_time_reached: { trigger: "mdi:timer-alert-outline" },
restarted: { trigger: "mdi:timer-refresh" },
started: { trigger: "mdi:timer-play" },
},
todo: {
item_added: { trigger: "mdi:clipboard-plus" },
item_completed: { trigger: "mdi:clipboard-check" },
item_removed: { trigger: "mdi:clipboard-minus" },
},
update: { became_available: { trigger: "mdi:package-up" } },
vacuum: {
errored: { trigger: "mdi:alert-circle-outline" },
paused_cleaning: { trigger: "mdi:pause" },
returned_to_dock: { trigger: "mdi:home-outline" },
started_cleaning: { trigger: "mdi:play" },
started_returning: { trigger: "mdi:home-import-outline" },
},
valve: {
closed: { trigger: "mdi:valve-closed" },
opened: { trigger: "mdi:valve-open" },
},
vibration: {
cleared: { trigger: "mdi:vibrate-off" },
detected: { trigger: "mdi:vibrate" },
},
water_heater: {
operation_mode_changed: { trigger: "mdi:water-boiler" },
target_temperature_changed: { trigger: "mdi:thermometer" },
target_temperature_crossed_threshold: { trigger: "mdi:thermometer" },
turned_off: { trigger: "mdi:water-boiler-off" },
turned_on: { trigger: "mdi:water-boiler" },
},
window: {
closed: { trigger: "mdi:window-closed" },
opened: { trigger: "mdi:window-open" },
},
zone: {
entered: { trigger: "mdi:map-marker-plus" },
left: { trigger: "mdi:map-marker-minus" },
occupancy_cleared: { trigger: "mdi:account-off" },
occupancy_detected: { trigger: "mdi:account-group" },
},
};
export const conditionIcons = {
air_quality: {
is_co2_value: { condition: "mdi:molecule-co2" },
is_co_cleared: { condition: "mdi:check-circle" },
is_co_detected: { condition: "mdi:molecule-co" },
is_co_value: { condition: "mdi:molecule-co" },
is_gas_cleared: { condition: "mdi:check-circle" },
is_gas_detected: { condition: "mdi:gas-cylinder" },
is_n2o_value: { condition: "mdi:factory" },
is_no2_value: { condition: "mdi:factory" },
is_no_value: { condition: "mdi:factory" },
is_ozone_value: { condition: "mdi:weather-sunny-alert" },
is_pm10_value: { condition: "mdi:blur" },
is_pm1_value: { condition: "mdi:blur" },
is_pm25_value: { condition: "mdi:blur" },
is_pm4_value: { condition: "mdi:blur" },
is_smoke_cleared: { condition: "mdi:check-circle" },
is_smoke_detected: { condition: "mdi:smoke-detector-variant" },
is_so2_value: { condition: "mdi:factory" },
is_voc_ratio_value: { condition: "mdi:air-filter" },
is_voc_value: { condition: "mdi:air-filter" },
},
alarm_control_panel: {
is_armed: { condition: "mdi:shield" },
is_armed_away: { condition: "mdi:shield-lock" },
is_armed_home: { condition: "mdi:shield-home" },
is_armed_night: { condition: "mdi:shield-moon" },
is_armed_vacation: { condition: "mdi:shield-airplane" },
is_disarmed: { condition: "mdi:shield-off" },
is_triggered: { condition: "mdi:bell-ring" },
},
assist_satellite: {
is_idle: { condition: "mdi:chat-sleep" },
is_listening: { condition: "mdi:chat-question" },
is_processing: { condition: "mdi:chat-processing" },
is_responding: { condition: "mdi:chat-alert" },
},
battery: {
is_charging: { condition: "mdi:battery-charging" },
is_level: { condition: "mdi:battery-unknown" },
is_low: { condition: "mdi:battery-alert" },
is_not_charging: { condition: "mdi:battery" },
is_not_low: { condition: "mdi:battery" },
},
calendar: { is_event_active: { condition: "mdi:calendar-check" } },
climate: {
is_cooling: { condition: "mdi:snowflake" },
is_drying: { condition: "mdi:water-percent" },
is_heating: { condition: "mdi:fire" },
is_hvac_mode: { condition: "mdi:thermostat" },
is_off: { condition: "mdi:power-off" },
is_on: { condition: "mdi:power-on" },
is_target_humidity: { condition: "mdi:water-percent" },
is_target_temperature: { condition: "mdi:thermometer" },
},
counter: { is_value: { condition: "mdi:counter" } },
cover: {
awning_is_closed: { condition: "mdi:storefront-outline" },
awning_is_open: { condition: "mdi:storefront-outline" },
blind_is_closed: { condition: "mdi:blinds-horizontal-closed" },
blind_is_open: { condition: "mdi:blinds-horizontal" },
curtain_is_closed: { condition: "mdi:curtains-closed" },
curtain_is_open: { condition: "mdi:curtains" },
shade_is_closed: { condition: "mdi:roller-shade-closed" },
shade_is_open: { condition: "mdi:roller-shade" },
shutter_is_closed: { condition: "mdi:window-shutter" },
shutter_is_open: { condition: "mdi:window-shutter-open" },
},
door: {
is_closed: { condition: "mdi:door-closed" },
is_open: { condition: "mdi:door-open" },
},
fan: {
is_off: { condition: "mdi:fan-off" },
is_on: { condition: "mdi:fan" },
},
garage_door: {
is_closed: { condition: "mdi:garage" },
is_open: { condition: "mdi:garage-open" },
},
gate: {
is_closed: { condition: "mdi:gate" },
is_open: { condition: "mdi:gate-open" },
},
humidifier: {
is_drying: { condition: "mdi:arrow-down-bold" },
is_humidifying: { condition: "mdi:arrow-up-bold" },
is_mode: { condition: "mdi:air-humidifier" },
is_off: { condition: "mdi:air-humidifier-off" },
is_on: { condition: "mdi:air-humidifier" },
is_target_humidity: { condition: "mdi:water-percent" },
},
humidity: { is_value: { condition: "mdi:water-percent" } },
illuminance: {
is_detected: { condition: "mdi:brightness-7" },
is_not_detected: { condition: "mdi:brightness-4" },
is_value: { condition: "mdi:brightness-percent" },
},
lawn_mower: {
is_docked: { condition: "mdi:home-import-outline" },
is_encountering_an_error: { condition: "mdi:alert-circle-outline" },
is_idle: { condition: "mdi:stop" },
is_mowing: { condition: "mdi:play" },
is_paused: { condition: "mdi:pause" },
is_returning: { condition: "mdi:home-import-outline" },
},
light: {
is_brightness: { condition: "mdi:lightbulb-on-50" },
is_off: { condition: "mdi:lightbulb-off" },
is_on: { condition: "mdi:lightbulb-on" },
},
lock: {
is_jammed: { condition: "mdi:lock-alert" },
is_locked: { condition: "mdi:lock" },
is_open: { condition: "mdi:lock-open-variant" },
is_unlocked: { condition: "mdi:lock-open-variant" },
},
media_player: {
is_muted: { condition: "mdi:volume-mute" },
is_not_playing: { condition: "mdi:stop" },
is_off: { condition: "mdi:cast-off" },
is_on: { condition: "mdi:cast" },
is_paused: { condition: "mdi:pause" },
is_playing: { condition: "mdi:play" },
is_unmuted: { condition: "mdi:volume-high" },
is_volume: { condition: "mdi:volume-medium" },
},
moisture: {
is_detected: { condition: "mdi:water" },
is_not_detected: { condition: "mdi:water-off" },
is_value: { condition: "mdi:water-percent" },
},
moon: {
is_phase: { condition: "mdi:moon-full" },
is_waning: { condition: "mdi:moon-waning-crescent" },
is_waxing: { condition: "mdi:moon-waxing-crescent" },
},
motion: {
is_detected: { condition: "mdi:motion-sensor" },
is_not_detected: { condition: "mdi:motion-sensor-off" },
},
occupancy: {
is_detected: { condition: "mdi:home-account" },
is_not_detected: { condition: "mdi:home-outline" },
},
power: { is_value: { condition: "mdi:flash" } },
remote: {
is_off: { condition: "mdi:remote-off" },
is_on: { condition: "mdi:remote" },
},
schedule: {
is_off: { condition: "mdi:calendar-blank" },
is_on: { condition: "mdi:calendar-clock" },
},
select: { is_option_selected: { condition: "mdi:format-list-bulleted" } },
siren: {
is_off: { condition: "mdi:bullhorn-outline" },
is_on: { condition: "mdi:bullhorn" },
},
sun: {
elevation: { condition: "mdi:sun-angle" },
is_ascending: { condition: "mdi:weather-sunset-up" },
is_blue_hour: { condition: "mdi:weather-sunset" },
is_descending: { condition: "mdi:weather-sunset-down" },
is_evening_twilight: { condition: "mdi:weather-sunset-down" },
is_golden_hour: { condition: "mdi:weather-sunset" },
is_midnight_sun: { condition: "mdi:weather-sunny" },
is_morning_twilight: { condition: "mdi:weather-sunset-up" },
is_night: { condition: "mdi:weather-night" },
is_polar_night: { condition: "mdi:weather-night" },
is_set: { condition: "mdi:weather-sunny-off" },
is_up: { condition: "mdi:weather-sunny" },
},
switch: {
is_off: { condition: "mdi:toggle-switch-variant-off" },
is_on: { condition: "mdi:toggle-switch-variant" },
},
temperature: { is_value: { condition: "mdi:thermometer" } },
text: { is_equal_to: { condition: "mdi:form-textbox" } },
timer: {
is_active: { condition: "mdi:timer" },
is_idle: { condition: "mdi:timer-off" },
is_paused: { condition: "mdi:timer-pause" },
},
todo: {
all_completed: { condition: "mdi:clipboard-check" },
incomplete: { condition: "mdi:clipboard-alert" },
},
update: {
is_available: { condition: "mdi:package-up" },
is_not_available: { condition: "mdi:package" },
},
vacuum: {
is_cleaning: { condition: "mdi:play" },
is_docked: { condition: "mdi:home-import-outline" },
is_encountering_an_error: { condition: "mdi:alert-circle-outline" },
is_paused: { condition: "mdi:pause" },
is_returning: { condition: "mdi:home-import-outline" },
},
valve: {
is_closed: { condition: "mdi:valve-closed" },
is_open: { condition: "mdi:valve-open" },
},
vibration: {
is_detected: { condition: "mdi:vibrate" },
is_not_detected: { condition: "mdi:vibrate-off" },
},
water_heater: {
is_off: { condition: "mdi:water-boiler-off" },
is_on: { condition: "mdi:water-boiler" },
is_operation_mode: { condition: "mdi:water-boiler" },
is_target_temperature: { condition: "mdi:thermometer" },
},
window: {
is_closed: { condition: "mdi:window-closed" },
is_open: { condition: "mdi:window-open" },
},
zone: {
in_zone: { condition: "mdi:map-marker-check" },
not_in_zone: { condition: "mdi:map-marker-remove" },
occupancy_is_detected: { condition: "mdi:account-group" },
occupancy_is_not_detected: { condition: "mdi:account-off" },
},
};
+1
View File
@@ -39,6 +39,7 @@ const backupInfo: BackupInfo = {
},
],
agent_errors: {},
agent_error_translations: {},
last_attempted_automatic_backup: recent,
last_completed_automatic_backup: recent,
last_action_event: { manager_state: "idle" },
+8
View File
@@ -42,4 +42,12 @@ export const mockBlueprint = (hass: MockHomeAssistant) => {
hass.mockWS("blueprint/list", (msg: { domain: BlueprintDomain }) =>
msg.domain === "script" ? scriptBlueprints : automationBlueprints
);
hass.mockWS(
"blueprint/delete",
(msg: { domain: BlueprintDomain; path: string }) => {
const blueprints =
msg.domain === "script" ? scriptBlueprints : automationBlueprints;
delete blueprints[msg.path];
}
);
};
+2
View File
@@ -12,6 +12,7 @@ import { mockDeviceAutomation } from "./device_automation";
import { mockEntityRegistrySettings } from "./entity_registry_settings";
import { mockEntitySources } from "./entity_sources";
import { mockExpose } from "./expose";
import { mockMarketplace } from "./marketplace";
import { mockNetwork } from "./network";
import { mockPerson } from "./person";
import { mockScene } from "./scene";
@@ -45,6 +46,7 @@ export const mockConfigPanel = (hass: MockHomeAssistant) => {
mockAssist(hass);
mockEntityRegistrySettings(hass);
mockSlugify(hass);
mockMarketplace(hass);
hass.mockWS("llm/api/list", () => ({
apis: [
{ id: "assist", name: "Assist" },
+9
View File
@@ -91,6 +91,15 @@ export const demoConfigEntries: {
supports_options: true,
},
},
{
type: "system",
entry: {
...baseEntry,
entry_id: "mock-marketplace",
domain: "marketplace",
title: "Marketplace",
},
},
...connectivityConfigEntries,
];
+2
View File
@@ -1,3 +1,4 @@
import { lorawanFixtures } from "./lorawan/fixtures";
import { bluetoothFixtures } from "./bluetooth/fixtures";
import { infraredFixtures } from "./infrared/fixtures";
import { matterFixtures } from "./matter/fixtures";
@@ -15,6 +16,7 @@ import { zwaveJsFixtures } from "./zwave_js/fixtures";
// data to mock. Each owns its own fixtures, so they can be added and removed
// one at a time.
const INTEGRATIONS: ConnectivityFixtures[] = [
lorawanFixtures,
bluetoothFixtures,
serialFixtures,
modbusFixtures,
+2
View File
@@ -1,3 +1,4 @@
import { mockLoRaWAN } from "./lorawan/mock";
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
import { mockBluetooth } from "./bluetooth/mock";
import { mockMatter } from "./matter/mock";
@@ -11,6 +12,7 @@ import { mockZwaveJs } from "./zwave_js/mock";
// The WebSocket mocks, code-split into the config panel chunk.
const MOCKS = [
mockLoRaWAN,
mockBluetooth,
mockSerial,
mockModbus,
@@ -0,0 +1,135 @@
import { manifest } from "../../manifest";
import {
configEntry,
device,
registryEntry,
withRegistryLinks,
} from "../helpers";
import type { ConnectivityFixtures } from "../types";
export const CHIRPSTACK_ENTRY = "mock-lorawan-chirpstack";
export const TTN_ENTRY = "mock-lorawan-ttn";
export const SENSECAP_ENTRY = "mock-lorawan-sensecap";
const REGISTRY_ENTRIES = [
registryEntry(
"sensor.lorawan_greenhouse_temperature",
"lorawan-greenhouse",
SENSECAP_ENTRY,
"sensecap_lorawan",
"Temperature"
),
registryEntry(
"sensor.lorawan_greenhouse_humidity",
"lorawan-greenhouse",
SENSECAP_ENTRY,
"sensecap_lorawan",
"Humidity"
),
registryEntry(
"sensor.lorawan_study_carbon_dioxide",
"lorawan-study",
SENSECAP_ENTRY,
"sensecap_lorawan",
"Carbon dioxide"
),
];
export const lorawanFixtures: ConnectivityFixtures = {
components: [
"lorawan",
"sensecap_lorawan",
"chirpstack",
"thethingsnetwork_lorawan",
],
commands: ["lorawan/"],
manifests: [
manifest("lorawan", "LoRaWAN", { integration_type: "helper" }),
manifest("sensecap_lorawan", "SenseCAP", {
integration_type: "device",
dependencies: ["lorawan"],
}),
manifest("chirpstack", "ChirpStack", { integration_type: "hub" }),
manifest("thethingsnetwork_lorawan", "The Things Network", {
integration_type: "hub",
}),
],
configEntries: [
{
type: "hub",
entry: configEntry(CHIRPSTACK_ENTRY, "chirpstack", "Home ChirpStack"),
},
{
type: "hub",
entry: configEntry(
TTN_ENTRY,
"thethingsnetwork_lorawan",
"The Things Network"
),
},
{
type: "device",
entry: configEntry(SENSECAP_ENTRY, "sensecap_lorawan", "SenseCAP"),
},
],
devices: [
device(
"lorawan-greenhouse",
"Greenhouse",
"Seeed Studio",
"S2101",
SENSECAP_ENTRY,
{
config_entries: [SENSECAP_ENTRY, CHIRPSTACK_ENTRY],
identifiers: [
["sensecap_lorawan", `${CHIRPSTACK_ENTRY}:0000000000000001`],
],
}
),
device(
"lorawan-study",
"Study air quality",
"Seeed Studio",
"S2103",
SENSECAP_ENTRY,
{
config_entries: [SENSECAP_ENTRY, TTN_ENTRY],
identifiers: [["sensecap_lorawan", `${TTN_ENTRY}:0000000000000002`]],
}
),
],
entityRegistryEntries: REGISTRY_ENTRIES,
entities: () =>
withRegistryLinks(REGISTRY_ENTRIES, {
"sensor.lorawan_greenhouse_temperature": {
entity_id: "sensor.lorawan_greenhouse_temperature",
state: "24.3",
attributes: {
friendly_name: "Greenhouse temperature",
device_class: "temperature",
state_class: "measurement",
unit_of_measurement: "°C",
},
},
"sensor.lorawan_greenhouse_humidity": {
entity_id: "sensor.lorawan_greenhouse_humidity",
state: "58",
attributes: {
friendly_name: "Greenhouse humidity",
device_class: "humidity",
state_class: "measurement",
unit_of_measurement: "%",
},
},
"sensor.lorawan_study_carbon_dioxide": {
entity_id: "sensor.lorawan_study_carbon_dioxide",
state: "652",
attributes: {
friendly_name: "Study carbon dioxide",
device_class: "carbon_dioxide",
state_class: "measurement",
unit_of_measurement: "ppm",
},
},
}),
};
+124
View File
@@ -0,0 +1,124 @@
import type { LoRaWANDevice } from "../../../../../src/data/lorawan";
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
import { minutesAgo } from "../helpers";
import { emitInitial } from "../subscription";
import { CHIRPSTACK_ENTRY, TTN_ENTRY } from "./fixtures";
const devices = (): LoRaWANDevice[] => {
const base: LoRaWANDevice = {
dev_eui: "0000000000000001",
name: "Greenhouse",
manufacturer: "Seeed Studio",
model: "S2101",
application_id: "home-sensors",
device_id: "lorawan-greenhouse",
connection: {
entry_id: CHIRPSTACK_ENTRY,
domain: "chirpstack",
title: "Home ChirpStack",
configuration_url: "https://www.chirpstack.io/",
},
support: "supported",
last_uplink: minutesAgo(2),
last_join: minutesAgo(2400),
last_status: minutesAgo(64),
receptions: [{ gateway_id: "home-indoor", rssi: -67, snr: 9.5 }],
battery_level: 87,
external_power_source: false,
downlink_margin: 12,
};
return [
base,
{
...base,
dev_eui: "0000000000000002",
name: "Study air quality",
model: "S2103",
device_id: "lorawan-study",
application_id: "demo-home",
connection: {
entry_id: TTN_ENTRY,
domain: "thethingsnetwork_lorawan",
title: "The Things Network",
configuration_url: "https://eu1.cloud.thethings.network/console",
},
receptions: [
{ gateway_id: "brooklyn-rooftop", rssi: -103, snr: -2.5 },
{ gateway_id: "brooklyn-community", rssi: -110, snr: -5.2 },
],
battery_level: 64,
last_uplink: minutesAgo(4),
last_status: minutesAgo(140),
},
{
...base,
dev_eui: "24E124128E123456",
name: "Irrigation controller",
manufacturer: "Milesight",
model: "UC511",
device_id: null,
support: "integration_not_configured",
battery_level: 93,
last_uplink: minutesAgo(7),
receptions: [{ gateway_id: "home-indoor", rssi: -83, snr: 6.8 }],
},
{
...base,
dev_eui: "A84041FFFF123456",
name: "Garage power meter",
manufacturer: "",
model: "",
device_id: null,
support: "missing_identity",
battery_level: null,
external_power_source: true,
last_uplink: minutesAgo(1),
receptions: [{ gateway_id: "home-indoor", rssi: -75, snr: 8.1 }],
},
{
...base,
dev_eui: "2CF7F1C071500199",
name: "New soil sensor",
model: "S2105",
device_id: null,
support: "unsupported_model",
last_uplink: null,
last_join: null,
last_status: null,
receptions: [],
battery_level: null,
external_power_source: null,
downlink_margin: null,
},
{
...base,
dev_eui: "2CF7F1C071500166",
name: "Garden shed",
device_id: null,
support: "integration_not_configured",
battery_level: 18,
last_uplink: minutesAgo(180),
last_status: minutesAgo(600),
receptions: [{ gateway_id: "home-indoor", rssi: -112, snr: -7.5 }],
},
];
};
export const mockLoRaWAN = (hass: MockHomeAssistant) => {
const deviceList = devices();
const snapshot = {
devices: deviceList,
connections: [
...new Map(
deviceList.map((device) => [
device.connection.entry_id,
device.connection,
])
).values(),
],
};
hass.mockWS("lorawan/devices/list", () => snapshot);
hass.mockWS("lorawan/devices/subscribe", (_msg, _hass, onChange) =>
emitInitial(() => onChange?.(snapshot))
);
};
+11 -9
View File
@@ -15,6 +15,7 @@ import type {
ZHANetworkBackup,
ZHANetworkSettings,
} from "../../../../../src/data/zha";
import { ZHADeviceType } from "../../../../../src/data/zha";
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
import { minutesAgo } from "../helpers";
@@ -56,7 +57,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-coordinator",
user_given_name: "Home Assistant Connect ZBT-1",
power_source: "Mains",
device_type: "Coordinator",
device_type: ZHADeviceType.Coordinator,
active_coordinator: true,
signature: {},
neighbors: [
@@ -83,7 +84,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-porch-light",
user_given_name: "Porch light",
power_source: "Mains",
device_type: "Router",
device_type: ZHADeviceType.Router,
active_coordinator: false,
signature: {},
neighbors: [
@@ -110,7 +111,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-hall-motion",
user_given_name: "Hall motion",
power_source: "Battery",
device_type: "EndDevice",
device_type: ZHADeviceType.EndDevice,
active_coordinator: false,
signature: {},
neighbors: [neighbor(PORCH_IEEE, "0x1a2b", "142", "Parent", "1")],
@@ -133,7 +134,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-tv-plug",
user_given_name: "TV plug",
power_source: "Mains",
device_type: "Router",
device_type: ZHADeviceType.Router,
active_coordinator: false,
signature: {},
neighbors: [neighbor(COORDINATOR_IEEE, "0x0000", "198", "Parent", "0")],
@@ -156,7 +157,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-kitchen-switch",
user_given_name: "Kitchen switch",
power_source: "Battery",
device_type: "EndDevice",
device_type: ZHADeviceType.EndDevice,
active_coordinator: false,
signature: {},
neighbors: [neighbor(PORCH_IEEE, "0x1a2b", "186", "Parent", "1")],
@@ -179,7 +180,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-landing-sensor",
user_given_name: "Landing sensor",
power_source: "Battery",
device_type: "EndDevice",
device_type: ZHADeviceType.EndDevice,
active_coordinator: false,
signature: {},
neighbors: [neighbor(OFFICE_IEEE, "0x7a8b", "164", "Parent", "2")],
@@ -202,7 +203,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-garage-contact",
user_given_name: "Garage contact",
power_source: "Battery",
device_type: "EndDevice",
device_type: ZHADeviceType.EndDevice,
active_coordinator: false,
signature: {},
neighbors: [neighbor(OFFICE_IEEE, "0x7a8b", "118", "Parent", "2")],
@@ -225,7 +226,7 @@ const DEVICES: ZHADevice[] = [
device_reg_id: "zha-office-plug",
user_given_name: "Office plug",
power_source: "Mains",
device_type: "Router",
device_type: ZHADeviceType.Router,
active_coordinator: false,
signature: {},
neighbors: [
@@ -593,7 +594,8 @@ export const mockZha = (hass: MockHomeAssistant) => {
hass.mockWS("zha/devices/bindable", (msg: { ieee: string }) =>
DEVICES.filter(
(candidate) =>
candidate.device_type === "Router" && candidate.ieee !== msg.ieee
candidate.device_type === ZHADeviceType.Router &&
candidate.ieee !== msg.ieee
)
);
hass.mockWS("zha/devices/bind", () => undefined);
+255 -3
View File
@@ -1,25 +1,277 @@
import type {
EntityRegistryDisplayEntryResponse,
EntityRegistryEntry,
ExtEntityRegistryEntry,
UpdateEntityRegistryEntryResult,
} from "../../../src/data/entity/entity_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
let entries: EntityRegistryEntry[] = [];
const newEntry = (
entry: Pick<EntityRegistryEntry, "entity_id" | "platform" | "unique_id">
): EntityRegistryEntry => {
const now = Date.now() / 1000;
return {
config_entry_id: null,
config_subentry_id: null,
device_id: null,
area_id: null,
disabled_by: null,
id: entry.entity_id,
name: null,
icon: null,
labels: [],
categories: {},
hidden_by: null,
entity_category: null,
has_entity_name: false,
options: null,
created_at: now,
modified_at: now,
...entry,
};
};
// The registry settings that the display registry holds. A display entry
// without a registry name shows the name of its state.
const displayFields = (entry: EntityRegistryEntry) => ({
...(entry.name && { name: entry.name }),
icon: entry.icon ?? undefined,
area_id: entry.area_id ?? undefined,
labels: entry.labels,
hidden: entry.hidden_by !== null,
});
const extEntry = (entry: EntityRegistryEntry): ExtEntityRegistryEntry => ({
...entry,
capabilities: {},
aliases: [],
});
export const mockEntityRegistry = (
hass: MockHomeAssistant,
data: EntityRegistryEntry[] = []
) => {
hass.mockWS("config/entity_registry/list", () => data);
entries = [...data];
// A new array, so subscribers see the registry changed after an update
hass.mockWS("config/entity_registry/list", () => [...entries]);
hass.mockWS(
"config/entity_registry/get_entries",
(msg: { entity_ids: string[] }) => {
const result: Record<string, ExtEntityRegistryEntry> = {};
for (const entityId of msg.entity_ids) {
const entry = data.find((e) => e.entity_id === entityId);
const entry = entries.find((e) => e.entity_id === entityId);
if (entry) {
result[entityId] = { ...entry, capabilities: {}, aliases: [] };
result[entityId] = extEntry(entry);
}
}
return result;
}
);
hass.mockWS(
"config/entity_registry/update",
(
msg: Pick<
EntityRegistryEntry,
| "entity_id"
| "name"
| "icon"
| "area_id"
| "labels"
| "categories"
| "hidden_by"
| "disabled_by"
> & { new_entity_id?: string },
currentHass: MockHomeAssistant
): UpdateEntityRegistryEntryResult => {
const {
entity_id: entityId,
new_entity_id: newEntityId,
...updates
} = msg;
// Renaming would have to move the entity in every mock, so refuse it
// instead of reporting a rename that did not happen
if (newEntityId && newEntityId !== entityId) {
throw new Error("Changing the entity ID is not supported in the demo");
}
// The demo entities without an entry get one, like any core entity has
const entry = {
...(entries.find((e) => e.entity_id === entityId) ??
newEntry({
entity_id: entityId,
platform: currentHass.entities[entityId]?.platform ?? "demo",
unique_id: entityId,
})),
...Object.fromEntries(
Object.entries(updates).filter(([key]) => key !== "type")
),
modified_at: Date.now() / 1000,
};
entries = [...entries.filter((e) => e.entity_id !== entityId), entry];
// Core updates the display registry too, only change what was updated
const display = currentHass.entities[entityId];
if (display) {
const fields = displayFields(entry);
currentHass.updateHass({
entities: {
...currentHass.entities,
[entityId]: {
...display,
...("name" in updates && { name: fields.name }),
...("icon" in updates && { icon: fields.icon }),
...("area_id" in updates && { area_id: fields.area_id }),
...("labels" in updates && { labels: fields.labels }),
...("hidden_by" in updates && { hidden: fields.hidden }),
},
},
});
}
currentHass.mockEvent("entity_registry_updated");
return { entity_entry: extEntry(entry) };
}
);
};
// Entries of a platform that were replaced, so their settings are restored
// when the entity comes back after switching demos
const retiredEntries = new Map<string, EntityRegistryEntry>();
// Replaces the entries of a platform, like core does for the entities of a
// demo config
export const setPlatformEntityRegistryEntries = (
hass: MockHomeAssistant,
platform: string,
platformEntries: Pick<EntityRegistryEntry, "entity_id" | "unique_id">[]
) => {
for (const entry of entries) {
if (entry.platform === platform) {
retiredEntries.set(`${platform}.${entry.unique_id}`, entry);
}
}
const restored: EntityRegistryEntry[] = [];
const platformRegistryEntries = platformEntries.map((entry) => {
const retired = retiredEntries.get(`${platform}.${entry.unique_id}`);
if (!retired) {
return newEntry({ ...entry, platform });
}
const restoredEntry = { ...retired, entity_id: entry.entity_id };
restored.push(restoredEntry);
return restoredEntry;
});
entries = [
...entries.filter((e) => e.platform !== platform),
...platformRegistryEntries,
];
// The display registry was rebuilt with the entities, restore its settings
if (restored.length) {
const entities = { ...hass.entities };
for (const entry of restored) {
if (entities[entry.entity_id]) {
entities[entry.entity_id] = {
...entities[entry.entity_id],
...displayFields(entry),
};
}
}
hass.updateHass({ entities });
}
hass.mockEvent("entity_registry_updated");
};
export const getEntityRegistryEntry = (entityId: string) =>
entries.find((e) => e.entity_id === entityId);
export const removeEntityRegistryEntry = (
hass: MockHomeAssistant,
entityId: string
) => {
entries = entries.filter((e) => e.entity_id !== entityId);
hass.mockEvent("entity_registry_updated");
};
export const removeLabelFromEntityRegistry = (
hass: MockHomeAssistant,
labelId: string
) => {
const withoutLabel = (labels: string[]) =>
labels.filter((id) => id !== labelId);
for (const [key, entry] of retiredEntries) {
if (entry.labels.includes(labelId)) {
retiredEntries.set(key, { ...entry, labels: withoutLabel(entry.labels) });
}
}
if (!entries.some((entry) => entry.labels.includes(labelId))) {
return;
}
entries = entries.map((entry) =>
entry.labels.includes(labelId)
? { ...entry, labels: withoutLabel(entry.labels) }
: entry
);
const entities = { ...hass.entities };
for (const entry of entries) {
const entity = entities[entry.entity_id];
if (entity?.labels.includes(labelId)) {
entities[entry.entity_id] = {
...entity,
labels: withoutLabel(entity.labels),
};
}
}
hass.updateHass({ entities });
hass.mockEvent("entity_registry_updated");
};
// Adds an entry like core does when an integration creates an entity
export const addEntityRegistryEntry = (
hass: MockHomeAssistant,
entry: Pick<EntityRegistryEntry, "entity_id" | "platform" | "unique_id">
) => {
entries = [
...entries.filter((e) => e.entity_id !== entry.entity_id),
newEntry(entry),
];
hass.mockEvent("entity_registry_updated");
};
// The app refetches the display registry on entity_registry_updated events.
// Answer with the current display registry, which the demo keeps in hass.
export const mockEntityRegistryDisplay = (hass: MockHomeAssistant) =>
hass.mockWS(
"config/entity_registry/list_for_display",
(
_msg,
currentHass: MockHomeAssistant
): EntityRegistryDisplayEntryResponse => {
const categories = [
...new Set(
Object.values(currentHass.entities)
.map((entity) => entity.entity_category)
.filter((category) => category !== undefined)
),
];
return {
entities: Object.values(currentHass.entities).map((entity) => ({
ei: entity.entity_id,
di: entity.device_id,
ai: entity.area_id,
np: entity.next_name_part,
lb: entity.labels,
ec: entity.entity_category
? categories.indexOf(entity.entity_category)
: undefined,
en: entity.name,
ic: entity.icon,
pl: entity.platform ?? "demo",
tk: entity.translation_key,
hb: entity.hidden,
dp: entity.display_precision,
hn: entity.has_entity_name,
})),
entity_categories: Object.fromEntries(
categories.map((category, index) => [index, category])
),
};
}
);
+22 -2
View File
@@ -31,7 +31,7 @@ const exposedEntities: Record<string, ExposeEntitySettings> = {
export const mockExpose = (hass: MockHomeAssistant) => {
hass.mockWS("homeassistant/expose_entity/list", () => ({
exposed_entities: exposedEntities,
exposed_entities: structuredClone(exposedEntities),
}));
hass.mockWS(
"homeassistant/expose_new_entities/get",
@@ -39,6 +39,26 @@ export const mockExpose = (hass: MockHomeAssistant) => {
expose_new: msg.assistant !== "cloud.google_assistant",
})
);
hass.mockWS("homeassistant/expose_entity", () => null);
hass.mockWS(
"homeassistant/expose_entity",
(msg: {
assistants: (keyof ExposeEntitySettings)[];
entity_ids: string[];
should_expose: boolean;
}) => {
const changedEntityIds = msg.entity_ids.filter((entityId) => {
const settings = (exposedEntities[entityId] ??= {});
let changed = false;
for (const assistant of msg.assistants) {
if (Boolean(settings[assistant]) !== msg.should_expose) {
changed = true;
}
settings[assistant] = msg.should_expose;
}
return changed;
});
return { changed_entity_ids: changedEntityIds };
}
);
hass.mockWS("homeassistant/expose_new_entities/set", () => null);
};
+11
View File
@@ -12,6 +12,17 @@ export const mockIcons = (hass: MockHomeAssistant) => {
category: IconCategory;
integration?: string;
}) => {
if (category === "triggers" || category === "conditions") {
const { triggerIcons, conditionIcons } =
await import("./automation_platforms_icons");
const icons: Record<string, unknown> =
category === "triggers" ? triggerIcons : conditionIcons;
return {
resources: integration
? { [integration]: icons[integration] ?? {} }
: icons,
};
}
if (integration) {
try {
const response = await fetch(
+25 -1
View File
@@ -1,7 +1,31 @@
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { removeLabelFromEntityRegistry } from "./entity_registry";
let labels: LabelRegistryEntry[] = [];
export const getLabelIds = () => labels.map((label) => label.label_id);
export const mockLabelRegistry = (
hass: MockHomeAssistant,
data: LabelRegistryEntry[] = []
) => hass.mockWS("config/label_registry/list", () => data);
) => {
labels = [...data];
hass.mockWS("config/label_registry/list", () =>
labels.map((label) => ({ ...label }))
);
hass.mockWS(
"config/label_registry/delete",
(msg: { label_id: string }, currentHass: MockHomeAssistant) => {
const index = labels.findIndex(
(label) => label.label_id === msg.label_id
);
if (index === -1) {
throw new Error(`Label ${msg.label_id} not found`);
}
labels.splice(index, 1);
removeLabelFromEntityRegistry(currentHass, msg.label_id);
hass.mockEvent("label_registry_updated");
}
);
};
+16 -2
View File
@@ -1,4 +1,5 @@
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { LovelaceDashboard } from "../../../src/data/lovelace/dashboard";
import type { LovelaceInfo } from "../../../src/data/lovelace/resource";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import {
@@ -11,10 +12,14 @@ import "../custom-cards/ha-demo-card";
import "../custom-cards/ha-demo-next-card";
import { mockMap } from "./map";
let dashboards: LovelaceDashboard[] = [];
export const mockLovelace = (
hass: MockHomeAssistant,
localizePromise: Promise<LocalizeFunc>
localizePromise: Promise<LocalizeFunc>,
data: LovelaceDashboard[] = []
) => {
dashboards = data;
hass.mockWS("lovelace/config", ({ url_path }) => {
if (url_path === "map") {
mockMap(hass);
@@ -34,7 +39,16 @@ export const mockLovelace = (
);
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
hass.mockWS("lovelace/dashboards/list", () =>
dashboards.map((dashboard) => ({ ...dashboard }))
);
hass.mockWS("lovelace/dashboards/delete", (msg: { dashboard_id: string }) => {
const index = dashboards.findIndex((d) => d.id === msg.dashboard_id);
if (index === -1) {
throw new Error(`Dashboard ${msg.dashboard_id} not found`);
}
dashboards.splice(index, 1);
});
};
customElements.whenDefined("hui-root").then(() => {
+8 -1
View File
@@ -221,7 +221,14 @@ const personEntity = (person: DemoPerson, now: number): EntityInput => {
const personHistory = (person: DemoPerson, now: number): EntityHistoryState[] =>
person.timeline.map(([hoursAgo, state]) => {
const time = (now - hoursAgo * HOUR) / 1000;
return { s: state, a: {}, lu: time, lc: time };
const zoneId =
state === "home" ? "home" : zones.find((zone) => zone.name === state)?.id;
return {
s: state,
a: { in_zones: zoneId ? [`zone.${zoneId}`] : [] },
lu: time,
lc: time,
};
});
export const mockMap = (hass: MockHomeAssistant) => {
+209
View File
@@ -0,0 +1,209 @@
import type { MarketplaceInfo } from "../../../src/data/marketplace/marketplace";
import type {
MarketplaceRelease,
RepositoryBase,
RepositoryInfo,
} from "../../../src/data/marketplace/repository";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
const baseRepository = {
can_install: true,
config_flow: false,
custom: false,
domain: null,
file_name: "",
hide: false,
homeassistant: null,
installed: false,
installed_version: "",
local_path: "",
new: false,
pending_upgrade: false,
status: "default" as const,
};
const repositories: RepositoryBase[] = [
{
...baseRepository,
id: "1",
category: "integration",
full_name: "AlexxIT/WebRTC",
name: "WebRTC Camera",
description: "Watch camera streams with low latency through WebRTC.",
authors: ["@AlexxIT"],
domain: "webrtc",
config_flow: true,
available_version: "v3.6.1",
downloads: 98000,
stars: 1900,
last_updated: "2026-09-12T08:21:00Z",
topics: ["camera", "webrtc"],
},
{
...baseRepository,
id: "2",
category: "integration",
full_name: "basnijholt/adaptive-lighting",
name: "Adaptive Lighting",
description:
"Change the brightness and color temperature of your lights over the day.",
authors: ["@basnijholt"],
domain: "adaptive_lighting",
config_flow: true,
available_version: "v1.26.0",
installed: true,
installed_version: "v1.26.0",
status: "installed",
downloads: 120000,
stars: 2300,
last_updated: "2026-08-30T17:02:00Z",
topics: ["lights", "circadian-rhythm"],
},
{
...baseRepository,
id: "3",
category: "integration",
full_name: "blakeblackshear/frigate-hass-integration",
name: "Frigate",
description: "Use the cameras and events of a Frigate NVR.",
authors: ["@blakeblackshear", "@dermotduffy", "@NickM-27"],
domain: "frigate",
config_flow: true,
available_version: "v5.9.0",
installed: true,
installed_version: "v5.8.0",
pending_upgrade: true,
status: "pending-upgrade",
downloads: 150000,
stars: 900,
last_updated: "2026-09-25T11:45:00Z",
topics: ["camera", "nvr"],
},
{
...baseRepository,
id: "4",
category: "plugin",
full_name: "piitaya/lovelace-mushroom",
name: "Mushroom",
description: "A collection of cards to build a clean dashboard.",
authors: ["@piitaya"],
file_name: "mushroom.js",
available_version: "v5.1.0",
installed: true,
installed_version: "v5.1.0",
status: "installed",
downloads: 310000,
stars: 4400,
last_updated: "2026-09-02T09:10:00Z",
topics: ["cards", "dashboard"],
},
{
...baseRepository,
id: "5",
category: "plugin",
full_name: "thomasloven/lovelace-card-mod",
name: "card-mod",
description: "Add CSS styles to any card of your dashboard.",
authors: ["@thomasloven"],
file_name: "card-mod.js",
available_version: "v4.0.0",
downloads: 280000,
stars: 1500,
last_updated: "2026-07-19T14:33:00Z",
topics: ["css", "dashboard"],
},
{
...baseRepository,
id: "6",
category: "plugin",
full_name: "custom-cards/button-card",
name: "button-card",
description: "A button card that you can change in almost every way.",
authors: ["@RomRider"],
file_name: "button-card.js",
available_version: "v5.0.0",
new: true,
status: "new",
downloads: 240000,
stars: 2100,
last_updated: "2026-09-28T19:05:00Z",
topics: ["button", "cards"],
},
{
...baseRepository,
id: "7",
category: "theme",
full_name: "catppuccin/home-assistant",
name: "Catppuccin",
description: "Soothing pastel themes for Home Assistant.",
authors: ["@catppuccin"],
file_name: "catppuccin.yaml",
available_version: "v2.1.0",
downloads: 45000,
stars: 300,
last_updated: "2026-06-11T07:48:00Z",
topics: ["theme"],
},
];
const repositoryInfo = (repository: RepositoryBase): RepositoryInfo => ({
...repository,
additional_info: `# ${repository.name}\n\n${repository.description}\n\nThis is a demo. Installing is not available.`,
default_branch: "main",
issues: 12,
releases: [repository.available_version],
ref: repository.available_version,
replaces_built_in: false,
selected_tag: null,
update_entity_id: null,
version_or_commit: "version",
});
const info: MarketplaceInfo = {
categories: ["integration", "plugin", "theme"],
disabled_reason: null,
github_connected: false,
has_pending_tasks: false,
lovelace_mode: "storage",
stage: "running",
startup: false,
version: "2.0.0",
warning_accepted: true,
};
// Commands that change the Marketplace are not mocked, so the data never
// changes and no signal fires.
export const mockMarketplace = (hass: MockHomeAssistant) => {
hass.mockWS("marketplace/info", () => info);
hass.mockWS("marketplace/repositories/list", () => repositories);
hass.mockWS(
"marketplace/repository/info",
({ repository_id }: { repository_id: string }) => {
const repository = repositories.find((repo) => repo.id === repository_id);
return repository
? repositoryInfo(repository)
: Promise.reject({ code: "not_found", message: "Unknown repository" });
}
);
hass.mockWS(
"marketplace/repository/releases",
({ repository_id }: { repository_id: string }): MarketplaceRelease[] => {
const repository = repositories.find((repo) => repo.id === repository_id);
return repository
? [
{
tag: repository.available_version,
name: repository.available_version,
published_at: String(repository.last_updated),
prerelease: false,
},
]
: [];
}
);
hass.mockWS("marketplace/subscribe", () => () => undefined);
};
+69 -22
View File
@@ -1,8 +1,10 @@
import { TZDate } from "@date-fns/tz";
import {
addDays,
addHours,
addMinutes,
addMonths,
constructFrom,
differenceInHours,
endOfDay,
} from "date-fns";
@@ -13,6 +15,18 @@ import type {
} from "../../../src/data/recorder";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
const getFirstDate = (
start: Date,
period: "5minute" | "hour" | "day" | "month"
): Date => {
if (period === "day" || period === "month") {
return constructFrom(start, start);
}
// Core returns UTC-aligned buckets whose start is at or after the request.
const interval = (period === "hour" ? 60 : 5) * 60 * 1000;
return constructFrom(start, Math.ceil(start.getTime() / interval) * interval);
};
const getNextDate = (
currentDate: Date,
period: "5minute" | "hour" | "day" | "month"
@@ -33,8 +47,7 @@ const generateMeanStatistics = (
maxDiff: number
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let currentDate = getFirstDate(start, period);
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
@@ -60,16 +73,17 @@ const generateSumStatistics = (
end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
maxDiff: number | ((start: Date, end: Date) => number)
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let currentDate = getFirstDate(start, period);
let sum = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const nextDate = getNextDate(currentDate, period);
const add = Math.random() * maxDiff;
const bound =
typeof maxDiff === "function" ? maxDiff(currentDate, nextDate) : maxDiff;
const add = Math.random() * bound;
sum += add;
statistics.push({
start: currentDate.getTime(),
@@ -96,8 +110,7 @@ const generateCurvedStatistics = (
metered: boolean
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let currentDate = getFirstDate(start, period);
let sum = initValue;
const hours = differenceInHours(end, start) - 1;
let i = 0;
@@ -136,6 +149,15 @@ const statisticsFunctions: Record<
period: "5minute" | "hour" | "day" | "month"
) => StatisticValue[]
> = {
"sensor.energy_water": (_id, start, end, period = "hour") =>
generateSumStatistics(
start,
end,
period,
4000,
(bucketStart, bucketEnd) =>
(20 * (bucketEnd.getTime() - bucketStart.getTime())) / 3600000
),
"sensor.energy_consumption_tarif_1": (
_id: string,
start: Date,
@@ -151,9 +173,9 @@ const statisticsFunctions: Record<
period === "day" ? 17 : 504
);
}
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
const morningEnd = addHours(start, 10);
const morningLow = generateSumStatistics(start, morningEnd, period, 0, 0.7);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const eveningStart = addHours(start, 20);
const morningFinalVal = morningLow.length
? morningLow[morningLow.length - 1].sum!
: 0;
@@ -188,8 +210,8 @@ const statisticsFunctions: Record<
period === "day" ? 17 : 504
);
}
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const morningEnd = addHours(start, 9);
const eveningStart = addHours(start, 20);
const highTarif = generateSumStatistics(
morningEnd,
eveningStart,
@@ -228,9 +250,12 @@ const statisticsFunctions: Record<
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const productionStart = addHours(start, 9);
const productionEnd = addHours(start, 21);
const dayEnd = constructFrom(
end,
Math.min(endOfDay(productionEnd).getTime(), end.getTime())
);
const production = generateCurvedStatistics(
productionStart,
productionEnd,
@@ -269,9 +294,12 @@ const statisticsFunctions: Record<
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const productionStart = addHours(start, 7);
const productionEnd = addHours(start, 23);
const dayEnd = constructFrom(
end,
Math.min(endOfDay(productionEnd).getTime(), end.getTime())
);
const production = generateCurvedStatistics(
productionStart,
productionEnd,
@@ -301,6 +329,11 @@ const statisticsFunctions: Record<
return [...morning, ...production, ...evening, ...rest];
},
};
// Prices are per source unit; water uses litres (4 currency units per m³).
const derivedCosts: Record<string, { source: string; price: number }> = {
"sensor.energy_water_cost": { source: "sensor.energy_water", price: 0.004 },
};
export const mockRecorder = (mockHass: MockHomeAssistant) => {
mockHass.mockWS(
"recorder/get_statistics_metadata",
@@ -313,14 +346,28 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
mockHass.mockWS(
"recorder/statistics_during_period",
({ statistic_ids, start_time, end_time, period }, hass): Statistics => {
const start = new Date(start_time);
const end = end_time ? new Date(end_time) : new Date();
// Calendar buckets follow the server timezone, independent of the browser.
const start = new TZDate(start_time, hass.config.time_zone);
const end = new TZDate(end_time ?? Date.now(), hass.config.time_zone);
const statistics: Record<string, StatisticValue[]> = {};
const cache: Record<string, StatisticValue[]> = {};
const getStats = (id: string) => {
cache[id] ??= statisticsFunctions[id](id, start, end, period);
return cache[id];
};
statistic_ids.forEach((id: string) => {
if (id in statisticsFunctions) {
statistics[id] = statisticsFunctions[id](id, start, end, period);
if (id in derivedCosts) {
const { source, price } = derivedCosts[id];
let cost = 0;
statistics[id] = getStats(source).map((statistic) => {
const change = statistic.change! * price;
cost += change;
return { ...statistic, change, state: cost, sum: cost };
});
} else if (id in statisticsFunctions) {
statistics[id] = getStats(id);
} else {
const entityState = hass.states[id];
const state = entityState ? Number(entityState.state) : 1;
+6
View File
@@ -12,6 +12,7 @@ import { configs as wcConfigs } from "eslint-plugin-wc";
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
import html from "@html-eslint/eslint-plugin";
import importX from "eslint-plugin-import-x";
import ha from "./build-scripts/eslint-rules/index.mjs";
const rspackConfigPath = fileURLToPath(
new URL("./rspack.config.cjs", import.meta.url)
@@ -33,6 +34,7 @@ export default tseslint.config(
{
plugins: {
"unused-imports": unusedImports,
ha,
},
languageOptions: {
@@ -231,6 +233,10 @@ export default tseslint.config(
],
"unused-imports/no-unused-imports": "error",
// Off because the existing backlog would fail lint:eslint's
// --max-warnings=0; run lint:element-imports to see it. Disable
// comments for it would be reported as unused, so none until it is on.
"ha/no-unused-element-import": "off",
"lit/attribute-names": "error",
"lit/attribute-value-entities": "off",
"lit/no-template-map": "off",
+9
View File
@@ -37,9 +37,11 @@ export default [
pages: [
"ha-form",
"ha-selector",
"ha-target-picker-exclusions",
"ha-select-box",
"ha-input",
"ha-textarea",
"ha-picker-combo-box",
],
},
{
@@ -52,6 +54,7 @@ export default [
"ha-control-switch",
"ha-slider",
"ha-control-slider",
"ha-control-scrubber",
"ha-control-circular-slider",
"ha-control-number-buttons",
"ha-control-select",
@@ -101,6 +104,7 @@ export default [
"entity-button-card",
"entity-filter-card",
"glance-card",
"sensor-card",
"tile-card",
"area-card",
],
@@ -123,6 +127,7 @@ export default [
"gauge-card",
"plant-card",
"map-card",
"calendar-card",
"media-control-card",
"media-player-row",
],
@@ -164,6 +169,10 @@ export default [
header: "Inputs and values",
pages: ["input-number", "input-text", "number", "timer"],
},
{
header: "Time and schedules",
pages: ["calendar"],
},
{
header: "System",
pages: ["update"],
+2 -5
View File
@@ -1,6 +1,5 @@
import { html, css, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import type { HomeAssistant } from "../../../src/types";
import "./demo-card";
@@ -18,9 +17,7 @@ class DemoCards extends LitElement {
render() {
return html`
<ha-demo-options>
<ha-formfield label="Show config">
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
</ha-formfield>
<ha-switch @change=${this._showConfigToggled}>Show config</ha-switch>
</ha-demo-options>
<div id="container">
<div class="cards">
@@ -51,7 +48,7 @@ class DemoCards extends LitElement {
demo-card {
margin: 16px 16px 32px;
}
ha-formfield {
ha-switch {
margin-right: 16px;
}
#container {
Loaded 100 of 1654 files, more files were not shown because too many files have changed in this diff. Show more