Compare commits

...
Author SHA1 Message Date
Paul Bottein d185e5ee37 Add settings button to AI task entity pickers 2026-10-13 14:56:12 +02:00
4 changed files with 179 additions and 105 deletions

No files matched your search

+26 -73
View File
@@ -4,14 +4,13 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { debounce } from "../common/util/debounce";
import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
import type { Agent } from "../data/conversation";
import { listAgents } from "../data/conversation";
import { getExtendedEntityRegistryEntry } from "../data/entity/entity_registry";
import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { EntityConfigFlow } from "../dialogs/config-flow/entity-config-flow";
import {
fetchEntityConfigFlow,
showEntityConfigFlow,
} from "../dialogs/config-flow/entity-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-select";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
@@ -34,9 +33,7 @@ export class HaConversationAgentPicker extends LitElement {
@state() _agents?: Agent[];
@state() private _configEntry?: ConfigEntry;
@state() private _subConfigEntry?: SubEntry;
@state() private _configFlow?: EntityConfigFlow;
protected render() {
if (!this._agents) {
@@ -79,16 +76,15 @@ export class HaConversationAgentPicker extends LitElement {
.options=${options}
></ha-select
>${
(this._subConfigEntry &&
this._configEntry?.supported_subentry_types[
this._subConfigEntry.subentry_type
]?.supports_reconfigure) ||
this._configEntry?.supports_options
this._configFlow
? html`<ha-icon-button
.label=${this.hass.localize(
"ui.components.conversation-agent-picker.settings"
)}
.path=${mdiCog}
@click=${this._openOptionsFlow}
@click=${this._openConfigFlow}
></ha-icon-button>`
: ""
: nothing
}
`;
}
@@ -102,40 +98,22 @@ export class HaConversationAgentPicker extends LitElement {
}
if (changedProperties.has("value")) {
this._maybeFetchConfigEntry();
this._fetchConfigFlow();
}
}
private async _maybeFetchConfigEntry() {
if (!this.value || !(this.value in this.hass.entities)) {
this._configEntry = undefined;
return;
}
private async _fetchConfigFlow() {
const entityId = this.value;
let configFlow: EntityConfigFlow | undefined;
try {
const regEntry = await getExtendedEntityRegistryEntry(
this.hass,
this.value
);
if (!regEntry.config_entry_id) {
this._configEntry = undefined;
return;
}
this._configEntry = (
await getConfigEntry(this.hass, regEntry.config_entry_id)
).config_entry;
if (!regEntry.config_subentry_id) {
this._subConfigEntry = undefined;
} else {
this._subConfigEntry = (
await getSubEntries(this.hass, regEntry.config_entry_id)
).find((entry) => entry.subentry_id === regEntry.config_subentry_id);
}
configFlow = entityId
? await fetchEntityConfigFlow(this.hass, entityId)
: undefined;
} catch (_err) {
this._configEntry = undefined;
this._subConfigEntry = undefined;
configFlow = undefined;
}
if (entityId === this.value) {
this._configFlow = configFlow;
}
}
@@ -203,35 +181,10 @@ export class HaConversationAgentPicker extends LitElement {
}
}
private async _openOptionsFlow() {
if (!this._configEntry) {
return;
private _openConfigFlow() {
if (this._configFlow) {
showEntityConfigFlow(this, this.hass, this._configFlow);
}
if (
this._subConfigEntry &&
this._configEntry.supported_subentry_types[
this._subConfigEntry.subentry_type
]?.supports_reconfigure
) {
showSubConfigFlowDialog(
this,
this._configEntry,
this._subConfigEntry.subentry_type,
{
startFlowHandler: this._configEntry.entry_id,
subEntryId: this._subConfigEntry.subentry_id,
}
);
return;
}
showOptionsFlowDialog(this, this._configEntry, {
manifest: await fetchIntegrationManifest(
this.hass,
this._configEntry.domain
),
});
}
static styles = css`
@@ -0,0 +1,59 @@
import type { ConfigEntry, SubEntry } from "../../data/config_entries";
import { getConfigEntry, getSubEntries } from "../../data/config_entries";
import { getExtendedEntityRegistryEntry } from "../../data/entity/entity_registry";
import { fetchIntegrationManifest } from "../../data/integration";
import type { HomeAssistant } from "../../types";
import { showOptionsFlowDialog } from "./show-dialog-options-flow";
import { showSubConfigFlowDialog } from "./show-dialog-sub-config-flow";
export interface EntityConfigFlow {
entry: ConfigEntry;
subEntry?: SubEntry;
}
export const fetchEntityConfigFlow = async (
hass: HomeAssistant,
entityId: string
): Promise<EntityConfigFlow | undefined> => {
if (!(entityId in hass.entities)) {
return undefined;
}
const regEntry = await getExtendedEntityRegistryEntry(hass, entityId);
if (!regEntry.config_entry_id) {
return undefined;
}
const { config_entry: entry } = await getConfigEntry(
hass,
regEntry.config_entry_id
);
if (regEntry.config_subentry_id) {
const subEntry = (await getSubEntries(hass, entry.entry_id)).find(
(sub) => sub.subentry_id === regEntry.config_subentry_id
);
if (
subEntry &&
entry.supported_subentry_types[subEntry.subentry_type]
?.supports_reconfigure
) {
return { entry, subEntry };
}
}
return entry.supports_options ? { entry } : undefined;
};
export const showEntityConfigFlow = async (
element: HTMLElement,
hass: HomeAssistant,
{ entry, subEntry }: EntityConfigFlow
) => {
if (subEntry) {
showSubConfigFlowDialog(element, entry, subEntry.subentry_type, {
startFlowHandler: entry.entry_id,
subEntryId: subEntry.subentry_id,
});
return;
}
showOptionsFlowDialog(element, entry, {
manifest: await fetchIntegrationManifest(hass, entry.domain),
});
};
+90 -30
View File
@@ -1,7 +1,7 @@
import { mdiHelpCircleOutline, mdiStarFourPoints } from "@mdi/js";
import { mdiCog, mdiHelpCircleOutline, mdiStarFourPoints } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
@@ -10,6 +10,7 @@ import type { HaProgressButton } from "../../../components/buttons/ha-progress-b
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-settings-row";
import {
AITaskEntityFeature,
@@ -17,6 +18,11 @@ import {
saveAITaskPreferences,
type AITaskPreferences,
} from "../../../data/ai_task";
import type { EntityConfigFlow } from "../../../dialogs/config-flow/entity-config-flow";
import {
fetchEntityConfigFlow,
showEntityConfigFlow,
} from "../../../dialogs/config-flow/entity-config-flow";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
@@ -36,6 +42,10 @@ export class AITaskPref extends LitElement {
@state() private _prefs?: AITaskPreferences;
@state() private _configFlows: Partial<
Record<keyof AITaskPreferences, EntityConfigFlow>
> = {};
private _gen_data_entity_id?: string | null;
private _gen_image_entity_id?: string | null;
@@ -47,6 +57,8 @@ export class AITaskPref extends LitElement {
}
fetchAITaskPreferences(this.hass).then((prefs) => {
this._prefs = prefs;
this._updateConfigFlow("gen_data_entity_id");
this._updateConfigFlow("gen_image_entity_id");
});
}
@@ -97,18 +109,7 @@ export class AITaskPref extends LitElement {
"ui.panel.config.ai_task.gen_data_description"
)}
</span>
<ha-entity-picker
data-name="gen_data_entity_id"
.disabled=${
this._prefs === undefined &&
isComponentLoaded(this.hass.config, "ai_task")
}
.value=${
this._gen_data_entity_id || this._prefs?.gen_data_entity_id
}
.entityFilter=${filterGenData}
@value-changed=${this._handlePrefChange}
></ha-entity-picker>
${this._renderPicker("gen_data_entity_id", filterGenData)}
</ha-settings-row>
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
@@ -119,18 +120,7 @@ export class AITaskPref extends LitElement {
"ui.panel.config.ai_task.gen_image_description"
)}
</span>
<ha-entity-picker
data-name="gen_image_entity_id"
.disabled=${
this._prefs === undefined &&
isComponentLoaded(this.hass.config, "ai_task")
}
.value=${
this._gen_image_entity_id || this._prefs?.gen_image_entity_id
}
.entityFilter=${filterGenImage}
@value-changed=${this._handlePrefChange}
></ha-entity-picker>
${this._renderPicker("gen_image_entity_id", filterGenImage)}
</ha-settings-row>
</div>
<div class="card-actions">
@@ -142,11 +132,71 @@ export class AITaskPref extends LitElement {
`;
}
private _renderPicker(
key: keyof AITaskPreferences,
entityFilter: (entity: HassEntity) => boolean
) {
const configFlow = this._configFlows[key];
return html`
<div class="picker">
<ha-entity-picker
data-name=${key}
.disabled=${
this._prefs === undefined &&
isComponentLoaded(this.hass.config, "ai_task")
}
.value=${this._entityId(key)}
.entityFilter=${entityFilter}
@value-changed=${this._handlePrefChange}
></ha-entity-picker>
${
configFlow
? html`<ha-icon-button
data-name=${key}
.label=${this.hass.localize("ui.panel.config.ai_task.settings")}
.path=${mdiCog}
@click=${this._openConfigFlow}
></ha-icon-button>`
: nothing
}
</div>
`;
}
private _entityId(key: keyof AITaskPreferences) {
return this[`_${key}`] || this._prefs?.[key];
}
private async _updateConfigFlow(key: keyof AITaskPreferences) {
const entityId = this._entityId(key);
let configFlow: EntityConfigFlow | undefined;
try {
configFlow = entityId
? await fetchEntityConfigFlow(this.hass, entityId)
: undefined;
} catch (_err) {
configFlow = undefined;
}
if (entityId === this._entityId(key)) {
this._configFlows = { ...this._configFlows, [key]: configFlow };
}
}
private _handlePrefChange(ev: ValueChangedEvent<string | undefined>) {
const input = ev.target as HaEntityPicker;
const key = input.dataset.name as keyof AITaskPreferences;
const value = ev.detail.value || null;
this[`_${key}`] = value;
this._updateConfigFlow(key);
}
private _openConfigFlow(ev: Event) {
const key = (ev.currentTarget as HTMLElement).dataset
.name as keyof AITaskPreferences;
const configFlow = this._configFlows[key];
if (configFlow) {
showEntityConfigFlow(this, this.hass, configFlow);
}
}
private async _update(ev) {
@@ -207,12 +257,22 @@ export class AITaskPref extends LitElement {
.card-actions {
text-align: right;
}
ha-entity-picker {
.picker {
display: flex;
align-items: center;
flex: 1;
margin-left: 16px;
min-width: 0;
margin-inline-start: var(--ha-space-4);
}
:host([narrow]) ha-entity-picker {
margin-left: 0;
:host([narrow]) .picker {
margin-inline-start: 0;
}
.picker ha-entity-picker {
flex: 1;
min-width: 0;
}
.picker ha-icon-button {
color: var(--secondary-text-color);
}
`;
}
+4 -2
View File
@@ -928,7 +928,8 @@
},
"conversation-agent-picker": {
"conversation_agent": "Conversation agent",
"none": "None"
"none": "None",
"settings": "Agent settings"
},
"country-picker": {
"country": "Country",
@@ -3048,7 +3049,8 @@
"gen_data_header": "Data generation tasks",
"gen_data_description": "Suggest automation names.",
"gen_image_header": "Image generation tasks",
"gen_image_description": "Generate images."
"gen_image_description": "Generate images.",
"settings": "Task settings"
},
"mcp": {
"header": "Model Context Protocol",