jsdom has no WebGL2, so the existing ha-map tests only ever run Leaflet.
With the WebGL2 probe stubbed and the MapLibre engine replaced by a fake,
this covers the runtime choice: MapLibre with WebGL2, Leaflet without it,
Leaflet after MapLibre fails to start, and a rebuild on Leaflet with the
entities redrawn after a fatal engine event.
ha-map no longer drives Leaflet directly. All primitive operations -
camera, HTML element markers, meter-radius circles, history paths,
clustering with a pluggable icon builder, scale ruler, dark mode - go
through a MapEngine interface, and the engine is selected at runtime:
- MapLibreMapEngine renders the vector base map natively where WebGL2 is
available, without Leaflet in the loop: continuous fractional zoom,
DOM markers synced to the basemap every frame, GeoJSON zone and
accuracy circles below the labels, GeoJSON history trails with hover
popups, and a pixel-distance cluster grid recomputed when the camera
settles. Dark mode swaps the style while carrying the custom layers
over, and rotation and pitch stay disabled for north-up dashboards.
- LeafletMapEngine wraps the existing behavior unchanged (vector tiles
through the maplibre adapter with the raster fallback, markercluster)
for browsers without WebGL2 - the legacy floor includes iOS 12-14 and
kiosk browsers without hardware acceleration - and for
ha-locations-editor, which manages raw Leaflet layers with
leaflet-draw and declares engine=leaflet.
A permanently lost WebGL context rebuilds the map on the Leaflet engine
instead of leaving a dead canvas. Zoom levels keep Leaflet semantics
across engines. MapLibre's stylesheet is shipped to /static/map for the
native engine's controls and popups.
* Exit scene live mode when leaving the editor
Leaving the page previously only unsubscribed, so live-activated
device states stayed behind. Restore stored states unless the tab
is hidden for panel suspend.
* Drop extra unit tests for scene live-mode disconnect.
* Restore scene live states only on editor disconnect.
Back and delete already unmount the editor, which now exits live
mode, so a second applyScene was redundant.
* Resubscribe to scene live edits after hidden-tab suspend.
Existing scenes stayed in live mode when the panel was reattached,
but the state_changed subscription was not restored.
* Keep the scene live subscription across hidden-tab suspend.
Unsubscribing and resubscribing on reattach races the closed
websocket; leaving the subscription in place lets the library
restore it after reconnect.
* Tear down scene live mode on hidden-tab route changes.
document.hidden is not enough: saving a new scene remounts a
new editor. Skip teardown only when an ancestor was detached
(panel suspend). Reuse an existing live subscription on
reattach so new scenes do not get duplicate listeners.
Co-authored-by: Petar Petrov <[email protected]>
---------
Co-authored-by: Cursor Agent <[email protected]>
* Serial: send consumers to their own config panel
A port used by Zigbee or Z-Wave led to that integration's entry on the
integration page, which is a device list — not where the radio on that port is
managed. Point those consumers at the integration's own panel instead.
Which panel that is was already decided in `ha-config-entry-row`, from the
panels an integration registers at runtime falling back to
`integrationsWithPanel`. That resolution moves to `getConfigPanelPath` so both
callers stay in step, and so a port picks up any panel the integration page
would already link to.
A consumer that is not running keeps its integration-page link: its panel is
not loaded to receive it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FjmjZsxhdj9ekGmhFHXg3p
* Serial: send the Thread radio to the Thread panel
A Thread radio is held by the Open Thread Border Router app rather than by a
config entry, so it never reached the panel resolution. Map the app to the
integration behind it and look the panel up from there.
The panel is only offered when its integration is loaded, which an app being
started does not imply.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FjmjZsxhdj9ekGmhFHXg3p
---------
Co-authored-by: Claude Opus 5 <[email protected]>
* Add Bluetooth to the demo
Load the bluetooth integration and mock its scanner, advertisement and
connection subscriptions, so the Bluetooth panel, its adapter info,
monitors and network map render in the demo.
Also adds the scaffolding the other Settings > Connectivity panels build
on: a per-integration fixtures contract, registry builders, and the two
registries that collect components, WS command prefixes and registry data
from each integration. `addEntities` now carries `device_id` and
`platform` into the mocked display entity registry, which the panels use
to count entities per device and per integration.
Mocked subscriptions emit their first message from a timeout. Subscribing
is synchronous in the mock, so an immediate callback lands before the
subscriber is ready: `createCollection` overwrote it with its empty
initial fetch, and pages that ignore messages received before their first
render dropped it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Serve integration manifests for the demo's config entries
The integration page reads back the manifest it fetches without guarding,
so opening one for a domain the demo had no manifest for threw. Bluetooth
now ships its manifest through the connectivity fixtures, and manifest/get
falls back to a generated manifest rather than answering with undefined,
which also covers the entries that already lacked one.
Adds a demo E2E test that opens the Bluetooth panel and asserts its counts
are non-zero. The connectivity mocks reach the panel through lazy mock
registration and deferred first emissions, neither of which the demo suite
covered; without them the panel renders but stays empty.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Cover the Bluetooth scanner details subscription in the demo E2E test
The dashboard's adapter count comes from the config entries, so the test
passed even with the scanner details subscription broken, while the
adapter page and network map stayed incomplete.
The adapter page renders a settings button only for adapters that are not
remote scanners, which it knows from the scanner type in those details, so
asserting one button across the three adapters covers the subscription
without asserting on copy.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep connectivity entities when switching demos
Switching demos replaces the whole state map, and only the energy entities
were added back, so every connectivity state disappeared until a reload.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Add Thread to the demo
Load the thread and otbr integrations and mock router discovery, dataset
listing and border router info, so the Thread panel renders in the demo.
The preferred network holds Home Assistant's own border router alongside a
HomePod mini and a Nest Hub; an Amazon Echo forms the one other network.
Widens ThreadRouter's brand union with "amazon", which the backend
already returns. The union is still not exhaustive.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Mock the Thread actions and match the dataset casing
The extended PAN ID was lowercase while the TLV string carries it uppercase.
The dataset dialog looks for one inside the other with a case-sensitive
`includes`, so the preferred network's info showed neither the border
router's URL nor its active dataset.
The panel also offers adding a dataset, choosing the preferred network and
border router, resetting the border router and changing its channel, none of
which were mocked, so each rejected as unimplemented. They now change the
mocked data, and `otbr/set_channel` answers with the delay the panel reads
back.
`ThreadRouter["brand"]` is no longer a union. It only reaches `brandsUrl`,
and the backend resolves it from the border agent's vendor name, so every
new vendor meant another frontend change for no typing benefit.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Push Thread network changes to the discovery stream
The panel groups the routers it draws from `thread/discover_routers`, not
from `otbr/info`, so changing the network only in the info left the border
router drawn under its old network for good. The mock now keeps its
subscribers and announces the router again when it moves.
A reset also has to put the border router on a network of its own instead of
reusing the existing extended PAN ID, which the panel groups by, and it adds
that network rather than taking over the preferred one, so the way back onto
the original network stays available.
`ThreadRouter["brand"]` is null for a vendor the backend does not know, and
the panel now leaves out the icon in that case rather than requesting one
that cannot exist.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Build the Thread datasets from real TLVs
The pasted dataset TLV was malformed: an odd number of hex digits, a network
name field declaring ten bytes for nine, and a network key that ran into the
next type, so a decoder derailed partway through. Datasets are now assembled
from type/length/value triplets, which keeps the declared lengths honest and
each dataset's own extended PAN ID and network name inside its TLV.
Each dataset carries its own, so `thread/get_dataset_tlv` answers for the
dataset it was asked about and rejects an unknown one, and moving the border
router carries the active dataset with it rather than leaving the previous
network's behind, which would have hidden the URL again on the new network.
An imported dataset takes its extended PAN ID from the type 0x02 field
instead of the first sixteen characters, which were the timestamp, and a
reset generates eight hexadecimal bytes rather than a value starting
"RESET".
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Import Thread datasets from the credentials given
The import invented the channel, network name and PAN ID and stored a
rebuilt TLV, so importing real credentials showed a different network than
the one submitted and read back different credentials. It now takes all four
fields from the TLV and keeps that TLV verbatim.
Anything that does not decode is refused rather than falling back to a
generated ID, so the panel shows its error path for malformed credentials.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Treat a Thread import as a revision of the network it names
Datasets sharing an extended PAN ID are revisions of one network, so
importing credentials for a network that is already known updates it rather
than adding a second card for it, and only a newer active timestamp wins.
Re-importing the credentials the demo itself shows used to duplicate the
network.
The active timestamp is now required, as the backend requires it, and a
reset leaves the default router unset, since nominating one is its own
action in the panel.
A router without a brand no longer reserves the avatar column, so its name
is not left indented against an empty graphic.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep the Thread dataset, credentials and channel in step
Deleting a dataset left its credentials behind, so reading them back returned
a network the panel had already dropped. They go with it now.
Changing the channel only moved the border router's reported channel, leaving
the dataset and its active TLV on the old one, so the channel prompt and the
dataset dialog disagreed on the next read. A reset had the mirror problem,
installing a channel 15 network without moving the reported channel. Both
carry the dataset, its TLV and the channel together.
Network names are decoded as UTF-8 rather than one character per byte, so a
non-ASCII name reads correctly and a sequence the backend would refuse is
refused here.
Adds a demo test for the panel: the two networks it starts with, the reset
and rejoin that only show up if the discovery subscription is told, and
credentials being per dataset and going away with one. Checked that it bites
by dropping the announcement, which fails it on the card count after a reset.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Refuse repeated tags and a zero channel in a Thread dataset
A repeated type is malformed and the backend's parser refuses it, rather
than letting the later one win, and a channel field carrying zero is not a
channel it accepts. An absent channel is still fine.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Change the Thread channel without rewriting the credentials
Rebuilding the dataset TLV from its summary replaced the network key, PSKc,
mesh-local prefix, security policy and anything else this mock does not
model with fixture constants, so changing the channel on an imported network
silently swapped its credentials for someone else's. Only the channel
triplet is rewritten now, leaving the rest of the TLV as it was.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Drop the remaining comments from the Thread mock
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
---------
Co-authored-by: Claude <[email protected]>
* Add Bluetooth to the demo
Load the bluetooth integration and mock its scanner, advertisement and
connection subscriptions, so the Bluetooth panel, its adapter info,
monitors and network map render in the demo.
Also adds the scaffolding the other Settings > Connectivity panels build
on: a per-integration fixtures contract, registry builders, and the two
registries that collect components, WS command prefixes and registry data
from each integration. `addEntities` now carries `device_id` and
`platform` into the mocked display entity registry, which the panels use
to count entities per device and per integration.
Mocked subscriptions emit their first message from a timeout. Subscribing
is synchronous in the mock, so an immediate callback lands before the
subscriber is ready: `createCollection` overwrote it with its empty
initial fetch, and pages that ignore messages received before their first
render dropped it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Serve integration manifests for the demo's config entries
The integration page reads back the manifest it fetches without guarding,
so opening one for a domain the demo had no manifest for threw. Bluetooth
now ships its manifest through the connectivity fixtures, and manifest/get
falls back to a generated manifest rather than answering with undefined,
which also covers the entries that already lacked one.
Adds a demo E2E test that opens the Bluetooth panel and asserts its counts
are non-zero. The connectivity mocks reach the panel through lazy mock
registration and deferred first emissions, neither of which the demo suite
covered; without them the panel renders but stays empty.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Cover the Bluetooth scanner details subscription in the demo E2E test
The dashboard's adapter count comes from the config entries, so the test
passed even with the scanner details subscription broken, while the
adapter page and network map stayed incomplete.
The adapter page renders a settings button only for adapters that are not
remote scanners, which it knows from the scanner type in those details, so
asserting one button across the three adapters covers the subscription
without asserting on copy.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep connectivity entities when switching demos
Switching demos replaces the whole state map, and only the energy entities
were added back, so every connectivity state disappeared until a reload.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Add Zigbee to the demo
Load the zha integration and mock its devices, groups, configuration and
network settings, so the Zigbee panel, its options, groups, network info
and network map render in the demo. Neighbor tables give the map a mesh of
routers and end devices.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Give the Zigbee temperature sensor a unique entity ID
sensor.bedroom_temperature already exists in the home demo config, whose
entities are added last and overwrote the display registry entry,
detaching it from its Zigbee device and undercounting the panel's
entities on that demo.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Label the Zigbee options and take the offline plug unavailable
The options page localizes every field through the backend translations and
falls back to the raw identifier, so the pages showed names like
alarm_master_code. Seed a key per schema field, for the global options as
well as the alarm ones.
The panel reports the TV plug as offline, so its entity has to agree.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Mock the Zigbee panel's write actions
Saving either options page, downloading a backup, changing the channel and
adding or removing a group or its members all rejected as unimplemented, so
every one of those visible controls failed. They now change the mocked state
that the panel refetches.
Saving merges into the stored configuration rather than replacing it, the
way the backend does, so saving one section leaves the rest alone.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Seed the ZHA unavailability timeouts and copy the configuration
The options page falls back to two hours when these keys are absent, so the
battery timeout showed 2 hours where ZHA's default is 6. Both are seeded
now.
The configuration response handed out the backing object, and both editors
mutate the fetched data as controls change, so an edit the user never saved
survived leaving the page. It is copied on the way out; only the update
command writes to the stored configuration.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* List the groupable Zigbee devices
The create group page and the add members dialog build their pickers from
this command, so returning nothing left both permanently empty and made the
group commands unreachable from the panel. The lights and plugs are listed
now.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep the Zigbee pairing page, backups and group rows honest
The add device page subscribes to the permit command as soon as it opens, so
with nothing registered it sat on its spinner for the full permit duration
behind a rejected subscription. A subscription that simply stays open is
enough, since nothing pairs in the demo.
A backup shared its network and node objects with the live settings, so
changing the channel afterwards rewrote the stored backup, which is the one
thing a backup must not do. They are copied now.
Group endpoints carried no entities, so both pickers labelled every light
and plug "No entities". Each now carries the entity the fixtures give it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Wire up the ZHA device pages in the demo
The ZHA info card and device actions look up the device's `zigbee`
connection and render nothing without it, so the demo devices only ever
showed the generic device page. Add the connection, and mock the two
commands that become reachable through it: the manage page's cluster
list (with each cluster's attributes and commands, so the attribute and
command tabs are usable), and the reconfiguration subscription, which
now walks the device's clusters and terminates instead of rejecting.
Written attributes are remembered, so reading one back agrees with what
the write button reported.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Carry the registry area on the ZHA device payloads
The group pickers read the area straight off the device payload rather
than resolving the device registry, so the plugs listed no area. The
backend fills it in from the registry; derive it from the registry
fixtures here for the same reason, so the two cannot drift apart.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Make the Zigbee bindings tab work
Both bind buttons ended in their error state: `zha/groups/bind` and
`zha/groups/unbind` were never registered. Registering them is not
enough on its own — group binding lists only a device's client side, so
with every mocked cluster on the server side the table read "No data"
and the button could never be enabled. The remotes now carry the `out`
clusters they would have on real hardware, and `zha/devices/bindable`
returns the routers, so the device half of the tab is no longer hidden.
Also drops the `any` from the configuration update payload, which
already has an exact type.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
---------
Co-authored-by: Claude <[email protected]>
* Add Bluetooth to the demo
Load the bluetooth integration and mock its scanner, advertisement and
connection subscriptions, so the Bluetooth panel, its adapter info,
monitors and network map render in the demo.
Also adds the scaffolding the other Settings > Connectivity panels build
on: a per-integration fixtures contract, registry builders, and the two
registries that collect components, WS command prefixes and registry data
from each integration. `addEntities` now carries `device_id` and
`platform` into the mocked display entity registry, which the panels use
to count entities per device and per integration.
Mocked subscriptions emit their first message from a timeout. Subscribing
is synchronous in the mock, so an immediate callback lands before the
subscriber is ready: `createCollection` overwrote it with its empty
initial fetch, and pages that ignore messages received before their first
render dropped it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Serve integration manifests for the demo's config entries
The integration page reads back the manifest it fetches without guarding,
so opening one for a domain the demo had no manifest for threw. Bluetooth
now ships its manifest through the connectivity fixtures, and manifest/get
falls back to a generated manifest rather than answering with undefined,
which also covers the entries that already lacked one.
Adds a demo E2E test that opens the Bluetooth panel and asserts its counts
are non-zero. The connectivity mocks reach the panel through lazy mock
registration and deferred first emissions, neither of which the demo suite
covered; without them the panel renders but stays empty.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Cover the Bluetooth scanner details subscription in the demo E2E test
The dashboard's adapter count comes from the config entries, so the test
passed even with the scanner details subscription broken, while the
adapter page and network map stayed incomplete.
The adapter page renders a settings button only for adapters that are not
remote scanners, which it knows from the scanner type in those details, so
asserting one button across the three adapters covers the subscription
without asserting on copy.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep connectivity entities when switching demos
Switching demos replaces the whole state map, and only the energy entities
were added back, so every connectivity state disappeared until a reload.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Add Z-Wave to the demo
Load the zwave_js integration and mock its network status, provisioning
entries and node and controller statistics, so the Z-Wave panel, its
options, statistics, network info, provisioned devices and network map
render in the demo. Last working routes give the map a mesh with
repeaters, asleep and dead nodes.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Give the Z-Wave motion sensor a unique entity ID
binary_sensor.kitchen_motion already exists in the home demo config, whose
entities are added last and overwrote the display registry entry,
detaching it from its Z-Wave device and undercounting the panel's entities
on that demo.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
---------
Co-authored-by: Claude <[email protected]>
* Add Bluetooth to the demo
Load the bluetooth integration and mock its scanner, advertisement and
connection subscriptions, so the Bluetooth panel, its adapter info,
monitors and network map render in the demo.
Also adds the scaffolding the other Settings > Connectivity panels build
on: a per-integration fixtures contract, registry builders, and the two
registries that collect components, WS command prefixes and registry data
from each integration. `addEntities` now carries `device_id` and
`platform` into the mocked display entity registry, which the panels use
to count entities per device and per integration.
Mocked subscriptions emit their first message from a timeout. Subscribing
is synchronous in the mock, so an immediate callback lands before the
subscriber is ready: `createCollection` overwrote it with its empty
initial fetch, and pages that ignore messages received before their first
render dropped it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Serve integration manifests for the demo's config entries
The integration page reads back the manifest it fetches without guarding,
so opening one for a domain the demo had no manifest for threw. Bluetooth
now ships its manifest through the connectivity fixtures, and manifest/get
falls back to a generated manifest rather than answering with undefined,
which also covers the entries that already lacked one.
Adds a demo E2E test that opens the Bluetooth panel and asserts its counts
are non-zero. The connectivity mocks reach the panel through lazy mock
registration and deferred first emissions, neither of which the demo suite
covered; without them the panel renders but stays empty.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Cover the Bluetooth scanner details subscription in the demo E2E test
The dashboard's adapter count comes from the config entries, so the test
passed even with the scanner details subscription broken, while the
adapter page and network map stayed incomplete.
The adapter page renders a settings button only for adapters that are not
remote scanners, which it knows from the scanner type in those details, so
asserting one button across the three adapters covers the subscription
without asserting on copy.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep connectivity entities when switching demos
Switching demos replaces the whole state map, and only the energy entities
were added back, so every connectivity state disappeared until a reload.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Add MQTT to the demo
Load the mqtt integration and mock topic subscriptions and device debug
info, so the MQTT panel renders in the demo and its listen card streams
messages on the topic you subscribe to.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Give the MQTT sensor entities unique entity IDs
sensor.kitchen_temperature already exists in the home demo config, whose
entities are added last and overwrote the display registry entry,
detaching it from its MQTT device and undercounting the panel's entities
on that demo.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Key the MQTT debug info by device and send real payloads
The debug info ignored the requested device, so the garage door's MQTT info
showed the fridge sensor's entity. Return the entities of the device that
was asked for, and nothing for one that is not MQTT.
The discovery payload is the config object, not a JSON string:
`mqtt-discovery-payload` dumps it as YAML, so a string rendered as one
quoted scalar. The frontend type said string while the component already
required an object; it now matches what the backend sends.
A subscription takes a topic filter, so echoing it back as the message topic
invented topics no broker could publish. Wildcard levels now resolve to a
concrete matching topic.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Allow an empty MQTT discovery payload
An entity with debug traffic but nothing discovered has no config, and the
backend sends an empty string, so narrowing the field to an object alone was
wrong in the other direction.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: Simon Lamon <[email protected]>
* Add Bluetooth to the demo
Load the bluetooth integration and mock its scanner, advertisement and
connection subscriptions, so the Bluetooth panel, its adapter info,
monitors and network map render in the demo.
Also adds the scaffolding the other Settings > Connectivity panels build
on: a per-integration fixtures contract, registry builders, and the two
registries that collect components, WS command prefixes and registry data
from each integration. `addEntities` now carries `device_id` and
`platform` into the mocked display entity registry, which the panels use
to count entities per device and per integration.
Mocked subscriptions emit their first message from a timeout. Subscribing
is synchronous in the mock, so an immediate callback lands before the
subscriber is ready: `createCollection` overwrote it with its empty
initial fetch, and pages that ignore messages received before their first
render dropped it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Serve integration manifests for the demo's config entries
The integration page reads back the manifest it fetches without guarding,
so opening one for a domain the demo had no manifest for threw. Bluetooth
now ships its manifest through the connectivity fixtures, and manifest/get
falls back to a generated manifest rather than answering with undefined,
which also covers the entries that already lacked one.
Adds a demo E2E test that opens the Bluetooth panel and asserts its counts
are non-zero. The connectivity mocks reach the panel through lazy mock
registration and deferred first emissions, neither of which the demo suite
covered; without them the panel renders but stays empty.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Cover the Bluetooth scanner details subscription in the demo E2E test
The dashboard's adapter count comes from the config entries, so the test
passed even with the scanner details subscription broken, while the
adapter page and network map stayed incomplete.
The adapter page renders a settings button only for adapters that are not
remote scanners, which it knows from the scanner type in those details, so
asserting one button across the three adapters covers the subscription
without asserting on copy.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep connectivity entities when switching demos
Switching demos replaces the whole state map, and only the energy entities
were added back, so every connectivity state disappeared until a reload.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Add Infrared to the demo
Load the infrared integration and add the emitter and receiver proxy
entities it reads, so the Infrared panel and its device list render in the
demo. The panel is entity driven, so it needs no WebSocket mock.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
---------
Co-authored-by: Claude <[email protected]>
* Add Matter to the demo
Load the matter integration and mock its network topology, so the Matter
panel and its network map render in the demo, with a Thread and Wi-Fi
topology that includes a border router and an offline node.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Key Matter node diagnostics by device
The diagnostics and ping mocks ignored the requested device, so every
Matter device reported node 1 on Thread and available. The device page
gates its actions on that: the offline garden sensor offered actions that
need a live node, and the Wi-Fi plug offered a Thread network link.
Both are now derived from the topology the map already renders, so a
device's node ID, transport, node type and availability match it, and an
unknown device is rejected as the backend would.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Give Matter fixtures unique entity IDs and mock its device actions
lock.front_door and sensor.garden_temperature already exist in the home
demo config, whose entities are added last and overwrote the display
registry entries, detaching them from their Matter devices. The panel
counted 3 entities instead of 5 on that demo. Renamed to a side door lock
and a patio sensor, which no demo config uses.
The device page offers commissioning, fabric and credential actions for an
available node, none of which were mocked, so each failed with
command_not_mocked.
Adds a demo E2E test for the panel and its map. Emptying the topology
subscription fails it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Mock the Matter lock services and fix the commissioning codes
The lock device exposes "Manage lock", whose dialog reads back the response
of matter.get_lock_info and matter.get_lock_users; neither was mocked, so
it always showed its load-failed alert.
setup_pin_code carried the manual pairing code. The three commissioning
codes now agree on the Matter test payload for passcode 20202021.
Pinging an unknown device rejects like the diagnostics command, matching
the backend, which resolves the device before acting either way.
The E2E test now reads the diagnostics of a Thread, a Wi-Fi and an offline
device, so a regression back to one shared response fails it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Return a credential result and gate the Matter Thread action
`setMatterLockCredential` reads `user_index` off the per-entity response, so
the demo's empty object made saving a code throw. Return the indices the
dialog reads back, and drop the mock for a `clear_lock_credential` service
the frontend never calls.
The device action linking to the Thread panel did not check that the
integration is loaded, unlike the same link on the Matter dashboard, so it
could navigate to a panel that is not there.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep the mocked Matter lock users across changes
The manage dialog reloads the list after every add, edit and delete, so the
static response made each change look like it was reverted. Keep the users
per lock entity and mutate them, and hand back a copy so the dialog's
reactive state sees a new list.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
---------
Co-authored-by: Claude <[email protected]>
* Add Bluetooth to the demo
Load the bluetooth integration and mock its scanner, advertisement and
connection subscriptions, so the Bluetooth panel, its adapter info,
monitors and network map render in the demo.
Also adds the scaffolding the other Settings > Connectivity panels build
on: a per-integration fixtures contract, registry builders, and the two
registries that collect components, WS command prefixes and registry data
from each integration. `addEntities` now carries `device_id` and
`platform` into the mocked display entity registry, which the panels use
to count entities per device and per integration.
Mocked subscriptions emit their first message from a timeout. Subscribing
is synchronous in the mock, so an immediate callback lands before the
subscriber is ready: `createCollection` overwrote it with its empty
initial fetch, and pages that ignore messages received before their first
render dropped it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Serve integration manifests for the demo's config entries
The integration page reads back the manifest it fetches without guarding,
so opening one for a domain the demo had no manifest for threw. Bluetooth
now ships its manifest through the connectivity fixtures, and manifest/get
falls back to a generated manifest rather than answering with undefined,
which also covers the entries that already lacked one.
Adds a demo E2E test that opens the Bluetooth panel and asserts its counts
are non-zero. The connectivity mocks reach the panel through lazy mock
registration and deferred first emissions, neither of which the demo suite
covered; without them the panel renders but stays empty.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Cover the Bluetooth scanner details subscription in the demo E2E test
The dashboard's adapter count comes from the config entries, so the test
passed even with the scanner details subscription broken, while the
adapter page and network map stayed incomplete.
The adapter page renders a settings button only for adapters that are not
remote scanners, which it knows from the scanner type in those details, so
asserting one button across the three adapters covers the subscription
without asserting on copy.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Keep connectivity entities when switching demos
Switching demos replaces the whole state map, and only the energy entities
were added back, so every connectivity state disappeared until a reload.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
* Add Serial to the demo
Load the usb integration and mock the serial port listing, so the Serial
panel renders in the demo with connected, available and disconnected
ports.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C6PpwxkMaHCWkEFA27YnLE
---------
Co-authored-by: Claude <[email protected]>
* Clarify what uninstalling an app deletes
The uninstall dialog offered a single switch labeled "Also remove app
data". That named the wrong thing twice over: the switch sends
remove_config, which Supervisor applies to the app's configuration
folder, while the app's data folder is deleted on uninstall either way,
independent of the switch.
Relabel the switch after the folder it actually removes, and state in the
dialog that uninstalling permanently deletes the app's stored data, so
the unconditional part of the operation is no longer implied to be
optional.
Rename the local variable and the uninstallHassioAddon parameter to
removeConfig to match the API field they carry.
* Move the app name out of the uninstall dialog title
Dialog guidelines ask to keep user generated content out of titles, as
names can get long enough to be unreadable there. The name now carries
the sentence in the body, which already describes what is deleted.
* Scope the uninstall warning to the app's private data folder
Uninstalling deletes the app's data folder and, when the app maps it,
the public config folder. Files the app wrote through the share, media
or Home Assistant config mappings stay where they are, so promising
that all of its data goes away is wrong for those apps.
Name the folder the deletion is limited to, and say the configuration
folder is only deleted if the app uses one, matching how the Supervisor
API documents the field. Also switch the label to "delete" for
consistency with the sentence above it.
* Name what an app actually keeps in its data folder
Checked the 30 most installed apps for what they write there. It holds
databases (MariaDB, InfluxDB, Grafana, UniFi, Nginx Proxy Manager,
AdGuard), credentials and network identity (Matter fabrics, the Thread
network key, Tailscale node state, Z-Wave JS settings, Mosquitto
accounts) and internal state such as caches and SSH host keys.
"Any other files it created" reaches past all of that. ESPHome keeps
device configuration in the Home Assistant config folder, Node-RED sets
its user directory to /config, WireGuard writes its keys to /ssl, and
those survive an uninstall.
* Fix punctuation in uninstall dialog text
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Add select-slider card feature for select/input_select tile control
* Modified select-slider card feature to become just a style of the preexistent select-options
* Modified name icons to segmented and removed suggestion
* Fixed issues with punctuation and spaces
* Renamed segmented to buttons
* Wait for connectivity settings readiness
* Wait for serial settings readiness
* Wait for storage settings readiness
* Simplify delayed serial scenario
* Test delayed connectivity readiness
* Fit the picker popover to its list and align it with the select menu
* Use styleMap for the popover custom properties
* Fix combo box list not rendering for direct embedders
* Rename AI tasks settings to AI and add Model Context Protocol card
The AI settings page now shows a second card for the Model Context
Protocol server. When no mcp_server config entry exists, a centered
button starts the config flow. When enabled, the card shows the server
URL, an alphabetical list of registered LLM APIs with copy buttons for
their URLs, a configure button that opens the options flow, and a
disable action that deletes the config entry.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Mock MCP server config entry and LLM APIs in demo
Lets the demo show the enabled state of the Model Context Protocol
card on the AI settings page.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Rename MCP server URL row and mock MCP in the demo
The row heading did not say what makes this endpoint different from the
per API endpoints below it. Call it "Your MCP URL" and show the config
entry title, so it is clear which APIs it serves.
Mock an MCP server config entry and the registered LLM APIs in the demo.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Simplify MCP card URLs to labeled copy buttons
Build the URLs from the origin Home Assistant is browsed on, so the
copied URL is reachable for whoever copies it.
Drop the URL text from the card and label the copy action, which was an
unlabeled icon that did not say it copies a URL. Name the API section
Individual MCP APIs so it is clear those are MCP endpoints too.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Move MCP disable action into a header overflow menu
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Align MCP card header actions and drop the disable icon
Put the help and overflow buttons in the header row so they line up with
each other and the title, instead of positioning them absolutely.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Rename MCP card row to Your MCP API
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Drop trailing word from MCP card description
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Put configure before copy and drop Calendar from the demo
Ordering the configure button first lines the copy buttons up across all
rows. Calendar is not an LLM API integrations register, so mock only
Assist and Music Assistant.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Always show the MCP configure button
The options flow is now in core, so the entry always supports options.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Tell users where to add the MCP URL
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Link the MCP docs from the card description
Point at the integration documentation for agent-specific instructions,
and call them AI agents.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Drop the MCP card help icon
The description links the documentation, so the header icon is a second
link to the same page.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Show MCP URLs in outlined rows
Give each URL its own outlined row with the URL under the name, and copy
with an icon button, so the URLs are visible before copying.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Use hassUrl for MCP URLs and show load errors
Build the URLs with the standard helper instead of the window location.
Catch a failing load and show an alert. Without it a failed load left
the card with neither the URLs nor the enable button.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
* Show a spinner while the MCP card loads
Without it the card showed only its description until the config entries
arrived.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RQUrBPMXN35UQ19ZYPRpEZ
---------
Co-authored-by: Claude <[email protected]>
* Remove web components polyfills and ES5 custom elements adapter
All supported browsers (legacy floor: Chrome 59, Safari/iOS 12, Firefox 94)
have native shadow DOM and custom elements, so the webcomponents bundle,
ShadyCSS branch, and lit polyfill-support are unreachable. The legacy build
now emits ES2017 classes, so custom-elements-es5-adapter and the
window.loadES5Adapter hook (no known third-party consumers) are removed.
Also stops shipping the never-loaded dialog-polyfill css and drops both
now-unused dependencies.
* Remove keyed-es5 Terser workaround
The custom keyed directive existed because Terser with ecma: 5 miscompiled
the destructured update() parameters (#28732). The legacy build now minifies
with ecma: 2017, so the stock lit-html keyed directive works in both builds.
* Remove old-browser JS shims and stale ES5 build references
Drops the IE-only navigator.msMaxTouchPoints check, replaces the
toggleAttribute helper with the native method (polyfilled automatically for
Chrome < 69 in the legacy build), and removes babel excludes for the
uninstalled proxy-polyfill and unfetch packages. Updates comments that
still described the legacy build as ES5.
* Remove vendor prefixes for no-longer-supported browsers
Deletes -ms- prefixes (IE/EdgeHTML only) and -webkit-/-moz- prefixed
declarations that every supported browser understands unprefixed, or that
Lightning CSS re-adds automatically from the unprefixed property in
production builds. Blocks that only had prefixed user-select now use the
standard property (previously Firefox got no user-select there at all).
Converts the four -webkit-linear-gradient() declarations - the sole
gradient syntax on those sliders - to standard linear-gradient().
* Remove dead html_url custom panel support
html_url pointed to an HTML Import, a Polymer-era feature removed from
Chrome in 2019 and never shipped elsewhere. The loader has not handled the
html type for years (it fell through to a rejection), and core's
panel_custom integration no longer accepts the option, so the branch was
unreachable. Also drops the ha-panel-${name} legacy tag naming that was
keyed on html_url.
* Repair list-plugins-and-polyfills script for Babel 8
The audit script died at startup since the Babel 8 update: preset-env no
longer exposes lib/debug.js (logPlugin is now inlined locally, built on the
public getInclusionReasons helper) and babel-plugin-polyfill-corejs3 v1 no
longer ships lib/shipped-proposals.js (list inlined).
Instead of invoking the preset with a hand-mocked plugin API - the part
that kept drifting - the plugin listing now runs a real transform of an
empty file with preset-env in debug mode, declaring the same caller
capabilities as babel-loader. The polyfill listing now passes the
configured core-js version to core-js-compat, mirroring the provider's own
filtering so the report cannot list modules the installed core-js lacks.
Output is byte-identical to the direct-invocation approach. Also documents
the script in the build-scripts README.
* Correct macOS floor for Safari 26 in companion app UA regex
Safari 26 ships for macOS 14 Sonoma and 15 Sequoia, not only macOS 26, so
the SAFARI_TO_MACOS entry breaking the minimum-supported-macOS pattern
would have sent updated macOS 14/15 companion apps to the legacy build
once the modern floor reaches Safari 26.
* Remove orphaned values left behind by vendor prefix removal
* Require macOS 14.6 for Safari 26 in companion app UA regex
Replace the wizard's own registration and sign-in screens with the cloud
panel's, extracted into cloud-register-card, so signing up mid-wizard gets
email-confirmation auto login instead of the legacy register plus login
polling. The wizard's copy of the login error ladder goes with it.
Drive the registration state from cloud/status rather than from the cloud
events, which carry nothing it does not already hold. The waiting view now
clears when the pending registration does, so a restart no longer leaves it
spinning on a registration the backend has forgotten.
Depends on the auto-login state fix in home-assistant/core#180423: without
it a retry loop that gives up before the register view stores its controller
leaves cloud/status reporting a pending registration with no reason, which
this no longer reads from the event instead.
* Group the connectivity panels behind a Connectivity settings page
Move the connectivity panels (Matter, Zigbee, Z-Wave, KNX, MQTT, Thread,
Bluetooth, serial, infrared, radio frequency, Insteon, and tags) behind a new
Connectivity page. It sits with Voice assistants in the second settings group,
where those panels used to be listed individually.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y7Yy1BcbkMjCZEtkTEqyKm
* Update config panel e2e coverage for the Connectivity page
The connectivity panels moved off the settings root, so split the link smoke
cases and assert them on the Connectivity page instead. Add a route smoke case
for /config/connectivity.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y7Yy1BcbkMjCZEtkTEqyKm
---------
Co-authored-by: Claude <[email protected]>
`ha-camera-stream` declares a `load` event and both of its video players fire it through `fireEvent(this, "load")`, but the MJPEG and snapshot fallback `<img>` never does. Every camera without a working stream therefore goes through the one path that stays silent.
`hui-image` listens for that event to record `_lastImageHeight`, and that value is what drops the `.ratio` class so the container can size to its content. Without it the container keeps the 16:9 padding box it falls back to before anything is measured, while the picture is drawn at its own ratio and overflows. Measured against a 600x410 camera in a 600px wide card: the container stays 337.5px tall while the picture is drawn 410px tall, and it is still so ten seconds after the card is built.
Firing the event from the fallback image settles it: the container becomes 410px, matching the picture, and the 16:9 fallback goes back to being a placeholder for the first frame instead of a permanent state. Cameras that do have a stream are unaffected — they already fire `load` from their player.
* Add timer data helpers for formatter-based display and duration serialization
Refactor computeDisplayTimer to take formatEntityState instead of hass so
context-migrated components can reuse it, add finishes_at to TimerEntity,
and add durationDataToTimerString for serializing duration input values.
* Modernize timer more-info dialog with state header and live countdown
Use ha-more-info-state-header with a ticking remaining-time display and
the standard more-info control layout. Adds timer to
DOMAINS_WITH_NEW_MORE_INFO, replacing the legacy state-card row.
* Add timer-actions and timer-presets card features
timer-actions shows start/pause/cancel/finish buttons (finish opt-in)
with state-aware disabling; start becomes restart while active.
timer-presets shows configurable one-tap durations that call timer.start.
Timer tile suggestions default to the timer-actions feature.
* Pulse timer red when it finishes
When a timer runs out or timer.finish is called, the tile icon and the
more-info countdown pulse red twice. Cancelling does not pulse: the new
timerJustFinished helper distinguishes the transitions via the
last_transition attribute. Honors prefers-reduced-motion.
* Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Send normalized duration for timer presets
Presets are parsed leniently by createDurationData like other duration
inputs, but the raw config value was sent to timer.start. A malformed
preset such as 1:nope:00 rendered as 1 h yet was rejected by core. Store
the normalized seconds so the label and the service call always match.
* Store timer presets on the entity instead of the card config
Review feedback: presets belong to the timer entity, like cover favorite
positions and light favorite colors. They now live in the entity registry
options and are edited in the more info dialog through the shared favorites
UI, which also brings reset and copy to other timers. The tile feature keeps
its style option but reads the presets from the registry.
* Reject a zero duration when adding or editing a timer preset
A zero duration passed the form's required check but was then treated as a
cancel, so saving the prefilled 0:00:00 silently did nothing. Form dialogs
now take an optional submit-time validate hook that blocks the submit and
shows a field error, and the preset dialog uses it to require at least one
second.
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Support multiple for media selector
* Update src/components/ha-selector/ha-selector-media.ts
Co-authored-by: Petar Petrov <[email protected]>
* add to UI
* merge fix
* add helper
* Modify media item picker for clearable and image upload
---------
Co-authored-by: Petar Petrov <[email protected]>
The secondary line of system log entries was made inline-block in #53575
to apply text direction. An inline-block is an atomic inline, so the
parent's text-overflow: ellipsis hid the whole line whenever it did not
fit, leaving only "…" on mobile. Use unicode-bidi: isolate instead so
direction still applies while the text truncates normally.
Fixes#53879
* refactor(profile): split general section into subpages with dashboard navigation
Replace the two-tab profile layout (general/security) with a dashboard-first
navigation structure. The general section is split into three focused subpages
(preferences, localization, browser), each wrapping the original row components
unchanged. A new dashboard page provides a user card (name, owner badge, logout)
and a navigation list linking to all four subpages.
Add ha-navigation-list as a reusable component for page navigation lists.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor: remove orphan translation keys after general section split
Drop tabs.general and current_user, which were only used in the now-deleted
ha-profile-section-general.ts and the old profileSections tab config.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(profile): align dashboard layout with Bluetooth panel pattern
Replace .content with .container (padding on wrapper, max-width/margin on
cards) to match the layout convention used in protocol panels like Bluetooth.
Always show the chevron in ha-navigation-list, removing the narrow condition.
Drop the unused narrow prop from ha-navigation-list.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor: move ha-config-navigation-list to src/components
Makes the component available outside the config panel. Update import
paths in the two existing config consumers and use it in the profile
dashboard. Remove the now-unused ha-navigation-list (which relied on
ha-md-list/ha-md-list-item).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: security subpage width and card spacing
Use .container > * instead of ha-card to apply max-width and margin,
since the security cards are custom elements whose inner ha-card lives
in their own shadow DOM and is unreachable from the parent stylesheet.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(profile): move language setting to localization subpage
Language is a regional/cultural preference, consistent with the other
settings in localization (time zone, date and time formats).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(profile): rename profile subpage labels
- "User preferences" -> "Appearance"
- "Browser settings" -> "This browser"
- "Mobile app settings" -> "This mobile app"
* feat(profile): surface theme picker on dashboard page
Move ha-pick-theme-row from the Appearance subpage to the profile
dashboard, between the user card and the navigation list. Theme and
dark mode are now accessible directly without navigating to a subpage.
Update Appearance description to reflect its new scope.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(profile): restructure theme row layout and labels
Split dark mode and color pickers into separate ha-settings-row elements
with headings. Move reset button outside the color-row to keep the label
vertically stable when the button appears. Add padding to reset row.
Rename "Reset" to "Reset colors" for clarity.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* add title to theme settings card
* refactor(profile): apply theme row layout improvements to ha-theme-settings
Migrate the dark mode and color pickers layout from ha-pick-theme-row into
ha-theme-settings: use separate ha-settings-row elements with headings for
dark mode and custom colors, move reset button outside as a sibling element.
Add colors label to ThemeSettingsLabels interface.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(profile): migrate theme settings to ha-list-item-base layout
Replace ha-settings-row elements in ha-theme-settings with ha-list-item-base
inside ha-list-base for a more compact appearance. Split color pickers into
individual rows (one per color). Reduce vertical padding to --ha-space-2.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(profile): combine color pickers into single row with label
Merge primary and accent color pickers into one ha-list-item-base row
under a "Custom colors" headline. Restore labels on individual pickers
and increase their min-width to 150px.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(profile): stack theme settings rows vertically in narrow mode
Reflect narrow prop on host and use ::part(base/end) to stack all
ha-list-item-base rows in column layout on narrow screens, preventing
headline truncation from oversized end slot controls.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* Fix test:e2e:demo
* fix broken links
* Add my links
* Use s instead of smal for ha-button size
Co-authored-by: Maarten Lakerveld <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Maarten Lakerveld <[email protected]>
The iOS workaround from #52925 only applied when the selector had an
explicit negative min. The numeric threshold selector used by the power
triggers passes a number selector without a min, so the digit-only keypad
(without a minus key) was still shown. Treat a missing min as allowing
negatives too.
Also leave inputmode unset instead of forcing "text": on a number input
iOS then shows the Numbers and Punctuation keyboard, which has a minus
key and fits numeric entry better than the full QWERTY keyboard.
Fixes#53747
* Load MapLibre RTL text plugin so Hebrew and Arabic map labels read correctly
The vector base map never registered MapLibre's RTL text plugin, so
right-to-left scripts were shaped left to right and every label came out
reversed. Ship @mapbox/mapbox-gl-rtl-text from /static/map/ alongside the
glyphs (no third-party host) and register it once, lazily, before the
first vector layer is created.
Fixes#53851
* Add the English name to map labels in non-Latin scripts
Before the move to vector tiles the CARTO basemap showed English names
everywhere. The OSM style shows local names, which most users cannot
read in Cyrillic, Arabic, Hebrew or CJK regions. Keep the local name and
add the English one from the tiles under it - in parentheses for street
labels, which cannot break lines. Latin-script names are left alone, so
Köln stays Köln.
* Respect gas display precision in gas graph
The gas graph previously formatted the total gas consumption using the generic number formatter, which could ignore the display precision configured on the selected gas entity.
Use the configured gas entity's display precision for both the visible total chip and the total consumption tooltip.
This keeps the gas graph consistent with the entity configuration. For example, a gas entity configured with 3 decimal places will display 0.009 m³ instead of being rounded to 0.01 m³.
* Use configured gas entity display precision in energy distribution card
The energy distribution card previously formatted gas usage with the generic
energy formatter, which could ignore the display precision configured on the
selected gas entity.
Use the display precision of the configured gas consumption entity when
formatting the gas value in the energy distribution card.
This keeps the displayed value consistent with the entity configuration. For
example, a gas entity configured with 3 decimal places will now display
0.009 m³ instead of being rounded to 0.01 m³.
* Respect gas display precision in energy sources table
The energy sources table previously formatted gas values using the generic number formatter, which could ignore the display precision configured on the selected gas entity.
Use the configured gas entity's display precision for both individual gas source rows and the gas total row.
This keeps gas values consistent with the entity configuration. For example, a gas entity configured with 3 decimal places will display 0.009 m³ instead of being rounded to 0.01 m³.
* Use highest gas display precision for energy distribution
The energy distribution card previously used the display precision of the first configured gas source for the aggregated gas value.
Use the highest configured display precision across all gas sources instead of special-casing the first source.
This avoids making source 0 authoritative for an aggregate value and preserves the greatest configured precision when multiple gas sources are present.
* Use highest gas display precision for energy distribution
The energy distribution card previously used the display precision of the first configured gas source for the aggregated gas value.
Use the highest configured display precision across all gas sources instead of special-casing the first source.
This avoids making source 0 authoritative for an aggregate value and preserves the greatest configured precision when multiple gas sources are present.
* Use highest gas display precision in gas graph
The gas graph previously used the display precision of the first configured gas source for its aggregated total.
Use the highest configured display precision across all gas sources instead of special-casing source 0.
This keeps the graph total and tooltip consistent with the greatest configured precision when multiple gas sources are present.
* Use display_precision for entity precision
Replace dp with display_precision when reading precision from hass.entities, matching the EntityRegistryDisplayEntry type used by the frontend.
* Use display_precision for entity precision
Replace dp with display_precision when reading precision from hass.entities, matching the EntityRegistryDisplayEntry type used by the frontend.
* Use display_precision for entity precision
Replace dp with display_precision when reading precision from hass.entities, matching the EntityRegistryDisplayEntry type used by the frontend.
* Use display_precision for entity precision
Replace dp with display_precision when reading precision from hass.entities, matching the EntityRegistryDisplayEntry type used by the frontend.
* Use display_precision for entity precision
Replace dp with display_precision when reading precision from hass.entities, matching the EntityRegistryDisplayEntry type used by the frontend.
* Use display_precision for entity precision
Replace dp with display_precision when reading precision from hass.entities, matching the EntityRegistryDisplayEntry type used by the frontend.
* Restore consumption formatting in energy distribution card
* Respect gas display precision in energy sources table
* Respect gas display precision in energy graph
* Respect display precision in formatConsumptionShort
* Pass gas display precision to consumption formatter
Add logic to calculate gas display precision based on available sources.
* Use gas display precision for graph y-axis
* Update distribution card on gas precision changes
* Update sources table on gas precision changes
* Update gas graph when display precision changes
Refresh the gas graph when a gas entity's display precision or unit of measurement changes, ensuring the configured precision is applied without waiting for new energy data.
* Update distribution card on gas unit changes
Refactor gas source display precision check and update unit of measurement comparison.
* Derive gas display precision with a getter
* Fix hass change handling in energy sources table
* Fix hass change handling in gas graph
* Fix hass change handling in energy distribution
* Fix gas unit narrowing in energy distribution
* Fix formatting in gas graph card
Refactor gasDisplayPrecisions assignment for better readability.
* Fix formatting in energy sources table
* Fix Prettier formatting in energy gas cards
---------
Co-authored-by: Petar Petrov <[email protected]>
wa-after-hide from a tooltip inside the drawer (e.g. the exact time
tooltip on a persistent notification) bubbled to the wa-drawer listener
and was treated as the drawer closing. Only handle the drawer's own event.
Fixes#52901
Core now proxies both vector and raster tiles, which is what lets them be
requested with an application User-Agent and without a referrer - a
browser can set neither. So the frontend stops talking to OpenStreetMap
and CARTO directly and goes through /api/map_tiles.
The proxy is token gated, so `ha-map` fetches one over the WebSocket
before setting up, following the brands token pattern: cached at module
level, refreshed well inside its lifetime so a dashboard left open for
days keeps working. The blocking wait is kept to about a second - a
backend without the proxy must not hold the map hostage - and the
remaining retries run in the background to ride through the window after
a restart where the WebSocket is up but the handler is not registered
yet.
Two things that are not obvious and cost a measurement each:
MapLibre's `transformRequest` has to return absolute URLs. Tiles are
fetched from a worker, which has no document to resolve a relative URL
against, and the TileJSON that core serves has relative `tiles`. Measured
with a relative TileJSON on one origin: with absolute URLs the style
loads and 8,456 features render; without them exactly one request is made
- the style - and nothing else loads, with no error reported anywhere.
Leaflet bakes its URL template at layer creation and throws while
building a tile URL if a template variable is undefined. So the raster
layer takes the token as an option Leaflet substitutes per request, which
also means a refreshed token is picked up without recreating the layer,
and an absent one is empty rather than missing: the tiles 403 and the
markers still draw.
The asset pipeline shrinks to generating two styles. Glyphs and sprites
come from the proxy, so the 48 MB build-time download, the digest
verification, the glyph range filtering and the bold-range guard are all
gone, along with 5.4 MB from the wheel. `localIdeographFontFamily` goes
too: the complete glyph set is reachable now, so CJK renders in Noto
rather than a device font.
Co-Authored-By: Claude Opus 5 <[email protected]>
* Remove no-op User-Agent header from Nominatim requests
`User-Agent` is a forbidden header name for browser fetch/XHR, so the
option passed to both Nominatim calls was silently dropped and the
browser's own User-Agent went out instead. Verified in Chromium against
a local echo server: the header never took effect, the outgoing UA was
the browser default, and zero preflights were received — had it been
applied, the non-safelisted header would have forced an OPTIONS
preflight that Nominatim does not answer.
Identification towards Nominatim is done by the `email` query parameter
both calls already send, which is what the Nominatim usage policy
accepts, so nothing changes on the wire. Drop the misleading dead code
and note in a comment why the header cannot be set from a browser.
`hass` is still needed by both functions for `hass.locale.language`, so
the signatures are unchanged.
Co-Authored-By: Claude Opus 5 <[email protected]>
* Apply suggestions from code review
Co-authored-by: Maarten Lakerveld <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Maarten Lakerveld <[email protected]>
Co-authored-by: Maarten Lakerveld <[email protected]>
* Fix editor state leaking when switching automations, scripts, or scenes
Recreate the page when the item id in the route changes so YAML mode,
undo history, and scene live state cannot carry over to the next item.
* Declare itemId on editor routes instead of guessing from the path
Path-shape matching remounted nested config routers, Lovelace, and
developer tools. Recreate only where edit/show routes opt in.
* Do not restore scene live states when the editor is torn down
A remounted editor starts in review; disconnect only needs to drop
the subscription, not undo device changes from live mode.
* Use the normal create path when remounting itemId pages
Clear the current page instead of calling _createPanel directly so
load and the loading screen still run. Drop the redundant cache delete.
* Fix dialog-form tests leaking focus restore after jsdom teardown
Nested submit/cancel schedules a nextRender callback that ran after
Vitest tore down HTMLElement, failing CI with an unhandled rejection.
A percentage has a real ceiling the way zero is a real floor, but the gap that
keeps series off the plot edges did not know that: a battery reading 20-100%
rounded out to an axis labelled up to 120%, and one sitting flat at 100% — a
device left on the charger — reached 160%.
Recognise the unit at the two line-chart call sites and hold the axis at 100,
mirroring the existing zero clamp. Only while the data stays under it, since
power factor is also reported in % and is signed.
* Show Z-Wave neighbor connections in the network visualization
* Fetch neighbors in bulk and hide them behind a toggle
* Warn that loading neighbor data turns off the adapter
* Guarantee a Y-axis gap on history and statistics charts
ECharts floors the axis minimum and ceils the maximum to a tick multiple,
so quantized states that land exactly on a tick leave the data flush against
the plot edge — which collapses the climate and humidifier action bands to
zero height, since an area fill is based at the axis minimum.
Widen the auto-scaled extent by a fraction of the span so that same rounding
always has something to round away, keeping the axis anchored at zero for
single-signed data and preserving the window ECharts gives a constant series.
* Widen the Y-axis gap threshold to 2% of the span
A nudge only large enough to break an exact tie left the gap unchanged
whenever the data sat just above a tick rather than on it, so a chart could
still render with a hairline of headroom.
Because ECharts rounds out to a whole tick afterwards, this constant does not
set the size of the resulting gap — it decides which axes count as too flat to
leave alone. At 2% the worst case over the tick cycle goes from nothing to
around 3% of the plot, while every measured chart keeps the framing it had.
* Deduplicate in-flight requests
* Use callWS for in-flight request ownership
* Avoid mutating shared request results
* Deep-freeze shared in-flight request results
Centralize immutability in shareInFlightRequest using deep-freeze so
callers do not need readonly types on shared API shapes.
---------
Co-authored-by: Petar Petrov <[email protected]>
The digital clock hardcoded 1.5rem/3rem/4rem, which ignored
--ha-font-size-scale and could not be overridden from a theme. Use
--ha-clock-card-digital-font-size-{small,medium,large} with defaults based
on the font size scale (24px/48px/64px).
Fixes#51602
The CSV header used the timestamps of every statistic in the energy
collection. Since the power sources chart (#27501) that includes the
power statistics, which are fetched at a finer period than the energy
statistics (hourly for a month view, where energy is daily), adding
empty hourly columns between the daily energy values. Collect the rows
first and build the time columns only from the data that is actually
exported.
Fixes#52381
* Prompt for unsaved changes on programmatic navigation
* Tidy unsaved-changes guard tests
* Drop navigations superseded while an unsaved-changes prompt is open
The assistants URL filter can skip creating a row for entities that are
only exposed via Cloud (Alexa/Google) manual config, but the sortable key
was still assigned to the missing row, throwing a TypeError and leaving
the page on an infinite spinner. Only assign the key when the row exists.
Fixes#53689
The chart data modules push a null y value to break the line where an
entity was unavailable. downSampleLineData read it with Number(), and
Number(null) is 0, which is not NaN, so the isNaN guard did not fire.
The marker then competed as a real value of 0 and won its frame's
minimum slot whenever the readings were positive, discarding the
frame's actual minimum and widening the rendered gap.
Keep markers out of the min/max comparisons entirely and hold at most
one per frame in its own slot. It is emitted, after the frame's values,
only when no kept value follows it: a marker followed by a value in its
own frame is a gap that closed within one frame, which is about one
device pixel wide and too narrow to show. That check runs per frame at
emit time, so the per-point path stays as it was. Keeping every marker
instead would blow up the output on series that are mostly null, such
as the climate heating dataset, which went from 823 to 14525 points
before this was bounded.
Skipping markers before the numeric work also makes gapped series
faster: 16% on a series with a few gaps, 27% on one that is mostly
gaps. Both now have benchmark coverage, which the gap path lacked.
Mean mode no longer averages markers in as zero.
* Add sesnor device class selector
* Remove unused constants
* Remove stale test case and import
* Fix CI tests
* Add domain and make selector generic
* Follow up comments
* Use import
* Add security dashboard alerts
* Fix security alerts editor and card defaults
* Use consistent security editor icons
* Fix security dashboard alert state
* Fix lint
* Render security alerts as individual cards
* Use context data in security alerts editor
* Add pulse for alerts
Co-authored-by: Paul Bottein <[email protected]>
* Use explicit cover and lock alert states
* Fix test
* Remove security panel rendering tests
* Use complete states
Co-authored-by: Paul Bottein <[email protected]>
* Remove camera
Co-authored-by: Paul Bottein <[email protected]>
* open state only
Co-authored-by: Paul Bottein <[email protected]>
* Remove security entity filter cache
* Format
* Update security alert visibility tests
---------
Co-authored-by: Paul Bottein <[email protected]>
The option handlers read the value from ev.target, which is whichever
element the click landed on. Real pointer events are retargeted to the
outer role="radio" element by .option .content { pointer-events: none },
but a screen reader's synthetic activation click is not hit-tested, so it
lands on the inner content element, which has no value.
ev.currentTarget is always the element the listener is bound to, so it
always carries the value.
* Focus nested form-dialog fields after a nested level is pushed.
Host focus on ha-form does not pierce selector shadows, so keyboard
focus stayed on the opener instead of the nested controls.
* Simplify nested form-dialog focus and fix CI types.
Wait one render and focus the first real control instead of waiting for
custom-element upgrades or walking a general focusable tree.
* Wait for lazy selector upgrades before nested form focus.
nextRender is not enough on a cold selector chunk; wait for undefined
custom elements so nested focus does not depend on network timing.
The security strategy step rendered ha-form with a data object built
from a @state field that was never assigned, so the form was always
bound to an undefined strategy. ha-form's own optimistic data update
made the radio look selected after a click, but the next re-render of
the step (triggered by any hass update) re-committed the undefined
value and cleared the selection.
Make the step a controlled component: the dialog already owns
_inclusionStrategy and resets it when navigating back, so pass it down
as a property instead of keeping a second, unused copy in the child.
Claude-Session: https://claude.ai/code/session_011MGgDoAUxzf9KBexn6uiPc
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
_getDetailData caches on stateObj alone while formatting via this.hass,
so results go stale when translation-based format functions reload (e.g.
backend translations finishing after open, or a language switch) until
the entity next changes state. Pass hass.formatEntityAttributeName as an
extra memoize argument so the cache invalidates when format functions
are recreated.
* Fix energy dashboard staying on yesterday after midnight.
* Catch up the energy live day before subscribe fetches.
* Keep energy day math in the server timezone so DST cannot skip a live day.
Browser-local addDays can jump a calendar day on a 23-hour DST fallback.
Assert against tz-internal endOfDay/addDays under Europe/Berlin so UTC CI
catches a regression, and prove the 01:00 timer and catch-up refresh fetch
the live day rather than only updating collection.start.
* Don't follow the live day from midnightRollover alone.
A stored non-today preset would otherwise be discarded on the first subscribe. Drop tautological UTC DST tests that cannot fail.
* Fix editing nested multiple object selectors
* Fix nested form dialog close and focus restoration
Cancel all pending nested form levels when the physical dialog closes, and restore focus to the opener after nested submit or cancel.
Add regression coverage for both behaviors.
* Fix selector object test types
* WIP: Initial attempt
* Arrange into sections
* Clean up UI
* Add descriptions
* Align with Core changes
* Address review feedback and add an info dialog
* Combine websocket commands into one
* Drop unnecessary info dialog rows
* Add a status bar
* Expose `resolved_device` as well
* Show result of action on button feature
* Add button entity
* Add to button entities
* add to tile icon
* Delay for spinner
* Remove result indicator from tile icon
* Add keyboard and screen reader navigation to charts via Chart2Music
* Mark charts as busy while the sonification chunk loads
* Skip chart sonification when there is nothing to navigate
* Drop the direct chart2music dependency
* Exclude legend-hidden series when judging chart sonifiability
* Push statistics chart times as numbers so sonification reads real dates
The greeting sits in the assistant's chat bubble, so render it in the pipeline's language instead of the interface language. Falls back to the interface language when no translation is available (issue #53703).
* fix flowType in config subentry flows/next_flow
* suggested changes
* Repair Flow Next Flow Repair
* Add repair flow support a new repair flow as the next_flow
* Add support for the next_flow being another repair flow
* fix lint errors
* Reviewer response and fix linter error
* Revert to generics to avoid multiple casts
* Move show-dialog-repair-flows to dialogs
* Update src/dialogs/config-flow/dialog-data-entry-flow.ts
* Apply suggestions from code review
* Apply suggestion from @MindFreeze
* format
---------
Co-authored-by: Petar Petrov <[email protected]>
Co-authored-by: Paulus Schoutsen <[email protected]>
* Show per-mount storage usage on the storage page
Each active mount now fetches its own usage from the Supervisor and renders
a bar with a used-of-total line under its address. The requests are not
awaited, so a slow or unreachable server cannot hold up the rows, and a
failed one simply leaves that row without usage.
fetchHostDisksUsage now takes a disk and an optional max_depth. The data
disk callers keep their depth of 3; mounts send no depth at all, since
walking one costs a round trip per directory and the Supervisor already
has a sensible per-target default.
* Vertically align ha-bar
---------
Co-authored-by: Simon Lamon <[email protected]>
The brand outline pointed at a token that doesn't exist, so CSS fell back
to the neutral border and brand outlined buttons drew a gray ring.
Disabled outlined buttons only reset their background and label, keeping
the variant-coloured border.
Also add outlined to the gallery: it has always been a valid appearance
but was missing from the documented list the gallery grid is built from,
which is why both bugs went unnoticed.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* Fix filter pane expand animation and use a bottom sheet in narrow mode
* Update src/common/controllers/filter-panel-controller.ts
Co-authored-by: Petar Petrov <[email protected]>
* Apply suggestion from @MindFreeze
---------
Co-authored-by: Petar Petrov <[email protected]>
* Ignore stale History statistics fetches after the selection changes.
A slower fetchStatistics call could finish after a newer sources or date-range request and replace the charts the user is looking at.
* Update src/panels/history/ha-panel-history.ts
---------
Co-authored-by: Paul Bottein <[email protected]>
* Redesign History and Activity filtering into a sources pane
Give both panels the toolbar + left pane layout of the data tables, and
merge target picking and filtering into one "Sources" surface, per the UX
discussion.
- ha-filter-pane: pane on wide screens, bottom sheet on narrow ones,
mirroring the filter pane of hass-tabs-subpage-data-table
- ha-sources-picker: target picker plus domain, device class and
integration filters, shared by both panels
- ha-filter-device-classes: new filter panel, labelled with the backend
device class names
- ha-filter-pane-chip and ha-empty-state: the chip with a filter count
badge and the centered placeholder both panels need
- ha-date-range-nav: the date range picker as one pill with previous and
next steppers, for the toolbar
- History draws timeline names above their bar (inside-labels), which
gives long names the full width
- Activity keeps a floating date header while scrolling, and reset moved
into the overflow menu next to refresh and download
- Drop the now unused compact and add-on-top modes of ha-target-picker,
including ha-target-picker-value-chip
- Right-align the clear button in the domains filter header, matching the
other filter panels
Co-Authored-By: Claude Opus 5 <[email protected]>
* Remove dead code breaking the type check
* Fix stuck loading state when the selection resolves to no entity
* Keep the target picker out of the redesign
* Remember the source filters across visits
* Truncate timeline names that do not fit the plot
* Show device class icons in the sources filter
* Open the sources pane by default on wide screens
* Show the entity count in the sources chip
* Count the same entities in the sources chip and the target picker
* Fix sources resolution, fetching and filter badge in History and Activity
* Update src/panels/history/ha-panel-history.ts
Co-authored-by: Petar Petrov <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Paul Bottein <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
* Add Matter network topology visualization page
* Add central Home Assistant node to Matter network graph
* Refine Matter graph HA node: theme-aware memoization, drop dead branch
* Render Matter 'unknown' link strength as a present, neutral link
* Pass devices to getDeviceArea so bridged nodes inherit their area
* Distinguish position-unknown Matter graph anchors and name border routers
Anchors to a hubless component only mean the node is reachable, so draw
them dotted instead of reusing the solid line that marks a real path
through a border router or access point.
Also prefer the border router mDNS host name over vendor and model,
which several vendors report identically on every unit.
* Name Matter access points by SSID and show their radio address
Access points were labelled with a BSSID, and the same address was
repeated as the node context. Prefer the SSID for the label and keep the
radio address as context only when it differs, so the radios of one mesh
stay distinguishable without repeating the label.
Falls back to today's BSSID against servers that do not send an SSID.
* Colour Matter graph links by transport and float unknown neighbours
Unknown Thread neighbours are not commissioned on our fabric, so Home
Assistant has no operational path to them. A group made only of unknown
devices no longer draws an anchor to the Home Assistant node and floats
instead of claiming a connection that does not exist.
Link colour now encodes the transport, purple for Thread and pink for
Wi-Fi, with the signal level left on the line width and spelled out in
the tooltip. A dead link keeps the disabled colour, which is the only
thing separating it from a healthy weak one now that both are width 1.
The edge tooltip names the network, since the graph legend describes
nodes only and cannot carry a link entry.
* Float Matter nodes whose route to Home Assistant is unknown
Only border routers and access points get an edge to the Home Assistant
node, because only those are a path we can actually see. A node in a
group with neither was previously anchored to Home Assistant with a
dotted line, which reads as a physical connection that was never
observed.
Removing the anchor makes the component walk that picked a
representative dead code, so it goes too.
* Match the dashboard's line semantics for Matter graph links
Dash an edge whose endpoint is inferred rather than commissioned, or is
offline, which is the same pair of conditions the matter.js dashboard
dashes on. Stop drawing a link whose every direction is dead: the
summary strength is the strongest direction, so none means a stale
neighbour entry, and the dashboard never draws one either.
That also retires the grey dead-link colour, since such links no longer
reach the graph.
* Carry the transport colour onto the Home Assistant edges
An edge from Home Assistant to a hub now takes the colour of the network
behind it, so one transport reads as one colour the whole way back
instead of changing hue at the hub.
Wi-Fi moves from pink to orange: pink sat close to the error red used
for offline nodes, while orange is far from both that red and the Thread
purple.
* Give the Wi-Fi access point node its transport colour
The access point wore indigo while everything else on its network was
orange. Deriving the category colour from the same helper the links use
keeps a hub and its links one hue, and gives the Wi-Fi colour a legend
entry it did not have before.
* Give the border router node its transport colour too
Both hub categories now derive their colour from the same helper as the
links, so a network is one hue from Home Assistant through its hub to
the devices, and each legend swatch keys the links of that transport.
The border router also gains contrast: the shared Thread purple is
legible on both the light and the dark card background, where the deep
purple it replaces was not.
* Add mqtt config dashboard shortcut
* Improve MQTT subscribe panel formatting and add copy button
* Standardize MQTT options flow button to a nav card style
* Refactor MQTT dashboard
* Improve style
* Add link to broker connection settings
* Test the effect of not exporting the SVG data
* Move MQTT svg icon to separate file
* Fix backup progress segment order to match backend execution order
The segmented backup progress bar showed Apps, Media, Home Assistant,
but Supervisor backs up Home Assistant first since 2024 (supervisor#5203),
then add-ons, then folders. The Home Assistant segment activated first
while Apps/Media incorrectly showed as completed, and the bar regressed
once the apps stage started.
Also move the await-addon-restart stages into the last creation group,
since Supervisor awaits add-on restarts after the backup file is
finished, keeping the progress bar monotonic.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Keep await restart stages out of MEDIA_STAGES
Give them their own constant and merge them into the last creation
group only where the ordering constraint applies.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Group backup setup stages with the first progress segment
addon_repositories/app_repositories (and docker_config on older
Supervisors) are emitted while the backup is initialized, before the
home_assistant stage, so grouping them with the apps segment made the
bar regress at the start of a backup.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
* Group sankey devices by count as well as by value
The 0.1%-of-total value threshold only catches devices that are a
negligible share of the home total. A breaker panel with dozens of
similar-sized circuit clamps sits entirely above it, so nothing groups and
every node collapses toward the 1px minimum with labels shrinking to match.
Cap the number of named children per parent too, defaulting to 20 (the
readable capacity of the default 400px card). Over-cap devices are grouped
into the same per-parent "Other" node the value threshold already produces,
so the flow arithmetic is unchanged, and the cap is configurable per card
via max_devices.
* Clarify the max_devices label is per upstream device, not per floor or area
* Make max_devices budget named devices, not nodes
A cap of 20 rendered 19 devices because a slot was reserved for the Other
node, and a cap of 1 rendered none. The option now means what its name and
its namesake on the devices graph mean: at most this many devices are shown
by name, with Other as overhead on top.
* Extract the sankey device cap into findDevicesOverCap
Moves the count-cap logic out of buildSankeyDeviceNodes into an exported
function that takes the graph it needs, so it can be unit tested directly,
and trims the commentary to the two non-obvious rules (why the whole subtree
is grouped, and why at least two devices are).
Repair fix flows build their menu from the suggestions the backend
reports. A suggestion this frontend has no translation for yet (e.g. a
newer Supervisor offering a new repair suggestion) rendered as an
empty, unlabeled menu entry. Show the raw option key instead, matching
the fallback the form step header already uses.
Co-authored-by: Claude Fable 5 <[email protected]>
* Support linking a person to an existing user
* Don't delete preexisting user on cancel submit
* retry ci
* Don't show choice when no floating users exist
* collapse to single dialog
---------
Co-authored-by: Simon Lamon <[email protected]>
* Add activity detail dialog
* Apply review fixes to activity detail dialog
* Resolve previous state from history in logbook detail dialog
* Disable text selection on clickable logbook rows
* Document run discovery as a fallback for missing context ids
* Scope the detail chain to the subject's cause path
* Replace clickable logbook rows with explicit detail affordances
* Show the cause badge on automation and integration rows
* Map scheduled and Home Assistant causes to their trigger icons
* Replace logbook detail affordances with full-width rows
* Frame the activity detail dialog with ha-grouped-list
* Add missing import
* Fix milliseconds placement in activity chain times
* Lay out inline features in two columns
* Detect stacked features from the slot
* Rename tile container layout classes
* Rename the below features in the layout helper
* Keep area card features below at full width
* Stretch the area card features below the inline one
* Keep area card features compact outside the compact type
* Revert "Keep area card features compact outside the compact type"
This reverts commit 48268cb4e2.
* Reapply "Keep area card features compact outside the compact type"
This reverts commit e10bb27e2f.
* Keep the inline feature compact on condensed cards
* Add child devices UI
Surface child devices throughout the config UI so they read as first-class
devices nested under their parent:
- Device page: a "Sub-devices" card lists a device's children, and a child's
page shows a "Part of <parent>" link (hardware/model/config-entry are
already inherited from the parent by the registry resolver).
- Integration page: children are nested and indented under their parent
device in the config-entry and subentry device lists.
- Device picker: children are ordered and indented under their parent with a
tree connector, mirroring the area/floor picker.
- Naming: the device picker's secondary label and search now include the
parent device name for children, so they stay identifiable in flat views.
Follow-up to the child devices data layer (#53617).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Add child devices to demo and gallery mock data
A power strip parent with two outlet children in the demo device stubs and
the ha-selector gallery demo, so child device rendering (nesting, tree
indentation, parent-context naming, inherited area) can be exercised without
a running backend.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Refine child devices UI and target resolution
Follow-up polish so child devices behave like a normal device everywhere:
- Targets & filters: a parent device qualifies for and resolves to its
children's entities (getDevices filter, deviceMeetsFilter,
deviceMeetsTargetSelector, resolveEntityIDs and the target-chip "split into
entities" expand), matching core's server-side target resolution. Selecting a
parent excludes its children from the picker.
- Pickers: keep a parent visible when a child matches the search (device and
target pickers), fix the target picker's nested order (unsorted search +
recomputed last-child flag), and render the sub-device tree in the target
picker's device group.
- Area/naming: the area field shows only the (inherited) area again; the parent
name remains a search term.
- Integration page: correct the tree end connector and align it in narrow mode.
- A parent-disabled child can no longer be enabled from the settings dialog.
- Devices dashboard: show "Part of <parent>" under a sub-device's name and add a
hidden-by-default, searchable and groupable Parent device column that groups a
parent together with its children.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Address review on child devices UI
Align target resolution with core and fix picker/table details:
- Only a directly targeted device expands to its child devices. Labels are
never inherited by children (core's target helper is explicit about this) and
areas resolve by effective-area membership, so deviceMeetsFilter and
deviceMeetsTargetSelector evaluate a device's own entities again.
- Add devicesInEffectiveArea, mirroring core's dr.async_entries_for_area: an
area contains its devices plus children that inherit the area, but not a child
with a different explicit area. Used for area expansion, area matching and the
area chip's split action.
- Device picker: add a searchFn that restores the nested parent/child order
after the fuzzy search and recomputes the last visible child, so a child can
no longer be ranked above its parent and connectors stay correct.
- Devices dashboard: derive the family group name from the family's parent
device with the same fallback for parents and children, so an unnamed parent
cannot end up in a different group than its children.
- Child devices card: pass the device registry so a child shows its inherited
area.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Fix formatting in device picker row renderer
Reindent the row renderer template after it gained a block body, so Prettier
is satisfied.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Make the child device tree connector span the full row
The connector was a fixed 48px box centred in a taller two-line row, and its
SVG was letterboxed by the default preserveAspectRatio, so the dashed line
stopped short of the row edges and consecutive children never visually
connected.
Let the indicator stretch (preserveAspectRatio="none") and give it the full row
height, so the line runs edge to edge with the elbow at the vertical centre.
non-scaling-stroke keeps the line width and dash pattern identical however far
it is stretched; existing 48x48 usages render unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
* Show remaining tile card features below when position is inline
Previously the tile card silently dropped every feature after the first
one when "features_position" was set to "inline". The first feature is
now rendered next to the name as before, and any additional features are
stacked underneath, the same way they are in "bottom" position.
The tile container gained a "features-bottom" slot for this, and the
card size and grid options now account for the extra rows.
* Add tests for hui-tile-card size and grid options calculations
* Enhance hui-tile-card to support inline feature layout and improve feature counting logic
* reduce vertical gap for inline features
---------
Co-authored-by: Claude Opus 5 <[email protected]>
The gallery build is the only e2e build that still emits source maps: unlike
demo and the e2e test app, `bundle.config.gallery` never accepted
`isTestBuild`, so the production build always used `nosources-source-map` —
even though the artifact is only ever loaded by Playwright.
The e2e run's gallery artifact contains 693 `.map` files (14 MB of a 105 MB
dist); the demo artifact, which already passes `is-test`, contains none.
Thread `isTestBuild` through the gallery config and set `is-test: true` for the
e2e gallery build, so those maps are no longer generated. The design
deployment and preview workflows do not set `IS_TEST`, so they keep their
source maps.
Co-authored-by: Claude Opus 4.8 <[email protected]>
* List running automations & scripts in restart dialog
* Don't need a new dialog
* Update src/translations/en.json
Co-authored-by: Norbert Rittel <[email protected]>
* Show running automations in quick bar restart confirmation
---------
Co-authored-by: Petar Petrov <[email protected]>
Co-authored-by: Norbert Rittel <[email protected]>
* Add context to entity details
* Include entity context in YAML details
* Remove category from entity details
* Only link integrations with config entries
* Always group updates page entities by integration
Every integration now gets its own card on the updates page, titled with
the integration name, even when it only has a single update entity. The
catch-all Integrations card is removed. The Update all button is only
shown for cards with more than one entity.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DTuGyvS1i7w1B1xEGCEVhs
* Show update button on cards with a single update
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: Paul Bottein <[email protected]>
* Add child devices to the device registry
Child devices arrive over the WebSocket as stripped entries in the flat
config/device_registry/list response (discriminated by the absence of
full-device fields). Resolve them into complete DeviceRegistryEntry
objects at ingestion so hass.devices only ever holds full entries and the
~200 downstream consumers stay unchanged:
- config-entry association comes from the child's own config_entry_id, so
children still appear under their integration;
- hardware/display fields are inherited from the parent device;
- connections/via_device_id are not inherited.
Adds parent_device_id and the new "device" disabled_by value to the types.
Frontend data layer for home-assistant/core#178666.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Resolve child device effective area in the UI
A child device without an area of its own inherits its parent's area,
mirroring core's async_get_effective_area_id. getDeviceArea now takes the
devices map and falls back to the parent, so children show in the parent's
area everywhere a normal device would: the device dashboard (area column,
grouping and filtering via computeDeviceAreaLabel) and the device page
header.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Address review on child devices data layer
- Discriminate stripped child entries on `connections` (present on every full
device, never on a stripped child) instead of the deprecated `config_entries`
compatibility field that core plans to remove.
- Make getDeviceArea's `devices` argument required and update all callers, so a
child device's effective (inherited) area is resolved consistently everywhere,
including the device picker's selected-value renderer and the integration
device rows.
- Lock the enable switch in the device settings dialog when a device is disabled
by its parent (disabled_by "device"); core ignores enabling a child while its
parent is disabled.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Inherit the parent device area when resolving entity areas
Extract getDeviceAreaId so an entity on a child device resolves to the child's
effective area (the parent's area when the child has none), matching core's
entity area resolution. Applies to both getEntityAreaId and
getEntityEntryContext, which previously read device.area_id directly.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
* Run the frontend build in parallel with lint and tests
The build job waited for lint and test because a full build was expensive
enough that we did not want to spend it on a PR that fails its checks. With
the rspack persistent cache it now takes ~3 min instead of ~5, and it is the
longest job in the run, so serialising it behind the others dominates CI
wall-clock: 8s + max(lint 86s, test 123s) + build 194s.
Depend only on prepare-dependencies so all three run together, which brings a
successful run down from ~5.5 min to ~3.5 min — the build itself becomes the
floor.
To avoid finishing an expensive build for a PR that is already broken, each of
the three jobs cancels the whole run when it fails. The cancel step needs
`actions: write`; on pull requests from forks the token stays read-only, so it
is a no-op there and the jobs just run to completion.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Keep the Actions write scope away from pull request code
The cancellation needed `actions: write`, and granting it at workflow scope
handed it to every job — including the ones that check out the pull request
and pass GITHUB_TOKEN into the gulp build, so PR-controlled code (or a
compromised dependency) would have had write access to Actions.
Move the cancellation into its own job that holds `actions: write` on its own
and never checks out the repository, so the elevated token is never exposed to
PR code. It cannot simply `needs` the checks — a dependent job only starts once
they have all finished, which is too late to cancel anything — so it polls the
run's job statuses and cancels on the first failure.
Costs one extra (idle) runner slot for the duration of the run.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
* Skip backend translations download in nightly build
The nightly only builds the app (build-app), which does not merge backend
translations — the shipped app fetches those from core at runtime. The
backend Lokalise export is a whole-project download across all languages
and the slowest part of the translations step. Skipping it, as the release
already does, cuts several minutes off every nightly.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Decouple translations artifact into a parallel job
The full translations (including the slow Lokalise backend/core export) are
only needed for the uploaded `translations` artifact, not the wheel:
build-app does not merge backend translations. Move that download and the
artifact upload into a separate `translations` job that runs in parallel
with the build, so the backend export no longer sits on the build's
critical path. Both jobs run in the same workflow run, so consumers still
find both the `wheels` and `translations` artifacts.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
* Recover from a stale build after boot (lazy-chunk 404s)
When the app stays open across a Home Assistant upgrade, the previous
build's content-hashed lazy chunks are deleted, so opening a dialog,
more-info, card, or panel that was not yet loaded 404s. Today that
dead-ends: dialogs fail silently, panels show a Back-only error screen.
Add a shared recovery authority (recover-stale-build.ts): detect a stale
hashed-chunk load failure and either reload onto the current build (drop
the service worker + caches, cache-busting nav, one-shot cooldown guard)
or, when an editor has unsaved changes, show a non-dismissable toast that
reloads once the dirty state clears. Hook it into the global
error/unhandledrejection handlers, the router's swallowed load error, and
give hass-error-screen a reload action. Chunk-error patterns come from a
JSON single source shared with the boot guard.
Part of home-assistant/epics#113.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Wire stale-build recovery into more paths, add tests
- make-dialog-manager: don't cache a rejected dialog import, so a stale
chunk 404 (or transient failure) no longer permanently breaks that
dialog until a full page reload — a later open re-imports.
- home-assistant: _checkUpdate now uses reloadFresh() instead of the
no-longer-effective location.reload(true) (forceGet is ignored by
modern browsers).
- connection-mixin: drop the dead reload(true) forceGet arg on the
safe_mode reload.
- Add unit tests for isStaleBuildError and the recoverFromStaleBuild
clean / dirty / dev-demo / non-stale / loop-guard branches.
Part of home-assistant/epics#113.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Recover in the companion app via the reload_and_clear_cache command
The iOS/Android companion app (WKWebView) has no service worker, and its
document HTTP cache is not cleared by the Cache API, so the web-level
reload path is ineffective there. When an external bus is present,
reloadFresh() now fires the native `frontend/reload_and_clear_cache`
command (shipped in home-assistant/iOS#5190) so the app purges its cache
and reloads; browsers still take the web path.
Adds the outgoing message type and extracts the bus transport into an
exported fireExternalBusMessage() so it can be sent without a hass/bus
reference.
Closes#53405. Part of home-assistant/epics#113.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Address Copilot review feedback
- reloadFresh: only send frontend/reload_and_clear_cache to the WebKit
(iOS) bridge; Android and browsers take the service-worker + cache-clear
path (Android's WebView has a service worker). Fail closed when the
sessionStorage cooldown marker can't be persisted so it can't loop. Return
whether a reload was actually started, so a guard-blocked failure is still
surfaced/logged instead of silently swallowed.
- Add a dirty-aware reloadForUpdate() and route _checkUpdate and the error
screen's Refresh button through it; drop the dirty toast's immediate
action (it auto-reloads once changes are saved/discarded).
- Set showReload on the error-screen element at the call site so router
overrides (e.g. ToolsRouter) can't drop it.
- Tests: exercise the WebKit bridge and the SW/cache-clear branch; assert
the dirty toast has no reload action.
Part of home-assistant/epics#113.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
* Unify back navigation across subpages
* Address back navigation review findings
* Fix state
* Add missing back path
* Remove back path from editors to fix unsaved changes prompt
* Use more specific back fallback paths
* Add initial value setting to input_boolean helper dialog
* Move toggle initial value into More options and clarify wording
* Move toggle initial value hint into an info tooltip
* Show target-state entity icon in scene editor review mode
Render the entity badge in review mode (previously live-only) across both
the device-grouped and standalone entity lists. In review mode the badge
uses the scene's stored target state instead of the entity's live state, so
the icon previews what the scene will set once applied.
* Strip stale entity_picture from the synthesized review-mode state
state-badge hides the icon and renders a background image whenever a state
carries an entity_picture (see its willUpdate). A scene snapshots that URL
with an access token that is stale by the time review mode renders, so an
image-backed entity - for example a robot vacuum's "Map data" camera entity
on its device - showed a blank badge in review mode instead of an icon.
Drop entity_picture / entity_picture_local from the synthesized state so the
entity's own icon resolves. This is not a pre-existing defect: it handles a
case that rendering the badge in review mode (previous commit) introduces.
* Handle null and scalar scene entity values in review mode badges
An entity left without a value in the YAML editor parses as null, which
crashed the review-mode render. The scene config API also returns raw
scenes.yaml content without validation, so hand-edited scenes deliver
boolean states as-is (YAML 1.1 parses unquoted on/off as booleans);
these previously rendered as if the entity had no state at all.
Booleans map to on/off to match how the backend applies them when a
scene is activated (_convert_states in the homeassistant scene
platform). The backend rejects null and numeric states at save, but
review mode renders before save, so the frontend has to tolerate them.
* Extract and memoize the scene target-state synthesis
The editor re-renders on every hass change, and building a fresh state
object per row each time defeated Lit dirty-checking: every state-badge
re-ran willUpdate and every ha-state-icon restarted its async icon
resolution. Memoizing the synthesized objects per config keeps the
references stable so unchanged badges skip all of that.
Moving the synthesis to src/data/scene.ts makes it unit-testable; the
null, boolean, numeric, string, and picture-stripping cases are now
covered by tests.
* Sanitize brightness and rgb_color in the synthesized review-mode state
state-badge does arithmetic on brightness and joins rgb_color, assuming
backend-shaped values. Hand-typed YAML can hold both as strings: a
string rgb_color threw a TypeError that left the badge blank, and a
string brightness computed a brightness(36049%) filter that washed the
icon out to invisible. Coerce numeric-string brightness and drop
malformed values so the badge always renders the target state.
* Borrow the live device_class for review-mode badge icons
Icon resolution keys on device_class, which string-shorthand and
hand-written minimal scene entries do not carry, so a garage cover fell
back to the generic window icon and sensors to the domain default. Only
this identity attribute is borrowed from the live state - merging
stateful attributes like rgb_color would mis-color an off target.
* Reject unusable scene targets and trim the badge-state synthesis
Review-mode badges now render only when the scene holds a usable
target state. Entries with no state to show - null values, dicts
without a state key, arrays, non-scalar states - yield no badge
instead of falling back to the live state, which was
indistinguishable from a real target and, for dicts without a state,
crashed state-badge via stateColorCss on lights.
rgb_color and brightness are dropped from inactive targets: a live
entity never carries them while off, and state-badge applies them
without checking activity, so a scene turning a light off rendered an
active-looking colored icon.
Entity pictures are stripped only for DOMAINS_WITH_DYNAMIC_PICTURE,
matching createHistoricState in the logbook; stable pictures on other
domains are kept.
The brightness/rgb_color type coercion and the live device_class
borrowing are removed: they defended against hand-typed shapes that
state-badge already warns about, and the borrowing made the memoized
synthesis depend on hass state outside its memoize key.
* Apply suggestion from @MindFreeze
---------
Co-authored-by: Przemysław Szypowicz <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
* Scale ruler option for map card
* Only show ruler for the unit system configured
* Move attribution when ruler is in the same corner
* Match ruler style with the map and HA
* Update map card gallery page with the scale ruler options
* Use "UNIT_KM" to set the ruler's unit system
* Remove position option in favor of a boolean for only the bottom-left position
* Add dark mode theme to scale ruler
* Fix scale ruler style not respecting theme_mode
* Make the map card scale ruler react to config changes
Redraw the control from update() so toggling the option or changing the
unit system takes effect without a reload, and hold the themed colors in
custom properties so the forced light/dark rules no longer duplicate the
token values.
---------
Co-authored-by: Petar Petrov <[email protected]>
* Add target humidity feature to climate entity
* Use target_humidity_step if any instead of fixed step
* Add target humidity for climate in suggestd tile card
fix(config): always show the Internet URL heading
The "Internet" heading above the external URL field is rendered
inside a `hasCloud` conditional, so only users logged in to Home
Assistant Cloud ever see it. Everyone else gets an unlabeled URL
input, while the "Local network" heading below it always renders.
That leaves the local heading as the only label on the card, and it
sits below the external field, so the external field reads as though
it belongs to the local section.
Move the heading out of the conditional so it renders for every
user, leaving `hasCloud` to gate only the "Use Home Assistant Cloud"
toggle. The external section now has the same structure as the local
one: a heading followed by its list item.
The label was shown to non-Cloud users before #22379, inline beside
the field rather than as a heading. That PR converted both labels to
headings, kept the Cloud branch and dropped the non-Cloud one.
Claude-Session: https://claude.ai/code/session_01GQCiHCdQ4p3cPxXG6XNAa9
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
gulpfile.js set process.env.UV_THREADPOOL_SIZE, but gulp's CLI loads the
gulpfile through liftoff, which does async fs work first. libuv has already
sized its threadpool to the default 4 by then, so the assignment never took
effect. Measured through the gulp CLI, effective threadpool size is 3.4 both
with and without the line, and only reaches 6.9 when the variable comes from
the environment.
Hoisting it earlier cannot fix it either, since the pool is sized before any
line of gulpfile.js runs, and setting it for real measurably changes nothing.
Remove it rather than plumb the variable through the build.
Compress with zopfli in a worker pool
compress-app gzips every build artifact with zopfli, but @gfx/zopfli is
synchronous WASM: it ran on the main thread, pinned a single core and blocked
the event loop for the whole step, so it dominated the production build.
Replace gulp-zopfli-green with an equivalent gulp transform that runs the same
compressor in a pool of worker_threads sized to availableParallelism(). The
compressed output is byte-identical, and @gfx/zopfli is no longer loaded on the
main thread of every gulp invocation.
On a 12-core machine compress-app drops from 6.98 min to 1.27 min, and the full
production build from 8.87 min to 3.32 min.
Truncate long input chip labels
A chip with a long label, like an SSH public key in an add-on config
list option, grew past its container so the trailing remove button
ended up outside the visible area and could not be clicked. Cap the
chip at the container width so the built-in label ellipsis applies,
and show the full value as a native tooltip in the select selector.
Co-authored-by: Przemysław Szypowicz <[email protected]>
* Drop websocket collections created by an iframe panel on teardown
`getCollection()` caches collections (entity registry, label registry, ...) on
the `Connection` object. A custom panel embedded in an iframe shares the
connection with the main window, so a collection the panel is the first to
request is created inside the iframe's realm.
When the user leaves the panel, `ha-panel-custom` removes the iframe and that
realm is destroyed - but the collection stays cached on the connection. Its
store, and the `setTimeout` that hands the cached state to new subscribers, are
gone, so every later subscriber waits forever for a callback that can never
fire. Since the registries moved to `LazyContextProvider` they are subscribed
on demand, which makes an iframe panel the first requester far more often: the
KNX panel's entity table renders empty after leaving and re-entering it, and
the main frontend can inherit the same dead cache.
Clean up from the iframe's own `pagehide` handler, while the realm is still
alive - `disconnectedCallback` on `ha-panel-custom` runs after the browsing
context is discarded, when `contentWindow` is already `null`.
Co-Authored-By: Claude Opus 5 <[email protected]>
* Use unknown instead of any in the CachedCollection shape
The helper never inspects the subscribed state, so the parameter type only
needs to be a placeholder.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
Leaflet computes the zoom level that fits a set of bounds from the current
size of the map container. When ha-map's fit runs before the browser has
laid out the container (a race that hits when the Leaflet chunks are
already cached), the container measures 0x0, the computed zoom collapses
to the minimum, and the map shows the entire world. The later
invalidateSize() from the resize observer fixes the size but keeps the
zoom, so the map stays on the world view.
Defer fitting while the container has no size and run the pending fit
once the resize observer reports a usable size.
Co-authored-by: Claude Fable 5 <[email protected]>
* Drop websocket collections created by an iframe panel on teardown
`getCollection()` caches collections (entity registry, label registry, ...) on
the `Connection` object. A custom panel embedded in an iframe shares the
connection with the main window, so a collection the panel is the first to
request is created inside the iframe's realm.
When the user leaves the panel, `ha-panel-custom` removes the iframe and that
realm is destroyed - but the collection stays cached on the connection. Its
store, and the `setTimeout` that hands the cached state to new subscribers, are
gone, so every later subscriber waits forever for a callback that can never
fire. Since the registries moved to `LazyContextProvider` they are subscribed
on demand, which makes an iframe panel the first requester far more often: the
KNX panel's entity table renders empty after leaving and re-entering it, and
the main frontend can inherit the same dead cache.
Clean up from the iframe's own `pagehide` handler, while the realm is still
alive - `disconnectedCallback` on `ha-panel-custom` runs after the browsing
context is discarded, when `contentWindow` is already `null`.
Co-Authored-By: Claude Opus 5 <[email protected]>
* Use unknown instead of any in the CachedCollection shape
The helper never inspects the subscribed state, so the parameter type only
needs to be a placeholder.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
* Reuse state-content for entities secondary info
* migrate area
* Skip schema calculation when using predefined schema.
* add tooltip, migrate none
* timestamp for weather
* Recover from a stale index.html at boot
A cached, stale index.html imports the previous build's content-hashed
entry bundles (core.<hash>.js / app.<hash>.js). After an upgrade those
files 404, app.js never runs, <home-assistant> is never defined, and the
launch screen never clears. No bundled JS can recover this, because the
bundle itself failed to load.
Add a tiny, prod-only inline guard in index.html that catches the failed
entry load (capture-phase resource error + unhandledrejection) and does a
single, loop-guarded cache-busting reload, dropping the service worker and
caches on https first. core.ts strips the cache-bust param after a
successful boot.
Part of home-assistant/epics#113.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Share stale-build recovery patterns via a single JSON source
Move the boot guard's chunk-detection regexes into
stale-build-patterns.json and inject them into the inline guard at build
time (entry-html.js), so they stay in sync with the bundled recovery util
(util/recover-stale-build.ts, in the follow-up post-boot recovery) which
reads the same file — no more manually kept-in-sync copies.
Part of home-assistant/epics#113.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Ask which occurrences to change in the recurring event dialog
Deleting or updating a recurring event offered "only this event" and "all
future events" as two long buttons, which a 320px dialog footer squeezed
until every word wrapped onto its own line.
Move the choice into the dialog body as a radio group and leave the footer
with Cancel plus a single confirm action. The dialog also becomes an alert,
so it stays a centered card on small viewports, and the confirm action is
only styled as destructive for the delete flows.
* Apply suggestions from code review
Co-authored-by: Aidan Timson <[email protected]>
* Update src/panels/calendar/confirm-event-dialog-box.ts
Co-authored-by: Aidan Timson <[email protected]>
---------
Co-authored-by: Aidan Timson <[email protected]>
Co-authored-by: Paul Bottein <[email protected]>
* use defaults provided by core for ports in strings, add url to new location after restart
* Apply suggestions from code review
Co-authored-by: Simon Lamon <[email protected]>
---------
Co-authored-by: Simon Lamon <[email protected]>
* Use backend slugify for the entity ID format preview
* Update src/panels/config/core/ha-config-entity-id-format.ts
Co-authored-by: Petar Petrov <[email protected]>
* Use top-level slugify websocket command
* Fix formatting
---------
Co-authored-by: Petar Petrov <[email protected]>
While the check or error icon covers the button, the button's content was
hidden with `visibility: hidden`, which also removes the label from the
accessibility tree. For the two seconds the result is shown, a screen
reader announced a button with no accessible name.
Fade the content out instead. It looks the same and reserves the same
space, but the label stays readable to assistive technology. The spinner
part keeps `visibility: hidden`, since a finished spinner should leave
the accessibility tree.
* Validate dialog-form on submit
* remove whitespace
* put errors on the stack
* Apply suggestion from @MindFreeze
Co-authored-by: Petar Petrov <[email protected]>
* Apply suggestion from @MindFreeze
* Fix botched suggestion apply in nested submit
---------
Co-authored-by: Petar Petrov <[email protected]>
* Use backend slugify for the entity ID format preview
* Update src/panels/config/core/ha-config-entity-id-format.ts
Co-authored-by: Petar Petrov <[email protected]>
* Use top-level slugify websocket command
* Fix formatting
---------
Co-authored-by: Petar Petrov <[email protected]>
* Shared build and runner for all build,dev,test flows
* Harden managed process lifecycle
* Make build workflows deterministic
* Test build management contracts
* Queue shared generated output work
* Isolate generated inputs for dev servers
* Preserve dev server child failures
* Make generated lock test deterministic
* Keep test navigator configurable
* Block concurrent frontend workflows
* Simplify workflow lock ownership
* Focus workflow locking on managed commands
* Trim workflow lock unit tests
* Consolidate dev server lifecycle handlers
* use defaults provided by core for ports in strings, add url to new location after restart
* Apply suggestions from code review
Co-authored-by: Simon Lamon <[email protected]>
---------
Co-authored-by: Simon Lamon <[email protected]>
* Search a media player's own library from the media browser
The media browser search only ever asked the media sources, so searching
inside a media player's own library (Music Assistant, Sonos, Squeezebox,
Jellyfin) failed. Ask the entity instead when the current item belongs to
it, the same way browsing already does.
* Update src/data/media_source.ts
* Remove blank line in media_source.ts
Removed unnecessary blank line at the top of media_source.ts
---------
Co-authored-by: Bram Kragten <[email protected]>
Co-authored-by: Simon Lamon <[email protected]>
* Add managed modern production builds
* Harden managed build process controls
* Update managed process testing guidance
* Harden managed process file operations
* Share generated output process ownership
* Clean stale build state on status
* Address managed build review feedback
Co-authored-by: timmo001 <[email protected]>
* Use single compress task
* 1
Co-authored-by: Petar Petrov <[email protected]>
* Forward SIGHUP to foreground processes
---------
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
* Fix Z-Wave JS config parameter value type
Type ZWaveJSNodeConfigParam.value as number | null instead of any,
matching what the backend sends. This surfaced two comparison bugs:
- The enumerated picker no-op check compared the stored number against
the picker's string value, so it never matched and re-selecting the
current option re-sent the command to the device.
- The numeric input guard coerced null (unknown value) to 0, so
entering 0 for a parameter with an unknown value was ignored.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Treat empty numeric input as invalid instead of 0
Co-Authored-By: Claude Fable 5 <[email protected]>
* Add live updates to Z-Wave JS node config parameters
Subscribe to config parameter value updates so the page reflects
changes made outside the UI (Z-Wave JS UI, another browser tab, or
the device itself) and so queued changes to sleeping nodes resolve
to a success message when the node wakes and applies them.
- Manage the subscription across connect/disconnect and device
navigation, deriving deviceId reactively from the route
- Show success results briefly, then clear them automatically
- Clear stale error results when the parameter changes externally
- Discard fetch responses that arrive after navigating to another node
Co-Authored-By: Claude Fable 5 <[email protected]>
* Address review comments
- Use slice() instead of deprecated substr()
- Clear pending result timeouts when navigating to another node
- Handle subscription promise rejections so an older backend without
the subscribe command degrades gracefully instead of logging
unhandled rejections
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
* Polish the launch screen
- Use the system sans-serif font so the launch screen no longer blocks on
loading Roboto
- Reduce the gap between the logo and the loading text
- Dim the loading text to 66% opacity
- Match the OHF logo variant to the applied theme instead of the system
color scheme
- Make the launch screen text and buttons unselectable
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Drop unit test
* Use classMap
* Drop view transition for css transition to avoid flash
---------
Co-authored-by: marcinbauer85 <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* Skip empty cells on the narrow data table secondary line
In narrow mode the main column renders every other visible column on a
secondary line, but the dot separator was inserted based on the column
index instead of whether the cell rendered anything. A row whose extra
columns are all empty showed a secondary line consisting only of dots,
and an empty column between two filled ones produced a double dot.
Filter empty cells out before joining, and return `nothing` instead of
`html`${nothing}`` for missing timestamps so those cells are detectably
empty too.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Extract / clarify data table column visibility
* Fix unbound method and add all-empty secondary line test
Co-authored-by: timmo001 <[email protected]>
* Fix prettier formatting in ha-data-table test
Co-authored-by: timmo001 <[email protected]>
---------
Co-authored-by: marcinbauer85 <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
description:Home Assistant frontend event patterns. Use when typing event handlers, using HASSDomEvent types, dispatching with fireEvent, or declaring HASSDomEvents and event maps.
---
# HA Frontend Events
Use this skill when implementing or reviewing event listeners and custom event contracts. Cross-load `ha-frontend-components` when the work also involves dialogs, forms, alerts, shortcuts, tooltips, panels, Lovelace cards, or buttons.
## Event Handling
Use the event types from `src/common/dom/fire_event.ts` instead of plain `Event`, generic `CustomEvent`, or element casts when they express the handler contract:
- Use `HASSDomCurrentTargetEvent<T>` to read the element on which the listener was registered through `ev.currentTarget`.
- Use `HASSDomTargetEvent<T>` only to read the element that originated the event through `ev.target`.
- Use `HASSDomEvent<T>` to read a custom event payload through `ev.detail`.
- Use `ValueChangedEvent<T>` from `src/types.ts` for the standard `value-changed` event.
- Prefer an event type exported by the component being listened to, such as `HaSelectSelectEvent<T, Clearable>` or `HaDropdownSelectEvent<TValue, TData>`, over reconstructing its detail type.
- Use `ActionHandlerEvent` from `src/data/lovelace/action_handler.ts` for Lovelace tap, hold, and double-tap handlers.
Import event and element types with `import type`:
Use intersections when a handler needs more than one facet of an event. Keep the native event type when the handler reads native fields such as `key`, modifier keys, `dataTransfer`, or focus relationships:
Dispatch Home Assistant component events with `fireEvent()` instead of constructing `Event` or `CustomEvent` directly. Register the event name and detail type by augmenting `HASSDomEvents`; use `undefined` when an event has no detail. `fireEvent()` constrains event names and supplied detail, and events bubble and cross shadow boundaries by default:
```ts
fireEvent(this,"item-selected",{id: item.id});
fireEvent(this,"refresh-requested");
```
When an event is already registered, derive handler and listener types from its registration rather than repeating the payload shape:
```ts
private_itemSelected(
ev: HASSDomEvent<HASSDomEvents["item-selected"]>
):void{
this._selectedId=ev.detail.id;
}
```
`HASSDomEvents` types `fireEvent()` calls. Augment `HTMLElementEventMap` for typed listeners on HTML elements, or `GlobalEventHandlersEventMap` when the event is handled on global event targets.
In component files, prefer placing global event declarations after the class at the bottom of the file. Preserve the existing placement when editing established files; foundational type, helper, and mixin files commonly keep declarations near the top before their consumers.
description:Home Assistant frontend Lit conventions. Use when working with reactive properties, internal state, DOM queries, lifecycle methods, or render-derived state.
---
# HA Frontend Lit
Use this skill when implementing or reviewing Lit component state, DOM access, lifecycle methods, or rendering behavior. Cross-load `ha-frontend-types` for Home Assistant data contracts, assertions, and lifecycle parameter types.
## Reactive Fields
This project currently uses Lit's TypeScript experimental decorators with `useDefineForClassFields: false`. Match existing declarations and do not introduce standard-decorator `accessor` syntax unless the project changes decorator mode.
- Use `@property()` for public reactive API and `@state()` for private reactive state.
- Prefer inferred types for initialized reactive fields when inference preserves the intended type; annotate when widening or an external contract requires it.
## DOM Queries
Prefer Lit's `@query()` or `@queryAll()` decorators for fixed selectors in the component's render root.
- Type the decorated field with the narrowest useful DOM or component interface.
- Keep the field optional when it may be absent at the point of access, including conditional rendering or pre-render lifecycle access.
- Use a definite assignment assertion only when every call site runs after the node is guaranteed to exist.
- The optional second argument to `@query()`, as in `@query("#target", true)`, caches the first query result. Use it only when later renders cannot replace the queried node.
- Use a direct query when the selector is dynamic or the target is outside the component's render root. Before querying a child, consider whether the required value belongs in parent state or data flow.
## Render-Derived State
- Prefer render-local values for inexpensive structures used only by that render.
- Assign a render-local value once when repeated evaluation is non-trivial or a local name improves clarity.
- Keep purely presentational derivations in `render()`. Use stored state or `willUpdate()` when the value must participate in lifecycle work, reflection, CSS, or non-render consumers.
- Use `memoizeOne` for pure, argument-derived transforms when stable input identity avoids meaningful repeated work. Keep inputs explicit and limited, and do not add caching without a credible benefit over computing the value directly.
@@ -22,7 +22,7 @@ When creating a pull request, use `.github/PULL_REQUEST_TEMPLATE.md` as the body
-`yarn lint` passes when practical for the scope.
-`yarn test` or focused relevant tests are green when practical for the scope.
-Tests are added or updated for new data processing and utilities where applicable.
-Each test added by the change protects real logic, not the look of a component.
- User-facing text is localized and follows `ha-frontend-user-facing-text` guidance.
- Components handle loading, error, unavailable, and missing-entity states.
- Entity existence is checked before property access.
@@ -31,6 +31,33 @@ When creating a pull request, use `.github/PULL_REQUEST_TEMPLATE.md` as the body
## Recurring Review Issues
Scope and public surface:
- Keep changes independently reviewable and limited to the requested area.
- Prefer existing Home Assistant helpers, Lit primitives, and component seams over parallel implementations.
- Challenge new public properties and optional feature surface when transient options or existing seams meet the requirement with less lifecycle and consistency cost.
Stateful and asynchronous UI:
- Review transitions in both directions, not only individual rendered states.
- When controls reappear, restore valid defaults instead of retaining state that was only valid while they were hidden.
- Establish immutable dirty-state baselines before asynchronous work, guard against stale responses, and preserve unsaved state in mounted editors.
- Determine an action's current meaning before applying dirty-state checks, especially when an action can change between Save and Close.
Readiness and invalidation:
- Treat readiness as the first displayable terminal result, including stable empty and error states.
- Register child readiness before resolving the parent, do not treat fallback work as terminal, and replay readiness correctly for cached or reused panels.
- Ensure every value read by memoized output participates in its 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.
- 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.
- Promote AI-review concerns into durable guidance only when supported by code evidence, reproduced behavior, an accepted corrective commit, or human-maintainer validation.
User experience and accessibility:
- Forms need proper labels, helper text, and validation feedback.
- Keep style-only comments secondary unless they affect maintainability or user experience.
- Prefer small, direct fixes over large refactors during review follow-up.
-Cross-load `ha-frontend-contexts`, `ha-frontend-components`, `ha-frontend-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
-Load the matching `ha-frontend-*` skill when a finding falls within its area.
yarn lint:types # TypeScript compiler, run without file arguments
yarn test# Vitest
yarn build # Full production build
yarn dev # App dev server
yarn dev:serve # Local serving dev server
```
@@ -28,12 +29,33 @@ Never run `tsc` or `yarn lint:types` with file arguments. File arguments make `t
For focused type feedback on one file, use editor diagnostics instead of a file-scoped `tsc` command.
## Unit And Utility Tests
## Production Builds
- Add or update Vitest tests for data processing, utility code, and behavior that can be tested without a browser.
- Mock WebSocket connections and API calls at boundaries.
- Cover loading, error, unavailable, and missing-entity states where relevant.
- Test accessibility-sensitive behavior when it can be asserted without brittle DOM internals.
Production builds support foreground and managed background execution:
```bash
yarn build # Full foreground build
yarn build --background # Full managed background build
yarn build --modern # Modern frontend_latest bundle only
yarn build --modern --background # Modern managed background build
yarn build --status
yarn build --logs [--follow]
yarn build --stop
```
Use `yarn build --modern --background` for production bundle-size or browser performance comparisons that only need modern browser output. It runs the normal metadata and static preparation, minifies and compresses the modern `frontend_latest` bundle and shared static assets, and generates modern-only entry pages and service workers. It deliberately skips the legacy bundle and its service worker.
Do not pass `--help`, `--background`, or `--modern` to `script/build_frontend`; that raw script does not parse arguments and always starts the full foreground build. Use `yarn build` for managed builds. App builds and development servers keep exclusive ownership of `hass_frontend/` for their lifetime.
Managed app, demo, gallery, and E2E app workflows share one lifetime lock, so only one build or development server can run at a time.
## When To Add Tests
- Write tests for code that computes something: data processing, utility functions, config validation, and what happens when the user interacts with a component.
- Do not write tests that check what a component looks like: its text, CSS classes, styles, or slots. Do not write tests that check the default value of an option.
- A component that only takes data from contexts and helpers and puts it in a template does not need a test.
- If you are not sure a test is useful, describe the test and what it would catch, and let the user decide.
- Tests never talk to a real Home Assistant. Replace `callWS`, `callApi`, and the connection with fakes.
## Dev Servers
@@ -41,7 +63,7 @@ For focused type feedback on one file, use editor diagnostics instead of a file-
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. The default is 8124, or 8123 in a devcontainer.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`.`yarn dev`, `yarn dev:serve`, `yarn dev:demo`, and `yarn dev:gallery` also support `--fetch-translations`; this runs translation fetching, including first-time GitHub device authentication, under the workflow lock before starting the watcher. It works in foreground and background modes. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal.`yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
## Playwright E2E
@@ -59,7 +81,7 @@ The custom development wrappers use `/__ha_dev_status` to identify and manage th
Local runs against a watched development server do not always match CI's clean build artifacts, environment, sharding, or worker configuration. Use background servers for the fast iteration loop, but confirm the relevant CI jobs complete successfully before considering E2E changes verified.
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs all three suites in parallel when every managed server is available, otherwise it runs them sequentially to prevent cold builds racing over shared generated assets. Run suites directly; piping through output truncation hides progress and failures.
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs suites sequentially when managed servers are unavailable to prevent cold builds racing over shared generated assets. Run suites directly; piping through output truncation hides progress and failures.
The app suite uses a stripped-down harness for e2e. Demo and gallery use their normal dev servers.
description:Home Assistant frontend TypeScript conventions. Use when defining or reviewing backend data contracts, optional schemas, shared types, assertions, or Lit lifecycle types.
---
# HA Frontend Types
Use this skill for Home Assistant-specific TypeScript contracts and type choices.
## Home Assistant Data Contracts
Verify data contracts against the source that owns them, such as Home Assistant Core, Supervisor, a WebSocket handler, or an exported component type. Do not shape a type around assumptions made by its current frontend consumers.
- Match required, optional, nullable, and defaulted fields to the producer and runtime contract. Preserve optional configuration fields when omission is supported and has defined behavior.
- Use distinct request and response types when their wire shapes differ.
- When changing a shared contract, check affected consumers, tests, fixtures, and mocks.
## Reuse Home Assistant Contracts
- Reuse the canonical Home Assistant type when one exists. Define shared contract types in the data or API module that owns them.
- Reuse types exported by components and helpers rather than reconstructing their payloads. For event types, follow `ha-frontend-events`.
- When one domain contract is used across modules, define and export it from the module that owns that contract.
Prefer an existing owning contract. Introduce a frontend-specific type when the frontend shape or boundary genuinely differs.
## Assertions
Prefer accurate types and runtime narrowing. Use assertions or TypeScript suppressions at boundaries where the runtime invariant is understood but cannot be expressed cleanly; keep them narrow and explain non-obvious invariants.
## Lit Lifecycle Types
For Lit lifecycle methods that receive changed properties, use `PropertyValues<this>` when the method only needs public reactive properties:
Use unparameterized `PropertyValues` when the method inspects private or protected reactive properties, which are not keys of `this`. Do not add assertions solely to retain `PropertyValues<this>`.
## Enforced Baseline
Use `import type` for type-only imports. This is enforced by the repository ESLint configuration.
@@ -33,6 +33,7 @@ Never run `tsc` or `yarn lint:types` with file arguments. When `tsc` receives fi
- Do not query or manipulate DOM manually when Lit decorators, component refs, or render state are appropriate.
- Scope styles to components, use theme custom properties, and keep layouts mobile-first and RTL-safe.
- All user-facing text must be localized through the translation system.
- Do not write tests just because you changed some code. Write a test when there is real logic that could break without anyone noticing, and explain what the test protects.
## Project Skills
@@ -40,6 +41,9 @@ Detailed guidance lives in project skills under `.agents/skills/`. Load the matc
-`ha-frontend-contexts`: Lit contexts, `hass` migration, and rerender-sensitive state access.
-`ha-frontend-components`: dialogs, forms, alerts, shortcuts, tooltips, panels, and Lovelace cards.
-`ha-frontend-events`: event handler typing, custom event dispatch, and event-map declarations.
-`ha-frontend-types`: backend data contracts, optional schemas, shared types, assertions, and lifecycle types.
-`ha-frontend-lit`: reactive fields, DOM queries, lifecycle behavior, and render-derived state.
"_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>`.",
"Use navigate(), updateHistoryState() or replaceCurrentUrl() from common/navigate. History entries carry the app's own bookkeeping, which a raw pushState/replaceState drops.",
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.