ha-frontend/src/panels/config/entities/entity-registry-settings-ed...

1526 lines
49 KiB
TypeScript

import "@material/mwc-button/mwc-button";
import "@material/mwc-formfield/mwc-formfield";
import { mdiContentCopy } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
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 { computeObjectId } from "../../../common/entity/compute_object_id";
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 { copyToClipboard } from "../../../common/util/copy-clipboard";
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-state-icon";
import "../../../components/ha-switch";
import "../../../components/ha-labels-picker";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import {
CAMERA_ORIENTATIONS,
CAMERA_SUPPORT_STREAM,
CameraPreferences,
fetchCameraPrefs,
STREAM_TYPE_HLS,
updateCameraPrefs,
} from "../../../data/camera";
import { ConfigEntry, deleteConfigEntry } 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,
LockEntityOptions,
SensorEntityOptions,
subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { entityIcon, entryIcon } from "../../../data/icons";
import { domainToName } from "../../../data/integration";
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
import {
createOptionsFlow,
handleOptionsFlowStep,
} from "../../../data/options_flow";
import {
getSensorDeviceClassConvertibleUnits,
getSensorNumericDeviceClasses,
} 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 { showToast } from "../../../util/toast";
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
["connectivity"], // Connectivity
["update"], // Update
],
};
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren", "valve"];
const SWITCH_AS_DOMAINS_INVERT = ["cover", "lock", "valve"];
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;
@property({ attribute: false }) public helperConfigEntry?: ConfigEntry;
@state() private _name!: string;
@state() private _icon!: string;
@state() private _entityId!: string;
@state() private _deviceClass?: string;
@state() private _switchAsDomain = "switch";
@state() private _switchAsInvert = false;
@state() private _areaId?: string | null;
@state() private _labels?: string[] | null;
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
@state() private _device?: DeviceRegistryEntry;
@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 _sensorNumericalDeviceClasses?: string[];
@state() private _weatherConvertibleUnits?: WeatherUnits;
@state() private _defaultCode?: string | null;
@state() private _noDeviceArea?: boolean;
private _origEntityId!: string;
private _deviceClassOptions?: string[][];
protected willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (
!changedProperties.has("entry") ||
changedProperties.get("entry")?.id === this.entry.id
) {
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._labels = this.entry.labels;
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;
this._switchAsInvert = this.entry.options?.switch_as_x?.invert === true;
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 === "lock") {
this._defaultCode = this.entry.options?.lock?.default_code;
}
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) ? stateValue! : 0;
return formatNumber(value, this.hass.locale, {
minimumFractionDigits: precision,
maximumFractionDigits: precision,
});
}
private _isInvalidDefaultCode(
codeFormat?: string,
value?: string | null
): boolean {
if (codeFormat && value) {
return !RegExp(codeFormat).test(value);
}
return false;
}
protected async updated(changedProps: PropertyValues): Promise<void> {
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") {
const { numeric_device_classes } = await getSensorNumericDeviceClasses(
this.hass
);
this._sensorNumericalDeviceClasses = numeric_device_classes;
} else {
this._sensorNumericalDeviceClasses = [];
}
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;
}
}
if (changedProps.has("helperConfigEntry")) {
if (this.helperConfigEntry?.domain === "switch_as_x") {
this._switchAsDomain = computeDomain(this.entry.entity_id);
} else {
this._switchAsDomain = "switch";
this._switchAsInvert = false;
}
}
}
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 invalidDefaultCode =
domain === "lock" &&
this._isInvalidDefaultCode(
stateObj?.attributes?.code_format,
this._defaultCode
);
const defaultPrecision =
this.entry.options?.sensor?.suggested_display_precision ?? undefined;
return html`
${this.hideName
? nothing
: html`<ha-textfield
.value=${this._name}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.name"
)}
.disabled=${this.disabled}
.placeholder=${this.entry.original_name}
@input=${this._nameChanged}
></ha-textfield>`}
${this.hideIcon
? nothing
: html`
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.icon"
)}
.placeholder=${this.entry.original_icon ||
stateObj?.attributes.icon ||
(stateObj && until(entityIcon(this.hass, stateObj))) ||
until(entryIcon(this.hass, this.entry))}
.disabled=${this.disabled}
>
${!this._icon && !stateObj?.attributes.icon && stateObj
? html`
<ha-state-icon
slot="fallback"
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-icon>
`
: nothing}
</ha-icon-picker>
`}
${domain === "switch"
? html`<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
naturalMenuWidth
fixedMenuPosition
@selected=${this._switchAsDomainChanged}
@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._switchAsDomain}
naturalMenuWidth
fixedMenuPosition
@selected=${this._switchAsDomainChanged}
@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>
${SWITCH_AS_DOMAINS_INVERT.includes(this._switchAsDomain)
? html`
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.invert.label"
)}</span
>
<span slot="description"
>${this.hass.localize(
`ui.dialogs.entity_registry.editor.invert.descriptions.${this._switchAsDomain}`
)}</span
>
<ha-switch
.checked=${this.entry.options?.switch_as_x?.invert}
@change=${this._switchAsInvertChanged}
></ha-switch>
</ha-settings-row>
`
: nothing} `
: nothing}
${this._deviceClassOptions
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass}
naturalMenuWidth
fixedMenuPosition
clearable
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[0],
this.hass.localize
).map(
(entry) => html`
<ha-list-item .value=${entry.deviceClass}>
${entry.label}
</ha-list-item>
`
)}
${this._deviceClassOptions[0].length &&
this._deviceClassOptions[1].length
? html`<li divider role="separator"></li>`
: ""}
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[1],
this.hass.localize
).map(
(entry) => html`
<ha-list-item .value=${entry.deviceClass}>
${entry.label}
</ha-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "number" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
this._numberDeviceClassConvertibleUnits?.includes(
stateObj?.attributes.unit_of_measurement
)
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.unit_of_measurement"
)}
.value=${stateObj.attributes.unit_of_measurement}
naturalMenuWidth
fixedMenuPosition
@selected=${this._unitChanged}
@closed=${stopPropagation}
>
${this._numberDeviceClassConvertibleUnits.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "lock"
? html`
<ha-textfield
.validationMessage=${this.hass.localize(
"ui.dialogs.entity_registry.editor.default_code_error"
)}
.value=${this._defaultCode == null ? "" : this._defaultCode}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.default_code"
)}
type="password"
.invalid=${invalidDefaultCode}
.disabled=${this.disabled}
@input=${this._defaultcodeChanged}
></ha-textfield>
`
: ""}
${domain === "sensor" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
this._sensorDeviceClassConvertibleUnits?.includes(
stateObj?.attributes.unit_of_measurement
)
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.unit_of_measurement"
)}
.value=${stateObj.attributes.unit_of_measurement}
naturalMenuWidth
fixedMenuPosition
@selected=${this._unitChanged}
@closed=${stopPropagation}
>
${this._sensorDeviceClassConvertibleUnits.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "sensor" &&
// Allow customizing the precision for a sensor with numerical device class,
// a unit of measurement or state class
((this._deviceClass &&
this._sensorNumericalDeviceClasses?.includes(this._deviceClass)) ||
stateObj?.attributes.unit_of_measurement ||
stateObj?.attributes.state_class)
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.precision"
)}
.value=${this._precision == null
? "default"
: this._precision.toString()}
naturalMenuWidth
fixedMenuPosition
@selected=${this._precisionChanged}
@closed=${stopPropagation}
>
<ha-list-item value="default"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.precision_default",
{
value: this.precisionLabel(
defaultPrecision,
stateObj?.state
),
}
)}</ha-list-item
>
${PRECISIONS.map(
(precision) => html`
<ha-list-item .value=${precision.toString()}>
${this.precisionLabel(precision, stateObj?.state)}
</ha-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "weather"
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.precipitation_unit"
)}
.value=${this._precipitation_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._precipitationUnitChanged}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.precipitation_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.pressure_unit"
)}
.value=${this._pressure_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._pressureUnitChanged}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.pressure_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.temperature_unit"
)}
.value=${this._temperature_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._temperatureUnitChanged}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.temperature_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.visibility_unit"
)}
.value=${this._visibility_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._visibilityUnitChanged}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.visibility_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.wind_speed_unit"
)}
.value=${this._wind_speed_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._windSpeedUnitChanged}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.wind_speed_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
`
: ""}
<ha-textfield
class="entityId"
.value=${computeObjectId(this._entityId)}
.prefix=${domain + "."}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_id"
)}
.disabled=${this.disabled}
required
@input=${this._entityIdChanged}
iconTrailing
autocapitalize="none"
autocomplete="off"
autocorrect="off"
input-spellcheck="false"
>
<ha-icon-button
@click=${this._copyEntityId}
slot="trailingIcon"
.path=${mdiContentCopy}
></ha-icon-button>
</ha-textfield>
${!this.entry.device_id
? html`<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.disabled=${this.disabled}
@value-changed=${this._areaPicked}
></ha-area-picker>`
: ""}
<ha-labels-picker
.hass=${this.hass}
.value=${this._labels}
.disabled=${this.disabled}
@value-changed=${this._labelsChanged}
></ha-labels-picker>
${this._cameraPrefs
? html`
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.preload_stream"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.preload_stream_description"
)}</span
>
<ha-switch
.checked=${this._cameraPrefs.preload_stream}
.disabled=${this.disabled}
@change=${this._handleCameraPrefsChanged}
>
</ha-switch>
</ha-settings-row>
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.stream_orientation"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.stream_orientation_description"
)}</span
>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.stream_orientation"
)}
naturalMenuWidth
fixedMenuPosition
.disabled=${this.disabled}
@selected=${this._handleCameraOrientationChanged}
@closed=${stopPropagation}
>
${CAMERA_ORIENTATIONS.map((num) => {
const localizeStr =
"ui.dialogs.entity_registry.editor.stream.stream_orientation_" +
num.toString();
return html`
<ha-list-item value=${num}>
${this.hass.localize(localizeStr as LocalizeKeys)}
</ha-list-item>
`;
})}
</ha-select>
</ha-settings-row>
`
: ""}
${this.helperConfigEntry &&
this.helperConfigEntry.supports_options &&
this.helperConfigEntry.domain !== "switch_as_x"
? html`
<ha-list-item
class="menu-item"
twoline
hasMeta
.disabled=${this.disabled}
@click=${this._showOptionsFlow}
>
<span
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.configure_state",
{
integration: domainToName(
this.hass.localize,
this.helperConfigEntry.domain
),
}
)}</span
>
<span slot="secondary"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.configure_state_secondary",
{
integration: domainToName(
this.hass.localize,
this.helperConfigEntry.domain
),
}
)}</span
>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: ""}
<ha-list-item
class="menu-item"
twoline
hasMeta
.disabled=${this.disabled}
@click=${this._handleVoiceAssistantsClicked}
>
<span
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.voice_assistants"
)}</span
>
<span slot="secondary">
${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"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
${this._disabledBy &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration"
? html`<ha-alert alert-type="warning"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
{
cause: this.hass.localize(
`config_entry.disabled_by.${this._disabledBy!}`
),
}
)}</ha-alert
>`
: ""}
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}</span
>
<ha-switch
.checked=${!this._disabledBy}
.disabled=${this.disabled ||
this._device?.disabled_by ||
(this._disabledBy &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration")}
@change=${this._enabledChanged}
></ha-switch>
</ha-settings-row>
${this._hiddenBy && this._hiddenBy !== "user"
? html`<ha-alert alert-type="warning"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_cause",
{
cause: this.hass.localize(
`config_entry.hidden_by.${this._hiddenBy!}`
),
}
)}</ha-alert
>`
: ""}
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.visible_label"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_description"
)}</span
>
<ha-switch
.checked=${!this._disabledBy && !this._hiddenBy}
.disabled=${this.disabled || this._disabledBy}
@change=${this._hiddenChanged}
></ha-switch>
</ha-settings-row>
${this.entry.device_id
? html`<ha-settings-row>
<span slot="heading"
>${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
})`
: ""}</span
>
<span slot="description"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_settings",
{
link: html`<button
class="link"
@click=${this._openDeviceSettings}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_area_link"
)}
</button>`,
}
)}</span
>
<ha-switch
.checked=${!this._areaId || this._noDeviceArea}
.disabled=${this.disabled}
@change=${this._useDeviceAreaChanged}
>
</ha-switch
></ha-settings-row>
${this._areaId || this._noDeviceArea
? html`<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
.disabled=${this.disabled}
@value-changed=${this._areaPicked}
></ha-area-picker>`
: ""} `
: ""}
`;
}
public async updateEntry(): Promise<{
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> = {
name: this._name.trim() || null,
icon: this._icon.trim() || null,
area_id: this._areaId || null,
labels: this._labels || [],
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 === "lock" &&
this.entry.options?.[domain]?.default_code !== this._defaultCode
) {
params.options_domain = domain;
params.options = this.entry.options?.[domain] || {};
(params.options as LockEntityOptions).default_code = this._defaultCode;
}
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 (domain === "switch" && this._switchAsDomain !== "switch") {
// generate config flow for switch_as_x
if (
await showConfirmationDialog(this, {
text: this.hass!.localize(
"ui.dialogs.entity_registry.editor.switch_as_x_confirm",
{
domain: domainToName(
this.hass.localize,
this._switchAsDomain
).toLowerCase(),
}
),
})
) {
const configFlow = await createConfigFlow(this.hass, "switch_as_x");
const configFlowResult = (await handleConfigFlowStep(
this.hass,
configFlow.flow_id,
{
entity_id: this._entityId.trim(),
invert: this._switchAsInvert,
target_domain: this._switchAsDomain,
}
)) 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
}
}
}
} else if (
this.helperConfigEntry?.domain === "switch_as_x" &&
this._switchAsDomain !== domain
) {
// change a current switch as x to something else
if (
await showConfirmationDialog(this, {
text:
this._switchAsDomain === "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._switchAsDomain
).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._switchAsDomain === "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,
invert: this._switchAsInvert,
target_domain: this._switchAsDomain,
}
)) 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
}
}
}
}
} else if (
this.helperConfigEntry?.domain === "switch_as_x" &&
this._switchAsDomain === domain &&
this._switchAsInvert !== this.entry.options?.switch_as_x?.invert
) {
// Change invert setting
const origEntityId = this.entry.options?.switch_as_x?.entity_id;
if (!origEntityId) {
// should not happen, guard for types
} else {
const configFlow = await createOptionsFlow(
this.hass,
this.helperConfigEntry.entry_id
);
await handleOptionsFlowStep(this.hass, configFlow.flow_id, {
invert: this._switchAsInvert,
});
}
}
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 {
fireEvent(this, "change");
this._name = ev.target.value;
}
private _iconChanged(ev: CustomEvent): void {
fireEvent(this, "change");
this._icon = ev.detail.value;
}
private async _copyEntityId(): Promise<void> {
await copyToClipboard(this._entityId);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
private _entityIdChanged(ev): void {
fireEvent(this, "change");
this._entityId = `${computeDomain(this._origEntityId)}.${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 _defaultcodeChanged(ev): void {
fireEvent(this, "change");
this._defaultCode = ev.target.value === "" ? null : 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 _switchAsDomainChanged(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._switchAsDomain = switchAs;
if (
(computeDomain(this.entry.entity_id) === "switch" &&
ev.target.value === "outlet") ||
ev.target.value === "switch"
) {
this._deviceClass = ev.target.value;
}
}
private _switchAsInvertChanged(ev): void {
this._switchAsInvert = ev.target.checked;
}
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 _labelsChanged(ev: CustomEvent) {
this._labels = 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,
this.hass.localize("ui.dialogs.entity_registry.editor.voice_assistants")
);
}
private async _showOptionsFlow() {
showOptionsFlowDialog(this, this.helperConfigEntry!);
}
private _switchAsDomainsSorted = memoizeOne(
(domains: string[], localize: LocalizeFunc) =>
domains
.map((domain) => ({
domain,
label: domainToName(localize, domain),
}))
.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-textfield.entityId {
--text-field-prefix-padding-right: 0;
--textfield-icon-trailing-padding: 0;
}
ha-textfield.entityId > ha-icon-button {
position: relative;
right: -8px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: -8px;
direction: var(--direction);
}
ha-switch {
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
ha-settings-row ha-switch {
margin-right: 0;
margin-inline-end: 0;
margin-inline-start: initial;
}
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;
}
}