Allow to change device names during config flow (#25142)

* Allow to change device names during config flow

* Update zha-device-card.ts

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
This commit is contained in:
Bram Kragten 2025-04-25 09:04:00 +02:00 committed by GitHub
parent 0229f67751
commit 221e1d9ed8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 57 deletions

View File

@ -1,4 +1,4 @@
import { html } from "lit"; import { html, nothing } from "lit";
import { import {
createConfigFlow, createConfigFlow,
deleteConfigFlow, deleteConfigFlow,
@ -194,13 +194,7 @@ export const showConfigFlowDialog = (
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
: ""} : nothing}
<p>
${hass.localize(
"ui.panel.config.integrations.config_flow.created_config",
{ name: step.title }
)}
</p>
`; `;
}, },

View File

@ -1,4 +1,4 @@
import { html } from "lit"; import { html, nothing } from "lit";
import type { ConfigEntry } from "../../data/config_entries"; import type { ConfigEntry } from "../../data/config_entries";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { import {
@ -200,13 +200,7 @@ export const showSubConfigFlowDialog = (
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
: ""} : nothing}
<p>
${hass.localize(
"ui.panel.config.integrations.config_flow.created_config",
{ name: step.title }
)}
</p>
`; `;
}, },

View File

@ -1,10 +1,13 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name"; import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import "../../components/ha-area-picker"; import "../../components/ha-area-picker";
@ -18,6 +21,8 @@ import { showAlertDialog } from "../generic/show-dialog-box";
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog"; import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
import type { FlowConfig } from "./show-dialog-data-entry-flow"; import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
import { brandsUrl } from "../../util/brands-url";
import { domainToName } from "../../data/integration";
@customElement("step-flow-create-entry") @customElement("step-flow-create-entry")
class StepFlowCreateEntry extends LitElement { class StepFlowCreateEntry extends LitElement {
@ -27,7 +32,12 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry; @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
navigateToResult = false; public navigateToResult = false;
@state() private _deviceUpdate: Record<
string,
{ name?: string; area?: string }
> = {};
private _devices = memoizeOne( private _devices = memoizeOne(
( (
@ -99,7 +109,13 @@ class StepFlowCreateEntry extends LitElement {
this.step.result?.entry_id this.step.result?.entry_id
); );
return html` return html`
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2> <h2>
${devices.length
? localize("ui.panel.config.integrations.config_flow.assign_area", {
number: devices.length,
})
: `${localize("ui.panel.config.integrations.config_flow.success")}!`}
</h2>
<div class="content"> <div class="content">
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)} ${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
${this.step.result?.state === "not_loaded" ${this.step.result?.state === "not_loaded"
@ -110,31 +126,62 @@ class StepFlowCreateEntry extends LitElement {
>` >`
: nothing} : nothing}
${devices.length === 0 ${devices.length === 0
? nothing ? html`<p>
: html`
<p>
${localize( ${localize(
"ui.panel.config.integrations.config_flow.found_following_devices" "ui.panel.config.integrations.config_flow.created_config",
)}: { name: this.step.title }
</p> )}
</p>`
: html`
<div class="devices"> <div class="devices">
${devices.map( ${devices.map(
(device) => html` (device) => html`
<div class="device"> <div class="device">
<div> <div class="device-info">
<b>${computeDeviceNameDisplay(device, this.hass)}</b ${this.step.result?.domain
><br /> ? html`<img
${!device.model && !device.manufacturer slot="graphic"
? html`&nbsp;` alt=${domainToName(
: html`${device.model} this.hass.localize,
${device.manufacturer this.step.result.domain
? html`(${device.manufacturer})` )}
: ""}`} src=${brandsUrl({
domain: this.step.result.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`
: nothing}
<div class="device-info-details">
<span>${device.model || device.manufacturer}</span>
${device.model
? html`<span class="secondary">
${device.manufacturer}
</span>`
: nothing}
</div> </div>
</div>
<ha-textfield
.label=${localize(
"ui.panel.config.integrations.config_flow.device_name"
)}
.placeholder=${computeDeviceNameDisplay(
device,
this.hass
)}
.value=${this._deviceUpdate[device.id]?.name ??
computeDeviceName(device)}
@change=${this._deviceNameChanged}
.device=${device.id}
></ha-textfield>
<ha-area-picker <ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.device=${device.id} .device=${device.id}
.value=${device.area_id ?? undefined} .value=${this._deviceUpdate[device.id]?.area ??
device.area_id ??
undefined}
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker> ></ha-area-picker>
</div> </div>
@ -146,14 +193,32 @@ class StepFlowCreateEntry extends LitElement {
<div class="buttons"> <div class="buttons">
<mwc-button @click=${this._flowDone} <mwc-button @click=${this._flowDone}
>${localize( >${localize(
"ui.panel.config.integrations.config_flow.finish" `ui.panel.config.integrations.config_flow.${!devices.length || Object.keys(this._deviceUpdate).length ? "finish" : "finish_skip"}`
)}</mwc-button )}</mwc-button
> >
</div> </div>
`; `;
} }
private _flowDone(): void { private async _flowDone(): Promise<void> {
if (Object.keys(this._deviceUpdate).length) {
await Promise.all(
Object.entries(this._deviceUpdate).map(([deviceId, update]) =>
updateDeviceRegistryEntry(this.hass, deviceId, {
name_by_user: update.name,
area_id: update.area,
}).catch((err: any) => {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_device",
{ error: err.message }
),
});
})
)
);
}
fireEvent(this, "flow-update", { step: undefined }); fireEvent(this, "flow-update", { step: undefined });
if (this.step.result && this.navigateToResult) { if (this.step.result && this.navigateToResult) {
navigate( navigate(
@ -165,21 +230,25 @@ class StepFlowCreateEntry extends LitElement {
private async _areaPicked(ev: CustomEvent) { private async _areaPicked(ev: CustomEvent) {
const picker = ev.currentTarget as any; const picker = ev.currentTarget as any;
const device = picker.device; const device = picker.device;
const area = ev.detail.value; const area = ev.detail.value;
try {
await updateDeviceRegistryEntry(this.hass, device, { if (!(device in this._deviceUpdate)) {
area_id: area, this._deviceUpdate[device] = {};
});
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_area",
{ error: err.message }
),
});
picker.value = null;
} }
this._deviceUpdate[device].area = area;
this.requestUpdate("_deviceUpdate");
}
private _deviceNameChanged(ev): void {
const picker = ev.currentTarget as any;
const device = picker.device;
const name = picker.value;
if (!(device in this._deviceUpdate)) {
this._deviceUpdate[device] = {};
}
this._deviceUpdate[device].name = name;
this.requestUpdate("_deviceUpdate");
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@ -188,18 +257,41 @@ class StepFlowCreateEntry extends LitElement {
css` css`
.devices { .devices {
display: flex; display: flex;
flex-wrap: wrap;
margin: -4px; margin: -4px;
max-height: 600px; max-height: 600px;
overflow-y: auto; overflow-y: auto;
flex-direction: column;
} }
.device { .device {
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
padding: 5px; padding: 6px;
border-radius: 4px; border-radius: 4px;
margin: 4px; margin: 4px;
display: inline-block; display: inline-block;
width: 250px; }
.device-info {
display: flex;
align-items: center;
gap: 8px;
}
.device-info img {
width: 40px;
height: 40px;
}
.device-info-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.secondary {
color: var(--secondary-text-color);
}
ha-textfield,
ha-area-picker {
display: block;
}
ha-textfield {
margin: 8px 0;
} }
.buttons > *:last-child { .buttons > *:last-child {
margin-left: auto; margin-left: auto;

View File

@ -189,7 +189,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_area", "ui.panel.config.integrations.config_flow.error_saving_device",
{ error: err.message } { error: err.message }
), ),
}); });

View File

@ -5304,19 +5304,21 @@
}, },
"config_flow": { "config_flow": {
"success": "Success", "success": "Success",
"assign_area": "Assign {number, plural,\n one {device}\n other {devices}\n} to area",
"device_name": "Device name",
"aborted": "Aborted", "aborted": "Aborted",
"close": "Close", "close": "Close",
"finish": "Finish", "finish": "Finish",
"finish_skip": "Skip and finish",
"submit": "Submit", "submit": "Submit",
"next": "Next", "next": "Next",
"preview": "Preview", "preview": "Preview",
"found_following_devices": "We found the following devices",
"yaml_only_title": "This integration cannot be added from the UI", "yaml_only_title": "This integration cannot be added from the UI",
"yaml_only": "You can add this integration by adding it to your ''configuration.yaml''. See the documentation for more information.", "yaml_only": "You can add this integration by adding it to your ''configuration.yaml''. See the documentation for more information.",
"open_documentation": "Open documentation", "open_documentation": "Open documentation",
"no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.", "no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.",
"not_all_required_fields": "Not all required fields are filled in.", "not_all_required_fields": "Not all required fields are filled in.",
"error_saving_area": "Error saving area: {error}", "error_saving_device": "Error updating device: {error}",
"created_config": "Created configuration for {name}.", "created_config": "Created configuration for {name}.",
"external_step": { "external_step": {
"description": "This step requires you to visit an external website to be completed.", "description": "This step requires you to visit an external website to be completed.",