Compare commits

...
Author SHA1 Message Date
Petar Petrov 4f671241ea Drop the hass-shaped context from the security alerts editor
Security entity helpers take states and registries as separate values.
2026-11-11 10:02:48 +02:00
Eric RouleauandCopilot 33b8afffca Show area in ZHA device bindings and neighbors (#55812)
* Show area in ZHA device bindings and neighbors

The 2026.11 naming change means device names no longer carry
their area, but the Manage Zigbee Device panel still showed only
the device name in two places, making identically named devices
indistinguishable:

- The Bindable devices dropdown in the Bindings tab now shows
  the area as the option's secondary line.
- The Neighbors tab gains a sortable, filterable Area column.

Area resolution is shared in getZHADeviceAreaName(): it resolves
via the device registry (so child devices inherit their parent's
area, matching the ZHA network visualization) and falls back to
the area reported by ZHA itself.

* Address Copilot review: registry-authoritative area, memoize by registries

- Treat an existing device registry entry as authoritative: if it
  has no effective area, the device has no area, even if ZHA still
  reports one from its cached response. The ZHA area_id fallback
  now applies only when the device has no registry entry at all,
  so clearing a device's area no longer leaves stale area text.
- Narrow getZHADeviceAreaName() to take the area and device
  registries instead of the whole hass object, and key the
  Neighbors memo on those registries. Entity-state updates replace
  hass, which rebuilt every neighbor row and reran table
  sorting/filtering on unrelated updates.

* Use device picker for ZHA bindings

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

* Reuse getZHADeviceAreaName helper in other places that also did a similar registry lookup

---------

Co-authored-by: Copilot <[email protected]>
2026-11-11 08:46:05 +02:00
renovate[bot] b157dd174e Update dependency maplibre-gl to v6.17.0 (#55844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-11 08:25:17 +02:00
renovate[bot] 4a0381e82d Update dependency @vvo/tzdb to v6.204.0 (#55843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-11 08:17:15 +02:00
renovate[bot] 92e248c913 Update dependency @versatiles/style to v9 (#55846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-11 08:15:54 +02:00
Maarten Lakerveld 5b1ecdb0b0 Let browsers below the legacy floor continue at their own risk (#55829)
* Let browsers below the legacy floor continue at their own risk

Browsers below the floor get a "Continue anyway" button on the launch
screen instead of a dead end, e.g. for a wall dashboard. The warning returns
monthly, and from the first reminder on offers "Don't show again".

* Space the launch screen without max() on Chrome below 79

The launch screen's top spacer and OHF logo margins use max() with var()
inside. Chrome below 79 drops those after the cascade, leaving no margin,
so the logo touched the bottom edge (Chrome 67 to 78 are in the legacy
floor). Plain margins now come first, the max() rules behind @supports.

* Keep the browser warning up while the app loads

Once the user continued, the launch screen went blank until the legacy
build took over, which can take a while on these devices. Keep the message
and only drop the buttons.

* Say the browser is no longer supported instead of too old

With the option to continue it does run, so "too old to run Home Assistant"
contradicted the button. The same text now reads right before and after
continuing.

* Use a plain color for the launch screen buttons

Theme variables are not set yet while the launch screen shows, so
var(--primary-color) always fell back anyway.
2026-11-10 16:53:24 +01:00
Zedas ee35dd8cf6 Send the final media player volume only once (#55828) 2026-11-10 15:04:46 +01:00
Daan VervackeandAidan Timson 42ce434a94 Add selection mode with bulk remove to the Marketplace repositories page (#55435)
* Add selection mode with bulk remove to the Marketplace repositories page

* Guard bulk remove against double clicks and drop stale selections

* Clear the table selection when a selected repository gets installed

* Restore the comments that still apply

* Use a Marketplace string for the bulk action menu

* Drop rendering-only tests

* Uncheck data table rows that can no longer be selected

* Use one Remove chip and let the table drop installed rows

---------

Co-authored-by: Aidan Timson <[email protected]>
2026-11-10 14:40:47 +01:00
Zedas 06d564dbd8 End the ZHA permit subscription when the search ends (#55822)
* End the ZHA permit subscription when the search ends

* End the ZHA search when the connection drops

* Don't send a stale ZHA search after a reconnect
2026-11-10 14:19:10 +01:00
Petar Petrov 93f0c2c45a Hold the last value across gaps in history graphs (#55817)
* Hold the last value across gaps in history graphs

* Hold values across gaps in every graph, without an opt-in

* Hold the real last state across gaps and keep their curves tight

* Only hold stretches the raw history was quiet for

* Keep the temperature forecast curving across gaps

* Decide which stretches to hold once, from the raw history

* Hold neither across unavailable states nor below the sampling frame
2026-11-10 13:53:49 +01:00
renovate[bot] fb40d07621 Update dependency @html-eslint/eslint-plugin to v0.68.0 (#55825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-10 11:53:04 +00:00
Zedas a6073e44f7 Translate the fold markers in the code editor (#55821) 2026-11-10 11:44:59 +00:00
Paulus SchoutsenandClaude Opus 5.5 df69f3c7ee Show which integrations use an integration loaded as a dependency (#55499)
* Show which integrations use an integration loaded as a dependency

Some integrations, like Modbus, have no config flow and are loaded
because another integration lists them in its manifest dependencies.
The frontend treated these as YAML integrations. The integration page
told the user to configure it in configuration.yaml, and the dashboard
showed a YAML card for it.

The integration page now fetches the loaded manifests for integrations
without a config flow. If a loaded integration depends on the domain,
the page shows "Used by <integrations>" instead of the YAML text. The
dashboard does not show a YAML card for a domain that a loaded
integration depends on.

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

* Show the integrations that use a dependency as linked cards

Replace the "Used by" text with a "Used by" section. It lists each
loaded integration that depends on this integration, with its logo and
a link to its integration page. The entries section shows "No entries"
instead of the YAML text for such an integration.

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

* Show only the "Used by" section for a dependency

An integration that other loaded integrations depend on, and that has no
config entries, now shows only the "Used by" section. The page does not
show the empty entries section. It also does not show the "Add entry"
button, which only opens the "set up in YAML" dialog for these
integrations.

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

* Match the demo Modbus and LoRaWAN manifests to core

Modbus and LoRaWAN have no config flow, and the integrations that use
them list them as dependencies. The demo now does the same, so their
integration pages show the "Used by" section.

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

* Hide system integrations from the "Used by" section

System integrations, like Default Configuration, depend on many
integrations. Listing them in "Used by" does not help the user.

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

* Read the dependents from the manifests context

The integration page now finds the integrations that depend on it from
the shared manifests context, instead of its own manifest/list call.
It subscribes to the context only for an integration without a config
flow, so other integration pages do not make the context fetch the
manifests.

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

* Link the integration page to the integration's settings panel

The Connectivity settings panels, like Zigbee and Modbus, are not
reachable from the integration page unless the integration has a config
entry. A user that opens the integration page looks for these settings.

The integration page now shows a button that opens the panel of the
integration. The button uses the panel name from the Connectivity page.
LoRaWAN, Modbus, Radio frequency, and USB (Serial) are added to the map
of integrations with a panel.

An integration with a panel and no config flow does not show the
"Add entry" button. For these integrations, it only opens the "set up
in YAML" dialog, and the panel is where to set them up.

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

* Hide the YAML texts for integrations set up in their panel

An integration with a settings panel and no config flow, like Infrared,
is set up in that panel. Its integration page told the user to set it up
in configuration.yaml. The page now shows only the settings button for
such an integration when it has no entries.

The demo Infrared and Radio frequency manifests now match core, so these
pages show it.

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

* Make the settings button primary when it is the only action

An integration that is set up in its panel shows no "Add entry" or
"Add device" button, so the settings button is its only action.

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

* Show the settings button only when there are no entries

Each config entry row of an integration with a panel already has a
link to that panel, so the button repeats it on pages like Zigbee.

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

* Link the MCP integration pages to the AI settings

The AI settings show how to connect to the MCP server and the URL of
each of its APIs. The MCP client and MCP server integration pages now
have an "AI settings" button that opens them. The button is the
primary action when the page has no "Add entry" button.

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

* Scroll the AI settings to the MCP card from the MCP pages

The AI settings page now scrolls to the section named in the URL hash,
like the backup settings page. The "AI settings" button on the MCP
integration pages links to the MCP card, which is below the AI task
card.

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

* Keep the demo route when opening the AI settings

The demo routes with the URL hash. The AI settings page cleared the
hash even when it did not name a section, so a reload of the demo lost
the page. It now clears the hash only after it scrolls to a section.

The "AI settings" button links to the page without a section in the
demo, because a link with a hash skips the demo route conversion.

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

* Move the settings panel and MCP buttons to their own PRs

This PR keeps the "Used by" section for integrations that other
integrations depend on. The settings panel button and the MCP link to
the AI settings move to separate PRs.

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

* Load the Modbus dependents in the demo

The dashboard reads the dependencies of loaded integrations only. The
demo listed Modbus as loaded, but not Flexit, SolarEdge Modbus, and
Fronius, so it showed a YAML card for Modbus.

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

* Sort the "Used by" list and wait for it before the YAML texts

The manifests come from a set in core, so their order can change
between restarts. The "Used by" list is now sorted by name.

Until the manifests load, the page does not know the dependents. It
showed the YAML texts and "Add entry" first and then replaced them with
"Used by". The page now holds back those texts and the button for an
integration without a config flow until the manifests load.

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

* Apply review feedback

- Make the settings button primary whenever it is the only action,
  like the MCP button. Thread, or MQTT with its one entry, have no
  "Add entry" button.
- Set the demo LoRaWAN manifest to the service type, like core, so the
  demo dashboard checks its dependents.
- Fix the punctuation of the YAML text.

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

* Show the entries while the dependents load

The page waits for the manifests of the loaded integrations before it
shows the YAML texts. If that request fails, the wait never ends. Only
a page without entries can show "Used by", so only such a page waits.
A page with entries now shows them even when the manifests do not load.

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

* Clear the manifest when the integration page changes domain

The integration page is reused when you follow a "Used by" link. It
kept the previous integration's manifest until the new one loaded, so
going back to Modbus from Flexit briefly showed "Add entry" and the
empty entries section. The page now clears the manifest on a domain
change, and a page without entries waits for the manifest before it
shows the YAML texts and "Add entry".

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

---------

Co-authored-by: Claude <[email protected]>
2026-11-10 10:55:56 +01:00
Markus TuominenandClaude Fable 5.1 95a009641f Fix integration icons being clipped into circles on the area page (#55820)
Stop rounding and distorting integration icons on the area page

The generic img rule meant for the area picture also applied to the 24px
brand icons in the device list, clipping square integration logos (for
example Awair) into circles. Scope that rule to the area picture and size
the icons with width/height attributes like the device page does.

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-10 10:17:22 +01:00
Wendelin 36f59f8495 Pass callWS to entity and device registry update helpers (#55819) 2026-11-10 11:02:12 +02:00
Stefan AgnerandClaude Opus 5.5 8a13bba521 Only match an OTBR to a Thread network it is part of (#55803)
* Only match an OTBR to a Thread network it is part of

findOTBRInfoForDataset returned the dataset's preferred border router
without checking that it is still on that network. When the border
router had moved to another network, the Thread panel offered to reset
it from the card of a network it no longer belongs to, which would
replace the border router's current network.

Require the extended PAN ID to match for the preferred border router
too, and drop the active dataset check in the network information page
that worked around the same problem.

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

* Update findOTBRInfoForDataset tests for the network check

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

* Keep findOTBRInfoForDataset fallback tests independent of key order

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-10 11:00:30 +02:00
Maarten Lakerveld cc7b1952c0 End slider and color wheel interaction when a drag is cancelled (#55804)
End slider and color wheel interaction when a pan is cancelled

ha-control-slider and ha-hs-color-picker restored their value on
pancancel without firing an event, so consumers never learned that the
interaction ended. The light color pickers then kept ignoring state
updates until the next interaction. Fire the same end-of-interaction
event as panend (and as ha-control-scrubber already does), and restore
the wheel's local value along with its cursor position.
2026-11-10 10:59:38 +02:00
karwosts 2edf6f91d0 Allow configurable time format for timer.finishes_at in state-display (#55813)
Allow custom time format for timer.finishes_at
2026-11-10 09:26:27 +01:00
jimjaeger 9d52517ae9 Retry translation fragments after a failed request (#55716)
* Retry translation fragments after a failed request

* Clear translation mock before each test
2026-11-10 08:51:58 +02:00
renovate[bot] 27a7f6335f Update dependency @vvo/tzdb to v6.203.0 (#55814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-10 08:25:58 +02:00
renovate[bot] cd44bfcebc Update dependency eslint-plugin-import-x to v4.18.0 (#55815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-10 08:25:55 +02:00
Elias Lecomte 8819844740 Fix single-day selection in the date range picker (#55718)
* Fix single-day selection in the date range picker

* Limit single-tap date selection to devices without hover
2026-11-10 08:23:21 +02:00
WendelinandClaude Opus 5.5 008adf1533 Move the create helper dialog and helper forms to contexts (#55794)
* Narrow helper create and area update data functions to the API they use

The create helper dialog can then pass the API context instead of the
full hass object.

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

* Consume contexts in helper forms instead of hass

The helper forms only read translations, locale and config, so they now
consume the internationalization and config contexts. This avoids
rerendering them on every unrelated hass change.

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

* Consume contexts in the create helper dialog instead of hass

The dialog reads translations, config, the API and the connection from
their contexts and no longer passes hass to the helper forms.

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

* Refactor API calls to use HomeAssistantApi for improved type safety and consistency across data handling

---------

Co-authored-by: Claude <[email protected]>
2026-11-09 20:54:31 +01:00
renovate[bot] 8c3d0f2f1e Update dependency serve to v14.2.7 (#55810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-09 20:52:23 +01:00
renovate[bot] 3f4f89480f Update tsparticles to v4.5.3 (#55811)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-09 19:51:19 +00:00
Matthias de BaatandClaude Opus 5.5 432762a2ea Add design to the AI policy (#55795)
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-09 20:45:01 +01:00
WendelinandClaude Opus 5.5 013cf8d7e6 Give automation editor rows a config path (#55801)
* Give automation editor rows a config path

Each list and row in the automation and script editor extends the path
of its ancestor with its own segment through a Lit context, e.g.
["actions", 0, "then", 1]. Rows that open in the sidebar register in a
per-editor registry by path, as groundwork to find the row a config path
belongs to, e.g. to point at an error in it.

Move the generic path type and helpers from the Lovelace editor to
common/util/config-path and use ConfigPath everywhere.

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

* Test config path helpers on their own

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-09 20:17:34 +02:00
Joost LekkerkerkerandClaude 1de489cca9 Sort device connections by type in device info card (#55797)
Claude-Session: https://claude.ai/code/session_01SLHDn4zYGvMHi4C8v3m45c

Co-authored-by: Claude <[email protected]>
2026-11-09 16:58:30 +01:00
Maarten Lakerveld 97af661779 Use the Backups menu caption as the backup page title (#55799)
The Settings > System menu said "Backups" while the page header said
"Backup". Reuse the caption key like the other system pages do, so
the two can't drift apart again.
2026-11-09 16:41:09 +01:00
Erwin Douna f702e369fc Add a health dialog to config entries (#55736)
* Add a health dialog to config entries

* Address review feedback

* Address review feedback

* Address review feedback

* Address review feedback

* Use the integration filter for the logs link
2026-11-09 16:40:04 +01:00
Maarten Lakerveld 78e0a92ddd Include disabled helpers in the helpers area filter (#55796)
The helpers table lists disabled helpers, but its area filter left them
out, so a disabled helper without an area disappeared when picking
No area, and one in an area disappeared when picking that area. Match
the entities page, which already passes include-disabled-entities.
2026-11-09 16:24:11 +01:00
Andreas d742259444 Fix stale color and slider jump after quick drag in light color pickers (#55780)
* Fix stale color sent after quick drag in light color pickers

When the color temperature slider is dragged and released within the
500 ms throttle window, the release handler skipped sending because the
picker value already equalled the final value, relying on the pending
throttled call. A state update from the leading throttled call then
reset the picker value (no longer interacting), so the trailing call
sent the stale initial value and the light ended up at the wrong color
temperature.

Cancel the pending throttled call on release and always send the final
value. Apply the same cancel to the RGB picker, where a pending trailing
call could send a stale color after the final one.

* Keep released value in light color pickers until state settles

After release, keep showing the released value for a short time and
ignore state updates in between, so that late state updates from calls
sent while dragging no longer make the slider jump back before the
final value is reported.

* Don't sync light color pickers while a new drag is active

If a new drag starts before the release hold of the previous one ends,
the hold timer must not overwrite the picker value with the reported
state, as that makes the control jump and lets a pending throttled call
send the older value. The release of the new drag starts a new hold.

* Address review: keep brightness normalization in sync, hold native input

Keep the brightness normalization of RGB-only lights in sync with the
reported state while the picker values are held (during a drag or the
release hold), as it is applied to the next color sent. Otherwise
successive color selections could dim the light repeatedly.

The native color input has no release event and sends throttled, so
refresh the release hold on every input. This keeps state updates from
calls sent while picking from overwriting the pending value.

* Use PendingValueSync in light color pickers and end interaction on release

Replace the fixed release hold timer with PendingValueSync, so state
updates are ignored only while a call is queued or in flight. Also clear
_isInteracting in the release handlers, so a keyboard adjustment or a
cancelled pan no longer stops the picker from following the state.
2026-11-09 16:22:09 +01:00
OllieandClaude Opus 5.5 623ca14356 Configure remote Bluetooth adapters from the adapters page (#55742)
* Configure remote Bluetooth adapters from the adapters page

Remote scanners such as ESPHome Bluetooth proxies have their scanning
mode and other settings in the options flow of the integration that
provides them. Show the configure button for remote adapters on the
Bluetooth adapters page and open that integration's options flow, found
through the scanner device's via device, so users no longer have to find
the device under Devices & services.

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

* Prefer the providing integration's device when finding the source entry

Other integrations may add config entries to the scanner's own device,
so look at the via device first and only fall back to the scanner's
device.

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

* Only use the via device and skip disabled source entries

Drop the fallback to the scanner's own device, which could pick up an
unrelated integration's entry, and don't offer to configure a remote
adapter whose providing config entry is disabled or has no options flow.

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

* Check for a configurable source entry in the template

Keep _getSourceConfigEntry a plain lookup, as suggested in review, and
skip the cog for disabled or non-configurable source entries where it
is rendered.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-09 16:10:41 +01:00
166 changed files with 3644 additions and 975 deletions

No files matched your search

+8
View File
@@ -28,10 +28,18 @@ AI can be a helpful tool for writing code and documentation. However, due to the
All contributions must be reviewed and understood by the contributor before submission. You should be able to explain every change in a pull request you submit. Pull requests that appear to be unreviewed AI output will be closed without review.
## Design contributions
AI can be a helpful tool to explore ideas and build mockups and prototypes. But AI tends to give the most common answer, not the one that fits Home Assistant and its users. So we require a human who brings the point of view and owns the design decisions.
You should be able to explain the problem your design solves and why you made your choices. If a mockup or prototype was mostly generated with AI, say so when you share it. Check your work against the [design guidelines](https://contributors.home-assistant.io/design/), as AI tools often make up components, colors, or spacing that don't exist in Home Assistant. Design proposals that look like unreviewed AI output may be closed or hidden.
## Our use of AI
Some of our projects use AI tools to assist with code reviews, issue triaging, reporting, and other project management tasks. These tools may leave comments on pull requests or issues. As with any automated tooling, these comments are not always correct.
We also use AI to help keep the design system, the Figma kit, and the code in sync. The design decisions behind them are made by people.
If an AI tool leaves a comment on your contribution, treat it as you would any other review comment. If you believe it is incorrect, say so; a brief explanation is sufficient. Maintainers always have the final say. If in doubt, ask a maintainer.
## Enforcement
@@ -44,14 +44,21 @@ export const lorawanFixtures: ConnectivityFixtures = {
],
commands: ["lorawan/"],
manifests: [
manifest("lorawan", "LoRaWAN", { integration_type: "helper" }),
manifest("lorawan", "LoRaWAN", {
config_flow: false,
integration_type: "service",
}),
manifest("sensecap_lorawan", "SenseCAP", {
integration_type: "device",
dependencies: ["lorawan"],
}),
manifest("chirpstack", "ChirpStack", { integration_type: "hub" }),
manifest("chirpstack", "ChirpStack", {
integration_type: "hub",
dependencies: ["lorawan"],
}),
manifest("thethingsnetwork_lorawan", "The Things Network", {
integration_type: "hub",
dependencies: ["lorawan"],
}),
],
configEntries: [
@@ -10,19 +10,26 @@ export const FRONIUS_ENTRY_ID = "mock-fronius";
export const RS485_PORT = "/dev/ttyUSB1";
export const modbusFixtures: ConnectivityFixtures = {
components: ["modbus"],
components: ["modbus", "flexit", "solaredge_modbus", "fronius"],
commands: ["modbus/"],
manifests: [
manifest("modbus", "Modbus", {
config_flow: false,
iot_class: "local_polling",
}),
manifest("flexit", "Flexit", {
integration_type: "device",
dependencies: ["modbus"],
iot_class: "local_polling",
}),
manifest("solaredge_modbus", "SolarEdge Modbus", {
integration_type: "device",
dependencies: ["modbus"],
iot_class: "local_polling",
}),
manifest("fronius", "Fronius", {
integration_type: "device",
dependencies: ["modbus"],
iot_class: "local_polling",
}),
],
+8 -8
View File
@@ -77,11 +77,11 @@
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.24",
"@tsparticles/engine": "4.5.2",
"@tsparticles/preset-links": "4.5.2",
"@versatiles/style": "8.1.1",
"@tsparticles/engine": "4.5.3",
"@tsparticles/preset-links": "4.5.3",
"@versatiles/style": "9.0.0",
"@vibrant/color": "4.0.4",
"@vvo/tzdb": "6.202.0",
"@vvo/tzdb": "6.204.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"barcode-detector": "3.2.3",
"cally": "0.9.2",
@@ -108,7 +108,7 @@
"leaflet.markercluster": "1.5.3",
"lit": "3.3.3",
"lit-html": "3.3.3",
"maplibre-gl": "6.16.0",
"maplibre-gl": "6.17.0",
"marked": "18.1.3",
"memoize-one": "6.0.0",
"nanoid": "6.0.2",
@@ -147,7 +147,7 @@
"@eslint/js": "10.0.1",
"@gfx/zopfli": "1.0.15",
"@home-assistant/components": "workspace:*",
"@html-eslint/eslint-plugin": "0.67.0",
"@html-eslint/eslint-plugin": "0.68.0",
"@lokalise/node-api": "17.0.1",
"@octokit/auth-oauth-device": "8.0.5",
"@octokit/rest": "22.0.1",
@@ -180,7 +180,7 @@
"eslint": "10.14.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.17.1",
"eslint-plugin-import-x": "4.18.0",
"eslint-plugin-lit": "2.3.1",
"eslint-plugin-lit-a11y": "5.1.3",
"eslint-plugin-unused-imports": "4.4.1",
@@ -208,7 +208,7 @@
"pinst": "3.0.0",
"prettier": "3.9.10",
"rspack-manifest-plugin": "5.2.2",
"serve": "14.2.6",
"serve": "14.2.7",
"sinon": "22.1.0",
"tar": "7.5.22",
"terser-webpack-plugin": "5.6.1",
+182 -236
View File
@@ -277,7 +277,7 @@ importers:
version: 1.0.3
'@maplibre/maplibre-gl-leaflet':
specifier: 0.1.4
version: 0.1.4(@types/[email protected])([email protected])([email protected]6.0)
version: 0.1.4(@types/[email protected])([email protected])([email protected]7.0)
'@mdi/js':
specifier: 7.4.47
version: 7.4.47
@@ -291,20 +291,20 @@ importers:
specifier: 0.5.24
version: 0.5.24
'@tsparticles/engine':
specifier: 4.5.2
version: 4.5.2
specifier: 4.5.3
version: 4.5.3
'@tsparticles/preset-links':
specifier: 4.5.2
version: 4.5.2
specifier: 4.5.3
version: 4.5.3
'@versatiles/style':
specifier: 8.1.1
version: 8.1.1([email protected]6.0)
specifier: 9.0.0
version: 9.0.0([email protected]7.0)
'@vibrant/color':
specifier: 4.0.4
version: 4.0.4
'@vvo/tzdb':
specifier: 6.202.0
version: 6.202.0
specifier: 6.204.0
version: 6.204.0
'@webcomponents/scoped-custom-element-registry':
specifier: 0.0.10
version: 0.0.10
@@ -384,8 +384,8 @@ importers:
specifier: 3.3.3
version: 3.3.3
maplibre-gl:
specifier: 6.16.0
version: 6.16.0
specifier: 6.17.0
version: 6.17.0
marked:
specifier: 18.1.3
version: 18.1.3
@@ -496,8 +496,8 @@ importers:
specifier: workspace:*
version: link:packages/components
'@html-eslint/eslint-plugin':
specifier: 0.67.0
version: 0.67.0([email protected]([email protected]))
specifier: 0.68.0
version: 0.68.0([email protected]([email protected]))
'@lokalise/node-api':
specifier: 17.0.1
version: 17.0.1
@@ -595,8 +595,8 @@ importers:
specifier: 0.13.11
version: 0.13.11([email protected])
eslint-plugin-import-x:
specifier: 4.17.1
version: 4.17.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
specifier: 4.18.0
version: 4.18.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
eslint-plugin-lit:
specifier: 2.3.1
version: 2.3.1([email protected]([email protected]))
@@ -679,8 +679,8 @@ importers:
specifier: 5.2.2
version: 5.2.2(@rspack/[email protected](@swc/[email protected]))
serve:
specifier: 14.2.6
version: 14.2.6([email protected])
specifier: 14.2.7
version: 14.2.7([email protected])
sinon:
specifier: 22.1.0
version: 22.1.0
@@ -2075,27 +2075,27 @@ packages:
resolution: {integrity: sha512-EX1qiX5h3LAoTjuE3rgfuRlNq3Rds5tGrgDGlHoFbxhXaCSTTtuYU6tdR8vgOXKGUDi5gBDrcoNdLYKFXSrknA==}
engines: {node: '>=14.17.0'}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-MPoKw4woCkuFYs2xdt0vEjzGbUzU8lxeTZkxyrtHM+zZlaK/u/lqcIapZmyCTQBqNE0H9J8zc9TmXjygfHcuXg==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-u3HioqcKIH6dVQp2uulmpcuUqgKLqiuOWyHAl7hV4hIUkuJ3ge7vEUwPoWvNRbfy3aWOMygttokdGxL5q9YFIg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-gr2wKoS1OOZa6uL8DDNCjvPyTu6mWsyQJ/WXmvP/DeFRe6pdumsb8ePYfP7qZW5741MtVd0OpLbxa82XivxxWQ==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-fiR8UTYBr4+fGftEG1u+tT3AiuOSowvSZbsY4tLcMl9hOuFzquHw4BIaFFlV3xTeqF5Y0M69aPJoEhFJUkQyog==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: '>=8.0.0 || ^10.0.0-0'
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-ejdtqLqclQPDfivgsD2yWHXa+FyyEytMlGNNfiHw/UbUeV9tn1hBjIZLJ5VvzjO0IweaAIWIZKlEPgKT6tYP8A==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-X22bU2qzEpzPI0bHtnfXzoUS8q/It/DNycJnIdsPzBDXcly+PItV5AJU4nm/+9FZCXSE3IZJvORRCTuvvpV8Bg==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-fvFAuAaKzkg9deV69uT1NoVJSwXTu3mvGDWzlZKe+xwm5JnkbWC626eyDRlAQDzff2/zX7tWFq4EMgOoZmgtRA==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-zLgikGAF7izvrvUefy5EBEl6WpM0sTN9xzldwp7UgkkjR+zaFvqi9YsOWj+Y7Tu3XMXI0d7Gxv7PPPumVmJTuw==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-leHUXnpFgRSR3o7rpw5OtOVGfzcChIgrhXDgPlBwdoL7V4+5KZjxGLJAPVwbj7sVVNrSWynlIQ1Wn4FuNKxZhw==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-qbIF3MqmRckg6pXhwY/Ytxnb0eOoq5LPVt9wq8N4UBQHxL2z4cF407FHAiLNhpy8DO4AJUQbI6IZzhN8fHkvPA==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-8w9V0sTm2DGLh2uslL7+Un9g/FHNcevgouiTLhlAJoBhhGOK5+fX76QCKdWbgmpudNGo8Nh1kHKsJP3a6twzXA==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-FW+FjFHp/w1V4uZ0us7keR6mAA+WGh6OsHvJ+Kzy8aBeogyOapDPEMT+zkXSsiJ+V9maJ6umqG20BXcs2y6lsw==}
'@humanfs/[email protected]':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
@@ -3270,85 +3270,85 @@ packages:
resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==}
engines: {node: '>=12'}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-bwvu84lMX5Sp79pyKu5yW2SFAqY52B7OlaxDjeeP3TiO7hJLFWxd8VPmSfOarcYK/z/uxp/dZfbcvp/5ylXyiQ==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-Fz2bTDc6Folav8isOk2scXM+LCSPhQ9hD8xCSmsEGuGH7JkE+XTsS4j6E9H6b+RFlZ1yb2TxdVBJnfXy8XM9Bw==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-lA+GP2EN5uN3TnOMEFqYBS0yLuQyZ2UlHy72TedgREGeEsrUoXB/FtZKseVt0g9FId4ZI6jXizxEavs65PbUsA==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-f0gHWnqChK4ksE9Bq3YN0iuJVMi2B81YSkCL5BrohuykoaKCLSVVSqjhefmELTMrdiFVsdt6WPJ+HjVQgjt7CA==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-NaxB1X/90lSu+uI0ayaaNC4okLIwJ93nBWvrdYHQ5/xsyan01qU+rnDjdg8Y9fPewncXlHkOaQezHeG4RjDubg==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-kbZoUA2uqR2043FHfH2+KFz7JhYTZjyTeDqVL/Z3z38mfdHEVrOJ4q+mwYjr+hX4tq2YoLf1D4DA6w/er1AlKA==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-JmaqBMZW/OzXGeQBz1g67EwTKeOYccIFrQMnBmiZgavO7LLhglABpyTWJAT9Cb7M/kgx3GIecZGkPxFzQ44pxQ==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-ak8AAPQKinJlwztRAo+OA5ItwjLtymmjayefQCF6K1FIWs5SaWiHWbzMB5vdOvuGUE6OOchqj8NLcu3HfHmdjw==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-kNwh59Oyi8lvFQYK9z4YgAhS0GcOfFLkIhafTo0qlklqSUl3CvZq0aS14t368oz4Qk/l29pvnLJQg8pmRxIMaw==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-NAm9yL8o3oaeYgB7WLzuoD0yfLw9kvEwu0N+UHTwoe+kYRLJvmoUyMlED7hPQHW74VTlSmuZ6gqWTnaOv1mFLA==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/plugin-interactivity': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/plugin-interactivity': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-7mO9tpJvXkbRIn4OjjUNoiPKAEJkEeazLl8eQB9W9lYpxuNCyZZ+sXJpS2LazyBNKTvGy1P0ekoX/rHqc84vKA==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-fJJp0xVEeGQDpkjGzvftMbCrLWtwqo+ees49gCnr9SZYVMRbxi3vQYukvi4ULOzYgEW5bTseVprDfiqOwfQJcw==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-gzcPgOzb3vP3p1AF/op5045buNYjsUOHoeTx9WHABrbDDVrQYUo1KsThg6Uo6Uahd8yMIuWUDlzjgF722KBn0w==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-k+AkTG8r2UTE3n2u4kmb9dODRVtBc3Yttk7/UQ5HPlcBMr8AnKbx9gyxzzCx2MaEFZG2My0cghJPvQvibrAW7Q==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-/gIqWofLBYLcYcEGt1jxZPD+kgCJOoajS22NxxbVi80MrUxvT586qqbhYiiAesBxRUgCZOJnK39BF2S4A0Ik7Q==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-jfyHjPvrc8ORwaiIJ3xgzIveRhlImn21TiblpakeF2Ft5HgxJ+Lb8eIN/2VUJFjdqLpQ3H1nGSJspHYzeU5RtQ==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-uzZGjqxFhz/LUAKnbBYApgelXBrXSJt25fad8TxL5RoDVGgd22pxPErwEtCTAbhJyL6Z5Clj9p5vk9Vve+sodg==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-4w1m8WFL5UJElwTO978P9fOub2vtLCYRLD0YpmXY+AumZw02wLRH6Al7xY2Sgchbgm/uLHkCDzeUTYUi9GzURg==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-Oh2FPN26M+JlSFvLr7rOJEyTO7n0AUu5x3gHCx4mX9Dqm0JQsL0T17nUMc8pra/nHOemiK3W+dCIm0Y7/8GQiQ==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-kyriGuLkOu5uVv3cHRacJRk8XMm9gga7OzCuf3oxbgfqqIaazva4Qw8N19RCpVCW1EfwyWFGq3h5VD/QplkA0w==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-y6s2yant8vBdvIw6chTlPVxf9T5lzdmpSyEsI6gX3obTSaJyhw6nbfl6iWcQNx1jjgp4dYVhEf8PneNMqC8VQw==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-lKlI9BaHXy+L30d2CKWWn3B9Ye7LOqRHav8rzXcxAQMfZxj4iK1UuGRT+27/KzQMZBYMPd9G4R7x7lV8RngFdQ==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-WH8dfjSMWa5uCzheemqy+UzQXIpIymWVrnBOK3L3eBusYFhAg3L0ChBhjKBDjPDlKOquoeRrdt5D8ebgBjlY8g==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-U21Aj8pPjxW7wEH1Itm7Ik1rBO5rTXyxKWLjZXEDlT1agXxcwCVbL/5rffUQzbeRFAk/t4KiUGi4rTvAS62wTQ==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-D6sKXKLPZZ45csOfj3mUQ1bzBgO5mJw7qfpAIYLlk7H+slrHlIoOwxwWgwcuBTY7JJ0jLgiq2062yLHxjEfdeA==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-jtJAqqnltG12YCy7VpTtIIEOlRNmVDz11SQrZlxFUHWgIYa0UcTEUDeLpP4h9IW7KtnxJKBo/xu1JLRq4tY6fw==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-/mJkwVoFZZl4TtkPmAhdZYuiSjaegJ4zs5l7YJdwKPnN7/6xmvJwrQhj9UGQHwusyUBZ5/V+6cXQFEG7Em154Q==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-VaWHjSacuvcDpUPKriQMeG4xzNTKRuK7tZJL0toDQdB4uei2LX5qUBUqTrI0oBWW5Rk0QdUZR83LkhCLVJWtew==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-vpSXG73F0lFMsb1uwgut7SiuFNF0Hk5QRD7i8DJO5oyyTrSkrHUjQHFFL7ONb+zmPzLC1J8SlfHXkldJZlpKww==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-gx9cV9LutgY0f25AQtFqW285jGCsHpAxYKbmoN4MOUEYOVX9WiDfrf59Ta9c/SelLvOanVsu5V1hVzBsJyX37w==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-6EXelSVOv0LQrAretlDMIxMV/e1+GfJftBfPsHuCTPrTbmcoEIHQRC9VsGQDS/k4ija6oA2GTqTvoni8Ztbk8A==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-I+tPJgq7M5Qcu4pX97J1Hfo0s+bnCmj32IiZ3AJTFFA/+xScaL0SF7T7tW5YtXR45ZwYjaSdnmQ80CwrLxJjdQ==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-XwcyTnRssZ+ssuL500ojFaMdR5w+eb9XVs7MHd2G8q3A5N5a2UAKIhExiTEbWyAGXSgfHo/LQxUTX2Lq1cxSRg==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-hh62wuc4HZ+BydUC62bA74hgr+qV888iWm23V222d6rqIkOpYzi5tcoPAh48Mgjl2gMSigTE6sySJez7reffNQ==}
peerDependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tufjs/[email protected]':
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
@@ -3785,8 +3785,8 @@ packages:
cpu: [x64]
os: [win32]
'@versatiles/style@8.1.1':
resolution: {integrity: sha512-wotbl1VYI0JK94oq7Ji/QTdUi4jHoXtWpBnia/tcFBvAM8Ho4URnRjkNdJ6VbMM/3PqoFEYhUispAhsdCvv5hA==}
'@versatiles/style@9.0.0':
resolution: {integrity: sha512-hKtVfdQvxLYX7BaGKQhgtcnOHtj+dc1qU7hXL3V4KzMWU+RZtmGI83m5JPxGIB5M/UoDKc3cyXZaf2OeiL5jCA==}
engines: {node: '>=20'}
peerDependencies:
maplibre-gl: '>=5.5.0'
@@ -3864,8 +3864,8 @@ packages:
'@vscode/[email protected]':
resolution: {integrity: sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==}
'@vvo/[email protected]2.0':
resolution: {integrity: sha512-MGsedU65ypk/Y7yrp+oOLBzhQj/124xvyoUzx7csPiBSf1b3lgTp5imatuWU4SF6s1Ue3I6GxC+GxE8R5O1l0w==}
'@vvo/[email protected]4.0':
resolution: {integrity: sha512-GC7i3D4rCs6l5jnf6NofoRRumZwVHS1cBNhgbenDLLyJNqJOrTo/8zrWovIrJl5BujiFurqn0Ayj10wysmOKrQ==}
'@web/[email protected]':
resolution: {integrity: sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==}
@@ -4050,9 +4050,6 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
[email protected]:
resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
[email protected]:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -4400,6 +4397,9 @@ packages:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==}
[email protected]:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'}
@@ -4420,10 +4420,6 @@ packages:
resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
engines: {node: '>=18.20'}
[email protected]:
resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
[email protected]:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
@@ -4909,6 +4905,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
[email protected]:
resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==}
[email protected]:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
engines: {node: '>= 0.4'}
@@ -4962,8 +4961,8 @@ packages:
eslint-plugin-import: '>=1.4.0'
webpack: '>=1.11.0'
[email protected]7.1:
resolution: {integrity: sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==}
[email protected]8.0:
resolution: {integrity: sha512-UfxHFmV+f9vwybrNn6LQ1rPzfeDMPGi/dRbqQnCFO/jBzYrujSOyX7jxqBSYr/HwXs/V4XJdEGPvC3Re46nzKg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/utils': ^8.56.0
@@ -5067,10 +5066,6 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
[email protected]:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==}
@@ -5288,10 +5283,6 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
@@ -5498,10 +5489,6 @@ packages:
resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==}
engines: {node: '>= 20'}
[email protected]:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
[email protected]:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
@@ -5638,11 +5625,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
[email protected]:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==}
engines: {node: '>= 0.4'}
@@ -5792,10 +5774,6 @@ packages:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
@@ -6138,8 +6116,8 @@ packages:
[email protected]:
resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==}
[email protected]6.0:
resolution: {integrity: sha512-gApO66EMtoDZuio680RUBvskR7ahFd/NNhLN4tBAs+Y7FeT4gQV8zVFgYVMRmFxQznOEzecW19w6xOd9XjZjiA==}
[email protected]7.0:
resolution: {integrity: sha512-2chIZwpLGenoUQOcnm9Pk4SLbA8ppNFq8jlpXiq9vcYm3Y1wJ3fc82FL0GLwBaPYJCLzPv/mC46KEupVov75rQ==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
[email protected]:
@@ -6435,10 +6413,6 @@ packages:
resolution: {integrity: sha512-vzc1svxw/kw1IRjFsLi6gaxe1Olqm88V0tIfu2u5raL0b1gChe6ZEXNkyUlKxUC7s/egt5NxZHkbY18tMKKLfQ==}
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
[email protected]:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -7040,8 +7014,8 @@ packages:
[email protected]:
resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==}
[email protected].6:
resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==}
[email protected].7:
resolution: {integrity: sha512-mTJ9LAyBwHhO2T/PNaI0oWxmKl7FFuJ+ZEHKL1rRO69fWV1Y4qcsFB3gpuD2HfHvJCwvJq5ChibjjAGe0wc0vw==}
engines: {node: '>= 14'}
hasBin: true
@@ -7295,10 +7269,6 @@ packages:
resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
@@ -7439,6 +7409,10 @@ packages:
resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
engines: {node: '>=20.0.0'}
[email protected]:
resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==}
engines: {node: ^16.14.0 || >= 17.3.0}
[email protected]:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
@@ -9823,40 +9797,40 @@ snapshots:
- '@types/node'
- '@types/react'
'@html-eslint/[email protected]7.0':
'@html-eslint/[email protected]8.0':
dependencies:
'@html-eslint/types': 0.67.0
'@html-eslint/types': 0.68.0
html-standard: 0.0.13
'@html-eslint/[email protected]7.0([email protected]([email protected]))':
'@html-eslint/[email protected]8.0([email protected]([email protected]))':
dependencies:
'@eslint/plugin-kit': 0.4.1
'@html-eslint/core': 0.67.0
'@html-eslint/parser': 0.67.0
'@html-eslint/template-parser': 0.67.0
'@html-eslint/template-syntax-parser': 0.67.0
'@html-eslint/types': 0.67.0
'@html-eslint/core': 0.68.0
'@html-eslint/parser': 0.68.0
'@html-eslint/template-parser': 0.68.0
'@html-eslint/template-syntax-parser': 0.68.0
'@html-eslint/types': 0.68.0
'@rviscomi/capo.js': 2.2.1
eslint: 10.14.0([email protected])
html-standard: 0.0.13
'@html-eslint/[email protected]7.0':
'@html-eslint/[email protected]8.0':
dependencies:
'@html-eslint/template-syntax-parser': 0.67.0
'@html-eslint/types': 0.67.0
'@html-eslint/template-syntax-parser': 0.68.0
'@html-eslint/types': 0.68.0
css-tree: 3.2.1
es-html-parser: 0.3.2
'@html-eslint/[email protected]7.0':
'@html-eslint/[email protected]8.0':
dependencies:
'@html-eslint/types': 0.67.0
'@html-eslint/types': 0.68.0
es-html-parser: 0.3.2
'@html-eslint/[email protected]7.0':
'@html-eslint/[email protected]8.0':
dependencies:
'@html-eslint/types': 0.67.0
'@html-eslint/types': 0.68.0
'@html-eslint/[email protected]7.0':
'@html-eslint/[email protected]8.0':
dependencies:
'@types/css-tree': 2.3.11
'@types/estree': 1.0.9
@@ -10106,11 +10080,11 @@ snapshots:
dependencies:
kdbush: 4.1.0
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]6.0)':
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]7.0)':
dependencies:
'@types/leaflet': 1.9.22
leaflet: 1.9.4
maplibre-gl: 6.16.0
maplibre-gl: 6.17.0
'@maplibre/[email protected]':
dependencies:
@@ -11118,88 +11092,88 @@ snapshots:
magic-string: 0.30.21
string.prototype.matchall: 4.0.12
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
'@tsparticles/[email protected].3':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/plugin-blend': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-hex-color': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-hsl-color': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-move': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-rgb-color': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/shape-circle': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-opacity': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-out-modes': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-paint': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-size': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/engine': 4.5.3
'@tsparticles/plugin-blend': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-hex-color': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-hsl-color': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-move': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-rgb-color': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/shape-circle': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-opacity': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-out-modes': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-paint': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-size': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2': {}
'@tsparticles/[email protected].3': {}
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)(@tsparticles/[email protected].2(@tsparticles/[email protected].2))':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)(@tsparticles/[email protected].3(@tsparticles/[email protected].3))':
dependencies:
'@tsparticles/canvas-utils': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/engine': 4.5.2
'@tsparticles/plugin-interactivity': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/canvas-utils': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/engine': 4.5.3
'@tsparticles/plugin-interactivity': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2':
'@tsparticles/[email protected].3':
dependencies:
'@tsparticles/basic': 4.5.2
'@tsparticles/engine': 4.5.2
'@tsparticles/interaction-particles-links': 4.5.2(@tsparticles/[email protected].2)(@tsparticles/[email protected].2(@tsparticles/[email protected].2))
'@tsparticles/plugin-interactivity': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/basic': 4.5.3
'@tsparticles/engine': 4.5.3
'@tsparticles/interaction-particles-links': 4.5.3(@tsparticles/[email protected].3)(@tsparticles/[email protected].3(@tsparticles/[email protected].3))
'@tsparticles/plugin-interactivity': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/animation-utils': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/engine': 4.5.2
'@tsparticles/animation-utils': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/engine': 4.5.2
'@tsparticles/engine': 4.5.3
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
dependencies:
'@tsparticles/animation-utils': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/engine': 4.5.2
'@tsparticles/animation-utils': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/engine': 4.5.3
'@tufjs/[email protected]': {}
@@ -11553,11 +11527,11 @@ snapshots:
'@unrs/[email protected]':
optional: true
'@versatiles/style@8.1.1([email protected]6.0)':
'@versatiles/style@9.0.0([email protected]7.0)':
dependencies:
'@maplibre/maplibre-gl-style-spec': 26.5.1
optionalDependencies:
maplibre-gl: 6.16.0
maplibre-gl: 6.17.0
'@vibrant/[email protected]': {}
@@ -11647,7 +11621,7 @@ snapshots:
'@vscode/[email protected]': {}
'@vvo/[email protected]2.0': {}
'@vvo/[email protected]4.0': {}
'@web/[email protected]':
dependencies:
@@ -11786,8 +11760,6 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.2
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -12165,6 +12137,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
source-map: 0.6.1
@@ -12179,12 +12153,6 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
arch: 2.2.0
execa: 5.1.1
is-wsl: 2.2.0
[email protected]:
dependencies:
string-width: 4.2.3
@@ -12693,6 +12661,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
es-errors: 1.3.0
@@ -12751,15 +12721,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]7.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
[email protected]8.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
dependencies:
'@typescript-eslint/types': 8.72.1
cjs-module-lexer: 2.3.0
comment-parser: 1.4.8
debug: 4.4.3([email protected])
es-module-lexer: 3.0.4
eslint: 10.14.0([email protected])
eslint-import-context: 0.1.9([email protected])
is-glob: 4.0.3
minimatch: 10.2.6
minimatch: 11.0.5
semver: 7.8.5
stable-hash-x: 0.2.0
unrs-resolver: 1.12.2
@@ -12884,18 +12856,6 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
cross-spawn: 7.0.6
get-stream: 6.0.1
human-signals: 2.1.0
is-stream: 2.0.1
merge-stream: 2.0.0
npm-run-path: 4.0.1
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
[email protected]: {}
[email protected]:
@@ -13145,8 +13105,6 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
[email protected]: {}
[email protected]:
dependencies:
call-bound: 1.0.4
@@ -13419,8 +13377,6 @@ snapshots:
- kerberos
- supports-color
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -13550,8 +13506,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
call-bound: 1.0.4
@@ -13674,10 +13628,6 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
is-docker: 2.2.1
[email protected]: {}
[email protected]: {}
@@ -14044,7 +13994,7 @@ snapshots:
[email protected]: {}
[email protected]6.0:
[email protected]7.0:
dependencies:
'@mapbox/point-geometry': 1.1.0
'@mapbox/tiny-sdf': 2.3.0
@@ -14368,10 +14318,6 @@ snapshots:
- kerberos
- supports-color
[email protected]:
dependencies:
path-key: 3.1.1
[email protected]:
dependencies:
boolbase: 1.0.0
@@ -15042,7 +14988,7 @@ snapshots:
path-to-regexp: 3.3.0
range-parser: 1.2.0
[email protected].6([email protected]):
[email protected].7([email protected]):
dependencies:
'@zeit/schemas': 2.36.0
ajv: 8.18.0
@@ -15050,10 +14996,10 @@ snapshots:
boxen: 7.0.0
chalk: 5.0.1
chalk-template: 0.4.0
clipboardy: 3.0.0
compression: 1.8.1([email protected])
is-port-reachable: 4.0.0
serve-handler: 6.1.7
tinyclip: 0.1.15
update-check: 1.5.4
transitivePeerDependencies:
- supports-color
@@ -15397,8 +15343,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -15514,6 +15458,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
+7
View File
@@ -0,0 +1,7 @@
/** Where a value is in a config, e.g. `["actions", 0, "then", 1]`. */
export type ConfigPath = (string | number)[];
export const stringifyPath = (path: ConfigPath): string => path.join("/");
export const pathEquals = (a: ConfigPath, b: ConfigPath): boolean =>
a.length === b.length && a.every((segment, index) => segment === b[index]);
+6 -2
View File
@@ -401,9 +401,13 @@ export class HaDataTable extends LitElement {
}
if (properties.has("data")) {
// Clean up checked rows that no longer exist in the data
// Clean up checked rows that no longer exist or can no longer be selected
if (this._checkedRows.length) {
const validIds = new Set(this.data.map((row) => String(row[this.id])));
const validIds = new Set(
this.data
.filter((row) => row.selectable !== false)
.map((row) => String(row[this.id]))
);
const validCheckedRows = this._checkedRows.filter((id) =>
validIds.has(id)
);
@@ -97,6 +97,8 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
@state() private _dateValue?: string;
@state() private _rangeIncomplete = false;
@state() private _timeValue = {
from: { hours: 0, minutes: 0 },
to: { hours: 23, minutes: 59 },
@@ -195,6 +197,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
.locale=${this._i18n.locale.language}
.focusedDate=${this._focusDate}
@focusday=${this._focusChanged}
@rangestart=${this._handleRangeStart}
@change=${this._handleChange}
show-outside-days
.firstDayOfWeek=${firstWeekdayIndex(this._i18n.locale)}
@@ -253,7 +256,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<ha-button appearance="plain" @click=${this._cancel}
>${this._i18n.localize("ui.common.cancel")}</ha-button
>
<ha-button .disabled=${!this._dateValue} @click=${this._save}
<ha-button
.disabled=${!this._dateValue || this._rangeIncomplete}
@click=${this._save}
>${this._i18n.localize(
"ui.components.date-range-picker.select"
)}</ha-button
@@ -280,7 +285,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
}
private _save() {
if (!this._dateValue) {
if (!this._dateValue || this._rangeIncomplete) {
return;
}
@@ -331,9 +336,21 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
this._focusDate = ev.target.focusedDate;
}
private _handleRangeStart(
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
) {
this._rangeIncomplete = matchMedia("(hover: hover)").matches;
if (this._rangeIncomplete) {
return;
}
const day = ev.target.tentative;
this._dateValue = `${day}/${day}`;
}
private _handleChange(
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
) {
this._rangeIncomplete = false;
const dateElement = ev.target as HTMLElementTagNameMap["calendar-range"];
this._dateValue = dateElement.value;
this._focusDate = dateElement.focusedDate;
+1
View File
@@ -219,6 +219,7 @@ export class HaControlSlider extends LitElement {
this.pressed = false;
this._hideTooltip();
this.value = savedValue;
fireEvent(this, "slider-moved", { value: undefined });
});
this._mc.on("panmove", (e) => {
if (this.disabled) return;
+4
View File
@@ -214,15 +214,19 @@ class HaHsColorPicker extends LitElement {
this._mc.add(new Tap({ event: "singletap" }));
let savedPosition;
let savedValue: [number, number] | undefined;
this._mc.on("panstart", (e) => {
if (this.disabled) return;
this._pressed = e.pointerType;
savedPosition = this._cursorPosition;
savedValue = this._localValue;
});
this._mc.on("pancancel", () => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = savedPosition;
this._localValue = savedValue;
fireEvent(this, "cursor-moved", { value: undefined });
});
this._mc.on("panmove", (e) => {
if (this.disabled) return;
@@ -21,6 +21,8 @@ export class HaActionSelector extends LitElement {
@property({ attribute: false }) public value?: Action;
@property() public name?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -51,6 +53,7 @@ export class HaActionSelector extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action
.pathKey=${this.name}
.disabled=${this.disabled}
.actions=${this._actions(this.value)}
.hass=${this.hass}
@@ -16,6 +16,8 @@ export class HaConditionSelector extends LitElement {
@property({ attribute: false }) public value?: Condition;
@property() public name?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -27,6 +29,7 @@ export class HaConditionSelector extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-condition
.pathKey=${this.name}
.disabled=${this.disabled}
.conditions=${this.value || []}
.hass=${this.hass}
@@ -17,6 +17,8 @@ export class HaTriggerSelector extends LitElement {
@property({ attribute: false }) public value?: Trigger;
@property() public name?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -32,6 +34,7 @@ export class HaTriggerSelector extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-trigger
.pathKey=${this.name}
.disabled=${this.disabled}
.triggers=${this._triggers(this.value)}
.hass=${this.hass}
+128
View File
@@ -0,0 +1,128 @@
import type { HassEntities } from "home-assistant-js-websocket";
import type { ConfigEntry } from "./config_entries";
import type { DataEntryFlowProgress } from "./data_entry_flow";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import type { IntegrationSetup } from "./integration";
import type { RepairsIssue } from "./repairs";
import type { LoggedError } from "./system_log";
import { getLoggedErrorIntegration } from "./system_log";
export type HealthStatus = "ok" | "warning" | "error" | "info";
export type OverallHealthStatus = Exclude<HealthStatus, "info">;
export interface ConfigEntryHealth {
status: OverallHealthStatus;
state: HealthStatus;
entities: {
status: HealthStatus;
total: number;
unavailable: EntityRegistryEntry[];
};
repairs: { status: HealthStatus; issues: RepairsIssue[] };
logs: { status: HealthStatus; errors: number; warnings: number };
flows: { status: HealthStatus; flows: DataEntryFlowProgress[] };
setupSeconds?: number;
}
const ATTENTION_SOURCES = ["reauth", "reconfigure"];
const STATE_STATUS: Record<ConfigEntry["state"], HealthStatus> = {
loaded: "ok",
setup_in_progress: "info",
unload_in_progress: "info",
not_loaded: "info",
setup_retry: "warning",
setup_error: "error",
migration_error: "error",
failed_unload: "error",
};
const worst = (statuses: HealthStatus[]): OverallHealthStatus =>
statuses.includes("error")
? "error"
: statuses.includes("warning")
? "warning"
: "ok";
export const computeConfigEntryHealth = (
entry: ConfigEntry,
entities: EntityRegistryEntry[],
states: HassEntities,
issues: RepairsIssue[],
log: LoggedError[],
flows: DataEntryFlowProgress[],
setups: IntegrationSetup[],
// Library loggers from the manifest, so dependency errors count too.
loggers: string[] = []
): ConfigEntryHealth => {
const enabled = entities.filter((entity) => !entity.disabled_by);
// Entities without a state yet (e.g. while the entry is not loaded) count as unavailable.
const unavailable = enabled.filter(
(entity) =>
!states[entity.entity_id] ||
states[entity.entity_id].state === "unavailable"
);
const entryIssues = issues.filter(
(issue) =>
!issue.ignored && (issue.issue_domain ?? issue.domain) === entry.domain
);
const entryLog = log.filter(
(item) =>
getLoggedErrorIntegration(item) === entry.domain ||
loggers.some(
(logger) => item.name === logger || item.name.startsWith(`${logger}.`)
)
);
const sumCount = (levels: LoggedError["level"][]) =>
entryLog
.filter((item) => levels.includes(item.level))
.reduce((total, item) => total + item.count, 0);
const errors = sumCount(["error", "critical"]);
const warnings = sumCount(["warning"]);
const entryFlows = flows.filter(
(flow) =>
flow.handler === entry.domain &&
flow.context.entry_id === entry.entry_id &&
ATTENTION_SOURCES.includes(flow.context.source)
);
// A disabled entry that failed to unload still needs a restart.
const state: HealthStatus =
entry.disabled_by && entry.state !== "failed_unload"
? "info"
: STATE_STATUS[entry.state];
const health: ConfigEntryHealth = {
status: "ok",
state,
entities: {
status: entry.disabled_by || !unavailable.length ? "ok" : "warning",
total: enabled.length,
unavailable,
},
repairs: {
status: entryIssues.some((issue) => issue.severity !== "warning")
? "error"
: entryIssues.length
? "warning"
: "ok",
issues: entryIssues,
},
logs: {
status: errors ? "error" : warnings ? "warning" : "ok",
errors,
warnings,
},
flows: { status: entryFlows.length ? "error" : "ok", flows: entryFlows },
setupSeconds: setups.find((setup) => setup.domain === entry.domain)
?.seconds,
};
health.status = worst([
state,
health.entities.status,
health.repairs.status,
health.logs.status,
health.flows.status,
]);
return health;
};
+3 -3
View File
@@ -1,6 +1,6 @@
import type { Connection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type {
DataEntryFlowProgress,
DataEntryFlowStep,
@@ -78,10 +78,10 @@ export const deleteConfigFlow = (
// The backend indexes a single key, and only these two exist.
export const getConfigFlowHandlers = (
hass: HomeAssistant,
callApi: HomeAssistantApi["callApi"],
type?: "helper" | "integration"
) =>
hass.callApi<string[]>(
callApi<string[]>(
"GET",
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
);
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
export interface Counter {
id: string;
@@ -25,10 +25,10 @@ export const fetchCounter = (hass: HomeAssistant) =>
hass.callWS<Counter[]>({ type: "counter/list" });
export const createCounter = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: CounterMutableParams
) =>
hass.callWS<Counter>({
callWS<Counter>({
type: "counter/create",
...values,
});
+3 -3
View File
@@ -1,6 +1,6 @@
import type { Connection } from "home-assistant-js-websocket";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../types";
import type { ConfigEntry } from "../config_entries";
import type {
EntityRegistryDisplayEntry,
@@ -231,11 +231,11 @@ export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
devices.filter((device) => device.area_id === areaId);
export const updateDeviceRegistryEntry = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
deviceId: string,
updates: Partial<DeviceRegistryEntryMutableParams>
) =>
hass.callWS<DeviceRegistryEntry>({
callWS<DeviceRegistryEntry>({
type: "config/device_registry/update",
device_id: deviceId,
...updates,
+9 -9
View File
@@ -5,7 +5,7 @@ import memoizeOne from "memoize-one";
import { computeDomain } from "../../common/entity/compute_domain";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { debounce } from "../../common/util/debounce";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../types";
import type { LightColor } from "../light";
import type { NextNamePart, RegistryEntry } from "../registry";
import type { Segment } from "../vacuum";
@@ -284,11 +284,11 @@ export const getExtendedEntityRegistryEntries = (
});
export const updateEntityRegistryEntry = (
hass: Pick<HomeAssistant, "callWS">,
callWS: HomeAssistantApi["callWS"],
entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<UpdateEntityRegistryEntryResult> =>
hass.callWS({
callWS({
type: "config/entity_registry/update",
entity_id: entityId,
...updates,
@@ -388,11 +388,11 @@ export interface AutomaticEntityIdOverrides {
}
export const getAutomaticEntityIds = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
entity_ids: string[],
overrides?: Record<string, AutomaticEntityIdOverrides>
) =>
hass.callWS<Record<string, string | null>>({
callWS<Record<string, string | null>>({
type: "config/entity_registry/get_automatic_entity_ids",
entity_ids,
...(overrides && { overrides }),
@@ -403,22 +403,22 @@ export const getAutomaticEntityIds = (
* the area. Returns the entity ID of each entity after the change.
*/
export const updateEntitiesArea = async (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
entityIds: string[],
areaId: string
): Promise<Record<string, string>> => {
await Promise.all(
entityIds.map((entityId) =>
updateEntityRegistryEntry(hass, entityId, { area_id: areaId })
updateEntityRegistryEntry(callWS, entityId, { area_id: areaId })
)
);
const newEntityIds = await getAutomaticEntityIds(hass, entityIds);
const newEntityIds = await getAutomaticEntityIds(callWS, entityIds);
const result: Record<string, string> = {};
for (const entityId of entityIds) {
const newEntityId = newEntityIds[entityId];
if (newEntityId) {
// eslint-disable-next-line no-await-in-loop
await updateEntityRegistryEntry(hass, entityId, {
await updateEntityRegistryEntry(callWS, entityId, {
new_entity_id: newEntityId,
});
}
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
export interface InputBoolean {
id: string;
@@ -17,10 +17,10 @@ export const fetchInputBoolean = (hass: HomeAssistant) =>
hass.callWS<InputBoolean[]>({ type: "input_boolean/list" });
export const createInputBoolean = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: InputBooleanMutableParams
) =>
hass.callWS<InputBoolean>({
callWS<InputBoolean>({
type: "input_boolean/create",
...values,
});
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
export interface InputButton {
id: string;
@@ -15,10 +15,10 @@ export const fetchInputButton = (hass: HomeAssistant) =>
hass.callWS<InputButton[]>({ type: "input_button/list" });
export const createInputButton = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: InputButtonMutableParams
) =>
hass.callWS<InputButton>({
callWS<InputButton>({
type: "input_button/create",
...values,
});
+2 -2
View File
@@ -45,10 +45,10 @@ export const fetchInputDateTime = (hass: HomeAssistant) =>
hass.callWS<InputDateTime[]>({ type: "input_datetime/list" });
export const createInputDateTime = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: InputDateTimeMutableParams
) =>
hass.callWS<InputDateTime>({
callWS<InputDateTime>({
type: "input_datetime/create",
...values,
});
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
export interface InputNumber {
id: string;
@@ -29,10 +29,10 @@ export const fetchInputNumber = (hass: HomeAssistant) =>
hass.callWS<InputNumber[]>({ type: "input_number/list" });
export const createInputNumber = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: InputNumberMutableParams
) =>
hass.callWS<InputNumber>({
callWS<InputNumber>({
type: "input_number/create",
...values,
});
+3 -3
View File
@@ -2,7 +2,7 @@ import type {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
interface InputSelectEntityAttributes extends HassEntityAttributeBase {
options: string[];
@@ -41,10 +41,10 @@ export const fetchInputSelect = (hass: HomeAssistant) =>
hass.callWS<InputSelect[]>({ type: "input_select/list" });
export const createInputSelect = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: InputSelectMutableParams
) =>
hass.callWS<InputSelect>({
callWS<InputSelect>({
type: "input_select/create",
...values,
});
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
export interface InputText {
id: string;
@@ -36,10 +36,10 @@ export const fetchInputText = (hass: HomeAssistant) =>
hass.callWS<InputText[]>({ type: "input_text/list" });
export const createInputText = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: InputTextMutableParams
) =>
hass.callWS<InputText>({
callWS<InputText>({
type: "input_text/create",
...values,
});
+2 -2
View File
@@ -196,8 +196,8 @@ export const fetchIntegrationManifest = (
.callWS<IntegrationManifest>({ type: "manifest/get", integration })
.then(sanitizeManifest);
export const fetchIntegrationSetups = (hass: HomeAssistant) =>
hass.callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
export const fetchIntegrationSetups = (callWS: HomeAssistant["callWS"]) =>
callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
export const fetchIntegrationLogInfo = (conn) =>
conn.sendMessagePromise({
+3 -3
View File
@@ -1,9 +1,9 @@
import type { Connection, HassEventBase } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import type { ConfigPath } from "../common/util/config-path";
import type { HuiBadge } from "../panels/lovelace/badges/hui-badge";
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
import type { LovelacePath } from "../panels/lovelace/editor/lovelace-path";
import type { Lovelace } from "../panels/lovelace/types";
import type { HomeAssistant } from "../types";
import type { LovelaceSectionConfig } from "./lovelace/config/section";
@@ -21,7 +21,7 @@ export interface LovelaceViewElement extends HTMLElement {
narrow?: boolean;
// Temporary compatibility: custom view layouts still read the view index
index?: number;
path?: LovelacePath;
path?: ConfigPath;
cards?: HuiCard[];
badges?: HuiBadge[];
sections?: HuiSection[];
@@ -34,7 +34,7 @@ export interface LovelaceSectionElement extends HTMLElement {
hass?: HomeAssistant;
lovelace?: Lovelace;
preview?: boolean;
path?: LovelacePath;
path?: ConfigPath;
cards?: HuiCard[];
badges?: HuiBadge[];
sections?: HuiSection[];
+3 -2
View File
@@ -21,8 +21,9 @@ export const findOTBRInfoForDataset = (
}
const preferredAddress = dataset.preferred_extended_address;
if (preferredAddress && otbrInfo[preferredAddress]) {
return otbrInfo[preferredAddress];
const preferred = preferredAddress ? otbrInfo[preferredAddress] : undefined;
if (preferred?.extended_pan_id === dataset.extended_pan_id) {
return preferred;
}
return Object.values(otbrInfo).find(
+2 -2
View File
@@ -15,7 +15,7 @@ export const regenerateEntityIds = async (
hass: HomeAssistant,
entities: string[]
): Promise<void> => {
const entityIdsMapping = await getAutomaticEntityIds(hass, entities);
const entityIdsMapping = await getAutomaticEntityIds(hass.callWS, entities);
const entityIdsEntries = Object.entries(entityIdsMapping);
@@ -95,7 +95,7 @@ export const regenerateEntityIds = async (
entityIdsEntries
.filter(([oldId, newId]) => newId && oldId !== newId)
.forEach(([oldEntityId, newEntityId]) =>
updateEntityRegistryEntry(hass, oldEntityId, {
updateEntityRegistryEntry(hass.callWS, oldEntityId, {
new_entity_id: newEntityId!,
}).catch((err: any) => {
showAlertDialog(element, {
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
export const weekdays = [
"sunday",
@@ -33,10 +33,10 @@ export const fetchSchedule = (hass: HomeAssistant) =>
hass.callWS<Schedule[]>({ type: "schedule/list" });
export const createSchedule = (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
values: ScheduleMutableParams
) =>
hass.callWS<Schedule>({
callWS<Schedule>({
type: "schedule/create",
...values,
});
+2 -2
View File
@@ -23,8 +23,8 @@ export interface LoggedError {
first_occurred: number;
}
export const fetchSystemLog = async (hass: HomeAssistant) => {
const log = await hass.callWS<LoggedError[]>({ type: "system_log/list" });
export const fetchSystemLog = async (callWS: HomeAssistant["callWS"]) => {
const log = await callWS<LoggedError[]>({ type: "system_log/list" });
for (const error of log) {
error.level = error.level.toLowerCase() as LoggedError["level"];
}
+6 -3
View File
@@ -11,7 +11,7 @@ import secondsToDuration from "../common/datetime/seconds_to_duration";
import { formatNumericDuration } from "../common/datetime/format_duration";
import type { FormatEntityStateFunc } from "../common/translations/entity-state";
import type { HaDurationData } from "../components/ha-duration-input";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { FrontendLocaleData } from "./translation";
export type TimerEntity = HassEntityBase & {
@@ -48,8 +48,11 @@ export interface TimerMutableParams {
export const fetchTimer = (hass: HomeAssistant) =>
hass.callWS<Timer[]>({ type: "timer/list" });
export const createTimer = (hass: HomeAssistant, values: TimerMutableParams) =>
hass.callWS<Timer>({
export const createTimer = (
callWS: HomeAssistantApi["callWS"],
values: TimerMutableParams
) =>
callWS<Timer>({
type: "timer/create",
...values,
});
@@ -303,7 +303,7 @@ class StepFlowCreateEntry extends LitElement {
if (Object.keys(this._deviceUpdate).length) {
const deviceUpdates = Object.entries(this._deviceUpdate).map(
([deviceId, update]) =>
updateDeviceRegistryEntry(this.hass, deviceId, {
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
name_by_user: update.name,
area_id: update.area,
}).catch((err: unknown) => {
@@ -333,14 +333,14 @@ class StepFlowCreateEntry extends LitElement {
});
const entityIdsMapping = await getAutomaticEntityIds(
this.hass,
this.hass.callWS,
entityIds
);
Object.entries(entityIdsMapping).forEach(([oldEntityId, newEntityId]) => {
if (newEntityId) {
entityUpdates.push(
updateEntityRegistryEntry(this.hass, oldEntityId, {
updateEntityRegistryEntry(this.hass.callWS, oldEntityId, {
new_entity_id: newEntityId,
}).catch((err) =>
showAlertDialog(this, {
@@ -423,7 +423,7 @@ class StepFlowCreateEntry extends LitElement {
): Promise<ExtEntityRegistryEntry[]> {
try {
const newEntityIds = await updateEntitiesArea(
this.hass,
this.hass.callWS,
this._entitiesWithoutDevice(entities).map((entity) => entity.entity_id),
areaId
);
@@ -146,7 +146,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
}
const result = await updateEntityRegistryEntry(
this._api,
this._api.callWS,
this.entry.entity_id,
{
options_domain: "cover",
@@ -75,7 +75,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private async _save(newFavoriteColors: LightColor[]): Promise<void> {
const result = await updateEntityRegistryEntry(
this._api,
this._api.callWS,
this.entry!.entity_id,
{
options_domain: "light",
@@ -15,6 +15,7 @@ import {
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { PendingValueSync } from "../../../../common/util/pending-value-sync";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-hs-color-picker";
import "../../../../components/ha-icon";
@@ -59,6 +60,8 @@ class LightRgbColorPicker extends LitElement {
@state() private _isInteracting?: boolean;
private _pendingSync = new PendingValueSync();
protected render() {
if (!this.stateObj) {
return nothing;
@@ -181,23 +184,29 @@ class LightRgbColorPicker extends LitElement {
`;
}
private _updateBrightnessAdjusted() {
const stateObj = this.stateObj;
this._brightnessAdjusted = undefined;
if (
stateObj.attributes.color_mode === LightColorMode.RGB &&
stateObj.attributes.rgb_color &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
) {
const maxVal = Math.max(...stateObj.attributes.rgb_color);
if (maxVal < 255) {
this._brightnessAdjusted = maxVal;
}
}
}
private _updateSliderValues() {
const stateObj = this.stateObj;
if (stateObj.state === "on") {
this._brightnessAdjusted = undefined;
if (
stateObj.attributes.color_mode === LightColorMode.RGB &&
stateObj.attributes.rgb_color &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
) {
const maxVal = Math.max(...stateObj.attributes.rgb_color);
if (maxVal < 255) {
this._brightnessAdjusted = maxVal;
}
}
this._updateBrightnessAdjusted();
this._wvSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBW &&
@@ -235,11 +244,23 @@ class LightRgbColorPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (this._isInteracting || !changedProps.has("stateObj")) {
if (!changedProps.has("stateObj")) {
return;
}
this._updateSliderValues();
const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined;
if (oldStateObj?.entity_id !== this.stateObj?.entity_id) {
this._throttleUpdateColor.cancel();
this._pendingSync.reset();
this._isInteracting = false;
this._updateSliderValues();
} else if (!this._isInteracting && this._pendingSync.shouldSync()) {
this._updateSliderValues();
} else if (this.stateObj.state === "on") {
// Keep the picker values, but the brightness normalization has to
// follow the reported state, as it is applied to the next color sent.
this._updateBrightnessAdjusted();
}
}
private _hsColorCursorMoved(ev: CustomEvent) {
@@ -250,6 +271,7 @@ class LightRgbColorPicker extends LitElement {
return;
}
this._hsPickerValue = color;
this._pendingSync.schedule();
this._throttleUpdateColor();
}
@@ -312,13 +334,23 @@ class LightRgbColorPicker extends LitElement {
this._colorBrightnessSliderValue = hsv[2] / 2.55;
}
this._pendingSync.schedule();
this._throttleUpdateColor();
}
private _hsColorChanged(ev: CustomEvent) {
this._isInteracting = false;
if (!ev.detail.value) {
// Nothing is sent, so stop blocking state updates.
this._throttleUpdateColor.cancel();
this._pendingSync.reset();
this._updateSliderValues();
return;
}
// A pending throttled call would otherwise send an older color after the
// final one.
this._throttleUpdateColor.cancel();
this._hsPickerValue = ev.detail.value;
this._updateColor();
@@ -366,11 +398,15 @@ class LightRgbColorPicker extends LitElement {
private _applyColor(color: LightColor, params?: Record<string, any>) {
fireEvent(this, "color-changed", color);
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
});
return this._pendingSync.run(
() =>
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
}),
() => this._updateSliderValues()
);
}
private _colorBrightnessSliderChanged(ev: CustomEvent) {
@@ -13,6 +13,7 @@ import {
} from "../../../../common/color/convert-light-color";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stateColorCss } from "../../../../common/entity/state_color";
import { PendingValueSync } from "../../../../common/util/pending-value-sync";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-control-slider";
import {
@@ -66,6 +67,8 @@ class LightColorTempPicker extends LitElement {
@state() private _isInteracting?: boolean;
private _pendingSync = new PendingValueSync();
protected render() {
if (!this.stateObj) {
return nothing;
@@ -125,11 +128,19 @@ class LightColorTempPicker extends LitElement {
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (this._isInteracting || !changedProps.has("stateObj")) {
if (!changedProps.has("stateObj")) {
return;
}
this._updateSliderValues();
const oldStateObj = changedProps.get("stateObj");
if (oldStateObj?.entity_id !== this.stateObj?.entity_id) {
this._throttleUpdateColorTemp.cancel();
this._pendingSync.reset();
this._isInteracting = false;
this._updateSliderValues();
} else if (!this._isInteracting && this._pendingSync.shouldSync()) {
this._updateSliderValues();
}
}
private _ctColorCursorMoved(ev: CustomEvent) {
@@ -142,6 +153,7 @@ class LightColorTempPicker extends LitElement {
}
this._ctPickerValue = ct;
this._pendingSync.schedule();
this._throttleUpdateColorTemp();
}
@@ -153,7 +165,16 @@ class LightColorTempPicker extends LitElement {
private _ctColorChanged(ev: CustomEvent) {
const ct = ev.detail.value;
if (isNaN(ct) || this._ctPickerValue === ct) {
this._isInteracting = false;
// A pending throttled call would otherwise send an older value after the
// final one.
this._throttleUpdateColorTemp.cancel();
if (isNaN(ct)) {
// Nothing is sent, so stop blocking state updates.
this._pendingSync.reset();
this._updateSliderValues();
return;
}
@@ -170,11 +191,15 @@ class LightColorTempPicker extends LitElement {
private _applyColor(color: LightColor, params?: Record<string, any>) {
fireEvent(this, "color-changed", color);
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
});
return this._pendingSync.run(
() =>
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
}),
() => this._updateSliderValues()
);
}
static get styles(): CSSResultGroup {
@@ -112,7 +112,7 @@ export class HaMoreInfoTimerPresets extends LitElement {
};
const result = await updateEntityRegistryEntry(
this._api,
this._api.callWS,
this.entry.entity_id,
{
options_domain: "timer",
@@ -82,7 +82,7 @@ export class HaMoreInfoViewVacuumSegmentMapping extends LitElement {
};
try {
await updateEntityRegistryEntry(this.hass, this.params.entityId, {
await updateEntityRegistryEntry(this.hass.callWS, this.params.entityId, {
options_domain: "vacuum",
options: options,
});
@@ -110,7 +110,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
currentOptions.favorite_positions = this._favoritePositions;
const result = await updateEntityRegistryEntry(
this._api,
this._api.callWS,
this.entry.entity_id,
{
options_domain: "valve",
@@ -124,7 +124,12 @@ class MoreInfoMediaPlayer extends LitElement {
private _volumeController = new VolumeSliderController({
getSlider: () => this._volumeSlider,
step: this._volumeStep,
onSetVolume: (value) => this._setVolume(value),
onSetVolume: (value) => {
// The final value is set right away, so drop the pending update from
// the input events before it, or the same value is sent twice
this._debouncedVolumeSet.cancel();
this._setVolume(value);
},
onSetVolumeDebounced: (value) => this._debouncedVolumeSet(value),
});
+1 -1
View File
@@ -132,7 +132,7 @@ const copyFavoriteOptionsToEntities = async (
if (selected?.entity) {
const result = await Promise.allSettled(
selected.entity.map((entityId: string) =>
updateEntityRegistryEntry(hass, entityId, {
updateEntityRegistryEntry(hass.callWS, entityId, {
options_domain: domain,
options,
})
+1 -1
View File
@@ -531,7 +531,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
}
const result = await updateEntityRegistryEntry(
this.hass,
this.hass.callWS,
favoritesContext.entry.entity_id,
{
options_domain: favoritesHandler.domain,
@@ -50,7 +50,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
});
return;
}
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
await updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
area_id: area,
});
this._nextStep();
@@ -351,7 +351,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
private async _done() {
if (this._deviceName) {
try {
updateDeviceRegistryEntry(this.hass, this.deviceId, {
updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
name_by_user: this._deviceName,
});
} catch (error: any) {
+39 -2
View File
@@ -45,15 +45,42 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
margin-top: 142px;
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
flex: 1;
padding-top: 16px;
}
#ha-launch-screen-info-box p {
max-width: 400px;
margin: 0 16px 16px;
text-align: center;
}
/* Margins, not flex `gap` (Safari 14.1). */
#ha-launch-screen .ha-launch-screen-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: -4px 16px;
}
#ha-launch-screen .ha-launch-screen-actions button {
margin: 4px;
padding: 8px 16px;
border: 1px solid transparent;
border-radius: 18px;
background: none;
color: #009ac7;
font: inherit;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
#ha-launch-screen .ha-launch-screen-actions .ha-launch-screen-continue {
border-color: currentColor;
}
.ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
margin: 48px 0;
display: flex;
flex-direction: column;
align-items: center;
@@ -73,6 +100,16 @@
aspect-ratio: 237 / 24;
height: auto;
}
/* max() with var() fails after the cascade, so plain fallbacks above
need @supports (Chrome below 79). `body`: Jinja2, see below. */
@supports (margin: max(1px, 2px)) {
body #ha-launch-screen .ha-launch-screen-spacer-top {
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
}
.ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
}
}
@media (prefers-color-scheme: dark) {
/*
* Core renders the app's index.html through Jinja2, where the minified
+77 -17
View File
@@ -65,23 +65,83 @@
</script>
<script>
if (!window.latestJS) {
// Below the [legacy] browserslist floor the entry bundles cannot be
// parsed, or dialogs cannot open, and nothing would ever leave the
// launch screen. Test the features that set the floors: BigInt for
// Chrome 67, native <dialog> for Safari 15.4 and Firefox 98. Not
// transpiled, so keep this ES5.
if (
typeof BigInt === "undefined" ||
typeof document.createElement("dialog").showModal !== "function"
) {
document.getElementById("ha-launch-screen-info-box").textContent =
"This browser is too old to run Home Assistant.";
} else {
window.customPanelJS = "<%= es5CustomPanelJS %>";
<% for (const entry of es5EntryJS) { %>
_ls("<%= entry %>", true);
<% } %>
}
// Floor check: BigInt for Chrome 67, <dialog> for Safari 15.4 and
// Firefox 98. Not transpiled, so keep this ES5.
(function () {
var ACCEPTED_KEY = "haUnsupportedBrowserAccepted";
var REMIND_AFTER = 30 * 24 * 60 * 60 * 1000;
function loadApp() {
window.customPanelJS = "<%= es5CustomPanelJS %>";
<% for (const entry of es5EntryJS) { %>
_ls("<%= entry %>", true);
<% } %>
}
if (
typeof BigInt !== "undefined" &&
typeof document.createElement("dialog").showModal === "function"
) {
loadApp();
return;
}
// Shown while the app loads too.
var box = document.getElementById("ha-launch-screen-info-box");
var message = document.createElement("p");
message.textContent =
"This browser is no longer supported. Some parts of Home Assistant may not work.";
box.appendChild(message);
// Stored: "always", or when the user last continued (ms).
var stored = null;
try {
stored = localStorage.getItem(ACCEPTED_KEY);
} catch (_err) {
// Ask on every load.
}
if (stored === "always") {
loadApp();
return;
}
var acceptedAt = Number(stored) || 0;
// A clock behind the stored time asks again.
var age = Date.now() - acceptedAt;
if (acceptedAt && age >= 0 && age < REMIND_AFTER) {
loadApp();
return;
}
var actions = document.createElement("div");
actions.className = "ha-launch-screen-actions";
box.appendChild(actions);
function addButton(label, value) {
var button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.addEventListener("click", function () {
try {
localStorage.setItem(ACCEPTED_KEY, value());
} catch (_err) {
// Continue this time only.
}
box.removeChild(actions);
loadApp();
});
actions.appendChild(button);
return button;
}
addButton("Continue anyway", function () {
return String(Date.now());
}).className = "ha-launch-screen-continue";
if (acceptedAt) {
addButton("Don't show again", function () {
return "always";
});
}
})();
}
</script>
@@ -857,6 +857,8 @@ class HaConfigAreaPage extends LitElement {
},
this.hass.auth.data.hassUrl
)}
width="24"
height="24"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>`;
@@ -1061,7 +1063,7 @@ class HaConfigAreaPage extends LitElement {
justify-content: space-around;
}
img {
.img-container img {
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
@@ -1115,11 +1117,6 @@ class HaConfigAreaPage extends LitElement {
ha-list-item-button ha-icon-next {
color: var(--secondary-text-color);
}
img[slot="start"] {
width: 24px;
height: 24px;
object-fit: contain;
}
ha-card > *:first-child {
margin-top: -16px;
}
@@ -128,6 +128,7 @@ import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
import { scrollRowIntoView } from "../scroll-row-into-view";
import { AutomationPathController } from "../automation-path";
export interface ActionElement extends LitElement {
action: Action;
@@ -182,6 +183,8 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public root = false;
@property({ attribute: false }) public index!: number;
@property({ type: Boolean }) public first?: boolean;
@property({ type: Boolean }) public last?: boolean;
@@ -194,6 +197,11 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => [this.index],
row: () => (this.optionsInSidebar ? this : undefined),
});
@storage({
key: "automationClipboard",
state: false,
@@ -43,6 +43,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
return html`
<ha-automation-option
path-key="choose"
.options=${options}
.disabled=${this.disabled}
@value-changed=${this._optionsChanged}
@@ -46,6 +46,7 @@ export class HaIfAction extends LitElement implements ActionElement {
)}:
</h4>
<ha-automation-condition
path-key="if"
.conditions=${action.if ?? []}
.disabled=${this.disabled}
@value-changed=${this._ifChanged}
@@ -60,6 +61,7 @@ export class HaIfAction extends LitElement implements ActionElement {
)}:
</h4>
<ha-automation-action
path-key="then"
.actions=${action.then ?? []}
.disabled=${this.disabled}
@value-changed=${this._thenChanged}
@@ -73,6 +75,7 @@ export class HaIfAction extends LitElement implements ActionElement {
)}:
</h4>
<ha-automation-action
path-key="else"
.actions=${action.else || []}
.disabled=${this.disabled}
@value-changed=${this._elseChanged}
@@ -35,6 +35,7 @@ export class HaParallelAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
path-key="parallel"
.actions=${action.parallel}
.narrow=${this.narrow}
.disabled=${this.disabled}
@@ -8,6 +8,7 @@ import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
import { AutomationPathController } from "../../automation-path";
import { isTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/ha-form/ha-form";
@@ -40,6 +41,11 @@ export class HaRepeatAction extends LitElement implements ActionElement {
@property({ type: Boolean, attribute: "indent" }) public indent = false;
// The form edits the repeat options, so its fields are keyed below "repeat"
protected readonly pathController = new AutomationPathController(this, {
segment: () => ["repeat"],
});
@query("ha-form")
private _formElement?: HaForm;
@@ -35,6 +35,7 @@ export class HaSequenceAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
path-key="sequence"
.actions=${action.sequence ?? []}
.narrow=${this.narrow}
.disabled=${this.disabled}
@@ -90,6 +90,7 @@ export class HaWaitForTriggerAction
${
this.indent || (!this.inSidebar && !this.indent)
? html`<ha-automation-trigger
path-key="wait_for_trigger"
class=${!this.inSidebar && !this.indent ? "expansion-panel" : ""}
.triggers=${ensureArray(this.action.wait_for_trigger)}
.hass=${this.hass}
@@ -0,0 +1,167 @@
import { ContextProvider, createContext } from "@lit/context";
import type { ReactiveElement } from "lit";
import {
consumeContext,
ContextController,
} from "../../../common/decorators/consume";
import {
pathEquals,
stringifyPath,
type ConfigPath,
} from "../../../common/util/config-path";
export const automationPathContext =
createContext<ConfigPath>("automation-path");
export interface AutomationPathRow extends HTMLElement {
readonly selected: boolean;
openSidebar(): void;
expand(): void;
}
const owner = (rows?: AutomationPathRow[]) => rows?.[rows.length - 1];
/**
* Rows that open in the sidebar, by path. Lets the editor find the row a
* config path belongs to, e.g. to show it or point at an error in it.
*/
export class AutomationRowRegistry {
// Rows claiming each path, the latest one owns it. While a row is dragged
// over another list it claims the path of a row there, which takes the path
// back once the dragged row leaves.
private _rows = new Map<
string,
{ path: ConfigPath; rows: AutomationPathRow[] }
>();
public register(path: ConfigPath, row: AutomationPathRow) {
const key = stringifyPath(path);
const entry = this._rows.get(key);
if (!entry) {
this._rows.set(key, { path, rows: [row] });
return;
}
entry.rows = [...entry.rows.filter((claimed) => claimed !== row), row];
}
public unregister(path: ConfigPath, row: AutomationPathRow) {
const key = stringifyPath(path);
const entry = this._rows.get(key);
if (!entry) {
return;
}
entry.rows = entry.rows.filter((claimed) => claimed !== row);
if (!entry.rows.length) {
this._rows.delete(key);
}
}
public pathOf(row: AutomationPathRow): ConfigPath | undefined {
return [...this._rows.values()].find((entry) => owner(entry.rows) === row)
?.path;
}
/** The rows containing `path`, outermost first. */
public rowsAlong(path: ConfigPath): AutomationPathRow[] {
return path
.map((_segment, index) =>
owner(this._rows.get(stringifyPath(path.slice(0, index + 1)))?.rows)
)
.filter((row): row is AutomationPathRow => row !== undefined);
}
}
export const automationRowRegistryContext =
createContext<AutomationRowRegistry>("automation-row-registry");
interface AutomationPathControllerOptions {
/** Path segments this element adds to the path of its ancestor. */
segment: () => ConfigPath;
/** The host while it is a row that opens in the sidebar. */
row?: () => AutomationPathRow | undefined;
}
/**
* Consumes the path of the closest ancestor and provides it extended by the
* host's own segment to its descendants.
*/
export class AutomationPathController extends ContextController {
public path: ConfigPath = [];
@consumeContext({ context: automationPathContext, subscribe: true })
private _parentPath: ConfigPath = [];
@consumeContext({ context: automationRowRegistryContext })
private _registry?: AutomationRowRegistry;
private _registeredPath?: ConfigPath;
private _registeredRow?: AutomationPathRow;
private readonly _provider: ContextProvider<typeof automationPathContext>;
constructor(
host: ReactiveElement,
private readonly _options: AutomationPathControllerOptions
) {
super(host);
this._provider = new ContextProvider(host, {
context: automationPathContext,
initialValue: this.path,
});
}
// The path is not rendered, so a changed ancestor path is passed on without
// updating the host. Only the host's own segment changes go through an update.
protected contextUpdated() {
this._sync();
}
public hostUpdate() {
this._sync();
}
public hostConnected() {
// A row moved by a drag reconnects without an update
this._sync();
}
private _sync() {
const path = [...this._parentPath, ...this._options.segment()];
if (!pathEquals(path, this.path)) {
this.path = path;
this._provider.setValue(path);
}
this._syncRegistration();
}
public hostDisconnected() {
this._unregister();
}
private _syncRegistration() {
if (!this._registry || !this._options.row) {
return;
}
const row = this.host.isConnected ? this._options.row() : undefined;
if (
this._registeredPath &&
(!row || !pathEquals(this._registeredPath, this.path))
) {
this._unregister();
}
if (row && !this._registeredPath) {
this._registry.register(this.path, row);
this._registeredPath = this.path;
this._registeredRow = row;
}
}
private _unregister() {
if (this._registeredPath && this._registeredRow) {
this._registry?.unregister(this._registeredPath, this._registeredRow);
}
this._registeredPath = undefined;
this._registeredRow = undefined;
}
}
@@ -96,6 +96,7 @@ import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
import { scrollRowIntoView } from "../scroll-row-into-view";
import { AutomationPathController } from "../automation-path";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -113,6 +114,8 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public root = false;
@property({ attribute: false }) public index!: number;
@property({ type: Boolean }) public first?: boolean;
@property({ type: Boolean }) public last?: boolean;
@@ -124,6 +127,11 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => [this.index],
row: () => (this.optionsInSidebar ? this : undefined),
});
@state() private _collapsed = true;
@state() private _isNew = false;
@@ -29,6 +29,7 @@ export abstract class HaLogicalCondition
protected render() {
return html`
<ha-automation-condition
path-key="conditions"
.conditions=${this.condition.conditions || []}
@value-changed=${this._valueChanged}
.hass=${this.hass}
@@ -1162,7 +1162,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
await updateEntityRegistryEntry(this.hass.callWS, entityId, {
categories: {
automation: this.entityRegistryUpdate.category || null,
},
@@ -1267,7 +1267,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
updateEntityRegistryEntry(this.hass.callWS, entityId, {
categories: { automation: category },
})
);
@@ -1306,7 +1306,7 @@ ${rejected
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
updateEntityRegistryEntry(this.hass.callWS, entityId, {
labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
@@ -1351,7 +1351,7 @@ ${rejected
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
updateEntityRegistryEntry(this.hass.callWS, entityId, {
area_id: area,
})
);
@@ -7,6 +7,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { nextRender } from "../../../common/util/render-status";
import type { AutomationClipboard } from "../../../data/automation";
import type { Constructor, HomeAssistant } from "../../../types";
import { AutomationPathController } from "./automation-path";
const rowKeys = new WeakMap<object, string>();
@@ -75,6 +76,13 @@ export const AutomationSortableListMixin = <T extends object>(
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
/** Key of this list in the config of the row or editor rendering it. */
@property({ attribute: "path-key" }) public pathKey?: string;
protected readonly pathController = new AutomationPathController(this, {
segment: () => (this.pathKey === undefined ? [] : [this.pathKey]),
});
@state() protected rowSortSelected?: number;
@state()
@@ -1,3 +1,4 @@
import { provide } from "@lit/context";
import { mdiContentSave } from "@mdi/js";
import {
html,
@@ -33,6 +34,10 @@ import type {
ValueChangedEvent,
} from "../../../types";
import { showToast } from "../../../util/toast";
import {
AutomationRowRegistry,
automationRowRegistryContext,
} from "./automation-path";
import "./ha-automation-sidebar";
import type HaAutomationSidebar from "./ha-automation-sidebar";
@@ -82,6 +87,9 @@ export const ManualEditorMixin = <TConfig>(
@query("ha-automation-sidebar")
protected sidebarElement?: HaAutomationSidebar;
@provide({ context: automationRowRegistryContext })
protected rowRegistry = new AutomationRowRegistry();
declare protected collapsableElements?: NodeListOf<
Element & {
expandAll(): void;
@@ -111,6 +111,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
<ha-automation-trigger
role="region"
path-key="triggers"
aria-labelledby="triggers-heading"
.triggers=${this.config.triggers || []}
.highlightedTriggers=${this.pastedConfig?.triggers}
@@ -149,6 +150,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
<ha-automation-condition
role="region"
path-key="conditions"
aria-labelledby="conditions-heading"
.conditions=${this.config.conditions || []}
.highlightedConditions=${this.pastedConfig?.conditions}
@@ -183,6 +185,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
<ha-automation-action
role="region"
path-key="actions"
aria-labelledby="actions-heading"
.actions=${this.config.actions || []}
.highlightedActions=${this.pastedConfig?.actions}
@@ -64,6 +64,7 @@ import {
} from "../styles";
import "../trigger/ha-automation-trigger-references";
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
import { AutomationPathController } from "../automation-path";
@customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement {
@@ -89,6 +90,11 @@ export default class HaAutomationOptionRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => (this.defaultActions ? ["default"] : [this.index]),
row: () => (this.optionsInSidebar ? this : undefined),
});
@state() private _expanded = false;
@state() private _selected = false;
@@ -448,6 +454,7 @@ export default class HaAutomationOptionRow extends LitElement {
)}:
</h4>
<ha-automation-condition
path-key="conditions"
.conditions=${ensureArray<string | Condition>(
this.option.conditions
)}
@@ -466,6 +473,7 @@ export default class HaAutomationOptionRow extends LitElement {
)}:
</h4>
<ha-automation-action
.pathKey=${this.option ? "sequence" : undefined}
.actions=${
(this.option
? ensureArray(this.option.sequence) || []
@@ -120,6 +120,7 @@ import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
import { scrollRowIntoView } from "../scroll-row-into-view";
import { AutomationPathController } from "../automation-path";
import { targetExpandGroupContext } from "../../../../components/target-picker/target-expand-group-context";
export interface TriggerElement extends LitElement {
@@ -164,7 +165,7 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public last?: boolean;
@property({ type: Number }) public index?: number;
@property({ attribute: false }) public index!: number;
@property({ type: Boolean }) public highlight?: boolean;
@@ -174,6 +175,11 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => [this.index],
row: () => (this.optionsInSidebar ? this : undefined),
});
@state() private _yamlMode = false;
@state() private _triggered = false;
@@ -26,6 +26,7 @@ export class HaTriggerList extends LitElement implements TriggerElement {
return html`
<ha-automation-trigger
path-key="triggers"
.triggers=${triggers}
.hass=${this.hass}
.disabled=${this.disabled}
@@ -216,7 +216,7 @@ class HaConfigBackupOverview extends LitElement {
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
.header=${this.hass.localize("ui.panel.config.backup.caption")}
>
<ha-dropdown
slot="toolbar-icon"
@@ -123,7 +123,7 @@ class DialogAssignCategory extends DirtyStateProviderMixin<AssignCategoryFormSta
this._error = undefined;
try {
await updateEntityRegistryEntry(
this.hass,
this.hass.callWS,
this._params!.entityReg.entity_id,
{
categories: { [this._scope!]: this._category || null },
@@ -17,6 +17,8 @@ import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
const ADDRESS_TYPES = ["mac", "bluetooth", "zigbee"];
@customElement("ha-device-info-card")
export class HaDeviceCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -234,9 +236,13 @@ export class HaDeviceCard extends LitElement {
}
protected _getAddresses() {
return this.device.connections.filter((conn) =>
["mac", "bluetooth", "zigbee"].includes(conn[0])
);
return this.device.connections
.filter((conn) => ADDRESS_TYPES.includes(conn[0]))
.sort(
(a, b) =>
ADDRESS_TYPES.indexOf(a[0]) - ADDRESS_TYPES.indexOf(b[0]) ||
stringCompare(a[1], b[1])
);
}
private _computeDeviceNameDisplay(deviceId: string) {
@@ -1600,7 +1600,11 @@ export class HaConfigDevicePage extends LitElement {
delete updates.disabled_by;
}
try {
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
await updateDeviceRegistryEntry(
this.hass.callWS,
this.deviceId,
updates
);
} catch (err: unknown) {
showAlertDialog(this, {
title: this.hass.localize(
@@ -1616,7 +1620,7 @@ export class HaConfigDevicePage extends LitElement {
};
private async _enableDevice(): Promise<void> {
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
await updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
disabled_by: null,
});
}
@@ -1167,7 +1167,7 @@ export class HaConfigDeviceDashboard extends LitElement {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
updateDeviceRegistryEntry(this.hass, deviceId, {
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
area_id: area,
})
);
@@ -1219,7 +1219,7 @@ ${rejected
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
updateDeviceRegistryEntry(this.hass, deviceId, {
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
labels:
action === "add"
? this.hass.devices[deviceId].labels.concat(label)
@@ -114,7 +114,7 @@ export class DialogVacuumSegmentMapping
last_seen_segments: mapper.lastSeenSegments,
};
await updateEntityRegistryEntry(this.hass, this._params.entityId, {
await updateEntityRegistryEntry(this.hass.callWS, this._params.entityId, {
options_domain: "vacuum",
options: options,
});
@@ -100,7 +100,6 @@ export class EntitySettingsHelperTab extends LitElement {
: html`
<span @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
disabled: this._item === null,
@@ -1414,7 +1414,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
const result = await updateEntityRegistryEntry(
this.hass!,
this.hass!.callWS,
this.entry.entity_id,
params
);
@@ -1593,7 +1593,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
private async _restoreEntityId(): Promise<void> {
// use the unsaved name and area, as they are part of the entity ID
const entityIds = await getAutomaticEntityIds(
this.hass,
this.hass.callWS,
[this._origEntityId],
{
[this._origEntityId]: {
@@ -1821,7 +1821,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
showDeviceRegistryDetailDialog(this, {
device: this._device!,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(this.hass, this._device!.id, updates);
await updateDeviceRegistryEntry(
this.hass.callWS,
this._device!.id,
updates
);
},
});
}
@@ -179,7 +179,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
showDeviceRegistryDetailDialog(this, {
device: device,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(this.hass, device.id, updates);
await updateDeviceRegistryEntry(this.hass.callWS, device.id, updates);
},
});
}
@@ -189,7 +189,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._submitting = true;
try {
const result = await updateEntityRegistryEntry(
this.hass!,
this.hass!.callWS,
this.entry.entity_id,
{ disabled_by: null }
);
@@ -1388,7 +1388,7 @@ export class HaConfigEntities extends LitElement {
const result = await Promise.allSettled(
this._selected.map(async (entity) => {
const updateResult = await updateEntityRegistryEntry(
this.hass,
this.hass.callWS,
entity,
{
disabled_by: null,
@@ -1453,7 +1453,7 @@ export class HaConfigEntities extends LitElement {
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, {
updateEntityRegistryEntry(this.hass.callWS, entity, {
disabled_by: "user",
})
);
@@ -1475,7 +1475,7 @@ export class HaConfigEntities extends LitElement {
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, {
updateEntityRegistryEntry(this.hass.callWS, entity, {
hidden_by: "user",
})
);
@@ -1486,7 +1486,7 @@ export class HaConfigEntities extends LitElement {
private _unhideSelected = () => {
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, {
updateEntityRegistryEntry(this.hass.callWS, entity, {
hidden_by: null,
})
);
@@ -1523,7 +1523,7 @@ export class HaConfigEntities extends LitElement {
return;
}
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
updateEntityRegistryEntry(this.hass.callWS, entityId, {
labels:
action === "add"
? entityReg.labels.concat(label)
@@ -1,11 +1,13 @@
import { mdiAlertOutline } from "@mdi/js";
import Fuse from "fuse.js";
import type { HassConfig } from "home-assistant-js-websocket";
import type { Connection, HassConfig } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { consume } from "../../../common/decorators/consume";
import { transform } from "../../../common/decorators/transform";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -29,6 +31,12 @@ import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import {
apiContext,
configContext,
connectionContext,
internationalizationContext,
} from "../../../data/context";
import { createCounter } from "../../../data/counter";
import {
fetchEntityRegistry,
@@ -58,7 +66,13 @@ import {
type FuseWeightedKey,
} from "../../../resources/fuseMultiTerm";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
import type {
Helper,
HelperCategory,
@@ -77,7 +91,7 @@ type HelperCreators = Record<
HelperDomain,
{
create: (
hass: HomeAssistant,
callWS: HomeAssistantApi["callWS"],
// Not properly typed because there is currently a mismatch for this._item between:
// 1. Type passed to form should be Helper
// 2. Type received by creator should be MutableParams version
@@ -147,7 +161,25 @@ const HELPER_SEARCH_KEYS: FuseWeightedKey[] = [
export class DialogHelperDetail extends DirtyStateProviderMixin<
{ item?: HelperFormItem; area?: string } | undefined
>()(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@consume({ context: connectionContext, subscribe: true })
@transform<HomeAssistantConnection, Connection>({
transformer: ({ connection }) => connection,
})
private _connection!: Connection;
@state() private _item?: HelperFormItem;
@@ -203,9 +235,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
}
this._open = true;
await this.updateComplete;
this.hass.loadFragmentTranslation("config");
const flows = await getConfigFlowHandlers(this.hass, "helper");
await this.hass.loadBackendTranslation("title", flows, true);
this._i18n.loadFragmentTranslation("config");
const flows = await getConfigFlowHandlers(this._api.callApi, "helper");
await this._i18n.loadBackendTranslation("title", flows, true);
// Ensure the titles are loaded before we render the flows.
this._helperFlows = flows;
@@ -214,13 +246,13 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._focusPicker();
}
protected updated(changedProps: PropertyValues<this>): void {
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
const oldHass = changedProps.get("hass");
const oldI18n = changedProps.get("_i18n") as
HomeAssistantInternationalization | undefined;
if (
oldHass &&
(oldHass.localize !== this.hass.localize ||
oldHass.config !== this.hass.config)
(oldI18n && oldI18n.localize !== this._i18n.localize) ||
changedProps.get("_hassConfig") !== undefined
) {
this._comboBox?.refreshItems();
}
@@ -272,7 +304,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
<div class="form" @value-changed=${this._valueChanged}>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
${dynamicElement(`ha-${this._domain}-form`, {
hass: this.hass,
item: this._item,
new: true,
autofocus: true,
@@ -295,7 +326,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
@click=${this._goBack}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.common.back")}
${this._i18n.localize("ui.common.back")}
</ha-button>`
}
<ha-button
@@ -303,7 +334,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
@click=${this._createItem}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
${this._i18n.localize("ui.panel.config.helpers.dialog.create")}
</ha-button>
</ha-dialog-footer>
`;
@@ -317,16 +348,16 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
mode="dialog"
.shown=${this._shown}
no-sort
.label=${this.hass.localize(
.label=${this._i18n.localize(
"ui.panel.config.integrations.search_helper"
)}
.getItems=${this._getItems}
.rowRenderer=${this._renderHelperRow}
.sections=${this._getSections(
this._helperFlows,
this.hass.localize,
this.hass.config,
this.hass.locale.language
this._i18n.localize,
this._hassConfig,
this._i18n.locale.language
)}
.selectedSection=${this._category}
@index-selected=${this._helperPicked}
@@ -342,12 +373,12 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
.preventScrimClose=${this.isDirtyState}
header-title=${
this._domain
? this.hass.localize(
? this._i18n.localize(
"ui.panel.config.helpers.dialog.create_platform",
{
platform:
(isHelperDomain(this._domain) &&
this.hass.localize(
this._i18n.localize(
`ui.panel.config.helpers.types.${
this._domain as HelperDomain
}`
@@ -355,7 +386,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._domain,
}
)
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")
: this._i18n.localize(
"ui.panel.config.helpers.dialog.create_helper"
)
}
@after-show=${this._dialogShown}
@closed=${this._dialogClosed}
@@ -441,9 +474,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._search = search;
const allItems = this._getHelperItems(
this._helperFlows ?? [],
this.hass.localize,
this.hass.config,
this.hass.locale.language
this._i18n.localize,
this._hassConfig,
this._i18n.locale.language
);
const items = search
? multiTermSortedSearch(
@@ -463,7 +496,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
const categoryItems = items.filter((item) => item.category === category);
return categoryItems.length
? [
this.hass.localize(
this._i18n.localize(
`ui.panel.config.helpers.dialog.categories.${category}`
),
...categoryItems,
@@ -498,7 +531,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip slot="end" .for="icon-${domain}">
${this.hass.localize(
${this._i18n.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: domain }
)}
@@ -523,7 +556,10 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._submitting = true;
this._error = "";
try {
const createdEntity = await HELPERS[domain].create(this.hass, this._item);
const createdEntity = await HELPERS[domain].create(
this._api.callWS,
this._item
);
// The dialog was closed or reopened while waiting
if (this._params !== params) {
return;
@@ -565,13 +601,13 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
try {
// The entity ID usually matches the helper ID, unless it was taken
const entry = await getExtendedEntityRegistryEntry(
this.hass,
this._api,
`${domain}.${uniqueId}`
).catch(() => undefined);
if (entry && isCreated(entry)) {
return entry.entity_id;
}
return (await fetchEntityRegistry(this.hass.connection)).find(isCreated)
return (await fetchEntityRegistry(this._connection)).find(isCreated)
?.entity_id;
} catch (_err) {
return undefined;
@@ -585,7 +621,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
let error = "Entity not found";
if (entityId) {
try {
return (await updateEntitiesArea(this.hass, [entityId], areaId))[
return (await updateEntitiesArea(this._api.callWS, [entityId], areaId))[
entityId
];
} catch (err: unknown) {
@@ -593,7 +629,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
}
}
showAlertDialog(this, {
text: this.hass.localize(
text: this._i18n.localize(
"ui.panel.config.integrations.config_flow.error_saving_entity",
{ error }
),
@@ -614,7 +650,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
const { id: domain, loaded } = ev.detail.item as HelperPickerItem;
if (!loaded) {
showAlertDialog(this, {
text: this.hass.localize(
text: this._i18n.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: domain }
),
@@ -637,7 +673,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
} else {
this._pendingConfigFlow = {
startFlowHandler: domain,
manifest: await fetchIntegrationManifest(this.hass, domain),
manifest: await fetchIntegrationManifest(this._api, domain),
dialogClosedCallback: this._params?.dialogClosedCallback,
navigateToResult: this._params?.navigateToResult,
};
@@ -1,7 +1,9 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-switch";
@@ -9,11 +11,12 @@ import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/input/ha-input";
import type { Counter } from "../../../../data/counter";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-counter-form")
class HaCounterForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -63,7 +66,7 @@ class HaCounterForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -73,12 +76,10 @@ class HaCounterForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -88,9 +89,7 @@ class HaCounterForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-input
@@ -98,9 +97,7 @@ class HaCounterForm extends LitElement {
.configValue=${"minimum"}
type="number"
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.minimum"
)}
.label=${this._localize("ui.dialogs.helper_settings.counter.minimum")}
.disabled=${this.disabled}
></ha-input>
<ha-input
@@ -108,9 +105,7 @@ class HaCounterForm extends LitElement {
.configValue=${"maximum"}
type="number"
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.maximum"
)}
.label=${this._localize("ui.dialogs.helper_settings.counter.maximum")}
.disabled=${this.disabled}
></ha-input>
<ha-input
@@ -118,13 +113,11 @@ class HaCounterForm extends LitElement {
.configValue=${"initial"}
type="number"
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.initial"
)}
.label=${this._localize("ui.dialogs.helper_settings.counter.initial")}
.disabled=${this.disabled}
></ha-input>
<ha-expansion-panel
header=${this.hass.localize(
header=${this._localize(
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
@@ -134,9 +127,7 @@ class HaCounterForm extends LitElement {
.configValue=${"step"}
type="number"
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.step"
)}
.label=${this._localize("ui.dialogs.helper_settings.counter.step")}
.disabled=${this.disabled}
></ha-input>
<div class="row">
@@ -148,9 +139,7 @@ class HaCounterForm extends LitElement {
>
</ha-switch>
<div>
${this.hass.localize(
"ui.dialogs.helper_settings.counter.restore"
)}
${this._localize("ui.dialogs.helper_settings.counter.restore")}
</div>
</div>
</ha-expansion-panel>
@@ -2,7 +2,9 @@ import { mdiInformationOutline } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-picker";
@@ -14,11 +16,12 @@ import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import type { InputBoolean } from "../../../../data/input_boolean";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_boolean-form")
class HaInputBooleanForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -52,7 +55,7 @@ class HaInputBooleanForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -62,12 +65,10 @@ class HaInputBooleanForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -77,13 +78,11 @@ class HaInputBooleanForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-expansion-panel
header=${this.hass.localize(
header=${this._localize(
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
@@ -101,7 +100,7 @@ class HaInputBooleanForm extends LitElement {
@change=${this._initialChanged}
>
<span slot="label">
${this.hass.localize(
${this._localize(
"ui.dialogs.helper_settings.input_boolean.initial"
)}
<ha-svg-icon
@@ -113,23 +112,23 @@ class HaInputBooleanForm extends LitElement {
></ha-svg-icon>
</span>
<ha-radio-option value="restore">
${this.hass.localize(
${this._localize(
"ui.dialogs.helper_settings.input_boolean.restore"
)}
</ha-radio-option>
<ha-radio-option value="on">
${this.hass.localize(
${this._localize(
"ui.dialogs.helper_settings.input_boolean.turn_on"
)}
</ha-radio-option>
<ha-radio-option value="off">
${this.hass.localize(
${this._localize(
"ui.dialogs.helper_settings.input_boolean.turn_off"
)}
</ha-radio-option>
</ha-radio-group>
<ha-tooltip for="initial-note" placement="top">
${this.hass.localize(
${this._localize(
"ui.dialogs.helper_settings.input_boolean.initial_helper"
)}
</ha-tooltip>
@@ -1,16 +1,19 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
import type { InputButton } from "../../../../data/input_button";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_button-form")
class HaInputButtonForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -40,7 +43,7 @@ class HaInputButtonForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -50,12 +53,10 @@ class HaInputButtonForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -65,9 +66,7 @@ class HaInputButtonForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
</div>
@@ -1,7 +1,9 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
import "../../../../components/radio/ha-radio-group";
@@ -9,11 +11,12 @@ import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import type { InputDateTime } from "../../../../data/input_datetime";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_datetime-form")
class HaInputDateTimeForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -54,7 +57,7 @@ class HaInputDateTimeForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -64,12 +67,10 @@ class HaInputDateTimeForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -79,13 +80,11 @@ class HaInputDateTimeForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-radio-group
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_datetime.mode"
)}
.value=${this._mode}
@@ -94,17 +93,13 @@ class HaInputDateTimeForm extends LitElement {
@change=${this._modeChanged}
>
<ha-radio-option value="date">
${this.hass.localize(
"ui.dialogs.helper_settings.input_datetime.date"
)}
${this._localize("ui.dialogs.helper_settings.input_datetime.date")}
</ha-radio-option>
<ha-radio-option value="time">
${this.hass.localize(
"ui.dialogs.helper_settings.input_datetime.time"
)}
${this._localize("ui.dialogs.helper_settings.input_datetime.time")}
</ha-radio-option>
<ha-radio-option value="datetime">
${this.hass.localize(
${this._localize(
"ui.dialogs.helper_settings.input_datetime.datetime"
)}
</ha-radio-option>
@@ -2,7 +2,9 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-device-class-picker";
import "../../../../components/ha-icon-picker";
import "../../../../components/radio/ha-radio-group";
@@ -13,7 +15,7 @@ import "../../../../components/input/ha-input";
import type { InputNumber } from "../../../../data/input_number";
import type { UnitOfMeasurementSelector } from "../../../../data/selector";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { ValueChangedEvent } from "../../../../types";
const UNIT_SELECTOR: UnitOfMeasurementSelector = {
unit_of_measurement: {},
@@ -21,7 +23,9 @@ const UNIT_SELECTOR: UnitOfMeasurementSelector = {
@customElement("ha-input_number-form")
class HaInputNumberForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -81,7 +85,7 @@ class HaInputNumberForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -91,12 +95,10 @@ class HaInputNumberForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -106,9 +108,7 @@ class HaInputNumberForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-input
@@ -117,7 +117,7 @@ class HaInputNumberForm extends LitElement {
type="number"
step="any"
@input=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_number.min"
)}
.disabled=${this.disabled}
@@ -128,7 +128,7 @@ class HaInputNumberForm extends LitElement {
type="number"
step="any"
@input=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_number.max"
)}
.disabled=${this.disabled}
@@ -137,7 +137,7 @@ class HaInputNumberForm extends LitElement {
<ha-radio-group
orientation="horizontal"
class="mode"
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_number.mode"
)}
.value=${this._mode}
@@ -146,12 +146,10 @@ class HaInputNumberForm extends LitElement {
@change=${this._modeChanged}
>
<ha-radio-option value="slider">
${this.hass.localize(
"ui.dialogs.helper_settings.input_number.slider"
)}
${this._localize("ui.dialogs.helper_settings.input_number.slider")}
</ha-radio-option>
<ha-radio-option value="box">
${this.hass.localize("ui.dialogs.helper_settings.input_number.box")}
${this._localize("ui.dialogs.helper_settings.input_number.box")}
</ha-radio-option>
</ha-radio-group>
<ha-input
@@ -160,7 +158,7 @@ class HaInputNumberForm extends LitElement {
type="number"
step="any"
@input=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_number.step"
)}
.disabled=${this.disabled}
@@ -174,7 +172,7 @@ class HaInputNumberForm extends LitElement {
@value-changed=${this._pickerValueChanged}
></ha-device-class-picker>
<ha-selector-unit_of_measurement
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_number.unit_of_measurement"
)}
.selector=${UNIT_SELECTOR}
@@ -3,7 +3,9 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { afterNextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
@@ -17,11 +19,12 @@ import type { HaInput } from "../../../../components/input/ha-input";
import type { InputSelect } from "../../../../data/input_select";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_select-form")
class HaInputSelectForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -93,7 +96,7 @@ class HaInputSelectForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -103,13 +106,11 @@ class HaInputSelectForm extends LitElement {
dialogInitialFocus
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
.value=${this._name}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.configValue=${"name"}
@input=${this._valueChanged}
.disabled=${this.disabled}
@@ -118,15 +119,11 @@ class HaInputSelectForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
<div class="header">
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}:
${this._localize("ui.dialogs.helper_settings.input_select.options")}:
</div>
<ha-sortable
@item-moved=${this._optionMoved}
@@ -135,7 +132,7 @@ class HaInputSelectForm extends LitElement {
>
<ha-list-base
class="options"
aria-label=${this.hass!.localize(
aria-label=${this._localize(
"ui.dialogs.helper_settings.input_select.options"
)}
>
@@ -160,7 +157,7 @@ class HaInputSelectForm extends LitElement {
<ha-icon-button
slot="end"
.index=${index}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
@@ -178,7 +175,7 @@ class HaInputSelectForm extends LitElement {
<ha-input
class="flex-auto"
id="option_input"
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_select.add_option"
)}
@keydown=${this._handleKeyAdd}
@@ -189,9 +186,7 @@ class HaInputSelectForm extends LitElement {
appearance="filled"
@click=${this._addOption}
.disabled=${this.disabled}
>${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add"
)}
>${this._localize("ui.dialogs.helper_settings.input_select.add")}
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
</ha-button>
</div>
@@ -222,10 +217,10 @@ class HaInputSelectForm extends LitElement {
const index = (ev.target as any).index;
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize(
title: this._localize(
"ui.dialogs.helper_settings.input_select.confirm_delete.delete"
),
text: this.hass.localize(
text: this._localize(
"ui.dialogs.helper_settings.input_select.confirm_delete.prompt"
),
destructive: true,
@@ -1,7 +1,9 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon-picker";
@@ -11,11 +13,12 @@ import "../../../../components/radio/ha-radio-option";
import "../../../../components/input/ha-input";
import type { InputText } from "../../../../data/input_text";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_text-form")
class HaInputTextForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -60,7 +63,7 @@ class HaInputTextForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -70,12 +73,10 @@ class HaInputTextForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -85,13 +86,11 @@ class HaInputTextForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-expansion-panel
header=${this.hass.localize(
header=${this._localize(
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
@@ -103,7 +102,7 @@ class HaInputTextForm extends LitElement {
min="0"
max="255"
@input=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_text.min"
)}
.disabled=${this.disabled}
@@ -115,13 +114,13 @@ class HaInputTextForm extends LitElement {
max="255"
type="number"
@input=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_text.max"
)}
></ha-input>
<ha-radio-group
orientation="horizontal"
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_text.mode"
)}
.value=${this._mode}
@@ -130,12 +129,10 @@ class HaInputTextForm extends LitElement {
@change=${this._modeChanged}
>
<ha-radio-option value="text">
${this.hass.localize(
"ui.dialogs.helper_settings.input_text.text"
)}
${this._localize("ui.dialogs.helper_settings.input_text.text")}
</ha-radio-option>
<ha-radio-option value="password">
${this.hass.localize(
${this._localize(
"ui.dialogs.helper_settings.input_text.password"
)}
</ha-radio-option>
@@ -144,10 +141,10 @@ class HaInputTextForm extends LitElement {
.value=${this._pattern || ""}
.configValue=${"pattern"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._localize(
"ui.dialogs.helper_settings.input_text.pattern_label"
)}
.hint=${this.hass!.localize(
.hint=${this._localize(
"ui.dialogs.helper_settings.input_text.pattern_helper"
)}
.disabled=${this.disabled}
@@ -9,6 +9,7 @@ import allLocales from "fullcalendar/locales-all";
import interactionPlugin from "fullcalendar/interaction";
import timeGridPlugin from "fullcalendar/timegrid";
import type { Day } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import { addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -16,9 +17,15 @@ import { customElement, property, query, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
import { formatTime24h } from "../../../../common/datetime/format_time";
import { useAmPm } from "../../../../common/datetime/use_am_pm";
import { consume } from "../../../../common/decorators/consume";
import { transform } from "../../../../common/decorators/transform";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
import {
configContext,
internationalizationContext,
} from "../../../../data/context";
import type { Schedule, ScheduleDay } from "../../../../data/schedule";
import { weekdays } from "../../../../data/schedule";
import { TimeZone } from "../../../../data/translation";
@@ -27,7 +34,10 @@ import {
fullCalendarTheme,
} from "../../../../resources/fullcalendar-theme";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type {
HomeAssistantConfig,
HomeAssistantInternationalization,
} from "../../../../types";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
const defaultFullCalendarConfig: CalendarOptions = {
@@ -52,7 +62,15 @@ const defaultFullCalendarConfig: CalendarOptions = {
@customElement("ha-schedule-form")
class HaScheduleForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
@property({ type: Boolean }) public new = false;
@@ -125,7 +143,7 @@ class HaScheduleForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._i18n) {
return nothing;
}
@@ -135,12 +153,12 @@ class HaScheduleForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._i18n.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._i18n.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -150,7 +168,7 @@ class HaScheduleForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
.label=${this._i18n.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
@@ -180,10 +198,11 @@ class HaScheduleForm extends LitElement {
this.calendar.addEventSource(this._events);
}
const oldHass = changedProps.get("hass") as HomeAssistant;
const oldI18n = changedProps.get("_i18n") as
HomeAssistantInternationalization | undefined;
if (oldHass && oldHass.language !== this.hass.language) {
this.calendar.setOption("locale", this.hass.language);
if (oldI18n && oldI18n.language !== this._i18n.language) {
this.calendar.setOption("locale", this._i18n.language);
}
}
@@ -196,19 +215,19 @@ class HaScheduleForm extends LitElement {
private _setupCalendar(): void {
const config: CalendarOptions = {
...defaultFullCalendarConfig,
locale: this.hass.language,
firstDay: firstWeekdayIndex(this.hass.locale),
locale: this._i18n.language,
firstDay: firstWeekdayIndex(this._i18n.locale),
slotHeaderFormat: {
hour: "numeric",
minute: undefined,
hour12: useAmPm(this.hass.locale),
meridiem: useAmPm(this.hass.locale) ? "narrow" : false,
hour12: useAmPm(this._i18n.locale),
meridiem: useAmPm(this._i18n.locale) ? "narrow" : false,
},
eventTimeFormat: {
hour: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
minute: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
hour12: useAmPm(this.hass.locale),
meridiem: useAmPm(this.hass.locale) ? "narrow" : false,
hour: useAmPm(this._i18n.locale) ? "numeric" : "2-digit",
minute: useAmPm(this._i18n.locale) ? "numeric" : "2-digit",
hour12: useAmPm(this._i18n.locale),
meridiem: useAmPm(this._i18n.locale) ? "narrow" : false,
},
};
@@ -237,7 +256,7 @@ class HaScheduleForm extends LitElement {
let date = nextDay(new Date(), i as Day);
if (
!isSameWeek(date, new Date(), {
weekStartsOn: firstWeekdayIndex(this.hass.locale),
weekStartsOn: firstWeekdayIndex(this._i18n.locale),
})
) {
date = addDays(date, -7);
@@ -276,14 +295,14 @@ class HaScheduleForm extends LitElement {
// Schedule is timezone unaware, we need to format it in local time
const endFormatted = formatTime24h(
end,
{ ...this.hass.locale, time_zone: TimeZone.local },
this.hass.config
{ ...this._i18n.locale, time_zone: TimeZone.local },
this._hassConfig
);
value.push({
from: formatTime24h(
start,
{ ...this.hass.locale, time_zone: TimeZone.local },
this.hass.config
{ ...this._i18n.locale, time_zone: TimeZone.local },
this._hassConfig
),
to:
!isSameDay(start, end) || endFormatted === "0:00"
@@ -309,7 +328,11 @@ class HaScheduleForm extends LitElement {
const value = this[`_${day}`][parseInt(index)];
const newValue = { ...this._item };
const endFormatted = formatTime24h(end, this.hass.locale, this.hass.config);
const endFormatted = formatTime24h(
end,
this._i18n.locale,
this._hassConfig
);
newValue[day] = [...newValue[day]];
newValue[day][index] = {
...newValue[day][index],
@@ -337,10 +360,14 @@ class HaScheduleForm extends LitElement {
const newDay = weekdays[start.getDay()];
const newValue = { ...this._item };
const endFormatted = formatTime24h(end, this.hass.locale, this.hass.config);
const endFormatted = formatTime24h(
end,
this._i18n.locale,
this._hassConfig
);
const event = {
...newValue[day][index],
from: formatTime24h(start, this.hass.locale, this.hass.config),
from: formatTime24h(start, this._i18n.locale, this._hassConfig),
to:
!isSameDay(start, end) || endFormatted === "0:00"
? "24:00"
@@ -2,7 +2,9 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-checkbox";
import "../../../../components/ha-duration-input";
import type { HaDurationData } from "../../../../components/ha-duration-input";
@@ -11,11 +13,12 @@ import "../../../../components/input/ha-input";
import type { ForDict } from "../../../../data/automation";
import type { DurationDict, Timer } from "../../../../data/timer";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-timer-form")
class HaTimerForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ type: Boolean }) public new = false;
@@ -57,7 +60,7 @@ class HaTimerForm extends LitElement {
}
protected render() {
if (!this.hass) {
if (!this._localize) {
return nothing;
}
@@ -67,12 +70,10 @@ class HaTimerForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
auto-validate
required
.validationMessage=${this.hass!.localize(
.validationMessage=${this._localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -82,9 +83,7 @@ class HaTimerForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-duration-input
@@ -99,7 +98,7 @@ class HaTimerForm extends LitElement {
@change=${this._toggleRestore}
.disabled=${this.disabled}
>
${this.hass.localize("ui.dialogs.helper_settings.timer.restore")}
${this._localize("ui.dialogs.helper_settings.timer.restore")}
</ha-checkbox>
</div>
`;
@@ -747,6 +747,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
>
<ha-filter-floor-areas
.type=${"entity"}
include-disabled-entities
.value=${this._filters["ha-filter-floor-areas"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -1162,7 +1163,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
updateEntityRegistryEntry(this.hass.callWS, entityId, {
categories: { helpers: category },
})
);
@@ -1200,7 +1201,7 @@ ${rejected
this._selected.forEach((entityId) => {
const labels = this._labelsForEntity(entityId);
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
updateEntityRegistryEntry(this.hass.callWS, entityId, {
labels:
action === "add"
? labels.concat(label)
@@ -1306,11 +1307,11 @@ ${rejected
});
return;
}
const handlers = await getConfigFlowHandlers(this.hass, "helper");
const handlers = await getConfigFlowHandlers(this.hass.callApi, "helper");
if (!handlers.includes(domain)) {
const integrations = await getConfigFlowHandlers(
this.hass,
this.hass.callApi,
"integration"
);
if (integrations.includes(domain)) {
@@ -0,0 +1,393 @@
import {
mdiAlertCircle,
mdiCheckCircle,
mdiInformation,
mdiAlert,
} from "@mdi/js";
import type { ContextType } from "@lit/context";
import type { HassEntities } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consume } from "../../../common/decorators/consume";
import { consumeEntityStates } from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/item/ha-list-item-base";
import "../../../components/list/ha-list-base";
import type { ConfigEntry } from "../../../data/config_entries";
import { fetchConfigFlowInProgress } from "../../../data/config_flow";
import {
apiContext,
configEntriesContext,
connectionContext,
internationalizationContext,
} from "../../../data/context";
import type {
ConfigEntryHealth,
HealthStatus,
} from "../../../data/config_entry_health";
import { computeConfigEntryHealth } from "../../../data/config_entry_health";
import {
domainToName,
fetchIntegrationSetups,
} from "../../../data/integration";
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import type { IntegrationSetup } from "../../../data/integration";
import type { RepairsIssue } from "../../../data/repairs";
import { fetchRepairsIssues } from "../../../data/repairs";
import type { LoggedError } from "../../../data/system_log";
import { fetchSystemLog } from "../../../data/system_log";
import { DialogMixin } from "../../../dialogs/dialog-mixin";
import { haStyleDialog } from "../../../resources/styles";
import { renderConfigEntryError } from "./ha-config-integration-page";
import type { ConfigEntryHealthDialogParams } from "./show-dialog-config-entry-health";
const STATUS_ICONS: Record<HealthStatus, string> = {
ok: mdiCheckCircle,
warning: mdiAlert,
error: mdiAlertCircle,
info: mdiInformation,
};
const MAX_LISTED = 5;
interface HealthData {
issues: RepairsIssue[];
log: LoggedError[];
flows: DataEntryFlowProgress[];
setups: IntegrationSetup[];
}
@customElement("dialog-config-entry-health")
class DialogConfigEntryHealth extends DialogMixin<ConfigEntryHealthDialogParams>(
LitElement
) {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: connectionContext, subscribe: true })
private _connection!: ContextType<typeof connectionContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
// Read by the @consumeEntityStates decorator below.
@state() protected _entityIds: string[] = [];
// Only this entry's entities, so availability stays live while open.
@state()
@consumeEntityStates({ entityIdPath: ["_entityIds"] })
private _states: HassEntities = {};
// params.entry is a snapshot; updates replace the entry object.
@state()
@consume({ context: configEntriesContext, subscribe: true })
@transform<ConfigEntry[], ConfigEntry | undefined>({
transformer: function (this: DialogConfigEntryHealth, entries) {
return entries?.find(
(entry) => entry.entry_id === this.params?.entry.entry_id
);
},
})
private _entry?: ConfigEntry;
@state() private _data?: HealthData;
@state() private _error?: string;
private _computeHealth = memoizeOne(
(
entry: ConfigEntry,
entities: ConfigEntryHealthDialogParams["entities"],
loggers: string[] | undefined,
states: HassEntities,
data: HealthData
) =>
computeConfigEntryHealth(
entry,
entities,
states,
data.issues,
data.log,
data.flows,
data.setups,
loggers
)
);
connectedCallback() {
super.connectedCallback();
this._entityIds = this.params!.entities.map((entity) => entity.entity_id);
this._load();
}
private async _load(): Promise<void> {
try {
const [{ issues }, log, flows, setups] = await Promise.all([
fetchRepairsIssues(this._connection.connection),
fetchSystemLog(this._api.callWS),
fetchConfigFlowInProgress(this._connection.connection),
fetchIntegrationSetups(this._api.callWS),
]);
this._data = { issues, log, flows, setups };
} catch (err: any) {
// WebSocket errors nest their message.
this._error =
err?.error?.message ||
err?.message ||
this._i18n.localize(
"ui.panel.config.integrations.config_entry.health.load_failed"
);
}
}
protected render() {
if (!this.params) {
return nothing;
}
return html`
<ha-dialog
open
header-title=${this._i18n.localize(
"ui.panel.config.integrations.config_entry.health.title",
{ title: this._currentEntry.title }
)}
>
${
this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._data
? this._renderHealth(
this._computeHealth(
this._currentEntry,
this.params.entities,
this.params.manifest?.loggers,
this._states,
this._data
)
)
: html`<ha-spinner></ha-spinner>`
}
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this._i18n.localize("ui.common.close")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private get _currentEntry(): ConfigEntry {
return this._entry ?? this.params!.entry;
}
private _renderHealth(health: ConfigEntryHealth) {
const entry = this._currentEntry;
const key = "ui.panel.config.integrations.config_entry.health";
// Repairs and log lines are only known per integration, not per entry.
const integration = domainToName(this._i18n.localize, entry.domain);
const entityName = (entityId: string) =>
this._states[entityId]?.attributes.friendly_name ?? entityId;
const more = (count: number) =>
count > MAX_LISTED
? html`<li>
${this._i18n.localize(`${key}.and_more`, {
count: count - MAX_LISTED,
})}
</li>`
: nothing;
return html`
<ha-alert
.alertType=${
health.status === "ok"
? "success"
: health.status === "error"
? "error"
: "warning"
}
>
${this._i18n.localize(`${key}.summary.${health.status}`)}
</ha-alert>
<ha-list-base>
${this._row(
health.state,
this._i18n.localize(
entry.disabled_by && entry.state !== "failed_unload"
? `${key}.disabled`
: `ui.panel.config.integrations.config_entry.state.${entry.state}`
),
entry.state === "failed_unload"
? html`<p>
${this._i18n.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
)}
</p>`
: health.state === "warning" || health.state === "error"
? html`<p>${renderConfigEntryError(this._i18n, entry)}</p>`
: nothing
)}
${
health.flows.flows.length
? this._row(
health.flows.status,
this._i18n.localize(`${key}.flows`),
html`<a href="/config/integrations/dashboard">
${this._i18n.localize(`${key}.flows_link`)}
</a>`
)
: nothing
}
${
health.entities.total
? this._row(
health.entities.status,
this._i18n.localize(`${key}.entities`, {
available:
health.entities.total - health.entities.unavailable.length,
total: health.entities.total,
}),
health.entities.unavailable.length
? html`<ul>
${health.entities.unavailable
.slice(0, MAX_LISTED)
.map(
(item) =>
html`<li>${entityName(item.entity_id)}</li>`
)}
${more(health.entities.unavailable.length)}
</ul>
<a
href=${`/config/entities?historyBack=1&config_entry=${entry.entry_id}`}
>
${this._i18n.localize(`${key}.entities_link`)}
</a>`
: nothing
)
: nothing
}
${this._row(
health.repairs.status,
this._i18n.localize(`${key}.repairs`, {
count: health.repairs.issues.length,
integration,
}),
health.repairs.issues.length
? html`<a href="/config/repairs">
${this._i18n.localize(`${key}.repairs_link`)}
</a>`
: nothing
)}
${this._row(
health.logs.status,
this._i18n.localize(`${key}.logs`, {
errors: health.logs.errors,
warnings: health.logs.warnings,
integration,
}),
health.logs.errors || health.logs.warnings
? html`<a
href=${`/config/logs?integration=${encodeURIComponent(entry.domain)}`}
>
${this._i18n.localize(`${key}.logs_link`)}
</a>`
: nothing
)}
${
health.setupSeconds !== undefined
? this._row(
"info",
health.setupSeconds < 0.1
? this._i18n.localize(`${key}.setup_time_fast`, {
integration,
})
: this._i18n.localize(`${key}.setup_time`, {
integration,
seconds: health.setupSeconds.toFixed(1),
})
)
: nothing
}
</ha-list-base>
`;
}
private _row(
status: HealthStatus,
text: string,
details: TemplateResult | typeof nothing = nothing
) {
return html`<ha-list-item-base>
<ha-svg-icon
slot="start"
class=${status}
.path=${STATUS_ICONS[status]}
></ha-svg-icon>
<span slot="headline">${text}</span>
${
details === nothing
? nothing
: html`<div slot="supporting-text">${details}</div>`
}
</ha-list-item-base>`;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-spinner {
display: block;
margin: var(--ha-space-6) auto;
}
ha-list-base {
margin-top: var(--ha-space-2);
}
/* Row items keep text on one line; errors and counts need to wrap. */
[slot="headline"],
[slot="supporting-text"] {
white-space: normal;
}
[slot="supporting-text"] p,
[slot="supporting-text"] ul {
margin: 0;
}
[slot="supporting-text"] ul {
padding-inline-start: var(--ha-space-5);
}
a {
color: var(--primary-color);
}
ha-svg-icon {
flex-shrink: 0;
}
ha-svg-icon.ok {
color: var(--success-color);
}
ha-svg-icon.warning {
color: var(--warning-color);
}
ha-svg-icon.error {
color: var(--error-color);
}
ha-svg-icon.info {
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-config-entry-health": DialogConfigEntryHealth;
}
}
@@ -255,7 +255,11 @@ class HaConfigEntryDeviceRow extends LitElement {
showDeviceRegistryDetailDialog(this, {
device: this.device,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(this.hass, this.device.id, updates);
await updateDeviceRegistryEntry(
this.hass.callWS,
this.device.id,
updates
);
},
});
};
@@ -339,7 +343,7 @@ class HaConfigEntryDeviceRow extends LitElement {
}
}
await updateDeviceRegistryEntry(this.hass, this.device.id, {
await updateDeviceRegistryEntry(this.hass.callWS, this.device.id, {
disabled_by: disable ? "user" : null,
});
};
@@ -8,6 +8,7 @@ import {
mdiDotsVertical,
mdiDownload,
mdiHandExtendedOutline,
mdiHeartPulse,
mdiPlayCircleOutline,
mdiPlus,
mdiProgressHelper,
@@ -48,6 +49,7 @@ import {
updateConfigEntry,
} from "../../../data/config_entries";
import { groupDevicesByParent } from "../../../data/device/device_registry";
import { showConfigEntryHealthDialog } from "./show-dialog-config-entry-health";
import type { DiagnosticInfo } from "../../../data/diagnostics";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
@@ -335,6 +337,12 @@ export class HaConfigEntryRow extends LitElement {
`
: nothing
}
<ha-dropdown-item value="health">
<ha-svg-icon slot="icon" .path=${mdiHeartPulse}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.health.menu"
)}
</ha-dropdown-item>
${
!item.disabled_by &&
RECOVERABLE_STATES.includes(item.state) &&
@@ -559,6 +567,14 @@ export class HaConfigEntryRow extends LitElement {
this._handleReload();
return;
}
if (value === "health") {
showConfigEntryHealthDialog(this, {
entry: this.data.entry,
entities: this._getEntities(),
manifest: this.manifest,
});
return;
}
if (value === "rename") {
this._handleRename();
return;
@@ -21,13 +21,17 @@ import memoizeOne from "memoize-one";
import { ensureArray } from "../../../common/array/ensure-array";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
import { ContextSubscriptionController } from "../../../common/decorators/consume";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import {
caseInsensitiveStringCompare,
stringCompare,
} from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
integrationQueryParamConfig,
@@ -39,7 +43,9 @@ import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import "../../../components/ha-icon-next";
import "../../../components/item/ha-list-item-base";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import { getSignedPath } from "../../../data/auth";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
@@ -59,7 +65,9 @@ import {
getCoreLogFileDownloadUnavailableReason,
getErrorLogDownloadUrl,
} from "../../../data/error_log";
import { manifestsContext } from "../../../data/context";
import type {
DomainManifestLookup,
IntegrationLogInfo,
IntegrationManifest,
} from "../../../data/integration";
@@ -109,7 +117,7 @@ export interface ConfigEntryData {
const AI_SETTINGS_DOMAINS = ["mcp", "mcp_server"];
export const renderConfigEntryError = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "localize" | "loadBackendTranslation">,
entry: ConfigEntry
): TemplateResult => {
if (entry.reason) {
@@ -167,6 +175,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
@state() private _manifest?: IntegrationManifest;
/** The manifests of all loaded integrations, to find the dependents. */
@state() private _loadedManifests?: DomainManifestLookup;
private _manifestsConsumer?: ContextSubscriptionController<DomainManifestLookup>;
@state() private _extraConfigEntries?: ConfigEntry[];
@state() private _diagnosticHandler?: DiagnosticInfo;
@@ -263,6 +276,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this.hass.loadBackendTranslation("config", [this.domain]);
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
this._extraConfigEntries = undefined;
this._manifest = undefined;
this._resetDebugLogReady();
this._filter = "";
this._subEntries = {};
@@ -415,6 +429,25 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this.domain
);
// Without the dependents, a dependency looks like a YAML integration.
const dependents =
this._manifest && !this._manifest.config_flow
? this._getDependents(
this.domain,
this._loadedManifests,
this.hass.localize,
this.hass.locale.language
)
: [];
const showDependents = normalEntries.length === 0 && dependents.length > 0;
// Hold back the YAML texts and "Add entry" until the manifest and the
// dependents are known. Entries do not wait, so a failed request cannot
// hide them.
const dependentsLoading =
normalEntries.length === 0 &&
(!this._manifest ||
(!this._manifest.config_flow && !this._loadedManifests));
const configPanel = isComponentLoaded(this.hass.config, this.domain)
? getConfigPanelPath(this.domain, this.hass.panels)
: undefined;
@@ -425,6 +458,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
const showAddEntry =
this._manifest?.integration_type !== "hardware" &&
!showDependents &&
!dependentsLoading &&
!setUpInPanel &&
(!this._manifest?.single_config_entry ||
(normalData.length === 0 && attentionData.length === 0));
@@ -614,6 +649,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
${
normalEntries.length === 0 &&
!showDependents &&
!dependentsLoading &&
!setUpInPanel &&
this._manifest &&
!this._manifest.config_flow &&
@@ -726,7 +763,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
${
configPanel
? html`<ha-button
.appearance=${setUpInPanel ? "accent" : "outlined"}
.appearance=${
showAddEntry || canAddDevice ? "outlined" : "accent"
}
href=${`/${configPanel}`}
>
${this.hass.localize(
@@ -975,7 +1014,21 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
: nothing
}
${
setUpInPanel && normalEntries.length === 0
showDependents
? html`<div class="section">
<h3 class="section-header">
${this.hass.localize(
"ui.panel.config.integrations.integration_page.used_by"
)}
</h3>
${this._renderDependents(dependents)}
</div>`
: nothing
}
${
showDependents ||
dependentsLoading ||
(setUpInPanel && normalEntries.length === 0)
? nothing
: html`<div class="section">
<h3 class="section-header">
@@ -1032,6 +1085,76 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
`;
}
// Only an integration without a config flow needs the loaded manifests, so
// other integration pages do not make the context fetch them.
private _watchLoadedManifests(): void {
if (this._manifestsConsumer) {
return;
}
this._manifestsConsumer = new ContextSubscriptionController(
this,
manifestsContext,
(manifests) => {
this._loadedManifests = manifests;
}
);
}
private _getDependents = memoizeOne(
(
domain: string,
manifests: DomainManifestLookup | undefined,
localize: LocalizeFunc,
language: string
) =>
Object.values(manifests ?? {})
.filter(
// System integrations, like Default Configuration, are not useful to list.
(manifest) =>
manifest.integration_type !== "system" &&
manifest.dependencies?.includes(domain)
)
// The manifests come in no fixed order.
.sort((a, b) =>
stringCompare(
domainToName(localize, a.domain, a),
domainToName(localize, b.domain, b),
language
)
)
);
private _renderDependents(dependents: IntegrationManifest[]) {
return html`<ha-list-base>
${dependents.map(
(manifest) =>
html`<ha-list-item-button
class="dependent"
href=${`/config/integrations/integration/${manifest.domain}`}
>
<img
slot="start"
alt=""
src=${brandsUrl(
{
domain: manifest.domain,
type: "icon@2x",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<div slot="headline">
${domainToName(this.hass.localize, manifest.domain, manifest)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
)}
</ha-list-base>`;
}
private _onImageLoad(ev) {
ev.target.style.display = "inline-block";
}
@@ -1072,6 +1195,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
return;
}
this._manifest = await fetchIntegrationManifest(this.hass, this.domain);
if (!this._manifest.config_flow) {
this._watchLoadedManifests();
}
if (
this._manifest.integration_type &&
!["device", "hub", "service"].includes(this._manifest.integration_type)
@@ -1748,6 +1874,17 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
ha-list-item-base.discovered {
--ha-row-item-min-height: 72px;
}
ha-list-item-button.dependent {
--ha-row-item-min-height: 72px;
}
ha-list-item-button.dependent + ha-list-item-button.dependent {
border-top: var(--ha-border-width-sm) solid var(--divider-color);
}
ha-list-item-button.dependent img {
width: 40px;
height: 40px;
object-fit: contain;
}
ha-list-item-base.config_entry {
position: relative;
}
@@ -243,6 +243,14 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
] => {
const entryDomains = new Set(configEntries.map((entry) => entry.domain));
// An integration loaded as a dependency of another is not set up in YAML.
const dependencies = new Set<string>();
for (const component of components) {
manifests[component.split(".")[0]]?.dependencies?.forEach((domain) =>
dependencies.add(domain)
);
}
const domains = new Set<string>();
for (const component of components) {
@@ -250,6 +258,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
const manifest = manifests[componentDomain];
if (
!entryDomains.has(componentDomain) &&
!dependencies.has(componentDomain) &&
manifest &&
!manifest.config_flow &&
(!manifest.integration_type ||
@@ -33,6 +33,7 @@ import {
getConfigEntries,
} from "../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import { domainToName } from "../../../../../data/integration";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage";
@@ -51,6 +52,10 @@ export class BluetoothAdapterInfoPage extends LitElement {
@state() private _configEntries: ConfigEntry[] = [];
// Config entries of the integrations that provide remote scanners
// (for example ESPHome Bluetooth proxies), keyed by entry ID.
@state() private _sourceConfigEntries: Record<string, ConfigEntry> = {};
@state() private _connectionAllocationData: BluetoothAllocationsData[] = [];
@state() private _scannerStates: Record<string, BluetoothScannerState> = {};
@@ -74,9 +79,45 @@ export class BluetoothAdapterInfoPage extends LitElement {
}
private async _loadConfigEntries(): Promise<void> {
this._configEntries = await getConfigEntries(this.hass, {
domain: "bluetooth",
});
const configEntries = await getConfigEntries(this.hass);
const bluetoothEntries: ConfigEntry[] = [];
const sourceEntries: Record<string, ConfigEntry> = {};
for (const configEntry of configEntries) {
if (configEntry.domain === "bluetooth") {
bluetoothEntries.push(configEntry);
} else {
sourceEntries[configEntry.entry_id] = configEntry;
}
}
this._configEntries = bluetoothEntries;
this._sourceConfigEntries = sourceEntries;
}
/**
* Remote scanners (such as ESPHome Bluetooth proxies) are configured by the
* integration that provides them. The Bluetooth integration creates its own
* device for a remote scanner, linked through `via_device_id` to the device
* of the integration that provides it, so look there for that
* integration's config entry.
*/
private _getSourceConfigEntry(
device: DeviceRegistryEntry
): ConfigEntry | undefined {
const sourceDevice = device.via_device_id
? this.hass.devices[device.via_device_id]
: undefined;
if (!sourceDevice) {
return undefined;
}
if (
sourceDevice.primary_config_entry &&
sourceDevice.primary_config_entry in this._sourceConfigEntries
) {
return this._sourceConfigEntries[sourceDevice.primary_config_entry];
}
return sourceDevice.config_entries
.map((id) => this._sourceConfigEntries[id])
.find((e) => e !== undefined);
}
private async _subscribeBluetoothConnectionAllocations(): Promise<void> {
@@ -289,6 +330,10 @@ export class BluetoothAdapterInfoPage extends LitElement {
const scannerType: HaScannerType =
scannerDetails?.scanner_type ?? "unknown";
const isRemoteScanner = scannerType === "remote";
// Remote scanners are configured through the integration providing them.
const optionsEntry = isRemoteScanner
? this._getSourceConfigEntry(device)
: entry;
const hasMismatch = scannerState && isScannerStateMismatch(scannerState);
const allocations = scannerDetails
@@ -323,16 +368,30 @@ export class BluetoothAdapterInfoPage extends LitElement {
<div slot="headline">${deviceName}</div>
<div slot="supporting-text">${supportingParts.join(" · ")}</div>
${
!isRemoteScanner
optionsEntry &&
(!isRemoteScanner ||
(optionsEntry.supports_options && !optionsEntry.disabled_by))
? html`<ha-icon-button
slot="end"
.path=${mdiCogOutline}
.entry=${entry}
.entry=${optionsEntry}
@click=${this._openOptionFlow}
@keydown=${stopKeydownEnterSpacePropagation}
.label=${this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)}
.label=${
isRemoteScanner
? this.hass.localize(
"ui.panel.config.bluetooth.source_option_flow",
{
integration: domainToName(
this.hass.localize,
optionsEntry.domain
),
}
)
: this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)
}
></ha-icon-button>`
: nothing
}
@@ -309,7 +309,7 @@ class DialogMatterAddDevice extends LitElement {
const areaChanged = area !== origArea;
if (nameChanged || areaChanged) {
await updateDeviceRegistryEntry(this.hass, device.id, {
await updateDeviceRegistryEntry(this.hass.callWS, device.id, {
...(nameChanged && { name_by_user: name || null }),
...(areaChanged && { area_id: area || null }),
}).catch((err: Error) =>
@@ -328,12 +328,15 @@ class DialogMatterAddDevice extends LitElement {
.map((e) => e.entity_id);
if (entityIds.length) {
const mapping = await getAutomaticEntityIds(this.hass, entityIds);
const mapping = await getAutomaticEntityIds(
this.hass.callWS,
entityIds
);
await Promise.allSettled(
Object.entries(mapping)
.filter((entry): entry is [string, string] => !!entry[1])
.map(([oldId, newId]) =>
updateEntityRegistryEntry(this.hass, oldId, {
updateEntityRegistryEntry(this.hass.callWS, oldId, {
new_entity_id: newId,
}).catch((err: Error) =>
showAlertDialog(this, {
@@ -355,7 +358,7 @@ class DialogMatterAddDevice extends LitElement {
undefined;
if (deviceClass !== origClass) {
await updateEntityRegistryEntry(
this.hass,
this.hass.callWS,
this._mainEntity.entity_id,
{ device_class: deviceClass || null }
).catch((err: Error) =>
@@ -64,10 +64,7 @@ class ThreadNetworkInfoPage extends LitElement {
this._otbrLoading = true;
const otbrInfo = await getOTBRInfo(this.hass).catch(() => undefined);
const otbr = findOTBRInfoForDataset(otbrInfo, this._dataset);
if (otbr?.active_dataset_tlvs?.includes(this._dataset.extended_pan_id)) {
this._otbrInfo = otbr;
}
this._otbrInfo = findOTBRInfoForDataset(otbrInfo, this._dataset);
this._otbrLoading = false;
}
@@ -1,6 +1,24 @@
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { computeAreaName } from "../../../../../common/entity/compute_area_name";
import { getDeviceArea } from "../../../../../common/entity/context/get_device_context";
import type { Cluster, ZHADevice, ZHAGroup } from "../../../../../data/zha";
import { ZHADeviceType } from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
export const getZHADeviceAreaName = (
device: ZHADevice,
areas: HomeAssistant["areas"],
devices: HomeAssistant["devices"]
): string | undefined => {
// The device registry is authoritative: an entry with no effective
// area means the device has no area, even if ZHA still reports one
// from its cached response. Resolve via the registry so a child
// device inherits its parent's area, matching the ZHA network
// visualization.
const haDevice = devices[device.device_reg_id];
const area = haDevice ? getDeviceArea(haDevice, areas, devices) : undefined;
return area ? computeAreaName(area) : undefined;
};
export const formatAsPaddedHex = (value: string | number): string => {
let hex = value;
@@ -1,3 +1,4 @@
import type { Connection } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -42,6 +43,11 @@ class ZHAAddDevicesPage extends LitElement {
private _wakeLock?: Promise<WakeLockSentinel>;
private _connection?: Connection;
// Set for the search in flight, so a pending subscription can tell it ended
private _search?: object;
public connectedCallback(): void {
super.connectedCallback();
if (this.route && this.route.path && this.route.path !== "") {
@@ -194,28 +200,34 @@ class ZHAAddDevicesPage extends LitElement {
}
}
// The backend ends the subscription when the connection drops, and it is not
// sent again, so end the search here and show "Search again"
private _connectionLost = () => {
this._unsubscribe();
};
private _unsubscribe(): Promise<void> {
this._active = false;
this._search = undefined;
this._connection?.removeEventListener("disconnected", this._connectionLost);
this._connection = undefined;
if (this._addDevicesTimeoutHandle) {
clearTimeout(this._addDevicesTimeoutHandle);
}
let unsubscribed = Promise.resolve();
if (this._subscribed) {
unsubscribed = this._subscribed.then((unsub) => unsub());
unsubscribed = this._subscribed.then(
(unsub) => unsub(),
// A subscription that was cancelled or lost with the connection has
// nothing to unsubscribe
() => undefined
);
this._subscribed = undefined;
}
this._wakeLock?.then((wakeLock) => wakeLock.release());
return unsubscribed;
}
private _deactivate(): void {
this._active = false;
if (this._addDevicesTimeoutHandle) {
clearTimeout(this._addDevicesTimeoutHandle);
}
this._wakeLock?.then((wakeLock) => wakeLock.release());
}
private _subscribe(): void {
if (!this.hass) {
return;
@@ -225,22 +237,35 @@ class ZHAAddDevicesPage extends LitElement {
// session before starting the next one
const unsubscribed = this._unsubscribe();
this._active = true;
const search = {};
this._search = search;
const data: any = { type: "zha/devices/permit", duration: 254 };
if (this._ieeeAddress) {
data.ieee = this._ieeeAddress;
}
const connection = this.hass.connection;
this._connection = connection;
connection.addEventListener("disconnected", this._connectionLost);
this._subscribed = unsubscribed
// A failed unsubscribe leaves nothing to wait for
.catch(() => undefined)
.then(() =>
// Each subscription opens the network for joining and turns on ZHA
// debug logging, so don't send it again after a reconnect
connection.subscribeMessage(
(message) => this._handleMessage(message),
data
data,
{
resubscribe: false,
// While offline, subscribeMessage waits for the connection. Don't
// open the network if this search ended in the meantime
preCheck: () => this._search === search,
}
)
);
this._addDevicesTimeoutHandle = setTimeout(
() => this._deactivate(),
// End the subscription with the search, so debug logging ends too
() => this._unsubscribe(),
254000
);
if ("wakeLock" in navigator) {
@@ -2,12 +2,12 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import "../../../../../components/device/ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "../../../../../components/device/ha-device-picker";
import type { ZHADevice } from "../../../../../data/zha";
import { bindDevices, unbindDevices } from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
@customElement("zha-device-binding-control")
export class ZHADeviceBindingControl extends LitElement {
@@ -17,8 +17,6 @@ export class ZHADeviceBindingControl extends LitElement {
@property({ attribute: false }) public bindableDevices: ZHADevice[] = [];
@state() private _bindTargetIndex = -1;
@state() private _deviceToBind?: ZHADevice;
@state() private _bindingOperationInProgress = false;
@@ -29,7 +27,6 @@ export class ZHADeviceBindingControl extends LitElement {
changedProperties.has("device") && this.device?.ieee !== oldDevice?.ieee;
if (deviceChanged || changedProperties.has("bindableDevices")) {
this._bindTargetIndex = -1;
this._deviceToBind = undefined;
}
super.updated(changedProperties);
@@ -39,21 +36,15 @@ export class ZHADeviceBindingControl extends LitElement {
return html`
<div class="content">
<div class="command-picker">
<ha-select
<ha-device-picker
label=${this.hass!.localize(
"ui.panel.config.zha.device_binding.picker_label"
)}
class="menu"
.value=${String(this._bindTargetIndex)}
@selected=${this._bindTargetIndexChanged}
.options=${this.bindableDevices.map((device, idx) => ({
value: String(idx),
label: device.user_given_name
? device.user_given_name
: device.name,
}))}
.value=${this._deviceToBind?.device_reg_id}
.deviceFilter=${this._filterBindableDevices}
@value-changed=${this._bindTargetChanged}
>
</ha-select>
</ha-device-picker>
</div>
<div class="card-actions">
<ha-progress-button
@@ -81,12 +72,16 @@ export class ZHADeviceBindingControl extends LitElement {
`;
}
private _bindTargetIndexChanged(event: HaSelectSelectEvent): void {
this._bindTargetIndex = Number(event.detail.value);
this._deviceToBind =
this._bindTargetIndex === -1
? undefined
: this.bindableDevices[this._bindTargetIndex];
private _filterBindableDevices: HaDevicePickerDeviceFilterFunc = (device) =>
this.bindableDevices.some(
(bindableDevice) => bindableDevice.device_reg_id === device.id
);
private _bindTargetChanged(event: ValueChangedEvent<string>): void {
event.stopPropagation();
this._deviceToBind = this.bindableDevices.find(
(device) => device.device_reg_id === event.detail.value
);
}
private async _onBindDevicesClick(ev: CustomEvent): Promise<void> {
@@ -131,7 +126,8 @@ export class ZHADeviceBindingControl extends LitElement {
return [
haStyle,
css`
.menu {
ha-device-picker {
display: block;
width: 100%;
}
@@ -129,7 +129,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
const oldDeviceName = device.user_given_name || device.name;
const newDeviceName = (event.target as HaInput).value;
this.device.user_given_name = newDeviceName;
await updateDeviceRegistryEntry(this.hass, device.device_reg_id, {
await updateDeviceRegistryEntry(this.hass.callWS, device.device_reg_id, {
name_by_user: newDeviceName,
});
@@ -144,7 +144,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
);
const entityIdsMapping = await getAutomaticEntityIds(
this.hass,
this.hass.callWS,
entities.map((entity) => entity.entity_id)
);
@@ -155,7 +155,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
return undefined;
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
return updateEntityRegistryEntry(this.hass!.callWS, entity.entity_id, {
new_entity_id: newEntityId,
});
});
@@ -173,9 +173,13 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
const area = ev.detail.value;
try {
await updateDeviceRegistryEntry(this.hass, this.device!.device_reg_id, {
area_id: area,
});
await updateDeviceRegistryEntry(
this.hass.callWS,
this.device!.device_reg_id,
{
area_id: area,
}
);
this.device!.area_id = area;
} catch (err: any) {
showAlertDialog(this, {
@@ -17,10 +17,12 @@ import { fetchDevices } from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { zhaDevicePageCardStyles } from "./device-page/zha-device-page-card-styles";
import { getZHADeviceAreaName } from "./functions";
export interface DeviceRowData extends DataTableRowData {
id: string;
name: string;
area: string | undefined;
lqi: number;
depth: number;
relationship: string;
@@ -56,7 +58,9 @@ class ZHADeviceNeighbors extends LitElement {
private _deviceNeighbors = memoizeOne(
(
device: ZHADevice | undefined,
devices: Map<string, ZHADevice> | undefined
devices: Map<string, ZHADevice> | undefined,
areas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"]
) => {
const outputDevices: DeviceRowData[] = [];
if (device && devices) {
@@ -65,6 +69,9 @@ class ZHADeviceNeighbors extends LitElement {
outputDevices.push({
name:
zhaDevice?.user_given_name || zhaDevice?.name || neighbor.ieee,
area: zhaDevice
? getZHADeviceAreaName(zhaDevice, areas, haDevices)
: undefined,
id: zhaDevice?.device_reg_id || neighbor.ieee,
lqi: Number(neighbor.lqi),
depth: Number(neighbor.depth),
@@ -86,6 +93,11 @@ class ZHADeviceNeighbors extends LitElement {
direction: "asc",
flex: 2,
},
area: {
title: this.hass.localize("ui.panel.config.generic.headers.area"),
sortable: true,
filterable: true,
},
lqi: {
title: this.hass.localize("ui.panel.config.zha.neighbors.lqi"),
sortable: true,
@@ -101,6 +113,11 @@ class ZHADeviceNeighbors extends LitElement {
direction: "asc",
flex: 2,
},
area: {
title: this.hass.localize("ui.panel.config.generic.headers.area"),
sortable: true,
filterable: true,
},
lqi: {
title: this.hass.localize("ui.panel.config.zha.neighbors.lqi"),
sortable: true,
@@ -140,7 +157,12 @@ class ZHADeviceNeighbors extends LitElement {
return html`<ha-card class="empty-card">${this._error}</ha-card>`;
}
const neighbors = this._deviceNeighbors(this.device, this._devices);
const neighbors = this._deviceNeighbors(
this.device,
this._devices,
this.hass.areas,
this.hass.devices
);
if (!neighbors.length) {
return html`
@@ -1,14 +1,13 @@
import { mdiHelpNetworkOutline, mdiLeaf, mdiZigbee } from "@mdi/js";
import { getDeviceArea } from "../../../../../common/entity/context/get_device_context";
import type {
NetworkData,
NetworkLink,
NetworkNode,
} from "../../../../../components/chart/ha-network-graph";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import type { ZHADevice } from "../../../../../data/zha";
import { ZHADeviceType } from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
import { getZHADeviceAreaName } from "./functions";
function getLQIWidth(lqi: number): number {
return lqi > 200 ? 3 : lqi > 100 ? 2 : 1;
@@ -120,16 +119,11 @@ export function createZHANetworkChartData(
category = 2; // End Device
}
const haDevice = hass.devices[device.device_reg_id] as
DeviceRegistryEntry | undefined;
const area = haDevice
? getDeviceArea(haDevice, hass.areas, hass.devices)
: undefined;
// Create node
nodes.push({
id: device.ieee,
name: device.user_given_name || device.name || device.ieee,
context: area?.name,
context: getZHADeviceAreaName(device, hass.areas, hass.devices),
category,
value: isCoordinator
? 3
Loaded 100 of 166 files, more files were not shown because too many files have changed in this diff. Show more