* {
+ color: var(--secondary-text-color);
+ }
`,
];
}
diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts
index 42337b40ff..5b7c0722b7 100644
--- a/src/dialogs/config-flow/show-dialog-config-flow.ts
+++ b/src/dialogs/config-flow/show-dialog-config-flow.ts
@@ -24,7 +24,7 @@ export const showConfigFlowDialog = (
loadDevicesAndAreas: true,
getFlowHandlers: async (hass) => {
const [handlers] = await Promise.all([
- getConfigFlowHandlers(hass),
+ getConfigFlowHandlers(hass, "integration"),
hass.loadBackendTranslation("title", undefined, true),
]);
diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts
index 9f2a7e6740..0beb4ac26f 100644
--- a/src/dialogs/config-flow/step-flow-pick-handler.ts
+++ b/src/dialogs/config-flow/step-flow-pick-handler.ts
@@ -216,15 +216,16 @@ class StepFlowPickHandler extends LitElement {
if (handler.is_add) {
if (handler.slug === "zwave_js") {
- const entries = await getConfigEntries(this.hass);
- const entry = entries.find((ent) => ent.domain === "zwave_js");
+ const entries = await getConfigEntries(this.hass, {
+ domain: "zwave_js",
+ });
- if (!entry) {
+ if (!entries.length) {
return;
}
showZWaveJSAddNodeDialog(this, {
- entry_id: entry.entry_id,
+ entry_id: entries[0].entry_id,
});
} else if (handler.slug === "zha") {
navigate("/config/zha/add");
diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts
new file mode 100644
index 0000000000..de1dbe1f10
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-update.ts
@@ -0,0 +1,212 @@
+import "@material/mwc-button/mwc-button";
+import "@material/mwc-linear-progress/mwc-linear-progress";
+import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators";
+import { supportsFeature } from "../../../common/entity/supports-feature";
+import "../../../components/ha-checkbox";
+import "../../../components/ha-formfield";
+import "../../../components/ha-markdown";
+import { UNAVAILABLE_STATES } from "../../../data/entity";
+import {
+ updateIsInstalling,
+ UpdateEntity,
+ UPDATE_SUPPORT_BACKUP,
+ UPDATE_SUPPORT_INSTALL,
+ UPDATE_SUPPORT_PROGRESS,
+ UPDATE_SUPPORT_SPECIFIC_VERSION,
+} from "../../../data/update";
+import type { HomeAssistant } from "../../../types";
+
+@customElement("more-info-update")
+class MoreInfoUpdate extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj?: UpdateEntity;
+
+ protected render(): TemplateResult {
+ if (
+ !this.hass ||
+ !this.stateObj ||
+ UNAVAILABLE_STATES.includes(this.stateObj.state)
+ ) {
+ return html``;
+ }
+
+ const skippedVersion =
+ this.stateObj.attributes.latest_version &&
+ this.stateObj.attributes.skipped_version ===
+ this.stateObj.attributes.latest_version;
+
+ return html`
+ ${this.stateObj.attributes.in_progress
+ ? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) &&
+ typeof this.stateObj.attributes.in_progress === "number"
+ ? html``
+ : html``
+ : ""}
+ ${this.stateObj.attributes.title
+ ? html`${this.stateObj.attributes.title}
`
+ : ""}
+
+
+
+ ${this.hass.localize(
+ "ui.dialogs.more_info_control.update.current_version"
+ )}
+
+
+ ${this.stateObj.attributes.current_version ??
+ this.hass.localize("state.default.unavailable")}
+
+
+
+
+ ${this.hass.localize(
+ "ui.dialogs.more_info_control.update.latest_version"
+ )}
+
+
+ ${this.stateObj.attributes.latest_version ??
+ this.hass.localize("state.default.unavailable")}
+
+
+
+ ${this.stateObj.attributes.release_url
+ ? html``
+ : ""}
+ ${this.stateObj.attributes.release_summary
+ ? html`
+ `
+ : ""}
+ ${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
+ ? html`
+
+
+ `
+ : ""}
+
+
+
+ ${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
+
+ ${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
+ ? html`
+
+ ${this.hass.localize(
+ "ui.dialogs.more_info_control.update.install"
+ )}
+
+ `
+ : ""}
+
+ `;
+ }
+
+ get _shouldCreateBackup(): boolean | null {
+ if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
+ return null;
+ }
+ const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
+ if (checkbox) {
+ return checkbox.checked;
+ }
+ return true;
+ }
+
+ private _handleInstall(): void {
+ const installData: Record = {
+ entity_id: this.stateObj!.entity_id,
+ };
+
+ if (this._shouldCreateBackup) {
+ installData.backup = true;
+ }
+
+ if (
+ supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) &&
+ this.stateObj!.attributes.latest_version
+ ) {
+ installData.version = this.stateObj!.attributes.latest_version;
+ }
+
+ this.hass.callService("update", "install", installData);
+ }
+
+ private _handleSkip(): void {
+ this.hass.callService("update", "skip", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ hr {
+ border-color: var(--divider-color);
+ border-bottom: none;
+ margin: 16px 0;
+ }
+ ha-expansion-panel {
+ margin: 16px 0;
+ }
+ .row {
+ margin: 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ .actions {
+ margin: 8px 0 0;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ .actions mwc-button {
+ margin: 0 4px 4px;
+ }
+ a {
+ color: var(--primary-color);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-update": MoreInfoUpdate;
+ }
+}
diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts
index 02abe5e731..3df5e85b4b 100644
--- a/src/dialogs/more-info/state_more_info_control.ts
+++ b/src/dialogs/more-info/state_more_info_control.ts
@@ -25,6 +25,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
script: () => import("./controls/more-info-script"),
sun: () => import("./controls/more-info-sun"),
timer: () => import("./controls/more-info-timer"),
+ update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),
diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts
index 6ac3504e5e..9fc15b0c07 100644
--- a/src/onboarding/onboarding-integrations.ts
+++ b/src/onboarding/onboarding-integrations.ts
@@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement {
}
private async _loadConfigEntries() {
- const entries = await getConfigEntries(this.hass!);
- // We filter out the config entry for the local weather and rpi_power.
+ const entries = await getConfigEntries(this.hass!, { type: "integration" });
+ // We filter out the config entries that are automatically created during onboarding.
// It is one that we create automatically and it will confuse the user
// if it starts showing up during onboarding.
this._entries = entries.filter(
diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts
index 99f48c8989..78d56a0b1b 100644
--- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts
@@ -58,12 +58,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
return;
}
- const configEntries = await getConfigEntries(this.hass);
+ const configEntries = await getConfigEntries(this.hass, {
+ domain: "zwave_js",
+ });
let zwaveJsConfEntries = 0;
for (const entry of configEntries) {
- if (entry.domain !== "zwave_js") {
- continue;
- }
if (zwaveJsConfEntries) {
this._multipleConfigEntries = true;
}
diff --git a/src/panels/config/energy/components/ha-energy-gas-settings.ts b/src/panels/config/energy/components/ha-energy-gas-settings.ts
index 3f92d1a023..4e65150f2b 100644
--- a/src/panels/config/energy/components/ha-energy-gas-settings.ts
+++ b/src/panels/config/energy/components/ha-energy-gas-settings.ts
@@ -121,6 +121,7 @@ export class EnergyGasSettings extends LitElement {
showEnergySettingsGasDialog(this, {
unit: getEnergyGasUnitCategory(this.hass, this.preferences),
saveCallback: async (source) => {
+ delete source.unit_of_measurement;
await this._savePreferences({
...this.preferences,
energy_sources: this.preferences.energy_sources.concat(source),
diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts
index 700329dd1c..46ac49d885 100644
--- a/src/panels/config/energy/components/ha-energy-grid-settings.ts
+++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts
@@ -54,7 +54,7 @@ export class EnergyGridSettings extends LitElement {
@property({ attribute: false })
public validationResult?: EnergyPreferencesValidation;
- @state() private _configEntries?: ConfigEntry[];
+ @state() private _co2ConfigEntry?: ConfigEntry;
protected firstUpdated() {
this._fetchCO2SignalConfigEntries();
@@ -195,28 +195,28 @@ export class EnergyGridSettings extends LitElement {
"ui.panel.config.energy.grid.grid_carbon_footprint"
)}
- ${this._configEntries?.map(
- (entry) => html`
-

-
${entry.title}
-
-
-
-
-
`
- )}
- ${this._configEntries?.length === 0
- ? html`
+ ${this._co2ConfigEntry
+ ? html`
+

+
${this._co2ConfigEntry.title}
+
+
+
+
+
`
+ : html`
- `
- : ""}
+ `}
`;
}
private async _fetchCO2SignalConfigEntries() {
- this._configEntries = (await getConfigEntries(this.hass)).filter(
- (entry) => entry.domain === "co2signal"
- );
+ const entries = await getConfigEntries(this.hass, { domain: "co2signal" });
+ this._co2ConfigEntry = entries.length ? entries[0] : undefined;
}
private _addCO2Sensor() {
diff --git a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts
index 1f2acaf624..8ec83a64b8 100644
--- a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts
+++ b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts
@@ -176,9 +176,17 @@ export class DialogEnergySolarSettings
private async _fetchSolarForecastConfigEntries() {
const domains = this._params!.info.solar_forecast_domains;
- this._configEntries = (await getConfigEntries(this.hass)).filter((entry) =>
- domains.includes(entry.domain)
- );
+ this._configEntries =
+ domains.length === 0
+ ? []
+ : domains.length === 1
+ ? await getConfigEntries(this.hass, {
+ type: "integration",
+ domain: domains[0],
+ })
+ : (await getConfigEntries(this.hass, { type: "integration" })).filter(
+ (entry) => domains.includes(entry.domain)
+ );
}
private _handleForecastChanged(ev: CustomEvent) {
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 913bcd8353..210d931a6c 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
@@ -10,50 +10,11 @@ import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event";
-import {
- deleteCounter,
- fetchCounter,
- updateCounter,
-} from "../../../../../data/counter";
import {
ExtEntityRegistryEntry,
removeEntityRegistryEntry,
} from "../../../../../data/entity_registry";
-import {
- deleteInputBoolean,
- fetchInputBoolean,
- updateInputBoolean,
-} from "../../../../../data/input_boolean";
-import {
- deleteInputButton,
- fetchInputButton,
- updateInputButton,
-} from "../../../../../data/input_button";
-import {
- deleteInputDateTime,
- fetchInputDateTime,
- updateInputDateTime,
-} from "../../../../../data/input_datetime";
-import {
- deleteInputNumber,
- fetchInputNumber,
- updateInputNumber,
-} from "../../../../../data/input_number";
-import {
- deleteInputSelect,
- fetchInputSelect,
- updateInputSelect,
-} from "../../../../../data/input_select";
-import {
- deleteInputText,
- fetchInputText,
- updateInputText,
-} from "../../../../../data/input_text";
-import {
- deleteTimer,
- fetchTimer,
- updateTimer,
-} from "../../../../../data/timer";
+import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@@ -69,49 +30,6 @@ import "../../../helpers/forms/ha-timer-form";
import "../../entity-registry-basic-editor";
import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
-const HELPERS = {
- input_boolean: {
- fetch: fetchInputBoolean,
- update: updateInputBoolean,
- delete: deleteInputBoolean,
- },
- input_button: {
- fetch: fetchInputButton,
- update: updateInputButton,
- delete: deleteInputButton,
- },
- input_text: {
- fetch: fetchInputText,
- update: updateInputText,
- delete: deleteInputText,
- },
- input_number: {
- fetch: fetchInputNumber,
- update: updateInputNumber,
- delete: deleteInputNumber,
- },
- input_datetime: {
- fetch: fetchInputDateTime,
- update: updateInputDateTime,
- delete: deleteInputDateTime,
- },
- input_select: {
- fetch: fetchInputSelect,
- update: updateInputSelect,
- delete: deleteInputSelect,
- },
- counter: {
- fetch: fetchCounter,
- update: updateCounter,
- delete: deleteCounter,
- },
- timer: {
- fetch: fetchTimer,
- update: updateTimer,
- delete: deleteTimer,
- },
-};
-
@customElement("entity-settings-helper-tab")
export class EntityRegistrySettingsHelper extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -198,7 +116,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
}
private async _getItem() {
- const items = await HELPERS[this.entry.platform].fetch(this.hass!);
+ const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
this._item = items.find((item) => item.id === this.entry.unique_id) || null;
}
@@ -206,7 +124,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
this._submitting = true;
try {
if (this._componentLoaded && this._item) {
- await HELPERS[this.entry.platform].update(
+ await HELPERS_CRUD[this.entry.platform].update(
this.hass!,
this._item.id,
this._item
@@ -236,7 +154,10 @@ export class EntityRegistrySettingsHelper extends LitElement {
try {
if (this._componentLoaded && this._item) {
- await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
+ await HELPERS_CRUD[this.entry.platform].delete(
+ this.hass!,
+ this._item.id
+ );
} else {
const stateObj = this.hass.states[this.entry.entity_id];
if (!stateObj?.attributes.restored) {
diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts
index 10d3bdf994..de7d374318 100644
--- a/src/panels/config/entities/entity-registry-basic-editor.ts
+++ b/src/panels/config/entities/entity-registry-basic-editor.ts
@@ -1,13 +1,13 @@
-import "../../../components/ha-expansion-panel";
import "@material/mwc-formfield/mwc-formfield";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-area-picker";
+import "../../../components/ha-expansion-panel";
+import "../../../components/ha-radio";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
-import "../../../components/ha-radio";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
@@ -182,9 +182,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
- .disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
- this._device?.disabled_by ||
- (this._disabledBy && this._disabledBy !== "user")}
+ .disabled=${this._device?.disabled_by ||
+ (this._disabledBy &&
+ !(
+ this._disabledBy === "user" ||
+ this._disabledBy === "integration"
+ ))}
@change=${this._viewStatusChanged}
>
@@ -197,9 +200,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="hidden"
.checked=${this._hiddenBy !== null}
- .disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
- Boolean(this._device?.disabled_by) ||
- (this._disabledBy && this._disabledBy !== "user")}
+ .disabled=${this._device?.disabled_by ||
+ (this._disabledBy &&
+ !(
+ this._disabledBy === "user" ||
+ this._disabledBy === "integration"
+ ))}
@change=${this._viewStatusChanged}
>
@@ -212,9 +218,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="disabled"
.checked=${this._disabledBy !== null}
- .disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
- Boolean(this._device?.disabled_by) ||
- (this._disabledBy && this._disabledBy !== "user")}
+ .disabled=${this._device?.disabled_by ||
+ (this._disabledBy &&
+ !(
+ this._disabledBy === "user" ||
+ this._disabledBy === "integration"
+ ))}
@change=${this._viewStatusChanged}
>
diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts
index c4278a7936..34d099db0d 100644
--- a/src/panels/config/entities/entity-registry-settings.ts
+++ b/src/panels/config/entities/entity-registry-settings.ts
@@ -42,6 +42,12 @@ 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 {
+ ConfigEntry,
+ deleteConfigEntry,
+ getConfigEntries,
+} from "../../../data/config_entries";
+import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
const OVERRIDE_DEVICE_CLASSES = {
cover: [
@@ -83,6 +89,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _device?: DeviceRegistryEntry;
+ @state() private _helperConfigEntry?: ConfigEntry;
+
@state() private _error?: string;
@state() private _submitting?: boolean;
@@ -103,6 +111,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
];
}
+ 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 updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
@@ -215,6 +237,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@value-changed=${this._areaPicked}
>`
: ""}
+ ${this._helperConfigEntry
+ ? html`
+
+
+ ${this.hass.localize(
+ "ui.dialogs.entity_registry.editor.configure_state"
+ )}
+
+
+ `
+ : ""}
+
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
@@ -471,13 +508,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._submitting = true;
try {
- await removeEntityRegistryEntry(this.hass!, this._origEntityId);
+ if (this._helperConfigEntry) {
+ await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id);
+ } else {
+ await removeEntityRegistryEntry(this.hass!, this._origEntityId);
+ }
fireEvent(this, "close-dialog");
} finally {
this._submitting = false;
}
}
+ private async _showOptionsFlow() {
+ showOptionsFlowDialog(this, this._helperConfigEntry!);
+ }
+
static get styles(): CSSResultGroup {
return [
haStyle,
diff --git a/src/panels/config/helpers/const.ts b/src/panels/config/helpers/const.ts
index 2e927f66ad..c103332573 100644
--- a/src/panels/config/helpers/const.ts
+++ b/src/panels/config/helpers/const.ts
@@ -1,11 +1,11 @@
-import { Counter } from "../../../data/counter";
-import { InputBoolean } from "../../../data/input_boolean";
-import { InputButton } from "../../../data/input_button";
-import { InputDateTime } from "../../../data/input_datetime";
-import { InputNumber } from "../../../data/input_number";
-import { InputSelect } from "../../../data/input_select";
-import { InputText } from "../../../data/input_text";
-import { Timer } from "../../../data/timer";
+import type { Counter } from "../../../data/counter";
+import type { InputBoolean } from "../../../data/input_boolean";
+import type { InputButton } from "../../../data/input_button";
+import type { InputDateTime } from "../../../data/input_datetime";
+import type { InputNumber } from "../../../data/input_number";
+import type { InputSelect } from "../../../data/input_select";
+import type { InputText } from "../../../data/input_text";
+import type { Timer } from "../../../data/timer";
export const HELPER_DOMAINS = [
"input_boolean",
diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts
index bbd0b645e1..c3adf6a826 100644
--- a/src/panels/config/helpers/dialog-helper-detail.ts
+++ b/src/panels/config/helpers/dialog-helper-detail.ts
@@ -8,6 +8,8 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-dialog";
+import "../../../components/ha-circular-progress";
+import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean";
import { createInputButton } from "../../../data/input_button";
@@ -16,6 +18,7 @@ import { createInputNumber } from "../../../data/input_number";
import { createInputSelect } from "../../../data/input_select";
import { createInputText } from "../../../data/input_text";
import { createTimer } from "../../../data/timer";
+import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { Helper } from "./const";
@@ -27,6 +30,8 @@ import "./forms/ha-input_number-form";
import "./forms/ha-input_select-form";
import "./forms/ha-input_text-form";
import "./forms/ha-timer-form";
+import { domainToName } from "../../../data/integration";
+import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
const HELPERS = {
input_boolean: createInputBoolean,
@@ -47,7 +52,7 @@ export class DialogHelperDetail extends LitElement {
@state() private _opened = false;
- @state() private _platform?: string;
+ @state() private _domain?: string;
@state() private _error?: string;
@@ -55,102 +60,135 @@ export class DialogHelperDetail extends LitElement {
@query(".form") private _form?: HTMLDivElement;
- public async showDialog(): Promise {
- this._platform = undefined;
+ @state() private _helperFlows?: string[];
+
+ private _params?: ShowDialogHelperDetailParams;
+
+ public async showDialog(params: ShowDialogHelperDetailParams): Promise {
+ this._params = params;
+ this._domain = undefined;
this._item = undefined;
this._opened = true;
await this.updateComplete;
+ Promise.all([
+ getConfigFlowHandlers(this.hass, "helper"),
+ // Ensure the titles are loaded before we render the flows.
+ this.hass.loadBackendTranslation("title", undefined, true),
+ ]).then(([flows]) => {
+ this._helperFlows = flows;
+ });
}
public closeDialog(): void {
this._opened = false;
this._error = "";
+ this._params = undefined;
}
protected render(): TemplateResult {
+ let content: TemplateResult;
+
+ if (this._domain) {
+ content = html`
+
+
+ ${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
+
+
+ ${this.hass!.localize("ui.common.back")}
+
+ `;
+ } else if (this._helperFlows === undefined) {
+ content = html``;
+ } else {
+ const items: [string, string][] = [];
+
+ for (const helper of Object.keys(HELPERS)) {
+ items.push([
+ helper,
+ this.hass.localize(`ui.panel.config.helpers.types.${helper}`) ||
+ helper,
+ ]);
+ }
+
+ for (const domain of this._helperFlows) {
+ items.push([domain, domainToName(this.hass.localize, domain)]);
+ }
+
+ items.sort((a, b) => a[1].localeCompare(b[1]));
+
+ content = html`
+ ${items.map(([domain, label]) => {
+ // Only OG helpers need to be loaded prior adding one
+ const isLoaded =
+ !(domain in HELPERS) || isComponentLoaded(this.hass, domain);
+ return html`
+
+
+ ${label}
+
+ ${!isLoaded
+ ? html`
+ ${this.hass.localize(
+ "ui.dialogs.helper_settings.platform_not_loaded",
+ "platform",
+ domain
+ )}
+ `
+ : ""}
+ `;
+ })}
+
+ ${this.hass!.localize("ui.common.cancel")}
+
+ `;
+ }
+
return html`
- ${this._platform
- ? html`
-
-
- ${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
-
-
- ${this.hass!.localize("ui.common.back")}
-
- `
- : html`
- ${Object.keys(HELPERS).map((platform: string) => {
- const isLoaded = isComponentLoaded(this.hass, platform);
- return html`
-
-
-
- ${this.hass.localize(
- `ui.panel.config.helpers.types.${platform}`
- ) || platform}
-
-
- ${!isLoaded
- ? html`
- ${this.hass.localize(
- "ui.dialogs.helper_settings.platform_not_loaded",
- "platform",
- platform
- )}
- `
- : ""}
- `;
- })}
-
- ${this.hass!.localize("ui.common.cancel")}
-
- `}
+ ${content}
`;
}
@@ -160,13 +198,13 @@ export class DialogHelperDetail extends LitElement {
}
private async _createItem(): Promise {
- if (!this._platform || !this._item) {
+ if (!this._domain || !this._item) {
return;
}
this._submitting = true;
this._error = "";
try {
- await HELPERS[this._platform](this.hass, this._item);
+ await HELPERS[this._domain](this.hass, this._item);
this.closeDialog();
} catch (err: any) {
this._error = err.message || "Unknown error";
@@ -181,12 +219,22 @@ export class DialogHelperDetail extends LitElement {
}
ev.stopPropagation();
ev.preventDefault();
- this._platformPicked(ev);
+ this._domainPicked(ev);
}
- private _platformPicked(ev: Event): void {
- this._platform = (ev.currentTarget! as any).platform;
- this._focusForm();
+ private _domainPicked(ev: Event): void {
+ const domain = (ev.currentTarget! as any).domain;
+
+ if (domain in HELPERS) {
+ this._domain = domain;
+ this._focusForm();
+ } else {
+ showConfigFlowDialog(this, {
+ startFlowHandler: domain,
+ dialogClosedCallback: this._params!.dialogClosedCallback,
+ });
+ this.closeDialog();
+ }
}
private async _focusForm(): Promise {
@@ -195,7 +243,7 @@ export class DialogHelperDetail extends LitElement {
}
private _goBack() {
- this._platform = undefined;
+ this._domain = undefined;
this._item = undefined;
this._error = undefined;
}
diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts
index ac58f83e44..4ce9d00118 100644
--- a/src/panels/config/helpers/ha-config-helpers.ts
+++ b/src/panels/config/helpers/ha-config-helpers.ts
@@ -1,28 +1,58 @@
import { mdiPencilOff, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
-import { HassEntity } from "home-assistant-js-websocket";
+import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
-import memoize from "memoize-one";
+import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
+import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
+import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-icon";
import "../../../components/ha-svg-icon";
+import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
+import {
+ EntityRegistryEntry,
+ subscribeEntityRegistry,
+} from "../../../data/entity_registry";
+import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
+import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { configSections } from "../ha-panel-config";
import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
+// This groups items by a key but only returns last entry per key.
+const groupByOne = (
+ items: T[],
+ keySelector: (item: T) => string
+): Record => {
+ const result: Record = {};
+ for (const item of items) {
+ result[keySelector(item)] = item;
+ }
+ return result;
+};
+
+const getConfigEntry = (
+ entityEntries: Record,
+ configEntries: Record,
+ entityId: string
+) => {
+ const configEntryId = entityEntries![entityId]?.config_entry_id;
+ return configEntryId ? configEntries![configEntryId] : undefined;
+};
+
@customElement("ha-config-helpers")
-export class HaConfigHelpers extends LitElement {
+export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@@ -33,98 +63,122 @@ export class HaConfigHelpers extends LitElement {
@state() private _stateItems: HassEntity[] = [];
- private _columns = memoize((narrow, _language): DataTableColumnContainer => {
- const columns: DataTableColumnContainer = {
- icon: {
+ @state() private _entityEntries?: Record;
+
+ @state() private _configEntries?: Record;
+
+ private _columns = memoizeOne(
+ (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
+ const columns: DataTableColumnContainer = {
+ icon: {
+ title: "",
+ label: localize("ui.panel.config.helpers.picker.headers.icon"),
+ type: "icon",
+ template: (icon, helper: any) =>
+ icon
+ ? html` `
+ : html``,
+ },
+ name: {
+ title: localize("ui.panel.config.helpers.picker.headers.name"),
+ sortable: true,
+ filterable: true,
+ grows: true,
+ direction: "asc",
+ template: (name, item: any) =>
+ html`
+ ${name}
+ ${narrow
+ ? html` ${item.entity_id}
`
+ : ""}
+ `,
+ },
+ };
+ if (!narrow) {
+ columns.entity_id = {
+ title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
+ sortable: true,
+ filterable: true,
+ width: "25%",
+ };
+ }
+ columns.type = {
+ title: localize("ui.panel.config.helpers.picker.headers.type"),
+ sortable: true,
+ width: "25%",
+ filterable: true,
+ template: (type, row) =>
+ row.configEntry
+ ? domainToName(localize, type)
+ : html`
+ ${localize(`ui.panel.config.helpers.types.${type}`) || type}
+ `,
+ };
+ columns.editable = {
title: "",
label: this.hass.localize(
- "ui.panel.config.helpers.picker.headers.icon"
+ "ui.panel.config.helpers.picker.headers.editable"
),
type: "icon",
- template: (icon, helper: any) =>
- icon
- ? html` `
- : html``,
- },
- name: {
- title: this.hass.localize(
- "ui.panel.config.helpers.picker.headers.name"
- ),
- sortable: true,
- filterable: true,
- grows: true,
- direction: "asc",
- template: (name, item: any) =>
- html`
- ${name}
- ${narrow
- ? html` ${item.entity_id}
`
- : ""}
- `,
- },
- };
- if (!narrow) {
- columns.entity_id = {
- title: this.hass.localize(
- "ui.panel.config.helpers.picker.headers.entity_id"
- ),
- sortable: true,
- filterable: true,
- width: "25%",
- };
- }
- columns.type = {
- title: this.hass.localize("ui.panel.config.helpers.picker.headers.type"),
- sortable: true,
- width: "25%",
- filterable: true,
- template: (type) =>
- html`
- ${this.hass.localize(`ui.panel.config.helpers.types.${type}`) || type}
+ template: (editable) => html`
+ ${!editable
+ ? html`
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.entities.picker.status.readonly"
+ )}
+
+
+ `
+ : ""}
`,
- };
- columns.editable = {
- title: "",
- label: this.hass.localize(
- "ui.panel.config.helpers.picker.headers.editable"
- ),
- type: "icon",
- template: (editable) => html`
- ${!editable
- ? html`
-
-
-
- ${this.hass.localize(
- "ui.panel.config.entities.picker.status.readonly"
- )}
-
-
- `
- : ""}
- `,
- };
- return columns;
- });
+ };
+ return columns;
+ }
+ );
- private _getItems = memoize((stateItems: HassEntity[]) =>
- stateItems.map((entityState) => ({
- id: entityState.entity_id,
- icon: entityState.attributes.icon,
- name: entityState.attributes.friendly_name || "",
- entity_id: entityState.entity_id,
- editable: entityState.attributes.editable,
- type: computeStateDomain(entityState),
- }))
+ private _getItems = memoizeOne(
+ (
+ stateItems: HassEntity[],
+ entityEntries: Record,
+ configEntries: Record
+ ) =>
+ stateItems.map((entityState) => {
+ const configEntry = getConfigEntry(
+ entityEntries,
+ configEntries,
+ entityState.entity_id
+ );
+
+ return {
+ id: entityState.entity_id,
+ icon: entityState.attributes.icon,
+ name: entityState.attributes.friendly_name || "",
+ entity_id: entityState.entity_id,
+ editable:
+ configEntry !== undefined || entityState.attributes.editable,
+ type: configEntry
+ ? configEntry.domain
+ : computeStateDomain(entityState),
+ configEntry,
+ };
+ })
);
protected render(): TemplateResult {
- if (!this.hass || this._stateItems === undefined) {
+ if (
+ !this.hass ||
+ this._stateItems === undefined ||
+ this._entityEntries === undefined ||
+ this._configEntries === undefined
+ ) {
return html` `;
}
@@ -135,8 +189,12 @@ export class HaConfigHelpers extends LitElement {
back-path="/config"
.route=${this.route}
.tabs=${configSections.automations}
- .columns=${this._columns(this.narrow, this.hass.language)}
- .data=${this._getItems(this._stateItems)}
+ .columns=${this._columns(this.narrow, this.hass.localize)}
+ .data=${this._getItems(
+ this._stateItems,
+ this._entityEntries,
+ this._configEntries
+ )}
@row-click=${this._openEditDialog}
hasFab
clickable
@@ -160,32 +218,67 @@ export class HaConfigHelpers extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
- this._getStates();
+ this._getConfigEntries();
}
- protected updated(changedProps: PropertyValues) {
- super.updated(changedProps);
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (oldHass && this._stateItems) {
- this._getStates(oldHass);
+ protected willUpdate(changedProps: PropertyValues) {
+ super.willUpdate(changedProps);
+
+ if (!this._entityEntries || !this._configEntries) {
+ return;
+ }
+
+ let changed =
+ !this._stateItems ||
+ changedProps.has("_entityEntries") ||
+ changedProps.has("_configEntries");
+
+ if (!changed && changedProps.has("hass")) {
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ changed = !oldHass || oldHass.states !== this.hass.states;
+ }
+ if (!changed) {
+ return;
+ }
+
+ const extraEntities = new Set();
+
+ for (const entityEntry of Object.values(this._entityEntries)) {
+ if (
+ entityEntry.config_entry_id &&
+ entityEntry.config_entry_id in this._configEntries
+ ) {
+ extraEntities.add(entityEntry.entity_id);
+ }
+ }
+
+ const newStates = Object.values(this.hass!.states).filter(
+ (entity) =>
+ extraEntities.has(entity.entity_id) ||
+ HELPER_DOMAINS.includes(computeStateDomain(entity))
+ );
+
+ if (
+ this._stateItems.length !== newStates.length ||
+ !this._stateItems.every((val, idx) => newStates[idx] === val)
+ ) {
+ this._stateItems = newStates;
}
}
- private _getStates(oldHass?: HomeAssistant) {
- let changed = false;
- const tempStates = Object.values(this.hass!.states).filter((entity) => {
- if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) {
- return false;
- }
- if (oldHass?.states[entity.entity_id] !== entity) {
- changed = true;
- }
- return true;
- });
+ public hassSubscribe(): UnsubscribeFunc[] {
+ return [
+ subscribeEntityRegistry(this.hass.connection!, (entries) => {
+ this._entityEntries = groupByOne(entries, (entry) => entry.entity_id);
+ }),
+ ];
+ }
- if (changed || this._stateItems.length !== tempStates.length) {
- this._stateItems = tempStates;
- }
+ private async _getConfigEntries() {
+ this._configEntries = groupByOne(
+ await getConfigEntries(this.hass, { type: "helper" }),
+ (entry) => entry.entry_id
+ );
}
private async _openEditDialog(ev: CustomEvent): Promise {
@@ -196,6 +289,12 @@ export class HaConfigHelpers extends LitElement {
}
private _createHelpler() {
- showHelperDetailDialog(this);
+ showHelperDetailDialog(this, {
+ dialogClosedCallback: (params) => {
+ if (params.flowFinished) {
+ this._getConfigEntries();
+ }
+ },
+ });
}
}
diff --git a/src/panels/config/helpers/show-dialog-helper-detail.ts b/src/panels/config/helpers/show-dialog-helper-detail.ts
index 959f92ad75..83fbbce4ee 100644
--- a/src/panels/config/helpers/show-dialog-helper-detail.ts
+++ b/src/panels/config/helpers/show-dialog-helper-detail.ts
@@ -1,11 +1,20 @@
import { fireEvent } from "../../../common/dom/fire_event";
+import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
export const loadHelperDetailDialog = () => import("./dialog-helper-detail");
-export const showHelperDetailDialog = (element: HTMLElement) => {
+export interface ShowDialogHelperDetailParams {
+ // Only used for config entries
+ dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"];
+}
+
+export const showHelperDetailDialog = (
+ element: HTMLElement,
+ params: ShowDialogHelperDetailParams
+) => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-helper-detail",
dialogImport: loadHelperDetailDialog,
- dialogParams: {},
+ dialogParams: params,
});
};
diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts
index 5255d1197e..c860953efc 100644
--- a/src/panels/config/integrations/ha-config-integrations.ts
+++ b/src/panels/config/integrations/ha-config-integrations.ts
@@ -521,24 +521,26 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
private _loadConfigEntries() {
- getConfigEntries(this.hass).then((configEntries) => {
- this._configEntries = configEntries
- .map(
- (entry: ConfigEntry): ConfigEntryExtended => ({
- ...entry,
- localized_domain_name: domainToName(
- this.hass.localize,
- entry.domain
- ),
- })
- )
- .sort((conf1, conf2) =>
- caseInsensitiveStringCompare(
- conf1.localized_domain_name + conf1.title,
- conf2.localized_domain_name + conf2.title
+ getConfigEntries(this.hass, { type: "integration" }).then(
+ (configEntries) => {
+ this._configEntries = configEntries
+ .map(
+ (entry: ConfigEntry): ConfigEntryExtended => ({
+ ...entry,
+ localized_domain_name: domainToName(
+ this.hass.localize,
+ entry.domain
+ ),
+ })
)
- );
- });
+ .sort((conf1, conf2) =>
+ caseInsensitiveStringCompare(
+ conf1.localized_domain_name + conf1.title,
+ conf2.localized_domain_name + conf2.title
+ )
+ );
+ }
+ );
}
private async _scanUSBDevices() {
@@ -656,7 +658,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (!domain) {
return;
}
- const handlers = await getConfigFlowHandlers(this.hass);
+ const handlers = await getConfigFlowHandlers(this.hass, "integration");
if (!handlers.includes(domain)) {
showAlertDialog(this, {
diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts
index 9337854c8e..eaa03dc98f 100644
--- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts
+++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts
@@ -111,7 +111,9 @@ class HaPanelDevMqtt extends LitElement {
return;
}
const configEntryId = searchParams.get("config_entry") as string;
- const configEntries = await getConfigEntries(this.hass);
+ const configEntries = await getConfigEntries(this.hass, {
+ domain: "mqtt",
+ });
const configEntry = configEntries.find(
(entry) => entry.entry_id === configEntryId
);
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
index 3c75b59df0..9b5a5c8e37 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
@@ -384,7 +384,9 @@ class ZWaveJSConfigDashboard extends LitElement {
if (!this.configEntryId) {
return;
}
- const configEntries = await getConfigEntries(this.hass);
+ const configEntries = await getConfigEntries(this.hass, {
+ domain: "zwave_js",
+ });
this._configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId!
);
@@ -467,7 +469,9 @@ class ZWaveJSConfigDashboard extends LitElement {
if (!this.configEntryId) {
return;
}
- const configEntries = await getConfigEntries(this.hass);
+ const configEntries = await getConfigEntries(this.hass, {
+ domain: "zwave_js",
+ });
const configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId
);
diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
index 4b48ebb116..3183133beb 100644
--- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts
+++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
@@ -1,10 +1,12 @@
import "@material/mwc-button/mwc-button";
+import { mdiSlopeUphill } from "@mdi/js";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
+import "../../../components/ha-icon-overflow-menu";
import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
@@ -24,6 +26,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta";
+import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
const FIX_ISSUES_ORDER = {
no_state: 0,
@@ -111,6 +114,30 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
: ""}`,
width: "113px",
},
+ actions: {
+ title: "",
+ type: "overflow-menu",
+ template: (
+ _info,
+ statistic: StatisticsMetaData
+ ) => html`
+ showStatisticsAdjustSumDialog(this, {
+ statistic: statistic,
+ }),
+ },
+ ]}
+ style="color: var(--secondary-text-color)"
+ >`,
+ },
})
);
diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
new file mode 100644
index 0000000000..9819177799
--- /dev/null
+++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
@@ -0,0 +1,166 @@
+import "@material/mwc-button/mwc-button";
+import { LitElement, TemplateResult, html, CSSResultGroup } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import memoizeOne from "memoize-one";
+import "../../../components/ha-dialog";
+import { fireEvent } from "../../../common/dom/fire_event";
+import { haStyle, haStyleDialog } from "../../../resources/styles";
+import { HomeAssistant } from "../../../types";
+import "../../../components/ha-formfield";
+import "../../../components/ha-radio";
+import "../../../components/ha-form/ha-form";
+import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum";
+import type {
+ HaFormBaseSchema,
+ HaFormSchema,
+} from "../../../components/ha-form/types";
+import { adjustStatisticsSum } from "../../../data/history";
+import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
+import { showToast } from "../../../util/toast";
+
+let lastMoment: string | undefined;
+
+@customElement("dialog-statistics-adjust-sum")
+export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _params?: DialogStatisticsAdjustSumParams;
+
+ @state() private _data?: {
+ moment: string;
+ amount: number;
+ };
+
+ @state() private _busy = false;
+
+ public showDialog(params: DialogStatisticsAdjustSumParams): void {
+ this._params = params;
+ this._busy = false;
+ const now = new Date();
+ this._data = {
+ moment:
+ lastMoment ||
+ `${now.getFullYear()}-${
+ now.getMonth() + 1
+ }-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`,
+ amount: 0,
+ };
+ }
+
+ public closeDialog(): void {
+ this._params = undefined;
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this._params) {
+ return html``;
+ }
+
+ return html`
+
+
+
+
+
+
+ `;
+ }
+
+ private _getSchema = memoizeOne((statistic): HaFormSchema[] => [
+ {
+ type: "constant",
+ name: "name",
+ value: statistic.name || statistic.statistic_id,
+ },
+ {
+ name: "moment",
+ required: true,
+ selector: {
+ datetime: {},
+ },
+ },
+ {
+ name: "amount",
+ required: true,
+ default: 0,
+ selector: {
+ number: {
+ mode: "box",
+ step: 0.1,
+ unit_of_measurement: statistic.unit_of_measurement,
+ },
+ },
+ },
+ ]);
+
+ private _computeLabel(value: HaFormBaseSchema) {
+ switch (value.name) {
+ case "name":
+ return "Statistic";
+ case "moment":
+ return "Moment to adjust";
+ case "amount":
+ return "Amount";
+ default:
+ return value.name;
+ }
+ }
+
+ private _valueChanged(ev) {
+ this._data = ev.detail.value;
+ }
+
+ private async _fixIssue(): Promise {
+ this._busy = true;
+ try {
+ await adjustStatisticsSum(
+ this.hass,
+ this._params!.statistic.statistic_id,
+ this._data!.moment,
+ this._data!.amount
+ );
+ } catch (err: any) {
+ this._busy = false;
+ showAlertDialog(this, {
+ text: `Error adjusting sum: ${err.message || err}`,
+ });
+ return;
+ }
+ showToast(this, {
+ message: "Statistic sum adjusted",
+ });
+ lastMoment = this._data!.moment;
+ this.closeDialog();
+ }
+
+ static get styles(): CSSResultGroup {
+ return [haStyle, haStyleDialog];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-statistics-adjust-sum": DialogStatisticsFixUnsupportedUnitMetadata;
+ }
+}
diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts
index 1ce0c607ce..3168bffc9f 100644
--- a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts
+++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts
@@ -11,7 +11,7 @@ import {
} from "../../../data/history";
import "../../../components/ha-formfield";
import "../../../components/ha-radio";
-import { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed";
+import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed";
@customElement("dialog-statistics-fix-units-changed")
export class DialogStatisticsFixUnitsChanged extends LitElement {
diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts
index 93d1e320b1..4bfaebe489 100644
--- a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts
+++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts
@@ -8,7 +8,7 @@ import { HomeAssistant } from "../../../types";
import { updateStatisticsMetadata } from "../../../data/history";
import "../../../components/ha-formfield";
import "../../../components/ha-radio";
-import { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta";
+import type { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta";
@customElement("dialog-statistics-fix-unsupported-unit-meta")
export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts
new file mode 100644
index 0000000000..1db2c76307
--- /dev/null
+++ b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts
@@ -0,0 +1,20 @@
+import { fireEvent } from "../../../common/dom/fire_event";
+import { StatisticsMetaData } from "../../../data/history";
+
+export const loadAdjustSumDialog = () =>
+ import("./dialog-statistics-adjust-sum");
+
+export interface DialogStatisticsAdjustSumParams {
+ statistic: StatisticsMetaData;
+}
+
+export const showStatisticsAdjustSumDialog = (
+ element: HTMLElement,
+ detailParams: DialogStatisticsAdjustSumParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-statistics-adjust-sum",
+ dialogImport: loadAdjustSumDialog,
+ dialogParams: detailParams,
+ });
+};
diff --git a/src/panels/lovelace/editor/config-elements/config-elements-style.ts b/src/panels/lovelace/editor/config-elements/config-elements-style.ts
index 061d3aff36..2931974935 100644
--- a/src/panels/lovelace/editor/config-elements/config-elements-style.ts
+++ b/src/panels/lovelace/editor/config-elements/config-elements-style.ts
@@ -1,6 +1,10 @@
import { css } from "lit";
export const configElementStyle = css`
+ .card-config {
+ /* Cancels overlapping Margins for HAForm + Card Config options */
+ overflow: auto;
+ }
ha-switch {
padding: 16px 6px;
}
@@ -25,5 +29,6 @@ export const configElementStyle = css`
ha-textfield,
ha-icon-picker {
margin-top: 8px;
+ display: block;
}
`;
diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts
index e5f8525d99..2445a1e045 100644
--- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts
@@ -1,22 +1,22 @@
+import type { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
-import { assert, boolean, object, optional, string, assign } from "superstruct";
-import type { HassEntity } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
+import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
+import { computeDomain } from "../../../../common/entity/compute_domain";
+import { domainIcon } from "../../../../common/entity/domain_icon";
+import "../../../../components/ha-form/ha-form";
+import type { HaFormSchema } from "../../../../components/ha-form/types";
import { ActionConfig } from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import type { ButtonCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
-import "../../../../components/ha-form/ha-form";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
+import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
-import { baseLovelaceCardConfig } from "../structs/base-card-struct";
-import { computeDomain } from "../../../../common/entity/compute_domain";
-import { domainIcon } from "../../../../common/entity/domain_icon";
-import type { HaFormSchema } from "../../../../components/ha-form/types";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -149,38 +149,36 @@ export class HuiButtonCardEditor
@value-changed=${this._valueChanged}
>
`;
}
diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts
index 0df731ba97..a5e156c42d 100644
--- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts
@@ -1,6 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
-import { assert, object, optional, string, assign } from "superstruct";
+import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { ActionConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
@@ -9,9 +9,9 @@ import "../../components/hui-action-editor";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
+import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
-import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -78,32 +78,30 @@ export class HuiPictureCardEditor
.configValue=${"theme"}
@value-changed=${this._valueChanged}
>
-
-
-
-
+
+
`;
}
diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts
index 0d315cc889..ea529a2a35 100644
--- a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts
@@ -108,32 +108,30 @@ export class HuiPictureEntityCardEditor
@value-changed=${this._valueChanged}
>
`;
}
diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts
index adcd07a57e..d6c6e1a0ac 100644
--- a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts
@@ -1,13 +1,13 @@
-import "../../components/hui-action-editor";
-import "../../../../components/ha-form/ha-form";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { array, assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
+import "../../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { ActionConfig } from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import type { PictureGlanceCardConfig } from "../../cards/types";
+import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import type { EntityConfig } from "../../entity-rows/types";
import type { LovelaceCardEditor } from "../../types";
@@ -96,28 +96,26 @@ export class HuiPictureGlanceCardEditor
@value-changed=${this._valueChanged}
>