Allow to change switch as x (#16671)

This commit is contained in:
Bram Kragten 2023-05-30 21:32:06 +02:00 committed by GitHub
parent 0771a780d9
commit 7e5a85dbe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 223 additions and 79 deletions

View File

@ -95,12 +95,17 @@ export interface WeatherEntityOptions {
wind_speed_unit?: string | null; wind_speed_unit?: string | null;
} }
export interface SwitchAsXEntityOptions {
entity_id: string;
}
export interface EntityRegistryOptions { export interface EntityRegistryOptions {
number?: NumberEntityOptions; number?: NumberEntityOptions;
sensor?: SensorEntityOptions; sensor?: SensorEntityOptions;
lock?: LockEntityOptions; lock?: LockEntityOptions;
weather?: WeatherEntityOptions; weather?: WeatherEntityOptions;
light?: LightEntityOptions; light?: LightEntityOptions;
switch_as_x?: SwitchAsXEntityOptions;
conversation?: Record<string, unknown>; conversation?: Record<string, unknown>;
"cloud.alexa"?: Record<string, unknown>; "cloud.alexa"?: Record<string, unknown>;
"cloud.google_assistant"?: Record<string, unknown>; "cloud.google_assistant"?: Record<string, unknown>;

View File

@ -16,6 +16,7 @@ import {
} from "../../../../../data/entity_registry"; } from "../../../../../data/entity_registry";
import { HELPERS_CRUD } from "../../../../../data/helpers_crud"; import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { hideMoreInfoDialog } from "../../../../../dialogs/more-info/show-ha-more-info-dialog";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import type { Helper } from "../../../helpers/const"; import type { Helper } from "../../../helpers/const";
@ -148,8 +149,10 @@ export class EntityRegistrySettingsHelper extends LitElement {
this._item this._item
); );
} }
await this._registryEditor!.updateEntry(); const result = await this._registryEditor!.updateEntry();
fireEvent(this, "close-dialog"); if (result.close) {
hideMoreInfoDialog(this);
}
} catch (err: any) { } catch (err: any) {
this._error = err.message || "Unknown error"; this._error = err.message || "Unknown error";
} finally { } finally {

View File

@ -43,7 +43,7 @@ import {
STREAM_TYPE_HLS, STREAM_TYPE_HLS,
updateCameraPrefs, updateCameraPrefs,
} from "../../../data/camera"; } from "../../../data/camera";
import { ConfigEntry } from "../../../data/config_entries"; import { ConfigEntry, deleteConfigEntry } from "../../../data/config_entries";
import { import {
createConfigFlow, createConfigFlow,
handleConfigFlowStep, handleConfigFlowStep,
@ -57,10 +57,10 @@ import {
EntityRegistryEntry, EntityRegistryEntry,
EntityRegistryEntryUpdateParams, EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
fetchEntityRegistry,
SensorEntityOptions,
updateEntityRegistryEntry,
LockEntityOptions, LockEntityOptions,
SensorEntityOptions,
subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number"; import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
@ -307,6 +307,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._weatherConvertibleUnits = undefined; this._weatherConvertibleUnits = undefined;
} }
} }
if (changedProps.has("helperConfigEntry")) {
if (this.helperConfigEntry?.domain === "switch_as_x") {
this._switchAs = computeDomain(this.entry.entity_id);
} else {
this._switchAs = "switch";
}
}
} }
protected render() { protected render() {
@ -366,6 +373,74 @@ export class EntityRegistrySettingsEditor extends LitElement {
: undefined} : undefined}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-icon-picker>`} ></ha-icon-picker>`}
${domain === "switch"
? html`<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
naturalMenuWidth
fixedMenuPosition
@selected=${this._switchAsChanged}
@closed=${stopPropagation}
>
<ha-list-item
value="switch"
.selected=${!this._deviceClass || this._deviceClass === "switch"}
>
${domainToName(this.hass.localize, "switch")}
</ha-list-item>
<ha-list-item
value="outlet"
.selected=${this._deviceClass === "outlet"}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
)}
</ha-list-item>
<li divider role="separator"></li>
${this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
).map(
(entry) => html`
<ha-list-item .value=${entry.domain}>
${entry.label}
</ha-list-item>
`
)}
</ha-select>`
: this.helperConfigEntry?.domain === "switch_as_x"
? html`<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.switch_as_x"
)}
.value=${this._switchAs}
naturalMenuWidth
fixedMenuPosition
@selected=${this._switchAsChanged}
@closed=${stopPropagation}
>
<ha-list-item value="switch">
${domainToName(this.hass.localize, "switch")}
</ha-list-item>
<ha-list-item .value=${domain}>
${domainToName(this.hass.localize, domain)}
</ha-list-item>
<li divider role="separator"></li>
${this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
).map((entry) =>
domain === entry.domain
? nothing
: html`
<ha-list-item .value=${entry.domain}>
${entry.label}
</ha-list-item>
`
)}
</ha-select>`
: nothing}
${this._deviceClassOptions ${this._deviceClassOptions
? html` ? html`
<ha-select <ha-select
@ -599,45 +674,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
</ha-select> </ha-select>
` `
: ""} : ""}
${domain === "switch"
? html`<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
naturalMenuWidth
fixedMenuPosition
@selected=${this._switchAsChanged}
@closed=${stopPropagation}
>
<ha-list-item
value="switch"
.selected=${!this._deviceClass || this._deviceClass === "switch"}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_classes.switch.switch"
)}
</ha-list-item>
<ha-list-item
value="outlet"
.selected=${this._deviceClass === "outlet"}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
)}
</ha-list-item>
<li divider role="separator"></li>
${this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
).map(
(entry) => html`
<ha-list-item .value=${entry.domain}>
${entry.label}
</ha-list-item>
`
)}
</ha-select>`
: ""}
<ha-textfield <ha-textfield
error-message="Domain needs to stay the same" error-message="Domain needs to stay the same"
.value=${this._entityId} .value=${this._entityId}
@ -880,8 +916,16 @@ export class EntityRegistrySettingsEditor extends LitElement {
`; `;
} }
public async updateEntry(): Promise<ExtEntityRegistryEntry> { public async updateEntry(): Promise<{
const parent = (this.getRootNode() as ShadowRoot).host as HTMLElement; close: boolean;
entry: ExtEntityRegistryEntry;
}> {
let close = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
let parent: HTMLElement = this;
while (parent?.localName !== "home-assistant") {
parent = (parent.getRootNode() as ShadowRoot).host as HTMLElement;
}
const params: Partial<EntityRegistryEntryUpdateParams> = { const params: Partial<EntityRegistryEntryUpdateParams> = {
name: this._name.trim() || null, name: this._name.trim() || null,
@ -979,13 +1023,18 @@ export class EntityRegistrySettingsEditor extends LitElement {
}); });
} }
if (this._switchAs !== "switch") { if (domain === "switch" && this._switchAs !== "switch") {
// generate config flow for switch_as_x
if ( if (
await showConfirmationDialog(this, { await showConfirmationDialog(this, {
text: this.hass!.localize( text: this.hass!.localize(
"ui.dialogs.entity_registry.editor.switch_as_x_confirm", "ui.dialogs.entity_registry.editor.switch_as_x_confirm",
"domain", {
domain: domainToName(
this.hass.localize,
this._switchAs this._switchAs
).toLowerCase(),
}
), ),
}) })
) { ) {
@ -999,24 +1048,107 @@ export class EntityRegistrySettingsEditor extends LitElement {
} }
)) as DataEntryFlowStepCreateEntry; )) as DataEntryFlowStepCreateEntry;
if (configFlowResult.result?.entry_id) { if (configFlowResult.result?.entry_id) {
const unsub = await this.hass.connection.subscribeEvents(() => { try {
unsub(); const entry = await this._waitForEntityRegistryUpdate(
fetchEntityRegistry(this.hass.connection).then((entityRegistry) => { configFlowResult.result.entry_id
const entity = entityRegistry.find(
(reg) =>
reg.config_entry_id === configFlowResult.result!.entry_id
); );
if (!entity) { showMoreInfoDialog(parent, { entityId: entry.entity_id });
return; close = false;
} catch (err) {
// ignore
}
}
}
} else if (
this.helperConfigEntry?.domain === "switch_as_x" &&
this._switchAs !== domain
) {
// change a current switch as x to something else
if (
await showConfirmationDialog(this, {
text:
this._switchAs === "switch"
? this.hass!.localize(
"ui.dialogs.entity_registry.editor.switch_as_x_remove_confirm",
{
domain: domainToName(
this.hass.localize,
domain
).toLowerCase(),
}
)
: this.hass!.localize(
"ui.dialogs.entity_registry.editor.switch_as_x_change_confirm",
{
domain_1: domainToName(
this.hass.localize,
domain
).toLowerCase(),
domain_2: domainToName(
this.hass.localize,
this._switchAs
).toLowerCase(),
}
),
})
) {
const origEntityId = this.entry.options?.switch_as_x?.entity_id;
// remove current helper
await deleteConfigEntry(this.hass, this.helperConfigEntry.entry_id);
if (!origEntityId) {
// should not happen, guard for types
} else if (this._switchAs === "switch") {
// done, original switch is back
showMoreInfoDialog(parent, { entityId: origEntityId });
close = false;
} else {
const configFlow = await createConfigFlow(this.hass, "switch_as_x");
const configFlowResult = (await handleConfigFlowStep(
this.hass,
configFlow.flow_id,
{
entity_id: origEntityId,
target_domain: this._switchAs,
}
)) as DataEntryFlowStepCreateEntry;
if (configFlowResult.result?.entry_id) {
try {
const entry = await this._waitForEntityRegistryUpdate(
configFlowResult.result.entry_id
);
showMoreInfoDialog(parent, { entityId: entry.entity_id });
close = false;
} catch (err) {
// ignore
}
} }
showMoreInfoDialog(parent, { entityId: entity.entity_id });
});
}, "entity_registry_updated");
} }
} }
} }
return result.entity_entry; return { close, entry: result.entity_entry };
}
private async _waitForEntityRegistryUpdate(config_entry_id: string) {
return new Promise<EntityRegistryEntry>((resolve, reject) => {
const timeout = setTimeout(reject, 5000);
const unsub = subscribeEntityRegistry(
this.hass.connection,
(entityRegistry) => {
const entity = entityRegistry.find(
(reg) => reg.config_entry_id === config_entry_id
);
if (entity) {
clearTimeout(timeout);
unsub();
resolve(entity);
}
}
);
// @ts-ignore Force refresh
this.hass.connection._entityRegistry?.refresh();
});
} }
private _nameChanged(ev): void { private _nameChanged(ev): void {
@ -1090,7 +1222,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
const switchAs = ev.target.value === "outlet" ? "switch" : ev.target.value; const switchAs = ev.target.value === "outlet" ? "switch" : ev.target.value;
this._switchAs = switchAs; this._switchAs = switchAs;
if (ev.target.value === "outlet" || ev.target.value === "switch") { if (
(computeDomain(this.entry.entity_id) === "switch" &&
ev.target.value === "outlet") ||
ev.target.value === "switch"
) {
this._deviceClass = ev.target.value; this._deviceClass = ev.target.value;
} }
} }
@ -1177,9 +1313,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
private _switchAsDomainsSorted = memoizeOne( private _switchAsDomainsSorted = memoizeOne(
(domains: string[], localize: LocalizeFunc) => (domains: string[], localize: LocalizeFunc) =>
domains domains
.map((entry) => ({ .map((domain) => ({
domain: entry, domain,
label: domainToName(localize, entry), label: domainToName(localize, domain),
})) }))
.sort((a, b) => .sort((a, b) =>
stringCompare(a.label, b.label, this.hass.locale.language) stringCompare(a.label, b.label, this.hass.locale.language)

View File

@ -9,7 +9,7 @@ import "../../../components/ha-alert";
import { import {
ConfigEntry, ConfigEntry,
deleteConfigEntry, deleteConfigEntry,
getConfigEntries, getConfigEntry,
} from "../../../data/config_entries"; } from "../../../data/config_entries";
import { updateDeviceRegistryEntry } from "../../../data/device_registry"; import { updateDeviceRegistryEntry } from "../../../data/device_registry";
import { import {
@ -21,6 +21,7 @@ import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import { hideMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
@ -48,13 +49,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
protected firstUpdated(changedProps: PropertyValues): void { protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.entry.config_entry_id) { if (this.entry.config_entry_id) {
getConfigEntries(this.hass, { getConfigEntry(this.hass, this.entry.config_entry_id).then((entry) => {
type: ["helper"], this._helperConfigEntry = entry.config_entry;
domain: this.entry.platform,
}).then((entries) => {
this._helperConfigEntry = entries.find(
(ent) => ent.entry_id === this.entry.config_entry_id
);
}); });
} }
} }
@ -183,8 +179,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
private async _updateEntry(): Promise<void> { private async _updateEntry(): Promise<void> {
this._submitting = true; this._submitting = true;
try { try {
await this._registryEditor!.updateEntry(); const result = await this._registryEditor!.updateEntry();
fireEvent(this, "close-dialog"); if (result.close) {
hideMoreInfoDialog(this);
}
} catch (err: any) { } catch (err: any) {
this._error = err.message || "Unknown error"; this._error = err.message || "Unknown error";
} finally { } finally {

View File

@ -1002,6 +1002,7 @@
"visibility_unit": "Visibility unit", "visibility_unit": "Visibility unit",
"wind_speed_unit": "Wind speed unit", "wind_speed_unit": "Wind speed unit",
"device_class": "Show as", "device_class": "Show as",
"switch_as_x": "Show switch as",
"device_classes": { "device_classes": {
"binary_sensor": { "binary_sensor": {
"door": "Door", "door": "Door",
@ -1046,8 +1047,7 @@
"shutter": "Shutter" "shutter": "Shutter"
}, },
"switch": { "switch": {
"outlet": "Outlet", "outlet": "Outlet"
"switch": "Switch"
} }
}, },
"unavailable": "This entity is unavailable.", "unavailable": "This entity is unavailable.",
@ -1063,6 +1063,8 @@
"enable_entity": "Enable", "enable_entity": "Enable",
"open_device_settings": "Open device settings", "open_device_settings": "Open device settings",
"switch_as_x_confirm": "This switch will be hidden and a new {domain} will be added. Your existing configurations using the switch will continue to work.", "switch_as_x_confirm": "This switch will be hidden and a new {domain} will be added. Your existing configurations using the switch will continue to work.",
"switch_as_x_remove_confirm": "This {domain} will be removed and the original switch will be visible again. Your existing configurations using the {domain} will no longer work!",
"switch_as_x_change_confirm": "This {domain_1} will be removed and will be replaced by a new {domain_2}. Your existing configurations using the {domain_1} will no longer work!",
"enabled_description": "Disabled entities will not be added to Home Assistant.", "enabled_description": "Disabled entities will not be added to Home Assistant.",
"enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds", "enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds",
"enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities", "enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities",