From 19c9486351742076b539d676fd4251d0c81d7614 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 26 Apr 2023 12:23:53 +0200 Subject: [PATCH] Update entity settings, combine basic editor with normal editor (#16297) --- src/components/ha-combo-box.ts | 3 +- src/components/ha-settings-row.ts | 21 +- .../settings/entity-settings-helper-tab.ts | 29 +- .../entities/entity-registry-basic-editor.ts | 376 ------ .../entity-registry-settings-editor.ts | 1199 +++++++++++++++++ .../entities/entity-registry-settings.ts | 1194 +--------------- src/translations/en.json | 16 +- 7 files changed, 1267 insertions(+), 1571 deletions(-) delete mode 100644 src/panels/config/entities/entity-registry-basic-editor.ts create mode 100644 src/panels/config/entities/entity-registry-settings-editor.ts diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index 061a241e07..bc0153d885 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -14,8 +14,9 @@ import { customElement, property, query } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { fireEvent } from "../common/dom/fire_event"; import { HomeAssistant } from "../types"; -import "./ha-list-item"; import "./ha-icon-button"; +import "./ha-list-item"; +import "./ha-textfield"; import type { HaTextField } from "./ha-textfield"; registerStyles( diff --git a/src/components/ha-settings-row.ts b/src/components/ha-settings-row.ts index fc822203e0..5d97d39b95 100644 --- a/src/components/ha-settings-row.ts +++ b/src/components/ha-settings-row.ts @@ -52,18 +52,17 @@ export class HaSettingsRow extends LitElement { white-space: nowrap; } .body > .secondary { - font-family: var(--paper-font-body1_-_font-family); - -webkit-font-smoothing: var( - --paper-font-body1_-_-webkit-font-smoothing - ); - font-size: var(--paper-font-body1_-_font-size); - font-weight: var(--paper-font-body1_-_font-weight); - line-height: var(--paper-font-body1_-_line-height); - - color: var( - --paper-item-body-secondary-color, - var(--secondary-text-color) + display: block; + padding-top: 4px; + font-family: var( + --mdc-typography-body2-font-family, + var(--mdc-typography-font-family, Roboto, sans-serif) ); + -webkit-font-smoothing: antialiased; + font-size: var(--mdc-typography-body2-font-size, 0.875rem); + font-weight: var(--mdc-typography-body2-font-weight, 400); + line-height: normal; + color: var(--secondary-text-color); } .body[two-line] { min-height: calc( diff --git a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts index 7953416b7d..95659ca947 100644 --- a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts +++ b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts @@ -3,8 +3,8 @@ import { CSSResultGroup, html, LitElement, - PropertyValues, nothing, + PropertyValues, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; @@ -28,8 +28,9 @@ import "../../../helpers/forms/ha-input_select-form"; import "../../../helpers/forms/ha-input_text-form"; import "../../../helpers/forms/ha-schedule-form"; import "../../../helpers/forms/ha-timer-form"; -import "../../entity-registry-basic-editor"; -import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor"; +import "../../../voice-assistants/entity-voice-settings"; +import "../../entity-registry-settings-editor"; +import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor"; @customElement("entity-settings-helper-tab") export class EntityRegistrySettingsHelper extends LitElement { @@ -45,8 +46,8 @@ export class EntityRegistrySettingsHelper extends LitElement { @state() private _componentLoaded?: boolean; - @query("ha-registry-basic-editor") - private _registryEditor?: HaEntityRegistryBasicEditor; + @query("entity-registry-settings-editor") + private _registryEditor?: EntityRegistrySettingsEditor; protected firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties); @@ -75,7 +76,9 @@ export class EntityRegistrySettingsHelper extends LitElement { const stateObj = this.hass.states[this.entry.entity_id]; return html`
- ${this._error ? html`
${this._error}
` : ""} + ${this._error + ? html`${this._error}` + : ""} ${!this._componentLoaded ? this.hass.localize( "ui.dialogs.helper_settings.platform_not_loaded", @@ -93,10 +96,14 @@ export class EntityRegistrySettingsHelper extends LitElement { })} `} - + .disabled=${this._submitting} + @change=${this._entityRegistryChanged} + hideName + hideIcon + >
; - - @state() private _device?: DeviceRegistryEntry; - - @state() private _submitting = false; - - private _handleAliasesClicked(ev: CustomEvent) { - if (ev.detail.index !== 0) return; - const stateObj = this.hass.states[this.entry.entity_id]; - const name = - (stateObj && computeStateName(stateObj)) || this.entry.entity_id; - - showAliasesDialog(this, { - name, - aliases: this.entry!.aliases, - updateAliases: async (aliases: string[]) => { - const result = await updateEntityRegistryEntry( - this.hass, - this.entry.entity_id, - { aliases } - ); - fireEvent(this, "entity-entry-updated", result.entity_entry); - }, - }); - } - - public async updateEntry(): Promise { - this._submitting = true; - const params: Partial = { - new_entity_id: this._entityId.trim(), - area_id: this._areaId || null, - }; - if ( - this.entry.disabled_by !== this._disabledBy && - (this._disabledBy === null || this._disabledBy === "user") - ) { - params.disabled_by = this._disabledBy; - } - if ( - this.entry.hidden_by !== this._hiddenBy && - (this._hiddenBy === null || this._hiddenBy === "user") - ) { - params.hidden_by = this._hiddenBy; - } - try { - const result = await updateEntityRegistryEntry( - this.hass!, - this._origEntityId, - params - ); - if (result.require_restart) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_restart_confirm" - ), - }); - } - if (result.reload_delay) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_delay_confirm", - "delay", - result.reload_delay - ), - }); - } - } finally { - this._submitting = false; - } - } - - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeDeviceRegistry(this.hass.connection!, (devices) => { - this._deviceLookup = {}; - for (const device of devices) { - this._deviceLookup[device.id] = device; - } - if (!this._device && this.entry.device_id) { - this._device = this._deviceLookup[this.entry.device_id]; - } - }), - ]; - } - - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (!changedProperties.has("entry")) { - return; - } - if (this.entry) { - this._origEntityId = this.entry.entity_id; - this._entityId = this.entry.entity_id; - this._disabledBy = this.entry.disabled_by; - this._hiddenBy = this.entry.hidden_by; - this._areaId = this.entry.area_id; - this._device = - this.entry.device_id && this._deviceLookup - ? this._deviceLookup[this.entry.device_id] - : undefined; - } - } - - protected render() { - if ( - !this.hass || - !this.entry || - this.entry.entity_id !== this._origEntityId - ) { - return nothing; - } - const invalidDomainUpdate = - computeDomain(this._entityId.trim()) !== - computeDomain(this.entry.entity_id); - - return html` - - - - -
- ${this.hass.localize( - "ui.dialogs.entity_registry.editor.entity_status" - )}: -
-
- ${this._disabledBy && this._disabledBy !== "user" - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_cause", - "cause", - this.hass.localize( - `config_entry.disabled_by.${this._disabledBy}` - ) - ) - : ""} -
-
- - - - - - - - - -
- - ${this._disabledBy !== null - ? html` -
- ${this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_description" - )} -
- ` - : this._hiddenBy !== null - ? html` -
- ${this.hass.localize( - "ui.dialogs.entity_registry.editor.hidden_description" - )} -
- ` - : ""} - -
- ${this.hass.localize( - "ui.dialogs.entity_registry.editor.aliases_section" - )} -
- - 0} hasMeta> - - ${this.entry.aliases.length > 0 - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.configured_aliases", - { count: this.entry.aliases.length } - ) - : this.hass.localize( - "ui.dialogs.entity_registry.editor.no_aliases" - )} - - - ${[...this.entry.aliases] - .sort((a, b) => stringCompare(a, b, this.hass.locale.language)) - .join(", ")} - - - - -
- ${this.hass.localize( - "ui.dialogs.entity_registry.editor.aliases_description" - )} -
-
- `; - } - - private _areaPicked(ev: CustomEvent) { - this._areaId = ev.detail.value; - } - - private _entityIdChanged(ev): void { - this._entityId = ev.target.value; - } - - private _viewStatusChanged(ev: CustomEvent): void { - switch ((ev.target as any).value) { - case "enabled": - this._disabledBy = null; - this._hiddenBy = null; - break; - case "disabled": - this._disabledBy = "user"; - this._hiddenBy = null; - break; - case "hidden": - this._hiddenBy = "user"; - this._disabledBy = null; - break; - } - } - - static get styles() { - return css` - ha-switch { - margin-right: 16px; - } - .row { - margin-top: 8px; - color: var(--primary-text-color); - display: flex; - align-items: center; - } - .secondary { - color: var(--secondary-text-color); - } - ha-textfield { - display: block; - margin-bottom: 8px; - } - ha-expansion-panel { - margin-top: 8px; - } - .label { - margin-top: 16px; - } - .aliases { - border-radius: 4px; - margin-top: 4px; - margin-bottom: 4px; - --mdc-icon-button-size: 24px; - overflow: hidden; - } - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-registry-basic-editor": HaEntityRegistryBasicEditor; - } -} diff --git a/src/panels/config/entities/entity-registry-settings-editor.ts b/src/panels/config/entities/entity-registry-settings-editor.ts new file mode 100644 index 0000000000..1f9d4906f7 --- /dev/null +++ b/src/panels/config/entities/entity-registry-settings-editor.ts @@ -0,0 +1,1199 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-formfield/mwc-formfield"; +import { HassEntity } from "home-assistant-js-websocket"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { domainIcon } from "../../../common/entity/domain_icon"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import { formatNumber } from "../../../common/number/format_number"; +import { stringCompare } from "../../../common/string/compare"; +import { + LocalizeFunc, + LocalizeKeys, +} from "../../../common/translations/localize"; +import "../../../components/ha-alert"; +import "../../../components/ha-area-picker"; +import "../../../components/ha-icon"; +import "../../../components/ha-icon-button-next"; +import "../../../components/ha-icon-picker"; +import "../../../components/ha-list-item"; +import "../../../components/ha-radio"; +import "../../../components/ha-select"; +import "../../../components/ha-settings-row"; +import "../../../components/ha-switch"; +import type { HaSwitch } from "../../../components/ha-switch"; +import "../../../components/ha-textfield"; +import { + CameraPreferences, + CAMERA_ORIENTATIONS, + CAMERA_SUPPORT_STREAM, + fetchCameraPrefs, + STREAM_TYPE_HLS, + updateCameraPrefs, +} from "../../../data/camera"; +import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; +import { + createConfigFlow, + handleConfigFlowStep, +} from "../../../data/config_flow"; +import { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow"; +import { + DeviceRegistryEntry, + updateDeviceRegistryEntry, +} from "../../../data/device_registry"; +import { + EntityRegistryEntry, + EntityRegistryEntryUpdateParams, + ExtEntityRegistryEntry, + fetchEntityRegistry, + SensorEntityOptions, + updateEntityRegistryEntry, +} from "../../../data/entity_registry"; +import { domainToName } from "../../../data/integration"; +import { getNumberDeviceClassConvertibleUnits } from "../../../data/number"; +import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor"; +import { + getWeatherConvertibleUnits, + WeatherUnits, +} from "../../../data/weather"; +import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import { showVoiceAssistantsView } from "../../../dialogs/more-info/components/voice/show-view-voice-assistants"; +import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; + +const OVERRIDE_DEVICE_CLASSES = { + cover: [ + [ + "awning", + "blind", + "curtain", + "damper", + "door", + "garage", + "gate", + "shade", + "shutter", + "window", + ], + ], + binary_sensor: [ + ["lock"], // Lock + ["window", "door", "garage_door", "opening"], // Door + ["battery", "battery_charging"], // Battery + ["cold", "gas", "heat"], // Climate + ["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence + ["power", "plug", "light"], // Power + [ + "smoke", + "safety", + "sound", + "problem", + "tamper", + "carbon_monoxide", + "moisture", + ], // Alarm + ], +}; + +const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"]; + +const PRECISIONS = [0, 1, 2, 3, 4, 5, 6]; + +@customElement("entity-registry-settings-editor") +export class EntityRegistrySettingsEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Object }) public entry!: ExtEntityRegistryEntry; + + @property({ type: Boolean }) public hideName = false; + + @property({ type: Boolean }) public hideIcon = false; + + @property({ type: Boolean }) public disabled = false; + + @state() private _name!: string; + + @state() private _icon!: string; + + @state() private _entityId!: string; + + @state() private _deviceClass?: string; + + @state() private _switchAs = "switch"; + + @state() private _areaId?: string | null; + + @state() private _disabledBy!: EntityRegistryEntry["disabled_by"]; + + @state() private _hiddenBy!: EntityRegistryEntry["hidden_by"]; + + @state() private _device?: DeviceRegistryEntry; + + @state() private _helperConfigEntry?: ConfigEntry; + + @state() private _unit_of_measurement?: string | null; + + @state() private _precision?: number | null; + + @state() private _precipitation_unit?: string | null; + + @state() private _pressure_unit?: string | null; + + @state() private _temperature_unit?: string | null; + + @state() private _visibility_unit?: string | null; + + @state() private _wind_speed_unit?: string | null; + + @state() private _cameraPrefs?: CameraPreferences; + + @state() private _numberDeviceClassConvertibleUnits?: string[]; + + @state() private _sensorDeviceClassConvertibleUnits?: string[]; + + @state() private _weatherConvertibleUnits?: WeatherUnits; + + @state() private _noDeviceArea?: boolean; + + private _origEntityId!: string; + + private _deviceClassOptions?: string[][]; + + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + if (this.entry.config_entry_id) { + getConfigEntries(this.hass, { + type: ["helper"], + domain: this.entry.platform, + }).then((entries) => { + this._helperConfigEntry = entries.find( + (ent) => ent.entry_id === this.entry.config_entry_id + ); + }); + } + } + + protected willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (!changedProperties.has("entry")) { + return; + } + + this._name = this.entry.name || ""; + this._icon = this.entry.icon || ""; + this._deviceClass = + this.entry.device_class || this.entry.original_device_class; + this._origEntityId = this.entry.entity_id; + this._areaId = this.entry.area_id; + this._entityId = this.entry.entity_id; + this._disabledBy = this.entry.disabled_by; + this._hiddenBy = this.entry.hidden_by; + this._device = this.entry.device_id + ? this.hass.devices[this.entry.device_id] + : undefined; + + const domain = computeDomain(this.entry.entity_id); + + if (domain === "camera" && isComponentLoaded(this.hass, "stream")) { + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + if ( + stateObj && + supportsFeature(stateObj, CAMERA_SUPPORT_STREAM) && + // The stream component for HLS streams supports a server-side pre-load + // option that client initiated WebRTC streams do not + stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS + ) { + this._fetchCameraPrefs(); + } + } + + if (domain === "number" || domain === "sensor") { + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; + } + + if (domain === "sensor") { + this._precision = this.entry.options?.sensor?.display_precision; + } + + if (domain === "weather") { + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + this._precipitation_unit = stateObj?.attributes?.precipitation_unit; + this._pressure_unit = stateObj?.attributes?.pressure_unit; + this._temperature_unit = stateObj?.attributes?.temperature_unit; + this._visibility_unit = stateObj?.attributes?.visibility_unit; + this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit; + } + + const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain]; + + if (!deviceClasses) { + return; + } + + this._deviceClassOptions = [[], []]; + for (const deviceClass of deviceClasses) { + if (deviceClass.includes(this.entry.original_device_class!)) { + this._deviceClassOptions[0] = deviceClass; + } else { + this._deviceClassOptions[1].push(...deviceClass); + } + } + } + + private precisionLabel(precision?: number, stateValue?: string) { + const stateValueNumber = Number(stateValue); + const value = !isNaN(stateValueNumber) ? stateValueNumber : 0; + return formatNumber(value, this.hass.locale, { + minimumFractionDigits: precision, + maximumFractionDigits: precision, + }); + } + + protected async updated(changedProps: PropertyValues): Promise { + if (changedProps.has("_deviceClass")) { + const domain = computeDomain(this.entry.entity_id); + + if (domain === "number" && this._deviceClass) { + const { units } = await getNumberDeviceClassConvertibleUnits( + this.hass, + this._deviceClass + ); + this._numberDeviceClassConvertibleUnits = units; + } else { + this._numberDeviceClassConvertibleUnits = []; + } + if (domain === "sensor" && this._deviceClass) { + const { units } = await getSensorDeviceClassConvertibleUnits( + this.hass, + this._deviceClass + ); + this._sensorDeviceClassConvertibleUnits = units; + } else { + this._sensorDeviceClassConvertibleUnits = []; + } + } + if (changedProps.has("_entityId")) { + const domain = computeDomain(this.entry.entity_id); + + if (domain === "weather") { + const { units } = await getWeatherConvertibleUnits(this.hass); + this._weatherConvertibleUnits = units; + } else { + this._weatherConvertibleUnits = undefined; + } + } + } + + protected render() { + if (this.entry.entity_id !== this._origEntityId) { + return nothing; + } + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + + const domain = computeDomain(this.entry.entity_id); + + const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain; + + const defaultPrecision = + this.entry.options?.sensor?.suggested_display_precision ?? undefined; + + return html` + ${this.hideName + ? nothing + : html``} + ${this.hideIcon + ? nothing + : html``} + ${this._deviceClassOptions + ? html` + + + ${this._deviceClassesSorted( + domain, + this._deviceClassOptions[0], + this.hass.localize + ).map( + (entry) => html` + + ${entry.label} + + ` + )} + ${this._deviceClassOptions[0].length && + this._deviceClassOptions[1].length + ? html`
  • ` + : ""} + ${this._deviceClassesSorted( + domain, + this._deviceClassOptions[1], + this.hass.localize + ).map( + (entry) => html` + + ${entry.label} + + ` + )} +
    + ` + : ""} + ${domain === "number" && + this._deviceClass && + stateObj?.attributes.unit_of_measurement && + this._numberDeviceClassConvertibleUnits?.includes( + stateObj?.attributes.unit_of_measurement + ) + ? html` + + ${this._numberDeviceClassConvertibleUnits.map( + (unit: string) => html` + ${unit} + ` + )} + + ` + : ""} + ${domain === "sensor" && + this._deviceClass && + stateObj?.attributes.unit_of_measurement && + this._sensorDeviceClassConvertibleUnits?.includes( + stateObj?.attributes.unit_of_measurement + ) + ? html` + + ${this._sensorDeviceClassConvertibleUnits.map( + (unit: string) => html` + ${unit} + ` + )} + + ` + : ""} + ${domain === "sensor" && + // Allow customizing the precision for a sensor with numerical device class, + // a unit of measurement or state class + ((this._deviceClass && + !["date", "enum", "timestamp"].includes(this._deviceClass)) || + stateObj?.attributes.unit_of_measurement || + stateObj?.attributes.state_class) + ? html` + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.precision_default", + { + value: this.precisionLabel( + defaultPrecision, + stateObj?.state + ), + } + )} + ${PRECISIONS.map( + (precision) => html` + + ${this.precisionLabel(precision, stateObj?.state)} + + ` + )} + + ` + : ""} + ${domain === "weather" + ? html` + + ${this._weatherConvertibleUnits?.precipitation_unit.map( + (unit: string) => html` + ${unit} + ` + )} + + + ${this._weatherConvertibleUnits?.pressure_unit.map( + (unit: string) => html` + ${unit} + ` + )} + + + ${this._weatherConvertibleUnits?.temperature_unit.map( + (unit: string) => html` + ${unit} + ` + )} + + + ${this._weatherConvertibleUnits?.visibility_unit.map( + (unit: string) => html` + ${unit} + ` + )} + + + ${this._weatherConvertibleUnits?.wind_speed_unit.map( + (unit: string) => html` + ${unit} + ` + )} + + ` + : ""} + ${domain === "switch" + ? html` + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.device_classes.switch.switch" + )} + + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.device_classes.switch.outlet" + )} + +
  • + ${this._switchAsDomainsSorted( + SWITCH_AS_DOMAINS, + this.hass.localize + ).map( + (entry) => html` + + ${entry.label} + + ` + )} +
    ` + : ""} + + ${!this.entry.device_id || this._areaId || this._noDeviceArea + ? html`` + : ""} + ${this._cameraPrefs + ? html` + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.preload_stream" + )} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.preload_stream_description" + )} + + + + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.stream_orientation" + )} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.stream_orientation_description" + )} + + ${CAMERA_ORIENTATIONS.map((num) => { + const localizeStr = + "ui.dialogs.entity_registry.editor.stream.stream_orientation_" + + num.toString(); + return html` + + ${this.hass.localize(localizeStr as LocalizeKeys)} + + `; + })} + + + ` + : ""} + ${this._helperConfigEntry && this._helperConfigEntry.supports_options + ? html` + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.configure_state", + "integration", + domainToName( + this.hass.localize, + this._helperConfigEntry.domain + ) + )} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.configure_state_secondary", + "integration", + domainToName( + this.hass.localize, + this._helperConfigEntry.domain + ) + )} + + + ` + : ""} + + + Voice assistants + + ${this.entry.aliases.length + ? [...this.entry.aliases] + .sort((a, b) => stringCompare(a, b, this.hass.locale.language)) + .join(", ") + : this.hass.localize( + "ui.dialogs.entity_registry.editor.no_aliases" + )} + + + + + ${this._disabledBy && + this._disabledBy !== "user" && + this._disabledBy !== "integration" + ? html`${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_cause", + "cause", + this.hass.localize( + `config_entry.disabled_by.${this._disabledBy!}` + ) + )}` + : ""} + + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_label" + )} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_description" + )} + + + + ${this._hiddenBy && this._hiddenBy !== "user" + ? html`${this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_cause", + "cause", + this.hass.localize(`config_entry.hidden_by.${this._hiddenBy!}`) + )}` + : ""} + + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.visible_label" + )} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_description" + )} + + + + ${this.entry.device_id + ? html` + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.use_device_area" + )} + ${this.hass.devices[this.entry.device_id].area_id + ? `(${ + this.hass.areas[ + this.hass.devices[this.entry.device_id].area_id! + ]?.name + })` + : ""} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.change_device_settings", + { + link: html``, + } + )} + + ` + : ""} + `; + } + + public async updateEntry(): Promise { + const parent = (this.getRootNode() as ShadowRoot).host as HTMLElement; + + const params: Partial = { + name: this._name.trim() || null, + icon: this._icon.trim() || null, + area_id: this._areaId || null, + new_entity_id: this._entityId.trim(), + }; + + // Only update device class if changed by user + if ( + this._deviceClass !== + (this.entry.device_class || this.entry.original_device_class) + ) { + params.device_class = this._deviceClass; + } + + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + const domain = computeDomain(this.entry.entity_id); + + if ( + this.entry.disabled_by !== this._disabledBy && + (this._disabledBy === null || this._disabledBy === "user") + ) { + params.disabled_by = this._disabledBy; + } + if ( + this.entry.hidden_by !== this._hiddenBy && + (this._hiddenBy === null || this._hiddenBy === "user") + ) { + params.hidden_by = this._hiddenBy; + } + if ( + (domain === "number" || domain === "sensor") && + stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement + ) { + params.options_domain = domain; + params.options = this.entry.options?.[domain] || {}; + params.options.unit_of_measurement = this._unit_of_measurement; + } + if ( + domain === "sensor" && + this.entry.options?.[domain]?.display_precision !== this._precision + ) { + params.options_domain = domain; + params.options = params.options || this.entry.options?.[domain] || {}; + (params.options as SensorEntityOptions).display_precision = + this._precision; + } + if ( + domain === "weather" && + (stateObj?.attributes?.precipitation_unit !== this._precipitation_unit || + stateObj?.attributes?.pressure_unit !== this._pressure_unit || + stateObj?.attributes?.temperature_unit !== this._temperature_unit || + stateObj?.attributes?.visbility_unit !== this._visibility_unit || + stateObj?.attributes?.wind_speed_unit !== this._wind_speed_unit) + ) { + params.options_domain = "weather"; + params.options = { + precipitation_unit: this._precipitation_unit, + pressure_unit: this._pressure_unit, + temperature_unit: this._temperature_unit, + visibility_unit: this._visibility_unit, + wind_speed_unit: this._wind_speed_unit, + }; + } + + const result = await updateEntityRegistryEntry( + this.hass!, + this.entry.entity_id, + params + ); + if (result.require_restart) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_restart_confirm" + ), + }); + } + if (result.reload_delay) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_delay_confirm", + "delay", + result.reload_delay + ), + }); + } + + if (this._switchAs !== "switch") { + if ( + await showConfirmationDialog(this, { + text: this.hass!.localize( + "ui.dialogs.entity_registry.editor.switch_as_x_confirm", + "domain", + this._switchAs + ), + }) + ) { + const configFlow = await createConfigFlow(this.hass, "switch_as_x"); + const configFlowResult = (await handleConfigFlowStep( + this.hass, + configFlow.flow_id, + { + entity_id: this._entityId.trim(), + target_domain: this._switchAs, + } + )) as DataEntryFlowStepCreateEntry; + if (configFlowResult.result?.entry_id) { + const unsub = await this.hass.connection.subscribeEvents(() => { + unsub(); + fetchEntityRegistry(this.hass.connection).then((entityRegistry) => { + const entity = entityRegistry.find( + (reg) => + reg.config_entry_id === configFlowResult.result!.entry_id + ); + if (!entity) { + return; + } + showMoreInfoDialog(parent, { entityId: entity.entity_id }); + }); + }, "entity_registry_updated"); + } + } + } + + return result.entity_entry; + } + + private _nameChanged(ev): void { + fireEvent(this, "change"); + this._name = ev.target.value; + } + + private _iconChanged(ev: CustomEvent): void { + fireEvent(this, "change"); + this._icon = ev.detail.value; + } + + private _entityIdChanged(ev): void { + fireEvent(this, "change"); + this._entityId = ev.target.value; + } + + private _deviceClassChanged(ev): void { + fireEvent(this, "change"); + this._deviceClass = ev.target.value; + } + + private _unitChanged(ev): void { + fireEvent(this, "change"); + this._unit_of_measurement = ev.target.value; + } + + private _precipitationUnitChanged(ev): void { + fireEvent(this, "change"); + this._precipitation_unit = ev.target.value; + } + + private _precisionChanged(ev): void { + fireEvent(this, "change"); + this._precision = + ev.target.value === "default" ? null : Number(ev.target.value); + } + + private _pressureUnitChanged(ev): void { + fireEvent(this, "change"); + this._pressure_unit = ev.target.value; + } + + private _temperatureUnitChanged(ev): void { + fireEvent(this, "change"); + this._temperature_unit = ev.target.value; + } + + private _visibilityUnitChanged(ev): void { + fireEvent(this, "change"); + this._visibility_unit = ev.target.value; + } + + private _windSpeedUnitChanged(ev): void { + fireEvent(this, "change"); + this._wind_speed_unit = ev.target.value; + } + + private _switchAsChanged(ev): void { + if (ev.target.value === "") { + return; + } + + // If value is "outlet" that means the user kept the "switch" domain, but actually changed + // the device_class of the switch to "outlet". + const switchAs = ev.target.value === "outlet" ? "switch" : ev.target.value; + this._switchAs = switchAs; + + if (ev.target.value === "outlet" || ev.target.value === "switch") { + this._deviceClass = ev.target.value; + } + } + + private _useDeviceAreaChanged(ev): void { + this._noDeviceArea = !ev.target.checked; + if (!this._noDeviceArea) { + this._areaId = undefined; + } + } + + private _areaPicked(ev: CustomEvent) { + fireEvent(this, "change"); + this._areaId = ev.detail.value; + } + + private async _fetchCameraPrefs() { + this._cameraPrefs = await fetchCameraPrefs(this.hass, this.entry.entity_id); + } + + private async _handleCameraPrefsChanged(ev) { + const checkbox = ev.currentTarget as HaSwitch; + try { + this._cameraPrefs = await updateCameraPrefs( + this.hass, + this.entry.entity_id, + { + preload_stream: checkbox.checked!, + } + ); + } catch (err: any) { + showAlertDialog(this, { text: err.message }); + checkbox.checked = !checkbox.checked; + } + } + + private async _handleCameraOrientationChanged(ev) { + try { + this._cameraPrefs = await updateCameraPrefs( + this.hass, + this.entry.entity_id, + { + orientation: ev.currentTarget.value, + } + ); + } catch (err: any) { + showAlertDialog(this, { text: err.message }); + } + } + + private _enabledChanged(ev: CustomEvent): void { + if ((ev.target as any).checked) { + this._disabledBy = null; + } else { + this._disabledBy = "user"; + } + } + + private _hiddenChanged(ev: CustomEvent): void { + if ((ev.target as any).checked) { + this._hiddenBy = null; + } else { + this._hiddenBy = "user"; + } + } + + private _openDeviceSettings() { + showDeviceRegistryDetailDialog(this, { + device: this._device!, + updateEntry: async (updates) => { + await updateDeviceRegistryEntry(this.hass, this._device!.id, updates); + }, + }); + } + + private _handleVoiceAssistantsClicked() { + showVoiceAssistantsView(this, "Voice assistants"); + } + + private async _showOptionsFlow() { + showOptionsFlowDialog(this, this._helperConfigEntry!, null); + } + + private _switchAsDomainsSorted = memoizeOne( + (domains: string[], localize: LocalizeFunc) => + domains + .map((entry) => ({ + domain: entry, + label: domainToName(localize, entry), + })) + .sort((a, b) => + stringCompare(a.label, b.label, this.hass.locale.language) + ) + ); + + private _deviceClassesSorted = memoizeOne( + (domain: string, deviceClasses: string[], localize: LocalizeFunc) => + deviceClasses + .map((entry) => ({ + deviceClass: entry, + label: localize( + `ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}` + ), + })) + .sort((a, b) => + stringCompare(a.label, b.label, this.hass.locale.language) + ) + ); + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + :host { + display: block; + } + ha-switch { + margin-right: 16px; + } + ha-settings-row ha-switch { + margin-right: 0; + } + ha-textfield, + ha-icon-picker, + ha-select, + ha-area-picker { + display: block; + margin: 8px 0; + width: 100%; + } + li[divider] { + border-bottom-color: var(--divider-color); + } + ha-alert mwc-button { + width: max-content; + } + .menu-item { + border-radius: 4px; + margin-top: 3px; + margin-bottom: 3px; + overflow: hidden; + --mdc-list-side-padding: 13px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "entity-registry-settings-editor": EntityRegistrySettingsEditor; + } +} diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 1ab9238576..e5fcda5222 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,129 +1,30 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-formfield/mwc-formfield"; import "@material/mwc-list/mwc-list-item"; -import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { HassEntity } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; -import { computeDomain } from "../../../common/entity/compute_domain"; -import { domainIcon } from "../../../common/entity/domain_icon"; -import { supportsFeature } from "../../../common/entity/supports-feature"; -import { formatNumber } from "../../../common/number/format_number"; -import { stringCompare } from "../../../common/string/compare"; -import { - LocalizeFunc, - LocalizeKeys, -} from "../../../common/translations/localize"; import "../../../components/ha-alert"; -import "../../../components/ha-area-picker"; -import "../../../components/ha-expansion-panel"; -import "../../../components/ha-icon"; -import "../../../components/ha-icon-button-next"; -import "../../../components/ha-icon-picker"; -import "../../../components/ha-radio"; -import "../../../components/ha-select"; -import "../../../components/ha-settings-row"; -import "../../../components/ha-switch"; -import type { HaSwitch } from "../../../components/ha-switch"; -import "../../../components/ha-textfield"; +import { ConfigEntry, deleteConfigEntry } from "../../../data/config_entries"; +import { updateDeviceRegistryEntry } from "../../../data/device_registry"; import { - CameraPreferences, - CAMERA_ORIENTATIONS, - CAMERA_SUPPORT_STREAM, - fetchCameraPrefs, - STREAM_TYPE_HLS, - updateCameraPrefs, -} from "../../../data/camera"; -import { - ConfigEntry, - deleteConfigEntry, - getConfigEntries, -} from "../../../data/config_entries"; -import { - createConfigFlow, - handleConfigFlowStep, -} from "../../../data/config_flow"; -import { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow"; -import { - DeviceRegistryEntry, - subscribeDeviceRegistry, - updateDeviceRegistryEntry, -} from "../../../data/device_registry"; -import { - EntityRegistryEntry, - EntityRegistryEntryUpdateParams, ExtEntityRegistryEntry, - fetchEntityRegistry, removeEntityRegistryEntry, - SensorEntityOptions, updateEntityRegistryEntry, } from "../../../data/entity_registry"; -import { domainToName } from "../../../data/integration"; -import { getNumberDeviceClassConvertibleUnits } from "../../../data/number"; -import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor"; -import { - getWeatherConvertibleUnits, - WeatherUnits, -} from "../../../data/weather"; -import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { showAlertDialog, showConfirmationDialog, } from "../../../dialogs/generic/show-dialog-box"; -import { showVoiceAssistantsView } from "../../../dialogs/more-info/components/voice/show-view-voice-assistants"; -import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; +import "./entity-registry-settings-editor"; +import type { EntityRegistrySettingsEditor } from "./entity-registry-settings-editor"; -const OVERRIDE_DEVICE_CLASSES = { - cover: [ - [ - "awning", - "blind", - "curtain", - "damper", - "door", - "garage", - "gate", - "shade", - "shutter", - "window", - ], - ], - binary_sensor: [ - ["lock"], // Lock - ["window", "door", "garage_door", "opening"], // Door - ["battery", "battery_charging"], // Battery - ["cold", "gas", "heat"], // Climate - ["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence - ["power", "plug", "light"], // Power - [ - "smoke", - "safety", - "sound", - "problem", - "tamper", - "carbon_monoxide", - "moisture", - ], // Alarm - ], -}; - -const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"]; - -const PRECISIONS = [0, 1, 2, 3, 4, 5, 6]; +const invalidDomainUpdate = false; @customElement("entity-registry-settings") export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @@ -131,222 +32,28 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ type: Object }) public entry!: ExtEntityRegistryEntry; - @state() private _name!: string; - - @state() private _icon!: string; - - @state() private _entityId!: string; - - @state() private _deviceClass?: string; - - @state() private _switchAs = "switch"; - - @state() private _areaId?: string | null; - - @state() private _disabledBy!: EntityRegistryEntry["disabled_by"]; - - @state() private _hiddenBy!: EntityRegistryEntry["hidden_by"]; - - @state() private _device?: DeviceRegistryEntry; - @state() private _helperConfigEntry?: ConfigEntry; - @state() private _unit_of_measurement?: string | null; - - @state() private _precision?: number | null; - - @state() private _precipitation_unit?: string | null; - - @state() private _pressure_unit?: string | null; - - @state() private _temperature_unit?: string | null; - - @state() private _visibility_unit?: string | null; - - @state() private _wind_speed_unit?: string | null; - @state() private _error?: string; @state() private _submitting?: boolean; - @state() private _cameraPrefs?: CameraPreferences; - - @state() private _numberDeviceClassConvertibleUnits?: string[]; - - @state() private _sensorDeviceClassConvertibleUnits?: string[]; - - @state() private _weatherConvertibleUnits?: WeatherUnits; - - private _origEntityId!: string; - - private _deviceLookup?: Record; - - private _deviceClassOptions?: string[][]; - - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeDeviceRegistry(this.hass.connection!, (devices) => { - this._deviceLookup = {}; - for (const device of devices) { - this._deviceLookup[device.id] = device; - } - if (this.entry.device_id) { - this._device = this._deviceLookup[this.entry.device_id]; - } - }), - ]; - } - - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - if (this.entry.config_entry_id) { - getConfigEntries(this.hass, { - type: ["helper"], - domain: this.entry.platform, - }).then((entries) => { - this._helperConfigEntry = entries.find( - (ent) => ent.entry_id === this.entry.config_entry_id - ); - }); - } - } - - protected willUpdate(changedProperties: PropertyValues) { - super.willUpdate(changedProperties); - if (!changedProperties.has("entry")) { - return; - } - - this._error = undefined; - this._name = this.entry.name || ""; - this._icon = this.entry.icon || ""; - this._deviceClass = - this.entry.device_class || this.entry.original_device_class; - this._origEntityId = this.entry.entity_id; - this._areaId = this.entry.area_id; - this._entityId = this.entry.entity_id; - this._disabledBy = this.entry.disabled_by; - this._hiddenBy = this.entry.hidden_by; - this._device = - this.entry.device_id && this._deviceLookup - ? this._deviceLookup[this.entry.device_id] - : undefined; - - const domain = computeDomain(this.entry.entity_id); - - if (domain === "camera" && isComponentLoaded(this.hass, "stream")) { - const stateObj: HassEntity | undefined = - this.hass.states[this.entry.entity_id]; - if ( - stateObj && - supportsFeature(stateObj, CAMERA_SUPPORT_STREAM) && - // The stream component for HLS streams supports a server-side pre-load - // option that client initiated WebRTC streams do not - stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS - ) { - this._fetchCameraPrefs(); - } - } - - if (domain === "number" || domain === "sensor") { - const stateObj: HassEntity | undefined = - this.hass.states[this.entry.entity_id]; - this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; - } - - if (domain === "sensor") { - this._precision = this.entry.options?.sensor?.display_precision; - } - - if (domain === "weather") { - const stateObj: HassEntity | undefined = - this.hass.states[this.entry.entity_id]; - this._precipitation_unit = stateObj?.attributes?.precipitation_unit; - this._pressure_unit = stateObj?.attributes?.pressure_unit; - this._temperature_unit = stateObj?.attributes?.temperature_unit; - this._visibility_unit = stateObj?.attributes?.visibility_unit; - this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit; - } - - const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain]; - - if (!deviceClasses) { - return; - } - - this._deviceClassOptions = [[], []]; - for (const deviceClass of deviceClasses) { - if (deviceClass.includes(this.entry.original_device_class!)) { - this._deviceClassOptions[0] = deviceClass; - } else { - this._deviceClassOptions[1].push(...deviceClass); - } - } - } - - private precisionLabel(precision?: number, stateValue?: string) { - const stateValueNumber = Number(stateValue); - const value = !isNaN(stateValueNumber) ? stateValueNumber : 0; - return formatNumber(value, this.hass.locale, { - minimumFractionDigits: precision, - maximumFractionDigits: precision, - }); - } - - protected async updated(changedProps: PropertyValues): Promise { - if (changedProps.has("_deviceClass")) { - const domain = computeDomain(this.entry.entity_id); - - if (domain === "number" && this._deviceClass) { - const { units } = await getNumberDeviceClassConvertibleUnits( - this.hass, - this._deviceClass - ); - this._numberDeviceClassConvertibleUnits = units; - } else { - this._numberDeviceClassConvertibleUnits = []; - } - if (domain === "sensor" && this._deviceClass) { - const { units } = await getSensorDeviceClassConvertibleUnits( - this.hass, - this._deviceClass - ); - this._sensorDeviceClassConvertibleUnits = units; - } else { - this._sensorDeviceClassConvertibleUnits = []; - } - } - if (changedProps.has("_entityId")) { - const domain = computeDomain(this.entry.entity_id); - - if (domain === "weather") { - const { units } = await getWeatherConvertibleUnits(this.hass); - this._weatherConvertibleUnits = units; - } else { - this._weatherConvertibleUnits = undefined; - } - } - } + @query("entity-registry-settings-editor") + private _registryEditor?: EntityRegistrySettingsEditor; protected render() { - if (this.entry.entity_id !== this._origEntityId) { - return nothing; - } const stateObj: HassEntity | undefined = this.hass.states[this.entry.entity_id]; - const domain = computeDomain(this.entry.entity_id); - - const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain; - - const defaultPrecision = - this.entry.options?.sensor?.suggested_display_precision ?? undefined; + const device = this.entry.device_id + ? this.hass.devices[this.entry.device_id] + : undefined; return html` ${!stateObj ? html` - ${this._device?.disabled_by + ${device?.disabled_by ? html`${this.hass!.localize( "ui.dialogs.entity_registry.editor.device_disabled" )}${this._error}` : ""}
    - - - ${this._deviceClassOptions - ? html` - - - ${this._deviceClassesSorted( - domain, - this._deviceClassOptions[0], - this.hass.localize - ).map( - (entry) => html` - - ${entry.label} - - ` - )} - ${this._deviceClassOptions[0].length && - this._deviceClassOptions[1].length - ? html`
  • ` - : ""} - ${this._deviceClassesSorted( - domain, - this._deviceClassOptions[1], - this.hass.localize - ).map( - (entry) => html` - - ${entry.label} - - ` - )} -
    - ` - : ""} - ${domain === "number" && - this._deviceClass && - stateObj?.attributes.unit_of_measurement && - this._numberDeviceClassConvertibleUnits?.includes( - stateObj?.attributes.unit_of_measurement - ) - ? html` - - ${this._numberDeviceClassConvertibleUnits.map( - (unit: string) => html` - ${unit} - ` - )} - - ` - : ""} - ${domain === "sensor" && - this._deviceClass && - stateObj?.attributes.unit_of_measurement && - this._sensorDeviceClassConvertibleUnits?.includes( - stateObj?.attributes.unit_of_measurement - ) - ? html` - - ${this._sensorDeviceClassConvertibleUnits.map( - (unit: string) => html` - ${unit} - ` - )} - - ` - : ""} - ${domain === "sensor" && - // Allow customizing the precision for a sensor with numerical device class, - // a unit of measurement or state class - ((this._deviceClass && - !["date", "enum", "timestamp"].includes(this._deviceClass)) || - stateObj?.attributes.unit_of_measurement || - stateObj?.attributes.state_class) - ? html` - - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.precision_default", - { - value: this.precisionLabel( - defaultPrecision, - stateObj?.state - ), - } - )} - ${PRECISIONS.map( - (precision) => html` - - ${this.precisionLabel(precision, stateObj?.state)} - - ` - )} - - ` - : ""} - ${domain === "weather" - ? html` - - ${this._weatherConvertibleUnits?.precipitation_unit.map( - (unit: string) => html` - ${unit} - ` - )} - - - ${this._weatherConvertibleUnits?.pressure_unit.map( - (unit: string) => html` - ${unit} - ` - )} - - - ${this._weatherConvertibleUnits?.temperature_unit.map( - (unit: string) => html` - ${unit} - ` - )} - - - ${this._weatherConvertibleUnits?.visibility_unit.map( - (unit: string) => html` - ${unit} - ` - )} - - - ${this._weatherConvertibleUnits?.wind_speed_unit.map( - (unit: string) => html` - ${unit} - ` - )} - - ` - : ""} - ${domain === "switch" - ? html` - - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.device_classes.switch.switch" - )} - - - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.device_classes.switch.outlet" - )} - -
  • - ${this._switchAsDomainsSorted( - SWITCH_AS_DOMAINS, - this.hass.localize - ).map( - (entry) => html` - - ${entry.label} - - ` - )} -
    ` - : ""} - ${this._helperConfigEntry && this._helperConfigEntry.supports_options - ? html` -
    - - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.configure_state", - "integration", - domainToName( - this.hass.localize, - this._helperConfigEntry.domain - ) - )} - -
    - ` - : ""} - - ${!this.entry.device_id - ? html`` - : ""} - - 0} hasMeta> - Voice assistants - - ${this.entry.aliases.length - ? [...this.entry.aliases] - .sort((a, b) => - stringCompare(a, b, this.hass.locale.language) - ) - .join(", ") - : this.hass.localize( - "ui.dialogs.entity_registry.editor.no_aliases" - )} - - - - - ${this._cameraPrefs - ? html` - - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.stream.preload_stream" - )} - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.stream.preload_stream_description" - )} - - - - - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.stream.stream_orientation" - )} - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.stream.stream_orientation_description" - )} - - ${CAMERA_ORIENTATIONS.map((num) => { - const localizeStr = - "ui.dialogs.entity_registry.editor.stream.stream_orientation_" + - num.toString(); - return html` - - ${this.hass.localize(localizeStr as LocalizeKeys)} - - `; - })} - - - ` - : ""} - -
    - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.entity_status" - )} -
    -
    - ${this._disabledBy && - this._disabledBy !== "user" && - this._disabledBy !== "integration" - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_cause", - "cause", - this.hass.localize( - `config_entry.disabled_by.${this._disabledBy}` - ) - ) - : ""} -
    -
    - - - - - - - - - -
    - - ${this._disabledBy !== null - ? html` -
    - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_description" - )} -
    - ` - : this._hiddenBy !== null - ? html` -
    - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.hidden_description" - )} -
    - ` - : ""} -
    - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.aliases_description" - )} -
    - ${this.entry.device_id - ? html` -
    - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.change_area" - )} -
    - -
    - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.area_note" - )} - ${this._device - ? html` - - ` - : ""} -
    - ` - : ""} -
    + @change=${this._entityRegistryChanged} + >
    { - await updateDeviceRegistryEntry(this.hass, this._device!.id, updates); + await updateDeviceRegistryEntry(this.hass, device.id, updates); }, }); } - private _handleVoiceAssistantsClicked() { - showVoiceAssistantsView(this, "Voice assistants"); - } - private async _enableEntry() { this._error = undefined; this._submitting = true; try { const result = await updateEntityRegistryEntry( this.hass!, - this._origEntityId, + this.entry.entity_id, { disabled_by: null } ); fireEvent(this, "entity-entry-updated", result.entity_entry); @@ -1097,140 +163,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { private async _updateEntry(): Promise { this._submitting = true; - - const parent = (this.getRootNode() as ShadowRoot).host as HTMLElement; - - const params: Partial = { - name: this._name.trim() || null, - icon: this._icon.trim() || null, - area_id: this._areaId || null, - new_entity_id: this._entityId.trim(), - }; - - // Only update device class if changed by user - if ( - this._deviceClass !== - (this.entry.device_class || this.entry.original_device_class) - ) { - params.device_class = this._deviceClass; - } - - const stateObj: HassEntity | undefined = - this.hass.states[this.entry.entity_id]; - const domain = computeDomain(this.entry.entity_id); - - if ( - this.entry.disabled_by !== this._disabledBy && - (this._disabledBy === null || this._disabledBy === "user") - ) { - params.disabled_by = this._disabledBy; - } - if ( - this.entry.hidden_by !== this._hiddenBy && - (this._hiddenBy === null || this._hiddenBy === "user") - ) { - params.hidden_by = this._hiddenBy; - } - if ( - (domain === "number" || domain === "sensor") && - stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement - ) { - params.options_domain = domain; - params.options = this.entry.options?.[domain] || {}; - params.options.unit_of_measurement = this._unit_of_measurement; - } - if ( - domain === "sensor" && - this.entry.options?.[domain]?.display_precision !== this._precision - ) { - params.options_domain = domain; - params.options = params.options || this.entry.options?.[domain] || {}; - (params.options as SensorEntityOptions).display_precision = - this._precision; - } - if ( - domain === "weather" && - (stateObj?.attributes?.precipitation_unit !== this._precipitation_unit || - stateObj?.attributes?.pressure_unit !== this._pressure_unit || - stateObj?.attributes?.temperature_unit !== this._temperature_unit || - stateObj?.attributes?.visbility_unit !== this._visibility_unit || - stateObj?.attributes?.wind_speed_unit !== this._wind_speed_unit) - ) { - params.options_domain = "weather"; - params.options = { - precipitation_unit: this._precipitation_unit, - pressure_unit: this._pressure_unit, - temperature_unit: this._temperature_unit, - visibility_unit: this._visibility_unit, - wind_speed_unit: this._wind_speed_unit, - }; - } try { - const result = await updateEntityRegistryEntry( - this.hass!, - this._origEntityId, - params - ); - if (result.require_restart) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_restart_confirm" - ), - }); - } - if (result.reload_delay) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_delay_confirm", - "delay", - result.reload_delay - ), - }); - } - fireEvent(this as HTMLElement, "close-dialog"); + await this._registryEditor!.updateEntry(); + fireEvent(this, "close-dialog"); } catch (err: any) { this._error = err.message || "Unknown error"; } finally { this._submitting = false; } - - if (this._switchAs !== "switch") { - if ( - !(await showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.dialogs.entity_registry.editor.switch_as_x_confirm", - "domain", - this._switchAs - ), - })) - ) { - return; - } - const configFlow = await createConfigFlow(this.hass, "switch_as_x"); - const result = (await handleConfigFlowStep( - this.hass, - configFlow.flow_id, - { - entity_id: this._entityId.trim(), - target_domain: this._switchAs, - } - )) as DataEntryFlowStepCreateEntry; - if (!result.result?.entry_id) { - return; - } - const unsub = await this.hass.connection.subscribeEvents(() => { - unsub(); - fetchEntityRegistry(this.hass.connection).then((entityRegistry) => { - const entity = entityRegistry.find( - (reg) => reg.config_entry_id === result.result!.entry_id - ); - if (!entity) { - return; - } - showMoreInfoDialog(parent, { entityId: entity.entity_id }); - }); - }, "entity_registry_updated"); - } } private async _confirmDeleteEntry(): Promise { @@ -1250,7 +190,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { if (this._helperConfigEntry) { await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id); } else { - await removeEntityRegistryEntry(this.hass!, this._origEntityId); + await removeEntityRegistryEntry(this.hass!, this.entry.entity_id); } fireEvent(this, "close-dialog"); } finally { @@ -1258,36 +198,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } } - private async _showOptionsFlow() { - showOptionsFlowDialog(this, this._helperConfigEntry!, null); - } - - private _switchAsDomainsSorted = memoizeOne( - (domains: string[], localize: LocalizeFunc) => - domains - .map((entry) => ({ - domain: entry, - label: domainToName(localize, entry), - })) - .sort((a, b) => - stringCompare(a.label, b.label, this.hass.locale.language) - ) - ); - - private _deviceClassesSorted = memoizeOne( - (domain: string, deviceClasses: string[], localize: LocalizeFunc) => - deviceClasses - .map((entry) => ({ - deviceClass: entry, - label: localize( - `ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}` - ), - })) - .sort((a, b) => - stringCompare(a.label, b.label, this.hass.locale.language) - ) - ); - static get styles(): CSSResultGroup { return [ haStyle, @@ -1301,61 +211,17 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { .buttons { box-sizing: border-box; display: flex; - padding: 24px; - padding-top: 16px; + padding: 8px 16px 8px 24px; justify-content: space-between; - padding-bottom: max(env(safe-area-inset-bottom), 24px); + padding-bottom: max(env(safe-area-inset-bottom), 8px); background-color: var(--mdc-theme-surface, #fff); border-top: 1px solid var(--divider-color); position: sticky; bottom: 0px; } - ha-select { - width: 100%; - margin: 8px 0; - } - ha-switch { - margin-right: 16px; - } - ha-settings-row { - padding: 0; - } - ha-settings-row ha-switch { - margin-right: 0; - } - ha-textfield { - display: block; - margin: 8px 0; - } - ha-area-picker { - margin: 8px 0; - display: block; - } - .row { - margin: 8px 0; - color: var(--primary-text-color); - display: flex; - align-items: center; - } - .label { - margin-top: 16px; - } - .secondary { - margin: 8px 0; - } - li[divider] { - border-bottom-color: var(--divider-color); - } ha-alert mwc-button { width: max-content; } - .aliases { - border-radius: 4px; - margin-top: 4px; - margin-bottom: 4px; - --mdc-icon-button-size: 24px; - overflow: hidden; - } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 788a3ac2ca..e847913eb1 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1041,7 +1041,7 @@ "enabled_label": "Enabled", "disabled_label": "Disabled", "enabled_cause": "Cannot change status. Disabled by {cause}.", - "hidden_label": "Hidden", + "visible_label": "Visible", "hidden_cause": "Hidden by {cause}.", "device_disabled": "The device of this entity is disabled.", "entity_disabled": "This entity is disabled.", @@ -1056,12 +1056,11 @@ "confirm_delete": "Are you sure you want to delete this entity?", "update": "Update", "note": "Note: This might not work yet with all integrations.", - "advanced": "Advanced settings", - "area": "Set entity area only", - "area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.", - "follow_device_area": "Follow device area", - "change_device_area": "Change device area", + "use_device_area": "Use device area", + "change_device_settings": "You can {link} in the device settings", + "change_device_area_link": "change the device area", "configure_state": "{integration} options", + "configure_state_secondary": "Specific options for {integration}", "stream": { "preload_stream": "Preload camera stream", "preload_stream_description": "This keeps the camera stream open in the background so it shows quicker. Warning! This is device intensive.", @@ -1076,10 +1075,7 @@ "stream_orientation_7": "Rotate right and flip", "stream_orientation_8": "Rotate right" }, - "aliases_section": "Aliases", - "no_aliases": "No configured aliases", - "configured_aliases": "{count} configured {count, plural,\n one {alias}\n other {aliases}\n}", - "aliases_description": "Aliases are alternative names used in voice assistants to refer to this entity." + "no_aliases": "Configure aliases and expose settings for voice assistants" } }, "voice-settings": {