From f59cb661cd0ad522e3d686faf775371d80cf3ea6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Feb 2022 14:27:38 -0800 Subject: [PATCH 01/19] Add a docs icon to the config flow dialog --- .../config-flow/dialog-data-entry-flow.ts | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 29bea7491e..dc1edf1005 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import { mdiClose } from "@mdi/js"; +import { mdiClose, mdiHelpCircleOutline } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -33,6 +33,7 @@ import { } from "../../data/device_registry"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; +import { documentationUrl } from "../../util/documentation-url"; import { showAlertDialog } from "../generic/show-dialog-box"; import { DataEntryFlowDialogParams, @@ -231,14 +232,33 @@ class DataEntryFlowDialog extends LitElement { // to reset the element. "" : html` - +
+ ${this._step + ? html` + + ` + : ""} + +
${this._step === null ? this._handler ? html` * { + color: var(--secondary-text-color); + } `, ]; } From 35a41b3490aa8accf017e937b5188164162e16f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Feb 2022 08:35:29 -0800 Subject: [PATCH 02/19] Use same help icon everywhere --- src/dialogs/config-flow/dialog-data-entry-flow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index dc1edf1005..d41cdad08e 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import { mdiClose, mdiHelpCircleOutline } from "@mdi/js"; +import { mdiClose, mdiHelpCircle } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -244,7 +244,7 @@ class DataEntryFlowDialog extends LitElement { rel="noreferrer noopener" > From 6bf2111a3c2cbbc51efe76d44a0f774ab4b6a4e5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 08:23:05 +0100 Subject: [PATCH 03/19] Upload release assets (#11566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- .github/workflows/release.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1ae009b033..c7d3d8fa9c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,10 +10,18 @@ env: NODE_VERSION: 14 NODE_OPTIONS: --max_old_space_size=6144 +# Set default workflow permissions +# All scopes not mentioned here are set to no access +# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +permissions: + actions: none + jobs: release: name: Release runs-on: ubuntu-latest + permissions: + contents: write # Required to upload release assets steps: - name: Checkout the repository uses: actions/checkout@v2 @@ -47,6 +55,13 @@ jobs: script/release + - name: Upload release assets + uses: softprops/action-gh-release@v0.1.14 + with: + files: | + dist/*.whl + dist/*.tar.gz + wheels-init: name: Init wheels build needs: release From fa537968c41fdcecf8d6b048874e5e9da47ff096 Mon Sep 17 00:00:00 2001 From: Nick Iacullo Date: Mon, 21 Mar 2022 10:28:24 +0000 Subject: [PATCH 04/19] Update styles for hui-editor Update the background-color and text-color of the app-toolbar in hui-editor to match the styles of hui-root while in edit-mode. Previously, these properties were set using undefined css variables that could not be changed via themes (--dark-background-color and --dark-text-color). --- src/panels/lovelace/hui-editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts index 17f35b3d10..ce6c15e99e 100644 --- a/src/panels/lovelace/hui-editor.ts +++ b/src/panels/lovelace/hui-editor.ts @@ -148,8 +148,8 @@ class LovelaceFullConfigEditor extends LitElement { } app-toolbar { - background-color: var(--dark-background-color, #455a64); - color: var(--dark-text-color); + background-color: var(--app-header-edit-background-color, #455a64); + color: var(--app-header-edit-text-color, #fff); } mwc-button[disabled] { From ccf1fb573a3c2c395f50e5ffcab0528dbb6011c5 Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 22 Mar 2022 05:15:28 +0100 Subject: [PATCH 05/19] Fix gas energy graph units if stats added by external source (#11892) --- src/data/energy.ts | 34 ++++++++++++++++++- src/data/history.ts | 9 +++++ .../components/ha-energy-gas-settings.ts | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index d86419e9b2..7fd941b325 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -12,7 +12,12 @@ import { subscribeOne } from "../common/util/subscribe-one"; import { HomeAssistant } from "../types"; import { ConfigEntry, getConfigEntries } from "./config_entries"; import { subscribeEntityRegistry } from "./entity_registry"; -import { fetchStatistics, Statistics } from "./history"; +import { + fetchStatistics, + Statistics, + StatisticsMetaData, + getStatisticMetadata, +} from "./history"; const energyCollectionKeys: (string | undefined)[] = []; @@ -136,6 +141,7 @@ export interface GasSourceTypeEnergyPreference { entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; + unit_of_measurement?: string | null; } type EnergySource = @@ -271,6 +277,15 @@ const getEnergyData = async ( const consumptionStatIDs: string[] = []; const statIDs: string[] = []; + const gasSources: GasSourceTypeEnergyPreference[] = + prefs.energy_sources.filter( + (source) => source.type === "gas" + ) as GasSourceTypeEnergyPreference[]; + const gasStatisticIdsWithMeta: StatisticsMetaData[] = + await getStatisticMetadata( + hass, + gasSources.map((source) => source.stat_energy_from) + ); for (const source of prefs.energy_sources) { if (source.type === "solar") { @@ -280,6 +295,20 @@ const getEnergyData = async ( if (source.type === "gas") { statIDs.push(source.stat_energy_from); + const entity = hass.states[source.stat_energy_from]; + if (!entity) { + for (const statisticIdWithMeta of gasStatisticIdsWithMeta) { + if ( + statisticIdWithMeta?.statistic_id === source.stat_energy_from && + statisticIdWithMeta?.unit_of_measurement + ) { + source.unit_of_measurement = + statisticIdWithMeta?.unit_of_measurement === "Wh" + ? "kWh" + : statisticIdWithMeta?.unit_of_measurement; + } + } + } if (source.stat_cost) { statIDs.push(source.stat_cost); } @@ -559,6 +588,9 @@ export const getEnergyGasUnit = ( ? "kWh" : entity.attributes.unit_of_measurement; } + if (source.unit_of_measurement) { + return source.unit_of_measurement; + } } return undefined; }; diff --git a/src/data/history.ts b/src/data/history.ts index c81c592637..eae1b3c9b3 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -373,6 +373,15 @@ export const getStatisticIds = ( statistic_type, }); +export const getStatisticMetadata = ( + hass: HomeAssistant, + statistic_ids?: string[] +) => + hass.callWS({ + type: "recorder/get_statistics_metadata", + statistic_ids, + }); + export const fetchStatistics = ( hass: HomeAssistant, startTime: Date, 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), From 6ac51ede52350ca7a88800af0eb9c924d7044b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 22 Mar 2022 16:00:43 +0100 Subject: [PATCH 06/19] Change Netlify preview URL (#12095) --- gallery/script/netlify_build_gallery | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gallery/script/netlify_build_gallery b/gallery/script/netlify_build_gallery index a5732d4c83..db8be615d1 100755 --- a/gallery/script/netlify_build_gallery +++ b/gallery/script/netlify_build_gallery @@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" gulp build-gallery if [ $? -eq 0 ]; then - createStatus "success" "Build complete" "$DEPLOY_URL" + createStatus "success" "Build complete" "$DEPLOY_PRIME_URL" else createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" fi From 0e0e07437fa4b67beb89f00b99312d398ddfd12a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 10:08:43 -0500 Subject: [PATCH 07/19] Update src/dialogs/config-flow/dialog-data-entry-flow.ts --- src/dialogs/config-flow/dialog-data-entry-flow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index d41cdad08e..604d073345 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -243,7 +243,7 @@ class DataEntryFlowDialog extends LitElement { target="_blank" rel="noreferrer noopener" > Date: Tue, 22 Mar 2022 10:57:09 -0500 Subject: [PATCH 08/19] Stack Action Inputs in the Button Editor (#12076) * Stack Action Inputs in the Button Editor * update style * Update for other editors --- .../config-elements/config-elements-style.ts | 5 ++ .../config-elements/hui-button-card-editor.ts | 76 +++++++++---------- .../hui-picture-card-editor.ts | 54 +++++++------ .../hui-picture-entity-card-editor.ts | 50 ++++++------ .../hui-picture-glance-card-editor.ts | 46 ++++++----- 5 files changed, 114 insertions(+), 117 deletions(-) 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} >
-
- - -
+ + Date: Tue, 22 Mar 2022 18:23:54 +0100 Subject: [PATCH 09/19] Add support for update entities (#12059) * Add support for update entities * Apply suggestions from code review Co-authored-by: Zack Barett * Add to gallery * implement xx% * Adjustments for skipped * Add progress bar * Add UPDATE_SUPPORT_INSTALL * Allow skipping without install support * Add version to service call if supported * Adjust changelog link * Use Installing * adjustments * Use unavailable Co-authored-by: Zack Barett --- gallery/src/pages/more-info/update.markdown | 3 + gallery/src/pages/more-info/update.ts | 140 ++++++++++++ src/common/const.ts | 2 + src/common/entity/compute_state_display.ts | 28 +++ src/common/entity/domain_icon.ts | 10 + src/data/update.ts | 36 +++ .../more-info/controls/more-info-update.ts | 212 ++++++++++++++++++ .../more-info/state_more_info_control.ts | 1 + src/translations/en.json | 13 ++ 9 files changed, 445 insertions(+) create mode 100644 gallery/src/pages/more-info/update.markdown create mode 100644 gallery/src/pages/more-info/update.ts create mode 100644 src/data/update.ts create mode 100644 src/dialogs/more-info/controls/more-info-update.ts diff --git a/gallery/src/pages/more-info/update.markdown b/gallery/src/pages/more-info/update.markdown new file mode 100644 index 0000000000..e7540412e3 --- /dev/null +++ b/gallery/src/pages/more-info/update.markdown @@ -0,0 +1,3 @@ +--- +title: Update +--- diff --git a/gallery/src/pages/more-info/update.ts b/gallery/src/pages/more-info/update.ts new file mode 100644 index 0000000000..1488e318dc --- /dev/null +++ b/gallery/src/pages/more-info/update.ts @@ -0,0 +1,140 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { + UPDATE_SUPPORT_BACKUP, + UPDATE_SUPPORT_PROGRESS, + UPDATE_SUPPORT_INSTALL, +} from "../../../../src/data/update"; +import "../../../../src/dialogs/more-info/more-info-content"; +import { getEntity } from "../../../../src/fake_data/entity"; +import { + MockHomeAssistant, + provideHass, +} from "../../../../src/fake_data/provide_hass"; +import "../../components/demo-more-infos"; + +const base_attributes = { + title: "Awesome", + current_version: "1.2.2", + latest_version: "1.2.3", + release_url: "https://home-assistant.io", + supported_features: UPDATE_SUPPORT_INSTALL, + skipped_version: null, + in_progress: false, + release_summary: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.", +}; + +const ENTITIES = [ + getEntity("update", "update1", "on", { + ...base_attributes, + friendly_name: "Update", + }), + getEntity("update", "update2", "on", { + ...base_attributes, + title: null, + friendly_name: "Update without title", + }), + getEntity("update", "update3", "on", { + ...base_attributes, + release_url: null, + friendly_name: "Update without release_url", + }), + getEntity("update", "update4", "on", { + ...base_attributes, + release_summary: null, + friendly_name: "Update without release_summary", + }), + getEntity("update", "update5", "off", { + ...base_attributes, + current_version: "1.2.3", + friendly_name: "No update", + }), + getEntity("update", "update6", "off", { + ...base_attributes, + skipped_version: "1.2.3", + friendly_name: "Skipped version", + }), + getEntity("update", "update7", "on", { + ...base_attributes, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_BACKUP, + friendly_name: "With backup support", + }), + getEntity("update", "update8", "on", { + ...base_attributes, + in_progress: true, + friendly_name: "With true in_progress", + }), + getEntity("update", "update9", "on", { + ...base_attributes, + in_progress: 25, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 25 in_progress", + }), + getEntity("update", "update10", "on", { + ...base_attributes, + in_progress: 50, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 50 in_progress", + }), + getEntity("update", "update11", "on", { + ...base_attributes, + in_progress: 75, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 75 in_progress", + }), + getEntity("update", "update12", "unavailable", { + ...base_attributes, + in_progress: 50, + friendly_name: "Unavailable", + }), + getEntity("update", "update13", "on", { + ...base_attributes, + supported_features: 0, + friendly_name: "No install support", + }), + getEntity("update", "update14", "off", { + ...base_attributes, + current_version: null, + friendly_name: "Update without current_version", + }), + getEntity("update", "update15", "off", { + ...base_attributes, + latest_version: null, + friendly_name: "Update without latest_version", + }), +]; + +@customElement("demo-more-info-update") +class DemoMoreInfoUpdate extends LitElement { + @property() public hass!: MockHomeAssistant; + + @query("demo-more-infos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html` + ent.entityId)} + > + `; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.addEntities(ENTITIES); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-more-info-update": DemoMoreInfoUpdate; + } +} diff --git a/src/common/const.ts b/src/common/const.ts index f11e7d7380..ee9f94000c 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [ "scene", "sun", "timer", + "update", "vacuum", "water_heater", "weather", @@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [ "input_text", "number", "scene", + "update", "select", ]; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 3838218ea4..e78b889457 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -1,12 +1,18 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { FrontendLocaleData } from "../../data/translation"; +import { + updateIsInstalling, + UpdateEntity, + UPDATE_SUPPORT_PROGRESS, +} from "../../data/update"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; import { formatNumber, isNumericState } from "../number/format_number"; import { LocalizeFunc } from "../translations/localize"; import { computeStateDomain } from "./compute_state_domain"; +import { supportsFeature } from "./supports-feature"; export const computeStateDisplay = ( localize: LocalizeFunc, @@ -130,6 +136,28 @@ export const computeStateDisplay = ( } } + if (domain === "update") { + // When updating, and entity does not support % show "Installing" + // When updating, and entity does support % show "Installing (xx%)" + // When update available, show the version + // When the latest version is skipped, show the latest version + // When update is not available, show "Up-to-date" + // When update is not available and there is no latest_version show "Unavailable" + return compareState === "on" + ? updateIsInstalling(stateObj as UpdateEntity) + ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) + ? localize("ui.card.update.installing_with_progress", { + progress: stateObj.attributes.in_progress, + }) + : localize("ui.card.update.installing") + : stateObj.attributes.latest_version + : stateObj.attributes.skipped_version === + stateObj.attributes.latest_version + ? stateObj.attributes.latest_version ?? + localize("state.default.unavailable") + : localize("ui.card.update.up_to_date"); + } + return ( // Return device class translation (stateObj.attributes.device_class && diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 40b0106216..af89f00d16 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -26,8 +26,11 @@ import { mdiCheckCircleOutline, mdiCloseCircleOutline, mdiWeatherNight, + mdiPackage, + mdiPackageDown, } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; +import { updateIsInstalling, UpdateEntity } from "../../data/update"; /** * Return the icon to be used for a domain. * @@ -133,6 +136,13 @@ export const domainIcon = ( return stateObj?.state === "above_horizon" ? FIXED_DOMAIN_ICONS[domain] : mdiWeatherNight; + + case "update": + return compareState === "on" + ? updateIsInstalling(stateObj as UpdateEntity) + ? mdiPackageDown + : mdiPackageUp + : mdiPackage; } if (domain in FIXED_DOMAIN_ICONS) { diff --git a/src/data/update.ts b/src/data/update.ts new file mode 100644 index 0000000000..22891a92ac --- /dev/null +++ b/src/data/update.ts @@ -0,0 +1,36 @@ +import type { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; +import { supportsFeature } from "../common/entity/supports-feature"; + +export const UPDATE_SUPPORT_INSTALL = 1; +export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2; +export const UPDATE_SUPPORT_PROGRESS = 4; +export const UPDATE_SUPPORT_BACKUP = 8; + +interface UpdateEntityAttributes extends HassEntityAttributeBase { + current_version: string | null; + in_progress: boolean | number; + latest_version: string | null; + release_summary: string | null; + release_url: string | null; + skipped_version: string | null; + title: string | null; +} + +export interface UpdateEntity extends HassEntityBase { + attributes: UpdateEntityAttributes; +} + +export const updateUsesProgress = (entity: UpdateEntity): boolean => + supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) && + typeof entity.attributes.in_progress === "number"; + +export const updateCanInstall = (entity: UpdateEntity): boolean => + supportsFeature(entity, UPDATE_SUPPORT_INSTALL) && + entity.attributes.latest_version !== entity.attributes.current_version && + entity.attributes.latest_version !== entity.attributes.skipped_version; + +export const updateIsInstalling = (entity: UpdateEntity): boolean => + updateUsesProgress(entity) || !!entity.attributes.in_progress; 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/translations/en.json b/src/translations/en.json index 7b597b64c7..bb75c9224e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -228,6 +228,11 @@ "service": { "run": "Run" }, + "update": { + "installing": "Installing", + "installing_with_progress": "Installing ({progress}%)", + "up_to_date": "Up-to-date" + }, "timer": { "actions": { "start": "start", @@ -719,6 +724,14 @@ "rising": "Rising", "setting": "Setting" }, + "update": { + "current_version": "Current version", + "latest_version": "Latest version", + "release_announcement": "Read release announcement", + "skip": "Skip", + "install": "Install", + "create_backup": "Create backup before updating" + }, "updater": { "title": "Update Instructions" }, From 73f5580555340302038f327e54822cce61477495 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Mar 2022 12:47:12 -0700 Subject: [PATCH 10/19] Add support for integration type (#12077) --- src/common/entity/compute_state_domain.ts | 2 +- .../ha-selector/ha-selector-area.ts | 12 +- .../ha-selector/ha-selector-device.ts | 12 +- .../ha-selector/ha-selector-target.ts | 5 +- src/data/config_entries.ts | 20 +- src/data/config_flow.ts | 10 +- src/data/energy.ts | 8 +- src/data/helpers_crud.ts | 71 ++++ .../config-flow/show-dialog-config-flow.ts | 2 +- .../config-flow/step-flow-pick-handler.ts | 9 +- src/onboarding/onboarding-integrations.ts | 4 +- .../zwave_js/ha-device-info-zwave_js.ts | 7 +- .../components/ha-energy-grid-settings.ts | 54 ++- .../dialogs/dialog-energy-solar-settings.ts | 14 +- .../settings/entity-settings-helper-tab.ts | 93 +---- .../entities/entity-registry-settings.ts | 49 ++- src/panels/config/helpers/const.ts | 16 +- .../config/helpers/dialog-helper-detail.ts | 208 +++++++----- .../config/helpers/ha-config-helpers.ts | 317 ++++++++++++------ .../helpers/show-dialog-helper-detail.ts | 13 +- .../integrations/ha-config-integrations.ts | 38 ++- .../mqtt/mqtt-config-panel.ts | 4 +- .../zwave_js/zwave_js-config-dashboard.ts | 8 +- src/translations/en.json | 3 +- 24 files changed, 602 insertions(+), 377 deletions(-) create mode 100644 src/data/helpers_crud.ts diff --git a/src/common/entity/compute_state_domain.ts b/src/common/entity/compute_state_domain.ts index b4408257a6..1b972ea22f 100644 --- a/src/common/entity/compute_state_domain.ts +++ b/src/common/entity/compute_state_domain.ts @@ -1,4 +1,4 @@ -import { HassEntity } from "home-assistant-js-websocket"; +import type { HassEntity } from "home-assistant-js-websocket"; import { computeDomain } from "./compute_domain"; export const computeStateDomain = (stateObj: HassEntity) => diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 5f307dd013..704c79282a 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -28,7 +28,11 @@ export class HaAreaSelector extends LitElement { oldSelector !== this.selector && this.selector.area.device?.integration ) { - this._loadConfigEntries(); + getConfigEntries(this.hass, { + domain: this.selector.area.device.integration, + }).then((entries) => { + this._configEntries = entries; + }); } } } @@ -85,12 +89,6 @@ export class HaAreaSelector extends LitElement { } return true; }; - - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === this.selector.area.device?.integration - ); - } } declare global { diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index 2b0af5ace1..945c0de795 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -25,7 +25,11 @@ export class HaDeviceSelector extends LitElement { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); if (oldSelector !== this.selector && this.selector.device?.integration) { - this._loadConfigEntries(); + getConfigEntries(this.hass, { + domain: this.selector.device.integration, + }).then((entries) => { + this._configEntries = entries; + }); } } } @@ -88,12 +92,6 @@ export class HaDeviceSelector extends LitElement { } return true; }; - - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === this.selector.device.integration - ); - } } declare global { diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 4d81be67df..cf25317cce 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { private async _loadConfigEntries() { this._configEntries = (await getConfigEntries(this.hass)).filter( (entry) => - entry.domain === - (this.selector.target.device?.integration || - this.selector.target.entity?.integration) + entry.domain === this.selector.target.device?.integration || + entry.domain === this.selector.target.entity?.integration ); } diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index f2e84ddac0..b9e555998c 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [ "setup_retry", ]; -export const getConfigEntries = (hass: HomeAssistant) => - hass.callApi("GET", "config/config_entries/entry"); +export const getConfigEntries = ( + hass: HomeAssistant, + filters?: { type?: "helper" | "integration"; domain?: string } +): Promise => { + const params = new URLSearchParams(); + if (filters) { + if (filters.type) { + params.append("type", filters.type); + } + if (filters.domain) { + params.append("domain", filters.domain); + } + } + return hass.callApi( + "GET", + `config/config_entries/entry?${params.toString()}` + ); +}; export const updateConfigEntry = ( hass: HomeAssistant, diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index 39019393c5..c236a1b05c 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -65,8 +65,14 @@ export const ignoreConfigFlow = ( export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); -export const getConfigFlowHandlers = (hass: HomeAssistant) => - hass.callApi("GET", "config/config_entries/flow_handlers"); +export const getConfigFlowHandlers = ( + hass: HomeAssistant, + type?: "helper" | "integration" +) => + hass.callApi( + "GET", + `config/config_entries/flow_handlers${type ? `?type=${type}` : ""}` + ); export const fetchConfigFlowInProgress = ( conn: Connection diff --git a/src/data/energy.ts b/src/data/energy.ts index 7fd941b325..d9f4f1c0c2 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -247,14 +247,14 @@ const getEnergyData = async ( end?: Date ): Promise => { const [configEntries, entityRegistryEntries, info] = await Promise.all([ - getConfigEntries(hass), + getConfigEntries(hass, { domain: "co2signal" }), subscribeOne(hass.connection, subscribeEntityRegistry), getEnergyInfo(hass), ]); - const co2SignalConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); + const co2SignalConfigEntry = configEntries.length + ? configEntries[0] + : undefined; let co2SignalEntity: string | undefined; diff --git a/src/data/helpers_crud.ts b/src/data/helpers_crud.ts new file mode 100644 index 0000000000..c03ce664ac --- /dev/null +++ b/src/data/helpers_crud.ts @@ -0,0 +1,71 @@ +import { fetchCounter, updateCounter, deleteCounter } from "./counter"; +import { + fetchInputBoolean, + updateInputBoolean, + deleteInputBoolean, +} from "./input_boolean"; +import { + fetchInputButton, + updateInputButton, + deleteInputButton, +} from "./input_button"; +import { + fetchInputDateTime, + updateInputDateTime, + deleteInputDateTime, +} from "./input_datetime"; +import { + fetchInputNumber, + updateInputNumber, + deleteInputNumber, +} from "./input_number"; +import { + fetchInputSelect, + updateInputSelect, + deleteInputSelect, +} from "./input_select"; +import { fetchInputText, updateInputText, deleteInputText } from "./input_text"; +import { fetchTimer, updateTimer, deleteTimer } from "./timer"; + +export const HELPERS_CRUD = { + 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, + }, +}; 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/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-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-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._error ? html`
${this._error}
` : ""} + ${dynamicElement(`ha-${this._domain}-form`, { + hass: this.hass, + item: this._item, + new: true, + })} +
+ + ${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._error - ? html`
${this._error}
` - : ""} - ${dynamicElement(`ha-${this._platform}-form`, { - hass: this.hass, - item: this._item, - new: true, - })} -
- - ${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/translations/en.json b/src/translations/en.json index bb75c9224e..8218353cf8 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -823,7 +823,8 @@ "area": "Set entity area only", "area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.", "follow_device_area": "Follow device area", - "change_device_area": "Change device area" + "change_device_area": "Change device area", + "configure_state": "Configure State" } }, "helper_settings": { From 49124f6f09dfe0a7f024e3d85d929551dfda8fd0 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 14:53:22 -0500 Subject: [PATCH 11/19] Update When entity can change enabled or hidden (#12096) --- .../entities/entity-registry-basic-editor.ts | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) 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} > From 88af0aa78873946d485daf9f00fdd871a899807e Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 14:58:03 -0500 Subject: [PATCH 12/19] Add entity include and exclude to selector (#12078) Co-authored-by: Paulus Schoutsen --- src/components/entity/ha-entities-picker.ts | 20 ++++++ src/components/entity/ha-entity-picker.ts | 64 +++++++++++++++++-- .../ha-selector/ha-selector-entity.ts | 6 +- src/data/selector.ts | 2 + 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 062b79eced..3f6127e303 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -46,6 +46,22 @@ class HaEntitiesPickerLight extends LitElement { @property({ type: Array, attribute: "include-unit-of-measurement" }) public includeUnitOfMeasurement?: string[]; + /** + * List of allowed entities to show. Will ignore all other filters. + * @type {Array} + * @attr include-entities + */ + @property({ type: Array, attribute: "include-entities" }) + public includeEntities?: string[]; + + /** + * List of entities to be excluded. + * @type {Array} + * @attr exclude-entities + */ + @property({ type: Array, attribute: "exclude-entities" }) + public excludeEntities?: string[]; + @property({ attribute: "picked-entity-label" }) public pickedEntityLabel?: string; @@ -69,6 +85,8 @@ class HaEntitiesPickerLight extends LitElement { .hass=${this.hass} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} + .includeEntities=${this.includeEntities} + .excludeEntities=${this.excludeEntities} .includeDeviceClasses=${this.includeDeviceClasses} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .entityFilter=${this._entityFilter} @@ -84,6 +102,8 @@ class HaEntitiesPickerLight extends LitElement { .hass=${this.hass} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} + .includeEntities=${this.includeEntities} + .excludeEntities=${this.excludeEntities} .includeDeviceClasses=${this.includeDeviceClasses} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .entityFilter=${this._entityFilter} diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index a8d17084c6..9b13c75515 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -7,6 +7,7 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; +import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import "../ha-combo-box"; @@ -77,6 +78,22 @@ export class HaEntityPicker extends LitElement { @property({ type: Array, attribute: "include-unit-of-measurement" }) public includeUnitOfMeasurement?: string[]; + /** + * List of allowed entities to show. Will ignore all other filters. + * @type {Array} + * @attr include-entities + */ + @property({ type: Array, attribute: "include-entities" }) + public includeEntities?: string[]; + + /** + * List of entities to be excluded. + * @type {Array} + * @attr exclude-entities + */ + @property({ type: Array, attribute: "exclude-entities" }) + public excludeEntities?: string[]; + @property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property({ type: Boolean }) public hideClearIcon = false; @@ -109,7 +126,9 @@ export class HaEntityPicker extends LitElement { excludeDomains: this["excludeDomains"], entityFilter: this["entityFilter"], includeDeviceClasses: this["includeDeviceClasses"], - includeUnitOfMeasurement: this["includeUnitOfMeasurement"] + includeUnitOfMeasurement: this["includeUnitOfMeasurement"], + includeEntities: this["includeEntities"], + excludeEntities: this["excludeEntities"] ): HassEntityWithCachedName[] => { let states: HassEntityWithCachedName[] = []; @@ -139,6 +158,30 @@ export class HaEntityPicker extends LitElement { ]; } + if (includeEntities) { + entityIds = entityIds.filter((entityId) => + this.includeEntities!.includes(entityId) + ); + + return entityIds + .map((key) => ({ + ...hass!.states[key], + friendly_name: computeStateName(hass!.states[key]) || key, + })) + .sort((entityA, entityB) => + caseInsensitiveStringCompare( + entityA.friendly_name, + entityB.friendly_name + ) + ); + } + + if (excludeEntities) { + entityIds = entityIds.filter( + (entityId) => !excludeEntities!.includes(entityId) + ); + } + if (includeDomains) { entityIds = entityIds.filter((eid) => includeDomains.includes(computeDomain(eid)) @@ -151,10 +194,17 @@ export class HaEntityPicker extends LitElement { ); } - states = entityIds.sort().map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })); + states = entityIds + .map((key) => ({ + ...hass!.states[key], + friendly_name: computeStateName(hass!.states[key]) || key, + })) + .sort((entityA, entityB) => + caseInsensitiveStringCompare( + entityA.friendly_name, + entityB.friendly_name + ) + ); if (includeDeviceClasses) { states = states.filter( @@ -231,7 +281,9 @@ export class HaEntityPicker extends LitElement { this.excludeDomains, this.entityFilter, this.includeDeviceClasses, - this.includeUnitOfMeasurement + this.includeUnitOfMeasurement, + this.includeEntities, + this.excludeEntities ); if (this._initedStates) { (this.comboBox as any).filteredItems = this._states; diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index cbc5366953..8bf085f79f 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -6,8 +6,8 @@ import { subscribeEntityRegistry } from "../../data/entity_registry"; import { EntitySelector } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; -import "../entity/ha-entity-picker"; import "../entity/ha-entities-picker"; +import "../entity/ha-entity-picker"; @customElement("ha-selector-entity") export class HaEntitySelector extends SubscribeMixin(LitElement) { @@ -29,6 +29,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { .hass=${this.hass} .value=${this.value} .label=${this.label} + .includeEntities=${this.selector.entity.includeEntities} + .excludeEntities=${this.selector.entity.excludeEntities} .entityFilter=${this._filterEntities} .disabled=${this.disabled} allow-custom-entity @@ -41,6 +43,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { .hass=${this.hass} .value=${this.value} .entityFilter=${this._filterEntities} + .includeEntities=${this.selector.entity.includeEntities} + .excludeEntities=${this.selector.entity.excludeEntities} > `; } diff --git a/src/data/selector.ts b/src/data/selector.ts index b48c11b179..aba717b4a7 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -28,6 +28,8 @@ export interface EntitySelector { domain?: string | string[]; device_class?: string; multiple?: boolean; + includeEntities?: string[]; + excludeEntities?: string[]; }; } From afd2e71f6c490c2fdd0e4f0486f5451e5263ff65 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 17:39:22 -0500 Subject: [PATCH 13/19] change from hidden to not shown (#12097) --- src/translations/en.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 8218353cf8..8fe2de52a5 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2405,7 +2405,7 @@ "add_entities_lovelace": "Add to dashboard", "none": "This device has no entities", "show_less": "Show less", - "hidden_entities": "+{count} {count, plural,\n one {hidden entity}\n other {hidden entities}\n}" + "hidden_entities": "+{count} {count, plural,\n one {entity}\n other {entities}\n} not shown" }, "confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", "confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!", @@ -2430,7 +2430,7 @@ "filter": { "filter": "Filter", "show_disabled": "Show disabled devices", - "hidden_devices": "{number} hidden {number, plural,\n one {device}\n other {devices}\n}", + "hidden_devices": "{number} {number, plural,\n one {device}\n other {devices}\n} not shown", "show_all": "Show all" } } @@ -2449,7 +2449,7 @@ "show_disabled": "Show disabled entities", "show_unavailable": "Show unavailable entities", "show_readonly": "Show read-only entities", - "hidden_entities": "{number} hidden {number, plural,\n one {entity}\n other {entities}\n}", + "hidden_entities": "{number} {number, plural,\n one {entity}\n other {entities}\n} not shown", "show_all": "Show all" }, "status": { From 840858b18c2ec05c8d0e5360eb94a38808ceeec9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Mar 2022 15:40:00 -0700 Subject: [PATCH 14/19] Add statistic adjust dialog (#12101) Co-authored-by: Zack Barett --- .../ha-selector/ha-selector-datetime.ts | 3 +- .../ha-selector/ha-selector-number.ts | 2 +- src/data/history.ts | 13 ++ .../statistics/developer-tools-statistics.ts | 27 +++ .../dialog-statistics-adjust-sum.ts | 166 ++++++++++++++++++ .../dialog-statistics-fix-units-changed.ts | 2 +- ...og-statistics-fix-unsupported-unit-meta.ts | 2 +- .../show-dialog-statistics-adjust-sum.ts | 20 +++ 8 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts create mode 100644 src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts diff --git a/src/components/ha-selector/ha-selector-datetime.ts b/src/components/ha-selector/ha-selector-datetime.ts index 80add84bd4..caee6d8457 100644 --- a/src/components/ha-selector/ha-selector-datetime.ts +++ b/src/components/ha-selector/ha-selector-datetime.ts @@ -26,6 +26,7 @@ export class HaDateTimeSelector extends LitElement { protected render() { const values = this.value?.split(" "); + return html` stats.some((stat) => stat[type] !== null); + +export const adjustStatisticsSum = ( + hass: HomeAssistant, + statistic_id: string, + start_time: string, + adjustment: number +): Promise => + hass.callWS({ + type: "recorder/adjust_sum_statistics", + statistic_id, + start_time, + adjustment, + }); 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, + }); +}; From 2d9b50defc5ee96a8161374e63ce19112c4ccc70 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 18:33:16 -0500 Subject: [PATCH 15/19] Fix Duration Selector Default (#12098) * Fix Duration Default * USe initial form data function --- .../ha-form/compute-initial-ha-form-data.ts | 22 ++++++++++++++++++- .../ha-selector/ha-selector-duration.ts | 6 ++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/components/ha-form/compute-initial-ha-form-data.ts b/src/components/ha-form/compute-initial-ha-form-data.ts index 0e80433c74..7c5d728d91 100644 --- a/src/components/ha-form/compute-initial-ha-form-data.ts +++ b/src/components/ha-form/compute-initial-ha-form-data.ts @@ -1,4 +1,5 @@ -import { HaFormSchema } from "./types"; +import type { Selector } from "../../data/selector"; +import type { HaFormSchema } from "./types"; export const computeInitialHaFormData = ( schema: HaFormSchema[] @@ -31,6 +32,25 @@ export const computeInitialHaFormData = ( minutes: 0, seconds: 0, }; + } else if ("selector" in field) { + const selector: Selector = field.selector; + if ("boolean" in selector) { + data[field.name] = false; + } else if ("text" in selector) { + data[field.name] = ""; + } else if ("number" in selector) { + data[field.name] = "min" in selector.number ? selector.number.min : 0; + } else if ("select" in selector) { + if (selector.select.options.length) { + data[field.name] = selector.select.options[0][0]; + } + } else if ("duration" in selector) { + data[field.name] = { + hours: 0, + minutes: 0, + seconds: 0, + }; + } } }); return data; diff --git a/src/components/ha-selector/ha-selector-duration.ts b/src/components/ha-selector/ha-selector-duration.ts index 1471750d90..1f5e88b146 100644 --- a/src/components/ha-selector/ha-selector-duration.ts +++ b/src/components/ha-selector/ha-selector-duration.ts @@ -1,8 +1,8 @@ -import "../ha-duration-input"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { DurationSelector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { DurationSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-duration-input"; @customElement("ha-selector-duration") export class HaTimeDuration extends LitElement { From a7a347ed05ba75a7260d02fce4f9847f133c9537 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 19:08:30 -0500 Subject: [PATCH 16/19] Bumped version to 20220322.0 (#12102) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2afbb59467..e8f04faf57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220317.0 +version = 20220322.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From c0dce08e19cb2f965ba6700627db7cf4203df511 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Wed, 23 Mar 2022 04:51:35 +0100 Subject: [PATCH 17/19] Create user types page and rename the category (#12089) Co-authored-by: Zack Barett Co-authored-by: Paulus Schoutsen --- gallery/sidebar.js | 5 +++-- gallery/src/ha-gallery.ts | 4 ++++ gallery/src/pages/user-test/user-types.markdown | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 gallery/src/pages/user-test/user-types.markdown diff --git a/gallery/sidebar.js b/gallery/sidebar.js index d98d361224..14a8d38fe8 100644 --- a/gallery/sidebar.js +++ b/gallery/sidebar.js @@ -42,10 +42,11 @@ module.exports = [ }, { category: "user-test", - header: "User Tests", + header: "Users", + pages: ["user-types", "configuration-menu"], }, { category: "design.home-assistant.io", - header: "Design Documentation", + header: "About", }, ]; diff --git a/gallery/src/ha-gallery.ts b/gallery/src/ha-gallery.ts index e602a1916d..4f22bac519 100644 --- a/gallery/src/ha-gallery.ts +++ b/gallery/src/ha-gallery.ts @@ -45,6 +45,10 @@ class HaGallery extends LitElement { for (const page of group.pages!) { const key = `${group.category}/${page}`; const active = this._page === key; + if (!(key in PAGES)) { + console.error("Undefined page referenced in sidebar.js:", key); + continue; + } const title = PAGES[key].metadata.title || page; links.push(html` ${title} diff --git a/gallery/src/pages/user-test/user-types.markdown b/gallery/src/pages/user-test/user-types.markdown new file mode 100644 index 0000000000..eacc108cd4 --- /dev/null +++ b/gallery/src/pages/user-test/user-types.markdown @@ -0,0 +1,17 @@ +--- +title: "User types" +--- + +We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria aren’t demographic and don’t personify a group into a single character with a fictitious background story. + +# Outgrowers + +Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app. + +# Tinkerers + +Technoid users in home networking and development that know how to code. + +# Questioner + +Users who want more advanced home automation, but need support to make it work. From d6a1d5af79a15e4e21782ba929b9a0027a9a7672 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:22:12 +0100 Subject: [PATCH 18/19] Remove `setup.py` (#11593) --- setup.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 69bf65dd8a..0000000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Entry point for setuptools. Required for editable installs. -TODO: Remove file after updating to pip 21.3 -""" -from setuptools import setup - -setup() From 079cc39a6ed81af35403a1a3742d1def08a6e8f1 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 23 Mar 2022 14:24:55 +0100 Subject: [PATCH 19/19] Fix selecting 0 with number selector --- src/components/ha-selector/ha-selector-number.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 0094192f1b..8a9dfb1c3a 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -46,7 +46,7 @@ export class HaNumberSelector extends LitElement { class=${classMap({ single: this.selector.number.mode === "box" })} .min=${this.selector.number.min} .max=${this.selector.number.max} - .value=${this.value || ""} + .value=${this.value ?? ""} .step=${this.selector.number.step ?? 1} .disabled=${this.disabled} .required=${this.required}