Compare commits

...
Author SHA1 Message Date
Claude e82170a11a 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
2026-10-31 19:57:45 +00:00
Claude 178831c7c2 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
2026-10-31 15:10:26 +00:00
Claude 215e6a2514 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
2026-10-31 15:02:53 +00:00
Claude d3dc40398d 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
2026-10-31 14:31:07 +00:00
Claude 0c6bc28ed1 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
2026-10-30 19:55:37 +00:00
Claude e7a74930cc 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
2026-10-30 19:46:31 +00:00
Claude 3bd106e4cc 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
2026-10-30 19:43:42 +00:00
Claude 48960e377b 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
2026-10-30 19:38:40 +00:00
Claude 83893d942b 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
2026-10-30 18:47:34 +00:00
Claude ee89c980d0 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
2026-10-30 18:15:23 +00:00
Claude 216044b5f0 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
2026-10-30 17:55:36 +00:00
Claude 2051456f19 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
2026-10-30 17:47:57 +00:00
Claude 9a9f505ddd 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
2026-10-30 17:29:15 +00:00
Claude 412912d01c 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
2026-10-30 16:58:40 +00:00
5 changed files with 174 additions and 51 deletions

No files matched your search

@@ -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: "helper",
}),
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: [
@@ -13,16 +13,23 @@ export const modbusFixtures: ConnectivityFixtures = {
components: ["modbus"],
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",
}),
],
@@ -19,6 +19,7 @@ import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
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 {
@@ -37,7 +38,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";
@@ -57,7 +60,9 @@ import {
getCoreLogFileDownloadUnavailableReason,
getErrorLogDownloadUrl,
} from "../../../data/error_log";
import { manifestsContext } from "../../../data/context";
import type {
DomainManifestLookup,
IntegrationLogInfo,
IntegrationManifest,
} from "../../../data/integration";
@@ -160,6 +165,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;
@@ -396,6 +406,13 @@ 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)
: [];
const showDependents = normalEntries.length === 0 && dependents.length > 0;
const documentationLink = this._manifest?.is_built_in
? documentationUrl(this.hass, `/integrations/${this._manifest.domain}`)
: this._manifest?.documentation;
@@ -581,6 +598,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
${
normalEntries.length === 0 &&
!showDependents &&
this._manifest &&
!this._manifest.config_flow &&
this.hass.config.components.find(
@@ -702,6 +720,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
${
this._manifest?.integration_type !== "hardware" &&
!showDependents &&
(!this._manifest?.single_config_entry ||
(normalData.length === 0 && attentionData.length === 0))
? html`<ha-button
@@ -880,61 +899,127 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
`
: nothing
}
<div class="section">
<h3 class="section-header">
${
this._manifest?.integration_type
? this.hass.localize(
`ui.panel.config.integrations.integration_page.entries_${this._manifest.integration_type}`
)
: this.hass.localize(
`ui.panel.config.integrations.integration_page.entries`
)
}
</h3>
${
filteredNormalData.length === 0
? html`<div class="card-content no-entries">
${
this._filter
? this.hass.localize(
"ui.panel.config.integrations.none_found"
)
: this._manifest &&
!this._manifest.config_flow &&
this.hass.config.components.find(
(comp) => comp.split(".")[0] === this.domain
)
? this.hass.localize(
"ui.panel.config.integrations.integration_page.yaml_entry"
)
: this.hass.localize(
"ui.panel.config.integrations.integration_page.no_entries"
)
}
</div>`
: html`
${filteredNormalData.map(
(data) =>
html`<ha-config-entry-row
.hass=${this.hass}
.narrow=${this.narrow}
.manifest=${this._manifest}
.diagnosticHandler=${this._diagnosticHandler}
.entities=${this._entities}
.data=${data}
data-entry-id=${data.entry.entry_id}
></ha-config-entry-row>`
${
showDependents
? html`<div class="section">
<h3 class="section-header">
${this.hass.localize(
"ui.panel.config.integrations.integration_page.used_by"
)}
`
}
</div>
</h3>
${this._renderDependents(dependents)}
</div>`
: html`<div class="section">
<h3 class="section-header">
${
this._manifest?.integration_type
? this.hass.localize(
`ui.panel.config.integrations.integration_page.entries_${this._manifest.integration_type}`
)
: this.hass.localize(
`ui.panel.config.integrations.integration_page.entries`
)
}
</h3>
${
filteredNormalData.length === 0
? html`<div class="card-content no-entries">
${
this._filter
? this.hass.localize(
"ui.panel.config.integrations.none_found"
)
: this._manifest &&
!this._manifest.config_flow &&
this.hass.config.components.find(
(comp) => comp.split(".")[0] === this.domain
)
? this.hass.localize(
"ui.panel.config.integrations.integration_page.yaml_entry"
)
: this.hass.localize(
"ui.panel.config.integrations.integration_page.no_entries"
)
}
</div>`
: html`
${filteredNormalData.map(
(data) =>
html`<ha-config-entry-row
.hass=${this.hass}
.narrow=${this.narrow}
.manifest=${this._manifest}
.diagnosticHandler=${this._diagnosticHandler}
.entities=${this._entities}
.data=${data}
data-entry-id=${data.entry.entry_id}
></ha-config-entry-row>`
)}
`
}
</div>`
}
</div>
</hass-subpage>
`;
}
// 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) =>
Object.values(manifests ?? {}).filter(
// System integrations, like Default Configuration, are not useful to list.
(manifest) =>
manifest.integration_type !== "system" &&
manifest.dependencies?.includes(domain)
)
);
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";
}
@@ -975,6 +1060,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)
@@ -1624,6 +1712,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 ||
+1
View File
@@ -7808,6 +7808,7 @@
"entries_virtual": "[%key:ui::panel::config::integrations::integration_page::entries%]",
"entries_entity": "[%key:ui::panel::config::integrations::integration_page::entries%]",
"no_entries": "No entries",
"used_by": "Used by",
"yaml_entry": "This integration was not set up via the UI, you have either set it up in YAML or it is a dependency set up by another integration. If you want to configure it, you will need to do so in your configuration.yaml file.",
"attention_entries": "Needs attention",
"add_entry": "Add entry",