mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-22 15:38:13 +00:00
Compare commits
17 Commits
dev
...
areas-sugg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8af0eda5d1 | ||
|
|
c45217fd76 | ||
|
|
e440986ab3 | ||
|
|
7843d08193 | ||
|
|
c0e8cb8c97 | ||
|
|
3b3ffd339e | ||
|
|
1a0f1e9921 | ||
|
|
ee7d18c5ae | ||
|
|
59c9f67256 | ||
|
|
21d0ef14d5 | ||
|
|
d6b8a61013 | ||
|
|
02105a782e | ||
|
|
e33c8f74ba | ||
|
|
0c3706772f | ||
|
|
587cd9c293 | ||
|
|
f11750295e | ||
|
|
6381fcc47f |
@@ -16,8 +16,11 @@ import "../../../components/ha-labels-picker";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-suggest-with-ai-button";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import type { GenDataTaskResult } from "../../../data/ai_task";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
@@ -27,11 +30,19 @@ import {
|
||||
SENSOR_DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_DEVICE_CLASS_TEMPERATURE,
|
||||
} from "../../../data/sensor";
|
||||
import { fetchLabelRegistry } from "../../../data/label/label_registry";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import {
|
||||
type MetadataSuggestionInclude,
|
||||
type MetadataSuggestionResult,
|
||||
generateMetadataSuggestionTask,
|
||||
processMetadataSuggestion,
|
||||
} from "../common/suggest-metadata-ai";
|
||||
import { buildAreaMetadataInspirations } from "../common/suggest-metadata-inspirations";
|
||||
import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
|
||||
|
||||
const cropOptions: CropOptions = {
|
||||
@@ -75,6 +86,12 @@ class DialogAreaDetail
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _suggestionInclude: MetadataSuggestionInclude = {
|
||||
name: true,
|
||||
labels: true,
|
||||
floor: true,
|
||||
};
|
||||
|
||||
public async showDialog(
|
||||
params: AreaRegistryDetailDialogParams
|
||||
): Promise<void> {
|
||||
@@ -242,6 +259,70 @@ class DialogAreaDetail
|
||||
`;
|
||||
}
|
||||
|
||||
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> => {
|
||||
this._suggestionInclude = {
|
||||
...this._suggestionInclude,
|
||||
name: this._name.trim() === "",
|
||||
};
|
||||
|
||||
return generateMetadataSuggestionTask<{
|
||||
name: string;
|
||||
aliases: string[];
|
||||
labels: string[];
|
||||
floor: string | null;
|
||||
temperature_entity: string | null;
|
||||
humidity_entity: string | null;
|
||||
}>(
|
||||
this.hass.connection,
|
||||
this.hass.language,
|
||||
"area",
|
||||
{
|
||||
name: this._name,
|
||||
aliases: this._aliases,
|
||||
labels: this._labels.length
|
||||
? (await fetchLabelRegistry(this.hass.connection))
|
||||
.filter((label) => this._labels.includes(label.label_id))
|
||||
.map((label) => label.name)
|
||||
: [],
|
||||
floor: this._floor ? this.hass.floors?.[this._floor]?.name : null,
|
||||
temperature_entity: this._temperatureEntity
|
||||
? (this.hass.states[this._temperatureEntity]?.attributes
|
||||
?.friendly_name ?? null)
|
||||
: null,
|
||||
humidity_entity: this._humidityEntity
|
||||
? (this.hass.states[this._humidityEntity]?.attributes
|
||||
?.friendly_name ?? null)
|
||||
: null,
|
||||
},
|
||||
await buildAreaMetadataInspirations(this.hass.connection),
|
||||
this._suggestionInclude
|
||||
);
|
||||
};
|
||||
|
||||
private async _handleSuggestion(
|
||||
event: CustomEvent<GenDataTaskResult<MetadataSuggestionResult>>
|
||||
) {
|
||||
const result = event.detail;
|
||||
const processed = await processMetadataSuggestion(
|
||||
this.hass.connection,
|
||||
"area",
|
||||
result,
|
||||
this._suggestionInclude
|
||||
);
|
||||
|
||||
if (processed.name) {
|
||||
this._name = processed.name;
|
||||
}
|
||||
|
||||
if (processed.labels?.length) {
|
||||
this._labels = processed.labels;
|
||||
}
|
||||
|
||||
if (processed.floor) {
|
||||
this._floor = processed.floor;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
@@ -259,6 +340,12 @@ class DialogAreaDetail
|
||||
: this.hass.localize("ui.panel.config.areas.editor.create_area")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-suggest-with-ai-button
|
||||
slot="headerActionItems"
|
||||
.hass=${this.hass}
|
||||
.generateTask=${this._generateTask}
|
||||
@suggestion=${this._handleSuggestion}
|
||||
></ha-suggest-with-ai-button>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -423,13 +510,16 @@ class DialogAreaDetail
|
||||
ha-picture-upload,
|
||||
ha-expansion-panel {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.content {
|
||||
padding: 12px;
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.description {
|
||||
margin: 0 0 16px 0;
|
||||
margin: 0 0 var(--ha-space-4) 0;
|
||||
}
|
||||
ha-suggest-with-ai-button {
|
||||
margin: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -33,10 +33,10 @@ import type {
|
||||
} from "./show-dialog-automation-save";
|
||||
import {
|
||||
type MetadataSuggestionResult,
|
||||
SUGGESTION_INCLUDE_ALL,
|
||||
generateMetadataSuggestionTask,
|
||||
processMetadataSuggestion,
|
||||
} from "../../common/suggest-metadata-ai";
|
||||
import { buildEntityMetadataInspirations } from "../../common/suggest-metadata-inspirations";
|
||||
|
||||
@customElement("ha-dialog-automation-save")
|
||||
class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
@@ -341,10 +341,14 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
}
|
||||
return generateMetadataSuggestionTask<AutomationConfig | ScriptConfig>(
|
||||
this.hass.connection,
|
||||
this.hass.states,
|
||||
this.hass.language,
|
||||
this._params.domain,
|
||||
this._params.config
|
||||
this._params.config,
|
||||
await buildEntityMetadataInspirations(
|
||||
this.hass.connection,
|
||||
this.hass.states,
|
||||
this._params.domain
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -358,11 +362,12 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
const processed = await processMetadataSuggestion(
|
||||
this.hass.connection,
|
||||
this._params.domain,
|
||||
result,
|
||||
SUGGESTION_INCLUDE_ALL
|
||||
result
|
||||
);
|
||||
|
||||
this._newName = processed.name;
|
||||
if (processed.name) {
|
||||
this._newName = processed.name;
|
||||
}
|
||||
|
||||
if (processed.description) {
|
||||
this._newDescription = processed.description;
|
||||
@@ -432,7 +437,8 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0 24px 24px 24px;
|
||||
--dialog-content-padding: 0 var(--ha-space-6) var(--ha-space-6)
|
||||
var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-textfield,
|
||||
@@ -448,15 +454,15 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
ha-labels-picker,
|
||||
ha-area-picker,
|
||||
ha-chip-set:has(> ha-assist-chip) {
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-suggest-with-ai-button {
|
||||
margin: 8px 16px;
|
||||
margin: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,163 +1,84 @@
|
||||
import { dump } from "js-yaml";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import type { AITaskStructure, GenDataTaskResult } from "../../../data/ai_task";
|
||||
import { fetchCategoryRegistry } from "../../../data/category_registry";
|
||||
import {
|
||||
subscribeEntityRegistry,
|
||||
type EntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { subscribeLabelRegistry } from "../../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button";
|
||||
import {
|
||||
fetchCategories,
|
||||
fetchFloors,
|
||||
fetchLabels,
|
||||
} from "./suggest-metadata-helpers";
|
||||
|
||||
export interface MetadataSuggestionResult {
|
||||
name: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
labels?: string[];
|
||||
floor?: string;
|
||||
}
|
||||
|
||||
export type MetadataSuggestionDomain = "automation" | "script" | "scene";
|
||||
export type MetadataSuggestionDomain =
|
||||
| "automation"
|
||||
| "script"
|
||||
| "scene"
|
||||
| "area";
|
||||
|
||||
export interface MetadataSuggestionInclude {
|
||||
name: boolean;
|
||||
description?: boolean;
|
||||
categories?: boolean;
|
||||
labels?: boolean;
|
||||
floor?: boolean;
|
||||
}
|
||||
|
||||
type Categories = Record<string, string>;
|
||||
type Entities = Record<string, EntityRegistryEntry>;
|
||||
type Labels = Record<string, string>;
|
||||
|
||||
export const SUGGESTION_INCLUDE_ALL: MetadataSuggestionInclude = {
|
||||
export const SUGGESTION_INCLUDE_DEFAULT: MetadataSuggestionInclude = {
|
||||
name: true,
|
||||
description: true,
|
||||
categories: true,
|
||||
labels: true,
|
||||
} as const;
|
||||
|
||||
const tryCatchEmptyObject = <T>(promise: Promise<T>): Promise<T> =>
|
||||
promise.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching data for suggestion: ", err);
|
||||
return {} as T;
|
||||
});
|
||||
|
||||
const fetchCategories = (
|
||||
connection: HomeAssistant["connection"],
|
||||
domain: MetadataSuggestionDomain
|
||||
): Promise<Categories> =>
|
||||
tryCatchEmptyObject<Categories>(
|
||||
fetchCategoryRegistry(connection, domain).then((cats) =>
|
||||
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
|
||||
)
|
||||
);
|
||||
|
||||
const fetchEntities = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Entities> =>
|
||||
tryCatchEmptyObject<Entities>(
|
||||
subscribeOne(connection, subscribeEntityRegistry).then((ents) =>
|
||||
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
|
||||
)
|
||||
);
|
||||
|
||||
const fetchLabels = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Labels> =>
|
||||
tryCatchEmptyObject<Labels>(
|
||||
subscribeOne(connection, subscribeLabelRegistry).then((labs) =>
|
||||
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
||||
)
|
||||
);
|
||||
|
||||
function buildMetadataInspirations(
|
||||
domain: MetadataSuggestionDomain,
|
||||
states: HomeAssistant["states"],
|
||||
entities: Entities,
|
||||
categories?: Categories,
|
||||
labels?: Labels
|
||||
): string[] {
|
||||
const inspirations: string[] = [];
|
||||
|
||||
for (const entityId of Object.keys(entities)) {
|
||||
const entityEntry = entities[entityId];
|
||||
if (!entityEntry || computeDomain(entityId) !== domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entity = states[entityId];
|
||||
if (
|
||||
!entity ||
|
||||
entity.attributes.restored ||
|
||||
!entity.attributes.friendly_name
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let inspiration = `- ${entity.attributes.friendly_name}`;
|
||||
|
||||
// Get the category for this domain
|
||||
if (categories && categories[entityEntry.categories[domain]]) {
|
||||
inspiration += ` (category: ${categories[entityEntry.categories[domain]]})`;
|
||||
}
|
||||
|
||||
if (labels && entityEntry.labels.length) {
|
||||
inspiration += ` (labels: ${entityEntry.labels
|
||||
.map((label) => labels[label])
|
||||
.join(", ")})`;
|
||||
}
|
||||
|
||||
inspirations.push(inspiration);
|
||||
}
|
||||
|
||||
return inspirations;
|
||||
}
|
||||
// Always English to format lists in the prompt
|
||||
const PROMPT_LIST_FORMAT = new Intl.ListFormat("en", {
|
||||
style: "long",
|
||||
type: "conjunction",
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates an AI task for suggesting metadata
|
||||
* for automations or scripts based on their configuration.
|
||||
* Generates an AI task for suggesting metadata based on their configuration.
|
||||
*
|
||||
* @param connection - Home Assistant connection
|
||||
* @param states - Current state objects
|
||||
* @param language - User's language preference
|
||||
* @param domain - The domain to suggest metadata for (automation, script)
|
||||
* @param domain - The domain to suggest metadata for
|
||||
* @param config - The configuration to suggest metadata for
|
||||
* @param inspirations - Existing entries to use as inspiration
|
||||
* @param include - The metadata fields to include in the suggestion
|
||||
* @returns Promise resolving to the AI task structure
|
||||
*/
|
||||
export async function generateMetadataSuggestionTask<T>(
|
||||
connection: HomeAssistant["connection"],
|
||||
states: HomeAssistant["states"],
|
||||
language: HomeAssistant["language"],
|
||||
domain: MetadataSuggestionDomain,
|
||||
config: T,
|
||||
include = SUGGESTION_INCLUDE_ALL
|
||||
inspirations: string[] = [],
|
||||
include = SUGGESTION_INCLUDE_DEFAULT
|
||||
): Promise<SuggestWithAIGenerateTask> {
|
||||
const [categories, entities, labels] = await Promise.all([
|
||||
const [categories, floors] = await Promise.all([
|
||||
include.categories
|
||||
? fetchCategories(connection, domain)
|
||||
: Promise.resolve(undefined),
|
||||
fetchEntities(connection),
|
||||
include.labels ? fetchLabels(connection) : Promise.resolve(undefined),
|
||||
include.floor ? fetchFloors(connection) : Promise.resolve(undefined),
|
||||
]);
|
||||
|
||||
const inspirations = buildMetadataInspirations(
|
||||
domain,
|
||||
states,
|
||||
entities,
|
||||
categories,
|
||||
labels
|
||||
);
|
||||
|
||||
const structure: AITaskStructure = {
|
||||
name: {
|
||||
description: `The name of the ${domain}`,
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
...(include.name && {
|
||||
name: {
|
||||
description: `The name of the ${domain}`,
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(include.description && {
|
||||
description: {
|
||||
description: `A short description of the ${domain}`,
|
||||
@@ -193,49 +114,83 @@ export async function generateMetadataSuggestionTask<T>(
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(include.floor &&
|
||||
floors && {
|
||||
floor: {
|
||||
description: `The floor of the ${domain}`,
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: Object.values(floors).map((floor) => ({
|
||||
value: floor.floor_id,
|
||||
label: floor.name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const categoryLabelText: string[] = [];
|
||||
if (include.categories) {
|
||||
categoryLabelText.push("category");
|
||||
}
|
||||
if (include.labels) {
|
||||
categoryLabelText.push("labels");
|
||||
}
|
||||
const categoryLabelString =
|
||||
categoryLabelText.length > 0 ? `, ${categoryLabelText.join(" and ")}` : "";
|
||||
const requestedParts = [
|
||||
include.name ? "a name" : null,
|
||||
include.description ? "a description" : null,
|
||||
include.categories ? "a category" : null,
|
||||
include.labels ? "labels" : null,
|
||||
include.floor ? "a floor" : null,
|
||||
].filter((entry): entry is string => entry !== null);
|
||||
|
||||
const categoryLabels: string[] = [
|
||||
include.categories ? "category" : null,
|
||||
include.labels ? "labels" : null,
|
||||
include.floor ? "floor" : null,
|
||||
].filter((entry): entry is string => entry !== null);
|
||||
|
||||
const categoryLabelsText = PROMPT_LIST_FORMAT.format(categoryLabels);
|
||||
|
||||
const requestedPartsText = requestedParts.length
|
||||
? PROMPT_LIST_FORMAT.format(requestedParts)
|
||||
: "suggestions";
|
||||
|
||||
return {
|
||||
type: "data",
|
||||
task: {
|
||||
task_name: `frontend__${domain}__save`,
|
||||
instructions: `Suggest in language "${language}" a name${include.description ? ", description" : ""}${categoryLabelString} for the following Home Assistant ${domain}.
|
||||
|
||||
The name should be relevant to the ${domain}'s purpose.
|
||||
${
|
||||
inspirations.length
|
||||
? `The name should be in same style and sentence capitalization as existing ${domain}s.${
|
||||
include.categories || include.labels
|
||||
? `
|
||||
Suggest ${categoryLabelText.join(" and ")} if relevant to the ${domain}'s purpose.
|
||||
Only suggest ${categoryLabelText.join(" and ")} that are already used by existing ${domain}s.`
|
||||
: ""
|
||||
}`
|
||||
: `The name should be short, descriptive, sentence case, and written in the language ${language}.`
|
||||
}${
|
||||
include.description
|
||||
? `
|
||||
If the ${domain} contains 5+ steps, include a short description.`
|
||||
: ""
|
||||
}
|
||||
|
||||
For inspiration, here are existing ${domain}s:
|
||||
${inspirations.join("\n")}
|
||||
|
||||
The ${domain} configuration is as follows:
|
||||
|
||||
${dump(config)}
|
||||
`,
|
||||
instructions: [
|
||||
`Suggest in language "${language}" ${requestedPartsText} for the following Home Assistant ${domain}.`,
|
||||
"",
|
||||
include.name
|
||||
? `The name should be relevant to the ${domain}'s purpose.`
|
||||
: `The suggestions should be relevant to the ${domain}'s purpose.`,
|
||||
...(inspirations.length
|
||||
? [
|
||||
...(include.name
|
||||
? [
|
||||
`The name should be in same style and sentence capitalization as existing ${domain}s.`,
|
||||
]
|
||||
: []),
|
||||
...(include.categories || include.labels || include.floor
|
||||
? [
|
||||
`Suggest ${categoryLabelsText} if relevant to the ${domain}'s purpose.`,
|
||||
`Only suggest ${categoryLabelsText} that are already used by existing ${domain}s.`,
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: include.name
|
||||
? [
|
||||
`The name should be short, descriptive, sentence case, and written in the language ${language}.`,
|
||||
]
|
||||
: []),
|
||||
...(include.description
|
||||
? [`If the ${domain} contains 5+ steps, include a short description.`]
|
||||
: []),
|
||||
"",
|
||||
`For inspiration, here are existing ${domain}s:`,
|
||||
inspirations.join("\n"),
|
||||
"",
|
||||
`The ${domain} configuration is as follows:`,
|
||||
"",
|
||||
`${dump(config)}`,
|
||||
].join("\n"),
|
||||
structure,
|
||||
},
|
||||
};
|
||||
@@ -243,7 +198,7 @@ ${dump(config)}
|
||||
|
||||
/**
|
||||
* Processes the result of an AI task for suggesting metadata
|
||||
* for automations or scripts based on their configuration.
|
||||
* based on their configuration.
|
||||
*
|
||||
* @param connection - Home Assistant connection
|
||||
* @param domain - The domain of the ${domain}
|
||||
@@ -255,17 +210,18 @@ export async function processMetadataSuggestion(
|
||||
connection: HomeAssistant["connection"],
|
||||
domain: MetadataSuggestionDomain,
|
||||
result: GenDataTaskResult<MetadataSuggestionResult>,
|
||||
include: MetadataSuggestionInclude
|
||||
include = SUGGESTION_INCLUDE_DEFAULT
|
||||
): Promise<MetadataSuggestionResult> {
|
||||
const [categories, labels] = await Promise.all([
|
||||
const [categories, labels, floors] = await Promise.all([
|
||||
include.categories
|
||||
? fetchCategories(connection, domain)
|
||||
: Promise.resolve(undefined),
|
||||
include.labels ? fetchLabels(connection) : Promise.resolve(undefined),
|
||||
include.floor ? fetchFloors(connection) : Promise.resolve(undefined),
|
||||
]);
|
||||
|
||||
const processed: MetadataSuggestionResult = {
|
||||
name: result.data.name,
|
||||
name: include.name ? result.data.name : undefined,
|
||||
description: include.description ? result.data.description : undefined,
|
||||
};
|
||||
|
||||
@@ -302,5 +258,17 @@ export async function processMetadataSuggestion(
|
||||
}
|
||||
}
|
||||
|
||||
if (include.floor && floors && result.data.floor) {
|
||||
const floorId =
|
||||
result.data.floor in floors
|
||||
? result.data.floor
|
||||
: Object.entries(floors).find(
|
||||
([, floor]) => floor.name === result.data.floor
|
||||
)?.[0];
|
||||
if (floorId) {
|
||||
processed.floor = floorId;
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
72
src/panels/config/common/suggest-metadata-helpers.ts
Normal file
72
src/panels/config/common/suggest-metadata-helpers.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import { subscribeAreaRegistry } from "../../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import { fetchCategoryRegistry } from "../../../data/category_registry";
|
||||
import {
|
||||
subscribeEntityRegistry,
|
||||
type EntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { subscribeFloorRegistry } from "../../../data/ws-floor_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import { subscribeLabelRegistry } from "../../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { MetadataSuggestionDomain } from "./suggest-metadata-ai";
|
||||
|
||||
export type Categories = Record<string, string>;
|
||||
export type Entities = Record<string, EntityRegistryEntry>;
|
||||
export type Labels = Record<string, string>;
|
||||
export type Floors = Record<string, FloorRegistryEntry>;
|
||||
export type Areas = Record<string, AreaRegistryEntry>;
|
||||
|
||||
const tryCatchEmptyObject = <T>(promise: Promise<T>): Promise<T> =>
|
||||
promise.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching data for suggestion: ", err);
|
||||
return {} as T;
|
||||
});
|
||||
|
||||
export const fetchCategories = (
|
||||
connection: HomeAssistant["connection"],
|
||||
domain: MetadataSuggestionDomain
|
||||
): Promise<Categories> =>
|
||||
tryCatchEmptyObject<Categories>(
|
||||
fetchCategoryRegistry(connection, domain).then((cats) =>
|
||||
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
|
||||
)
|
||||
);
|
||||
|
||||
export const fetchLabels = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Labels> =>
|
||||
tryCatchEmptyObject<Labels>(
|
||||
subscribeOne(connection, subscribeLabelRegistry).then((labs) =>
|
||||
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
||||
)
|
||||
);
|
||||
|
||||
export const fetchFloors = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Floors> =>
|
||||
tryCatchEmptyObject<Floors>(
|
||||
subscribeOne(connection, subscribeFloorRegistry).then((floors) =>
|
||||
Object.fromEntries(floors.map((floor) => [floor.floor_id, floor]))
|
||||
)
|
||||
);
|
||||
|
||||
export const fetchAreas = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Areas> =>
|
||||
tryCatchEmptyObject<Areas>(
|
||||
subscribeOne(connection, subscribeAreaRegistry).then((areas) =>
|
||||
Object.fromEntries(areas.map((area) => [area.area_id, area]))
|
||||
)
|
||||
);
|
||||
|
||||
export const fetchEntities = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Entities> =>
|
||||
tryCatchEmptyObject<Entities>(
|
||||
subscribeOne(connection, subscribeEntityRegistry).then((ents) =>
|
||||
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
|
||||
)
|
||||
);
|
||||
82
src/panels/config/common/suggest-metadata-inspirations.ts
Normal file
82
src/panels/config/common/suggest-metadata-inspirations.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { MetadataSuggestionDomain } from "./suggest-metadata-ai";
|
||||
import {
|
||||
fetchAreas,
|
||||
fetchCategories,
|
||||
fetchEntities,
|
||||
fetchFloors,
|
||||
fetchLabels,
|
||||
} from "./suggest-metadata-helpers";
|
||||
|
||||
export const buildEntityMetadataInspirations = async (
|
||||
connection: HomeAssistant["connection"],
|
||||
states: HomeAssistant["states"],
|
||||
domain: MetadataSuggestionDomain
|
||||
): Promise<string[]> => {
|
||||
const [categories, entities, labels] = await Promise.all([
|
||||
fetchCategories(connection, domain),
|
||||
fetchEntities(connection),
|
||||
fetchLabels(connection),
|
||||
]);
|
||||
|
||||
return Object.values(entities).reduce<string[]>((inspirations, entry) => {
|
||||
if (!entry || computeDomain(entry.entity_id) !== domain) {
|
||||
return inspirations;
|
||||
}
|
||||
|
||||
const entity = states[entry.entity_id];
|
||||
if (
|
||||
!entity ||
|
||||
entity.attributes.restored ||
|
||||
!entity.attributes.friendly_name
|
||||
) {
|
||||
return inspirations;
|
||||
}
|
||||
|
||||
let inspiration = `- ${entity.attributes.friendly_name}`;
|
||||
|
||||
const category = entry.categories[domain];
|
||||
if (category && categories[category]) {
|
||||
inspiration += ` (category: ${categories[category]})`;
|
||||
}
|
||||
|
||||
if (entry.labels.length) {
|
||||
const labelNames = entry.labels
|
||||
.map((labelId) => labels[labelId])
|
||||
.filter(Boolean);
|
||||
if (labelNames.length) {
|
||||
inspiration += ` (labels: ${labelNames.join(", ")})`;
|
||||
}
|
||||
}
|
||||
|
||||
inspirations.push(inspiration);
|
||||
return inspirations;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const buildAreaMetadataInspirations = async (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<string[]> => {
|
||||
const [labels, floors, areas] = await Promise.all([
|
||||
fetchLabels(connection),
|
||||
fetchFloors(connection),
|
||||
fetchAreas(connection),
|
||||
]);
|
||||
|
||||
return Object.values(areas).reduce<string[]>((inspirations, area) => {
|
||||
if (!area.floor_id) {
|
||||
return inspirations;
|
||||
}
|
||||
|
||||
const floorName = floors[area.floor_id]?.name;
|
||||
const labelNames = area.labels
|
||||
.map((labelId) => labels[labelId])
|
||||
.filter(Boolean);
|
||||
|
||||
inspirations.push(
|
||||
`- ${area.name} (${floorName ? `floor: ${floorName}` : "no floor"}${labelNames.length ? `, labels: ${labelNames.join(", ")}` : ""})`
|
||||
);
|
||||
return inspirations;
|
||||
}, []);
|
||||
};
|
||||
@@ -33,9 +33,11 @@ import {
|
||||
generateMetadataSuggestionTask,
|
||||
processMetadataSuggestion,
|
||||
} from "../../common/suggest-metadata-ai";
|
||||
import { buildEntityMetadataInspirations } from "../../common/suggest-metadata-inspirations";
|
||||
import type { SceneConfig } from "../../../../data/scene";
|
||||
|
||||
const SUGGESTION_CONFIG: MetadataSuggestionInclude = {
|
||||
description: false,
|
||||
const SUGGESTION_INCLUDE: MetadataSuggestionInclude = {
|
||||
name: true,
|
||||
categories: true,
|
||||
labels: true,
|
||||
};
|
||||
@@ -281,13 +283,17 @@ class DialogSceneSave extends LitElement {
|
||||
}
|
||||
|
||||
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> =>
|
||||
generateMetadataSuggestionTask(
|
||||
generateMetadataSuggestionTask<SceneConfig>(
|
||||
this.hass.connection,
|
||||
this.hass.states,
|
||||
this.hass.language,
|
||||
"scene",
|
||||
this._params.config,
|
||||
SUGGESTION_CONFIG
|
||||
await buildEntityMetadataInspirations(
|
||||
this.hass.connection,
|
||||
this.hass.states,
|
||||
"scene"
|
||||
),
|
||||
SUGGESTION_INCLUDE
|
||||
);
|
||||
|
||||
private async _handleSuggestion(
|
||||
@@ -298,12 +304,14 @@ class DialogSceneSave extends LitElement {
|
||||
this.hass.connection,
|
||||
"scene",
|
||||
result,
|
||||
SUGGESTION_CONFIG
|
||||
SUGGESTION_INCLUDE
|
||||
);
|
||||
|
||||
this._newName = processed.name;
|
||||
if (this._error && this._newName.trim()) {
|
||||
this._error = false;
|
||||
if (processed.name) {
|
||||
this._newName = processed.name;
|
||||
if (this._error && this._newName.trim()) {
|
||||
this._error = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (processed.category) {
|
||||
|
||||
Reference in New Issue
Block a user