From da709cbbd10fc5eaa57e42e777ae09cb6ee52dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 11 Jul 2022 09:06:45 +0200 Subject: [PATCH 01/58] Add hacs_repository my redirect (#13153) --- src/panels/my/ha-panel-my.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index a7ef299ba5..93b3c3226f 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -216,6 +216,15 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({ // Moved from Supervisor panel in 2022.5 redirect: "/config/info", }, + hacs_repository: { + component: "hacs", + redirect: "/hacs/_my_redirect/hacs_repository", + params: { + owner: "string", + repository: "string", + category: "string?", + }, + }, }); const getRedirect = ( From cd4f6e19f48fcb91cd9f5cb049fb8eb023e19449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 11 Jul 2022 12:50:37 +0200 Subject: [PATCH 02/58] Await backup restore (#13167) --- .../dialogs/backup/dialog-hassio-backup.ts | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-backup.ts index a993113ee5..c5c868cd26 100644 --- a/hassio/src/dialogs/backup/dialog-hassio-backup.ts +++ b/hassio/src/dialogs/backup/dialog-hassio-backup.ts @@ -201,26 +201,24 @@ class HassioBackupDialog } if (!this._dialogParams?.onboarding) { - this.hass!.callApi( - "POST", + try { + await this.hass!.callApi( + "POST", - `hassio/${ - atLeastVersion(this.hass!.config.version, 2021, 9) - ? "backups" - : "snapshots" - }/${this._backup!.slug}/restore/partial`, - backupDetails - ).then( - () => { - this.closeDialog(); - }, - (error) => { - this._error = error.body.message; - } - ); + `hassio/${ + atLeastVersion(this.hass!.config.version, 2021, 9) + ? "backups" + : "snapshots" + }/${this._backup!.slug}/restore/partial`, + backupDetails + ); + this.closeDialog(); + } catch (error: any) { + this._error = error.body.message; + } } else { fireEvent(this, "restoring"); - fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, { + await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, { method: "POST", body: JSON.stringify(backupDetails), }); From 414db833591fe62b87c6aafe709c35e9f959b71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 11 Jul 2022 12:55:43 +0200 Subject: [PATCH 03/58] Hide homeassistant from partial restore if no version (#13168) --- .../components/supervisor-backup-content.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/hassio/src/components/supervisor-backup-content.ts b/hassio/src/components/supervisor-backup-content.ts index dd5c38a178..cea5951729 100644 --- a/hassio/src/components/supervisor-backup-content.ts +++ b/hassio/src/components/supervisor-backup-content.ts @@ -168,23 +168,24 @@ export class SupervisorBackupContent extends LitElement { : ""} ${this.backupType === "partial" ? html`
- - `} - > - - - - + ${this.backup?.homeassistant + ? html` + `} + > + + + ` + : ""} ${foldersSection?.templates.length ? html` Date: Mon, 11 Jul 2022 14:05:23 +0200 Subject: [PATCH 04/58] Allow customizing number unit of measurement --- src/data/entity_registry.ts | 4 +++ .../entities/entity-registry-settings.ts | 35 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 4c7a931d0f..77d9d00d8e 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -37,6 +37,10 @@ export interface SensorEntityOptions { unit_of_measurement?: string | null; } +export interface NumberEntityOptions { + unit_of_measurement?: string | null; +} + export interface WeatherEntityOptions { precipitation_unit?: string | null; pressure_unit?: string | null; diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 799d221004..02c59f09b8 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -105,6 +105,10 @@ const OVERRIDE_DEVICE_CLASSES = { ], }; +const OVERRIDE_NUMBER_UNITS = { + temperature: ["°C", "°F", "K"], +}; + const OVERRIDE_SENSOR_UNITS = { temperature: ["°C", "°F", "K"], pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], @@ -235,7 +239,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } } - if (domain === "sensor") { + if (domain === "number" || domain === "sensor") { const stateObj: HassEntity | undefined = this.hass.states[this.entry.entity_id]; this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; @@ -361,6 +365,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ` : ""} + ${domain === "number" && + this._deviceClass && + stateObj?.attributes.unit_of_measurement && + OVERRIDE_NUMBER_UNITS[this._deviceClass]?.includes( + stateObj?.attributes.unit_of_measurement + ) + ? html` + + ${OVERRIDE_NUMBER_UNITS[this._deviceClass].map( + (unit: string) => html` + ${unit} + ` + )} + + ` + : ""} ${domain === "sensor" && this._deviceClass && stateObj?.attributes.unit_of_measurement && @@ -861,10 +890,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { params.hidden_by = this._hiddenBy; } if ( - domain === "sensor" && + (domain === "number" || domain === "number") && stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement ) { - params.options_domain = "sensor"; + params.options_domain = domain; params.options = { unit_of_measurement: this._unit_of_measurement }; } if ( From 29c3fb0f92bc77900d58daecc3ac308deadbc391 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 11 Jul 2022 15:44:34 +0200 Subject: [PATCH 05/58] Fix energy demo (#13172) --- demo/src/ha-demo.ts | 31 +++++++++++++++++++++-- demo/src/stubs/config.ts | 41 ------------------------------- demo/src/stubs/config_entries.ts | 20 +++++++++++++++ demo/src/stubs/entity_registry.ts | 4 ++- 4 files changed, 52 insertions(+), 44 deletions(-) delete mode 100644 demo/src/stubs/config.ts create mode 100644 demo/src/stubs/config_entries.ts diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 958e14e0e3..91edb70aa5 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -21,8 +21,9 @@ import { mockSystemLog } from "./stubs/system_log"; import { mockTemplate } from "./stubs/template"; import { mockTranslations } from "./stubs/translations"; import { mockEnergy } from "./stubs/energy"; -import { mockConfig } from "./stubs/config"; import { energyEntities } from "./stubs/entities"; +import { mockConfigEntries } from "./stubs/config_entries"; +import { mockEntityRegistry } from "./stubs/entity_registry"; class HaDemo extends HomeAssistantAppEl { protected async _initializeHass() { @@ -51,8 +52,34 @@ class HaDemo extends HomeAssistantAppEl { mockMediaPlayer(hass); mockFrontend(hass); mockEnergy(hass); - mockConfig(hass); mockPersistentNotification(hass); + mockConfigEntries(hass); + mockEntityRegistry(hass, [ + { + config_entry_id: "co2signal", + device_id: "co2signal", + area_id: null, + disabled_by: null, + entity_id: "sensor.co2_intensity", + name: null, + icon: null, + platform: "co2signal", + hidden_by: null, + entity_category: null, + }, + { + config_entry_id: "co2signal", + device_id: "co2signal", + area_id: null, + disabled_by: null, + entity_id: "sensor.grid_fossil_fuel_percentage", + name: null, + icon: null, + platform: "co2signal", + hidden_by: null, + entity_category: null, + }, + ]); hass.addEntities(energyEntities()); diff --git a/demo/src/stubs/config.ts b/demo/src/stubs/config.ts deleted file mode 100644 index f77c8d3b09..0000000000 --- a/demo/src/stubs/config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; - -export const mockConfig = (hass: MockHomeAssistant) => { - hass.mockAPI("config/config_entries/entry?domain=co2signal", () => [ - { - entry_id: "co2signal", - domain: "co2signal", - title: "CO2 Signal", - source: "user", - state: "loaded", - supports_options: false, - supports_unload: true, - pref_disable_new_entities: false, - pref_disable_polling: false, - disabled_by: null, - reason: null, - }, - ]); - hass.mockWS("config/entity_registry/list", () => [ - { - config_entry_id: "co2signal", - device_id: "co2signal", - area_id: null, - disabled_by: null, - entity_id: "sensor.co2_intensity", - name: null, - icon: null, - platform: "co2signal", - }, - { - config_entry_id: "co2signal", - device_id: "co2signal", - area_id: null, - disabled_by: null, - entity_id: "sensor.grid_fossil_fuel_percentage", - name: null, - icon: null, - platform: "co2signal", - }, - ]); -}; diff --git a/demo/src/stubs/config_entries.ts b/demo/src/stubs/config_entries.ts new file mode 100644 index 0000000000..f81e11bc39 --- /dev/null +++ b/demo/src/stubs/config_entries.ts @@ -0,0 +1,20 @@ +import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockConfigEntries = (hass: MockHomeAssistant) => { + hass.mockWS("config_entries/get", () => [ + { + entry_id: "co2signal", + domain: "co2signal", + title: "CO2 Signal", + source: "user", + state: "loaded", + supports_options: false, + supports_remove_device: false, + supports_unload: true, + pref_disable_new_entities: false, + pref_disable_polling: false, + disabled_by: null, + reason: null, + }, + ]); +}; diff --git a/demo/src/stubs/entity_registry.ts b/demo/src/stubs/entity_registry.ts index 8f548629e7..422702b646 100644 --- a/demo/src/stubs/entity_registry.ts +++ b/demo/src/stubs/entity_registry.ts @@ -4,4 +4,6 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; export const mockEntityRegistry = ( hass: MockHomeAssistant, data: EntityRegistryEntry[] = [] -) => hass.mockWS("config/entity_registry/list", () => data); +) => { + hass.mockWS("config/entity_registry/list", () => data); +}; From 55e9ebc4d287fd73258f5e7c23f2ba4b621136e0 Mon Sep 17 00:00:00 2001 From: Yosi Levy <37745463+yosilevy@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:47:22 +0300 Subject: [PATCH 06/58] Energy panel/cards - RTL fixes (#13171) --- src/components/chart/ha-chart-base.ts | 1 + src/panels/config/energy/components/styles.ts | 6 +++++ .../energy/hui-energy-distribution-card.ts | 1 + .../energy/hui-energy-sources-table-card.ts | 7 ++++++ .../lovelace/cards/hui-humidifier-card.ts | 1 + .../components/hui-energy-period-selector.ts | 23 ++++++++----------- .../components/hui-generic-entity-row.ts | 5 +++- .../entity-rows/hui-sensor-entity-row.ts | 5 +++- 8 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 84c4132849..277ac615d0 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -376,6 +376,7 @@ export default class HaChartBase extends LitElement { .chartTooltip .title { text-align: center; font-weight: 500; + direction: ltr; } .chartTooltip .footer { font-weight: 500; diff --git a/src/panels/config/energy/components/styles.ts b/src/panels/config/energy/components/styles.ts index 5aab3c4dbc..d980606a44 100644 --- a/src/panels/config/energy/components/styles.ts +++ b/src/panels/config/energy/components/styles.ts @@ -8,6 +8,9 @@ export const energyCardStyles = css` height: 32px; width: 32px; margin-right: 8px; + margin-inline-end: 8px; + margin-inline-start: initial; + direction: var(--direction); } h3 { margin-top: 24px; @@ -24,6 +27,9 @@ export const energyCardStyles = css` .row ha-icon, .row img { margin-right: 16px; + margin-inline-end: 16px; + margin-inline-start: initial; + direction: var(--direction); } .row img { height: 24px; diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index c223a65de8..bfd67198ac 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -699,6 +699,7 @@ class HuiEnergyDistrubutionCard } .card-content { position: relative; + direction: ltr; } .lines { position: absolute; diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index 4b8681bd0e..cf584d0d73 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -900,6 +900,7 @@ export class HuiEnergySourcesTableCard .mdc-data-table__cell { color: var(--primary-text-color); border-bottom-color: var(--divider-color); + text-align: var(--float-start); } .mdc-data-table__row:not(.mdc-data-table__row--selected):hover { background-color: rgba(var(--rgb-primary-text-color), 0.04); @@ -925,6 +926,9 @@ export class HuiEnergySourcesTableCard .cell-bullet { width: 32px; padding-right: 0; + padding-inline-end: 0; + padding-inline-start: 16px; + direction: var(--direction); } .bullet { border-width: 1px; @@ -933,6 +937,9 @@ export class HuiEnergySourcesTableCard height: 16px; width: 32px; } + .mdc-data-table__cell--numeric { + direction: ltr; + } `; } } diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts index 9baf03d160..79ce84b871 100644 --- a/src/panels/lovelace/cards/hui-humidifier-card.ts +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -356,6 +356,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { height: 50%; top: 45%; left: 50%; + direction: ltr; } #set-values { diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index 38011288fd..4a943b9ab8 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -1,10 +1,5 @@ import "@material/mwc-button/mwc-button"; -import { - mdiChevronLeft, - mdiChevronRight, - mdiCompare, - mdiCompareRemove, -} from "@mdi/js"; +import { mdiCompare, mdiCompareRemove } from "@mdi/js"; import { addDays, addMonths, @@ -35,9 +30,12 @@ import { import { toggleAttribute } from "../../../common/dom/toggle_attribute"; import "../../../components/ha-button-toggle-group"; import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-button-prev"; +import "../../../components/ha-icon-button-next"; import { EnergyData, getEnergyDataCollection } from "../../../data/energy"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, ToggleButton } from "../../../types"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; @customElement("hui-energy-period-selector") export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { @@ -116,20 +114,18 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { this._endDate || new Date(), this.hass.locale )}`} - - + + > ${this.hass.localize( "ui.panel.lovelace.components.energy_period_selector.today" @@ -142,6 +138,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { .active=${this._period} dense @value-changed=${this._handleView} + .dir=${computeRTLDirection(this.hass)} > ${this.narrow ? html`
Date: Mon, 11 Jul 2022 09:46:32 -0500 Subject: [PATCH 07/58] Fix Suggested Value in HA-Form (#13173) --- src/components/ha-form/compute-initial-ha-form-data.ts | 2 +- src/components/ha-form/ha-form-integer.ts | 6 +++--- 2 files changed, 4 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 b70148fa86..b83835ad70 100644 --- a/src/components/ha-form/compute-initial-ha-form-data.ts +++ b/src/components/ha-form/compute-initial-ha-form-data.ts @@ -6,7 +6,7 @@ export const computeInitialHaFormData = ( ): Record => { const data = {}; schema.forEach((field) => { - if (field.description?.suggested_value) { + if (field.description?.suggested_value !== undefined) { data[field.name] = field.description.suggested_value; } else if ("default" in field) { data[field.name] = field.default; diff --git a/src/components/ha-form/ha-form-integer.ts b/src/components/ha-form/ha-form-integer.ts index 5873eff158..e71425ba27 100644 --- a/src/components/ha-form/ha-form-integer.ts +++ b/src/components/ha-form/ha-form-integer.ts @@ -3,15 +3,15 @@ import { CSSResultGroup, html, LitElement, - TemplateResult, PropertyValues, + TemplateResult, } from "lit"; import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { HaCheckbox } from "../ha-checkbox"; -import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types"; import "../ha-slider"; import { HaTextField } from "../ha-textfield"; +import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types"; @customElement("ha-form-integer") export class HaFormInteger extends LitElement implements HaFormElement { @@ -105,7 +105,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { } return ( - this.schema.description?.suggested_value || + this.schema.description?.suggested_value !== undefined || this.schema.default || this.schema.valueMin || 0 From c50cf78bb4a3b4a9f0ba8bb9b8de5dc0739583a5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:14:37 -0400 Subject: [PATCH 08/58] Allow stale ZHA coordinator entries to be deleted (#13154) Co-authored-by: Bram Kragten --- src/data/zha.ts | 1 + .../zha/device-actions.ts | 82 +++++++++---------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/data/zha.ts b/src/data/zha.ts index d1b6f2ab97..247f1f1827 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -26,6 +26,7 @@ export interface ZHADevice { power_source?: string; area_id?: string; device_type: string; + active_coordinator: boolean; signature: any; neighbors: Neighbor[]; pairing_status?: string; diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts index 476ff74993..5685b053d4 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts @@ -30,7 +30,7 @@ export const getZHADeviceActions = async ( const actions: DeviceAction[] = []; - if (zhaDevice.device_type !== "Coordinator") { + if (!zhaDevice.active_coordinator) { actions.push({ label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"), action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }), @@ -58,50 +58,50 @@ export const getZHADeviceActions = async ( ); } - if (zhaDevice.device_type !== "Coordinator") { - actions.push( - ...[ - { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.zigbee_information" + actions.push( + ...[ + { + label: hass.localize( + "ui.dialogs.zha_device_info.buttons.zigbee_information" + ), + action: () => showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }), + }, + { + label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"), + action: () => showZHAClusterDialog(el, { device: zhaDevice }), + }, + { + label: hass.localize( + "ui.dialogs.zha_device_info.buttons.view_in_visualization" + ), + action: () => + navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`), + }, + ] + ); + + if (!zhaDevice.active_coordinator) { + actions.push({ + label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"), + classes: "warning", + action: async () => { + const confirmed = await showConfirmationDialog(el, { + text: hass.localize( + "ui.dialogs.zha_device_info.confirmations.remove" ), - action: () => - showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }), - }, - { - label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"), - action: () => showZHAClusterDialog(el, { device: zhaDevice }), - }, - { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.view_in_visualization" - ), - action: () => - navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`), - }, - { - label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"), - classes: "warning", - action: async () => { - const confirmed = await showConfirmationDialog(el, { - text: hass.localize( - "ui.dialogs.zha_device_info.confirmations.remove" - ), - }); + }); - if (!confirmed) { - return; - } + if (!confirmed) { + return; + } - await hass.callService("zha", "remove", { - ieee: zhaDevice.ieee, - }); + await hass.callService("zha", "remove", { + ieee: zhaDevice.ieee, + }); - history.back(); - }, - }, - ] - ); + history.back(); + }, + }); } return actions; From a30c8205b1f6a13e60dff50dfa2cdaa43b3224da Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 11 Jul 2022 22:07:10 +0200 Subject: [PATCH 09/58] Update dialog styles (#13132) --- src/components/ha-dialog.ts | 22 ++++++++++++------- src/components/ha-target-picker.ts | 2 +- src/dialogs/generic/dialog-box.ts | 3 --- .../areas/dialog-area-registry-detail.ts | 3 --- .../dialog-device-registry-detail.ts | 3 --- .../entities/entity-registry-settings.ts | 8 +++---- .../config/person/dialog-person-detail.ts | 3 --- src/resources/styles.ts | 1 - 8 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 346410aa44..dfec72aeec 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -40,10 +40,13 @@ export class HaDialog extends DialogBase { z-index: var(--dialog-z-index, 7); -webkit-backdrop-filter: var(--dialog-backdrop-filter, none); backdrop-filter: var(--dialog-backdrop-filter, none); + --mdc-dialog-box-shadow: var(--dialog-box-shadow, none); + --mdc-typography-headline6-font-weight: 400; + --mdc-typography-headline6-font-size: 1.574rem; } .mdc-dialog__actions { justify-content: var(--justify-action-buttons, flex-end); - padding-bottom: max(env(safe-area-inset-bottom), 8px); + padding-bottom: max(env(safe-area-inset-bottom), 24px); } .mdc-dialog__actions span:nth-child(1) { flex: var(--secondary-action-button-flex, unset); @@ -54,17 +57,23 @@ export class HaDialog extends DialogBase { .mdc-dialog__container { align-items: var(--vertial-align-dialog, center); } + .mdc-dialog__title { + padding: 24px 24px 0 24px; + } + .mdc-dialog__actions { + padding: 0 24px 24px 24px; + } .mdc-dialog__title::before { display: block; - height: 20px; + height: 0px; } .mdc-dialog .mdc-dialog__content { position: var(--dialog-content-position, relative); - padding: var(--dialog-content-padding, 20px 24px); + padding: var(--dialog-content-padding, 24px); } :host([hideactions]) .mdc-dialog .mdc-dialog__content { padding-bottom: max( - var(--dialog-content-padding, 20px), + var(--dialog-content-padding, 24px), env(safe-area-inset-bottom) ); } @@ -72,10 +81,7 @@ export class HaDialog extends DialogBase { position: var(--dialog-surface-position, relative); top: var(--dialog-surface-top); min-height: var(--mdc-dialog-min-height, auto); - border-radius: var( - --ha-dialog-border-radius, - var(--ha-card-border-radius, 4px) - ); + border-radius: var(--ha-dialog-border-radius, 28px); } :host([flexContent]) .mdc-dialog .mdc-dialog__content { display: flex; diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index 8df2bea571..0243534269 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -314,7 +314,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { class="mdc-chip__icon mdc-chip__icon--trailing" tabindex="-1" role="button" - .label=${this.hass.localize("ui.components.target-picker.expand")} + .label=${this.hass.localize("ui.components.target-picker.remove")} .path=${mdiClose} hideTooltip .id=${id} diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index fdf2107756..7aa5ebb5e6 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -144,8 +144,6 @@ class DialogBox extends LitElement { } p { margin: 0; - padding-top: 6px; - padding-bottom: 24px; color: var(--primary-text-color); } .no-bottom-padding { @@ -157,7 +155,6 @@ class DialogBox extends LitElement { ha-dialog { --mdc-dialog-heading-ink-color: var(--primary-text-color); --mdc-dialog-content-ink-color: var(--primary-text-color); - --justify-action-buttons: space-between; /* Place above other dialogs */ --dialog-z-index: 104; } diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index fcf95fc6c1..c2fadae31d 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -178,9 +178,6 @@ class DialogAreaDetail extends LitElement { return [ haStyleDialog, css` - .form { - padding-bottom: 24px; - } ha-textfield { display: block; margin-bottom: 16px; diff --git a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts index 662e8f3ca9..b61ac0bdd8 100644 --- a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts @@ -169,9 +169,6 @@ class DialogDeviceRegistryDetail extends LitElement { haStyle, haStyleDialog, css` - .form { - padding-bottom: 24px; - } mwc-button.warning { margin-right: auto; } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 02c59f09b8..a680418fb5 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1052,12 +1052,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { bottom: 0; width: 100%; box-sizing: border-box; - border-top: 1px solid - var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); display: flex; - justify-content: space-between; - padding: 8px; - padding-bottom: max(env(safe-area-inset-bottom), 8px); + padding: 0 24px 24px 24px; + justify-content: flex-end; + padding-bottom: max(env(safe-area-inset-bottom), 24px); background-color: var(--mdc-theme-surface, #fff); } ha-select { diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 16fd99416c..c9d0c0c56a 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -454,9 +454,6 @@ class DialogPersonDetail extends LitElement { return [ haStyleDialog, css` - .form { - padding-bottom: 24px; - } ha-picture-upload, ha-textfield { display: block; diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 62af21f471..1a71387b56 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -316,7 +316,6 @@ export const haStyleDialog = css` } ha-dialog .form { - padding-bottom: 24px; color: var(--primary-text-color); } From 437723c6a6a616c3bae3084dbfdb252096c14aba Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 11 Jul 2022 20:05:47 -0500 Subject: [PATCH 10/58] Fix Number Selector Label (#13178) --- .../ha-selector/ha-selector-number.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 43ae1430ee..0ccfedafd2 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -4,9 +4,9 @@ import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../common/dom/fire_event"; import { NumberSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; +import "../ha-input-helper-text"; import "../ha-slider"; import "../ha-textfield"; -import "../ha-input-helper-text"; @customElement("ha-selector-number") export class HaNumberSelector extends LitElement { @@ -30,21 +30,25 @@ export class HaNumberSelector extends LitElement { const isBox = this.selector.number.mode === "box"; return html` - ${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
${!isBox - ? html` - ` + ? html` + ${this.label + ? html`${this.label}${this.required ? " *" : ""}` + : ""} + + + ` : ""} Date: Mon, 11 Jul 2022 19:07:07 -0700 Subject: [PATCH 11/58] Use entity name in device info page (#13165) * Use entity name in device info page * Adjust to new format * Use latest API * Fix types * Fix CI? Co-authored-by: Zack --- demo/src/ha-demo.ts | 12 ++++---- gallery/src/pages/misc/integration-card.ts | 1 + src/data/entity_registry.ts | 3 +- .../device-detail/ha-device-entities-card.ts | 30 ++++++++++++------- .../config/entities/ha-config-entities.ts | 1 + 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 91edb70aa5..625ddc6187 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -1,5 +1,4 @@ // Compat needs to be first import -import "../../src/resources/compatibility"; import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; import { navigate } from "../../src/common/navigate"; import { @@ -7,9 +6,14 @@ import { provideHass, } from "../../src/fake_data/provide_hass"; import { HomeAssistantAppEl } from "../../src/layouts/home-assistant"; +import "../../src/resources/compatibility"; import { HomeAssistant } from "../../src/types"; import { selectedDemoConfig } from "./configs/demo-configs"; import { mockAuth } from "./stubs/auth"; +import { mockConfigEntries } from "./stubs/config_entries"; +import { mockEnergy } from "./stubs/energy"; +import { energyEntities } from "./stubs/entities"; +import { mockEntityRegistry } from "./stubs/entity_registry"; import { mockEvents } from "./stubs/events"; import { mockFrontend } from "./stubs/frontend"; import { mockHistory } from "./stubs/history"; @@ -20,10 +24,6 @@ import { mockShoppingList } from "./stubs/shopping_list"; import { mockSystemLog } from "./stubs/system_log"; import { mockTemplate } from "./stubs/template"; import { mockTranslations } from "./stubs/translations"; -import { mockEnergy } from "./stubs/energy"; -import { energyEntities } from "./stubs/entities"; -import { mockConfigEntries } from "./stubs/config_entries"; -import { mockEntityRegistry } from "./stubs/entity_registry"; class HaDemo extends HomeAssistantAppEl { protected async _initializeHass() { @@ -66,6 +66,7 @@ class HaDemo extends HomeAssistantAppEl { platform: "co2signal", hidden_by: null, entity_category: null, + has_entity_name: false, }, { config_entry_id: "co2signal", @@ -78,6 +79,7 @@ class HaDemo extends HomeAssistantAppEl { platform: "co2signal", hidden_by: null, entity_category: null, + has_entity_name: false, }, ]); diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index aa26eb2c6b..cf12cefd35 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -194,6 +194,7 @@ const createEntityRegistryEntries = ( name: null, icon: null, platform: "updater", + has_entity_name: false, }, ]; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 77d9d00d8e..00ff94b00d 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -16,12 +16,13 @@ export interface EntityRegistryEntry { disabled_by: string | null; hidden_by: string | null; entity_category: "config" | "diagnostic" | null; + has_entity_name: boolean; + original_name?: string; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { unique_id: string; capabilities: Record; - original_name?: string; original_icon?: string; device_class?: string; original_device_class?: string; diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index ce1e310e54..e73d02c0c7 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -163,17 +163,27 @@ export class HaDeviceEntitiesCard extends LitElement { if (this.hass) { element.hass = this.hass; const stateObj = this.hass.states[entry.entity_id]; - const name = stripPrefixFromEntityName( - computeStateName(stateObj), - this.deviceName.toLowerCase() - ); - if (entry.hidden_by) { - config.name = `${ - name || computeStateName(stateObj) - } (${this.hass.localize("ui.panel.config.devices.entities.hidden")})`; - } else if (name) { - config.name = name; + + let name = entry.name + ? entry.name + : entry.has_entity_name + ? entry.original_name || this.deviceName + : stripPrefixFromEntityName( + computeStateName(stateObj), + this.deviceName.toLowerCase() + ); + + if (!name) { + name = computeStateName(stateObj); } + + if (entry.hidden_by) { + name += ` (${this.hass.localize( + "ui.panel.config.devices.entities.hidden" + )})`; + } + + config.name = name; } // @ts-ignore element.entry = entry; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 0a92026932..6d31575a33 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -736,6 +736,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { readonly: true, selectable: false, entity_category: null, + has_entity_name: false, }); } if (changed) { From d23fca4dd15b1cf078a466efa2ab9ec569335995 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Jul 2022 23:13:29 +0200 Subject: [PATCH 12/58] Correct display of barometric pressure and rain (#13183) --- src/data/weather.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/data/weather.ts b/src/data/weather.ts index f77da5c9b0..6c2fb20fcb 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -199,13 +199,15 @@ export const getWeatherUnit = ( case "visibility": return stateObj.attributes.visibility_unit || lengthUnit; case "precipitation": - return stateObj.attributes.precipitation_unit || lengthUnit === "km" - ? "mm" - : "in"; + return ( + stateObj.attributes.precipitation_unit || + (lengthUnit === "km" ? "mm" : "in") + ); case "pressure": - return stateObj.attributes.pressure_unit || lengthUnit === "km" - ? "hPa" - : "inHg"; + return ( + stateObj.attributes.pressure_unit || + (lengthUnit === "km" ? "hPa" : "inHg") + ); case "temperature": return ( stateObj.attributes.temperature_unit || From 24e54554ade9576fa65978e5d178f35d47b54fd7 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 13 Jul 2022 04:48:52 -0500 Subject: [PATCH 13/58] Fix History Graph Name not being friendly (#13179) --- src/data/history.ts | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 0fb7a8857c..dea32efec3 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -1,4 +1,4 @@ -import { HassEntity } from "home-assistant-js-websocket"; +import { HassEntities, HassEntity } from "home-assistant-js-websocket"; import { computeDomain } from "../common/entity/compute_domain"; import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; import { @@ -268,7 +268,8 @@ const processTimelineEntity = ( localize: LocalizeFunc, language: FrontendLocaleData, entityId: string, - states: EntityHistoryState[] + states: EntityHistoryState[], + current_state: HassEntity | undefined ): TimelineEntity => { const data: TimelineState[] = []; const first: EntityHistoryState = states[0]; @@ -292,7 +293,10 @@ const processTimelineEntity = ( } return { - name: computeStateNameFromEntityAttributes(entityId, states[0].a), + name: computeStateNameFromEntityAttributes( + entityId, + current_state?.attributes || first.a + ), entity_id: entityId, data, }; @@ -300,7 +304,8 @@ const processTimelineEntity = ( const processLineChartEntities = ( unit, - entities: HistoryStates + entities: HistoryStates, + hassEntities: HassEntities ): LineChartUnit => { const data: LineChartEntity[] = []; @@ -349,9 +354,16 @@ const processLineChartEntities = ( processedStates.push(processedState); } + const attributes = + entityId in hassEntities + ? hassEntities[entityId].attributes + : "friendly_name" in first.a + ? first.a + : undefined; + data.push({ domain, - name: computeStateNameFromEntityAttributes(entityId, first.a), + name: computeStateNameFromEntityAttributes(entityId, attributes || {}), entity_id: entityId, states: processedStates, }); @@ -411,7 +423,13 @@ export const computeHistory = ( if (!unit) { timelineDevices.push( - processTimelineEntity(localize, hass.locale, entityId, stateInfo) + processTimelineEntity( + localize, + hass.locale, + entityId, + stateInfo, + currentState + ) ); } else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) { lineChartDevices[unit][entityId].push(...stateInfo); @@ -424,7 +442,7 @@ export const computeHistory = ( }); const unitStates = Object.keys(lineChartDevices).map((unit) => - processLineChartEntities(unit, lineChartDevices[unit]) + processLineChartEntities(unit, lineChartDevices[unit], hass.states) ); return { line: unitStates, timeline: timelineDevices }; From b611a58fce1366fa10c3bebf7c8d34a9bb235d20 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 13 Jul 2022 10:51:17 -0500 Subject: [PATCH 14/58] Add support for Supported Brands (#13184) --- src/data/supported_brands.ts | 8 ++ .../config-flow/show-dialog-config-flow.ts | 7 +- .../show-dialog-data-entry-flow.ts | 6 +- .../config-flow/step-flow-pick-flow.ts | 1 + .../config-flow/step-flow-pick-handler.ts | 124 ++++++++++++++---- src/translations/en.json | 5 +- 6 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 src/data/supported_brands.ts diff --git a/src/data/supported_brands.ts b/src/data/supported_brands.ts new file mode 100644 index 0000000000..ef5994e871 --- /dev/null +++ b/src/data/supported_brands.ts @@ -0,0 +1,8 @@ +import type { HomeAssistant } from "../types"; + +export type SupportedBrandHandler = Record; + +export const getSupportedBrands = (hass: HomeAssistant) => + hass.callWS>({ + type: "supported_brands", + }); diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 53c92fd214..e987d6be40 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -7,6 +7,7 @@ import { handleConfigFlowStep, } from "../../data/config_flow"; import { domainToName } from "../../data/integration"; +import { getSupportedBrands } from "../../data/supported_brands"; import { DataEntryFlowDialogParams, loadDataEntryFlowDialog, @@ -22,12 +23,14 @@ export const showConfigFlowDialog = ( showFlowDialog(element, dialogParams, { loadDevicesAndAreas: true, getFlowHandlers: async (hass) => { - const [integrations, helpers] = await Promise.all([ + const [integrations, helpers, supportedBrands] = await Promise.all([ getConfigFlowHandlers(hass, "integration"), getConfigFlowHandlers(hass, "helper"), + getSupportedBrands(hass), hass.loadBackendTranslation("title", undefined, true), ]); - return { integrations, helpers }; + + return { integrations, helpers, supportedBrands }; }, createFlow: async (hass, handler) => { const [step] = await Promise.all([ diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index f6abcedc46..380faaf064 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -10,12 +10,14 @@ import { DataEntryFlowStepMenu, DataEntryFlowStepProgress, } from "../../data/data_entry_flow"; -import { IntegrationManifest } from "../../data/integration"; -import { HomeAssistant } from "../../types"; +import type { IntegrationManifest } from "../../data/integration"; +import type { SupportedBrandHandler } from "../../data/supported_brands"; +import type { HomeAssistant } from "../../types"; export interface FlowHandlers { integrations: string[]; helpers: string[]; + supportedBrands: Record; } export interface FlowConfig { loadDevicesAndAreas: boolean; diff --git a/src/dialogs/config-flow/step-flow-pick-flow.ts b/src/dialogs/config-flow/step-flow-pick-flow.ts index cb942fe3da..fd24243a0b 100644 --- a/src/dialogs/config-flow/step-flow-pick-flow.ts +++ b/src/dialogs/config-flow/step-flow-pick-flow.ts @@ -1,5 +1,6 @@ import "@polymer/paper-item"; import "@polymer/paper-item/paper-icon-item"; +import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 797ae3c4af..6812d118ec 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -15,18 +15,19 @@ import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; import { navigate } from "../../common/navigate"; -import "../../components/search-input"; import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { LocalizeFunc } from "../../common/translations/localize"; import "../../components/ha-icon-next"; +import "../../components/search-input"; import { getConfigEntries } from "../../data/config_entries"; import { domainToName } from "../../data/integration"; import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; import { documentationUrl } from "../../util/documentation-url"; -import { configFlowContentStyles } from "./styles"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; import { FlowHandlers } from "./show-dialog-data-entry-flow"; +import { configFlowContentStyles } from "./styles"; interface HandlerObj { name: string; @@ -35,6 +36,10 @@ interface HandlerObj { is_helper?: boolean; } +interface SupportedBrandObj extends HandlerObj { + supported_flows: string[]; +} + declare global { // for fire event interface HASSDomEvents { @@ -63,11 +68,22 @@ class StepFlowPickHandler extends LitElement { h: FlowHandlers, filter?: string, _localize?: LocalizeFunc - ): [HandlerObj[], HandlerObj[]] => { - const integrations: HandlerObj[] = h.integrations.map((handler) => ({ - name: domainToName(this.hass.localize, handler), - slug: handler, - })); + ): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => { + const integrations: (HandlerObj | SupportedBrandObj)[] = + h.integrations.map((handler) => ({ + name: domainToName(this.hass.localize, handler), + slug: handler, + })); + + for (const [domain, domainBrands] of Object.entries(h.supportedBrands)) { + for (const [slug, name] of Object.entries(domainBrands)) { + integrations.push({ + slug, + name, + supported_flows: [domain], + }); + } + } if (filter) { const options: Fuse.IFuseOptions = { @@ -238,27 +254,10 @@ class StepFlowPickHandler extends LitElement { } private async _handlerPicked(ev) { - const handler: HandlerObj = ev.currentTarget.handler; + const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler; if (handler.is_add) { - if (handler.slug === "zwave_js") { - const entries = await getConfigEntries(this.hass, { - domain: "zwave_js", - }); - - if (!entries.length) { - return; - } - - showZWaveJSAddNodeDialog(this, { - entry_id: entries[0].entry_id, - }); - } else if (handler.slug === "zha") { - navigate("/config/zha/add"); - } - - // This closes dialog. - fireEvent(this, "flow-update"); + this._handleAddPicked(handler.slug); return; } @@ -269,11 +268,84 @@ class StepFlowPickHandler extends LitElement { return; } + if ("supported_flows" in handler) { + const slug = handler.supported_flows[0]; + + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.supported_brand_flow", + { + supported_brand: handler.name, + flow_domain_name: domainToName(this.hass.localize, slug), + } + ), + confirm: () => { + if (["zha", "zwave_js"].includes(slug)) { + this._handleAddPicked(slug); + return; + } + + fireEvent(this, "handler-picked", { + handler: slug, + }); + }, + }); + + return; + } + fireEvent(this, "handler-picked", { handler: handler.slug, }); } + private async _handleAddPicked(slug: string): Promise { + if (slug === "zwave_js") { + const entries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); + + if (!entries.length) { + // If the component isn't loaded, ask them to load the integration first + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.missing_zwave_js" + ), + confirm: () => { + fireEvent(this, "handler-picked", { + handler: "zwave_js", + }); + }, + }); + return; + } + + showZWaveJSAddNodeDialog(this, { + entry_id: entries[0].entry_id, + }); + } else if (slug === "zha") { + // If the component isn't loaded, ask them to load the integration first + if (!isComponentLoaded(this.hass, "zha")) { + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.missing_zha" + ), + confirm: () => { + fireEvent(this, "handler-picked", { + handler: "zha", + }); + }, + }); + return; + } + + navigate("/config/zha/add"); + } + + // This closes dialog. + fireEvent(this, "flow-update"); + } + private _maybeSubmit(ev: KeyboardEvent) { if (ev.key !== "Enter") { return; diff --git a/src/translations/en.json b/src/translations/en.json index cc53fac488..d916459869 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2850,7 +2850,10 @@ "error": "Error", "could_not_load": "Config flow could not be loaded", "not_loaded": "The integration could not be loaded, try to restart Home Assistant.", - "missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?" + "missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?", + "supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?", + "missing_zwave_js": "To add a Z-Wave device, you first need to set up the Z-Wave integration. Do you want to do that now?", + "missing_zha": "To add a Zigbee device, you first need to set up the Zigbee Home Automation integration. Do you want to do that now?" } }, "users": { From 20bdb9ff35b5baea67bb2cf23d9c6712f0d80bc3 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Mon, 18 Jul 2022 06:35:51 -0700 Subject: [PATCH 15/58] Use theme default font for charts (#13210) * Use theme default font for charts * Prettier --- src/components/chart/ha-chart-base.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 277ac615d0..30dc0f548b 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -188,6 +188,10 @@ export default class HaChartBase extends LitElement { ChartConstructor.defaults.color = computedStyles.getPropertyValue( "--secondary-text-color" ); + ChartConstructor.defaults.font.family = + computedStyles.getPropertyValue("--mdc-typography-body1-font-family") || + computedStyles.getPropertyValue("--mdc-typography-font-family") || + "Roboto, Noto, sans-serif"; this.chart = new ChartConstructor(ctx, { type: this.chartType, From b131b255ecdbb76661da04ad4cd2c5ef2aa4ee96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 17:09:37 +0200 Subject: [PATCH 16/58] Bump actions/stale from 3.0.13 to 5.1.0 (#13212) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bcb543cf49..6c32d86e4e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 90 days stale policy - uses: actions/stale@v3.0.13 + uses: actions/stale@v5.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 From e4d233afa8797e129361979335ead15f345f65fe Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 18 Jul 2022 15:07:55 -0500 Subject: [PATCH 17/58] Filter Integration in Target and Area selectors + clean up some code (#13202) --- .../ha-selector/ha-selector-area.ts | 102 ++++------- .../ha-selector/ha-selector-device.ts | 56 ++---- .../ha-selector/ha-selector-entity.ts | 37 +--- .../ha-selector/ha-selector-target.ts | 161 ++++++++---------- src/data/device_registry.ts | 27 ++- src/data/entity_registry.ts | 13 ++ src/data/selector.ts | 112 +++++++++--- 7 files changed, 246 insertions(+), 262 deletions(-) diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index bdc8ec9b12..c6f1be6efd 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -1,8 +1,9 @@ -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { html, LitElement } from "lit"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { DeviceRegistryEntry } from "../../data/device_registry"; +import type { DeviceRegistryEntry } from "../../data/device_registry"; +import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { EntityRegistryEntry, subscribeEntityRegistry, @@ -11,7 +12,11 @@ import { EntitySources, fetchEntitySourcesWithCache, } from "../../data/entity_sources"; -import { AreaSelector } from "../../data/selector"; +import type { AreaSelector } from "../../data/selector"; +import { + filterSelectorDevices, + filterSelectorEntities, +} from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; import "../ha-area-picker"; @@ -29,13 +34,15 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { @property() public helper?: string; + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + @state() private _entitySources?: EntitySources; @state() private _entities?: EntityRegistryEntry[]; - @property({ type: Boolean }) public disabled = false; - - @property({ type: Boolean }) public required = true; + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); public hassSubscribe(): UnsubscribeFunc[] { return [ @@ -45,7 +52,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { ]; } - protected updated(changedProperties) { + protected updated(changedProperties: PropertyValues): void { if ( changedProperties.has("selector") && (this.selector.area.device?.integration || @@ -58,7 +65,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { } } - protected render() { + protected render(): TemplateResult { if ( (this.selector.area.device?.integration || this.selector.area.entity?.integration) && @@ -77,12 +84,6 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { no-add .deviceFilter=${this._filterDevices} .entityFilter=${this._filterEntities} - .includeDeviceClasses=${this.selector.area.entity?.device_class - ? [this.selector.area.entity.device_class] - : undefined} - .includeDomains=${this.selector.area.entity?.domain - ? [this.selector.area.entity.domain] - : undefined} .disabled=${this.disabled} .required=${this.required} > @@ -98,27 +99,22 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { no-add .deviceFilter=${this._filterDevices} .entityFilter=${this._filterEntities} - .includeDeviceClasses=${this.selector.area.entity?.device_class - ? [this.selector.area.entity.device_class] - : undefined} - .includeDomains=${this.selector.area.entity?.domain - ? [this.selector.area.entity.domain] - : undefined} .disabled=${this.disabled} .required=${this.required} > `; } - private _filterEntities = (entity: EntityRegistryEntry): boolean => { - const filterIntegration = this.selector.area.entity?.integration; - if ( - filterIntegration && - this._entitySources?.[entity.entity_id]?.domain !== filterIntegration - ) { - return false; + private _filterEntities = (entity: HassEntity): boolean => { + if (!this.selector.area.entity) { + return true; } - return true; + + return filterSelectorEntities( + this.selector.area.entity, + entity, + this._entitySources + ); }; private _filterDevices = (device: DeviceRegistryEntry): boolean => { @@ -126,47 +122,17 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { return true; } - const { - manufacturer: filterManufacturer, - model: filterModel, - integration: filterIntegration, - } = this.selector.area.device; + const deviceIntegrations = + this._entitySources && this._entities + ? this._deviceIntegrationLookup(this._entitySources, this._entities) + : undefined; - if (filterManufacturer && device.manufacturer !== filterManufacturer) { - return false; - } - if (filterModel && device.model !== filterModel) { - return false; - } - if (filterIntegration && this._entitySources && this._entities) { - const deviceIntegrations = this._deviceIntegrations( - this._entitySources, - this._entities - ); - if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { - return false; - } - } - return true; + return filterSelectorDevices( + this.selector.area.device, + device, + deviceIntegrations + ); }; - - private _deviceIntegrations = memoizeOne( - (entitySources: EntitySources, entities: EntityRegistryEntry[]) => { - const deviceIntegrations: Record = {}; - - for (const entity of entities) { - const source = entitySources[entity.entity_id]; - if (!source?.domain) { - continue; - } - if (!deviceIntegrations[entity.device_id!]) { - deviceIntegrations[entity.device_id!] = []; - } - deviceIntegrations[entity.device_id!].push(source.domain); - } - return deviceIntegrations; - } - ); } declare global { diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index ae0dbb07f7..bfb0928524 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -2,8 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { ConfigEntry } from "../../data/config_entries"; import type { DeviceRegistryEntry } from "../../data/device_registry"; +import { getDeviceIntegrationLookup } from "../../data/device_registry"; import { EntityRegistryEntry, subscribeEntityRegistry, @@ -13,6 +13,7 @@ import { fetchEntitySourcesWithCache, } from "../../data/entity_sources"; import type { DeviceSelector } from "../../data/selector"; +import { filterSelectorDevices } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../types"; import "../device/ha-device-picker"; @@ -34,12 +35,12 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { @property() public helper?: string; - @state() public _configEntries?: ConfigEntry[]; - @property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public required = true; + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); + public hassSubscribe(): UnsubscribeFunc[] { return [ subscribeEntityRegistry(this.hass.connection!, (entities) => { @@ -107,48 +108,17 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { } private _filterDevices = (device: DeviceRegistryEntry): boolean => { - const { - manufacturer: filterManufacturer, - model: filterModel, - integration: filterIntegration, - } = this.selector.device; + const deviceIntegrations = + this._entitySources && this._entities + ? this._deviceIntegrationLookup(this._entitySources, this._entities) + : undefined; - if (filterManufacturer && device.manufacturer !== filterManufacturer) { - return false; - } - if (filterModel && device.model !== filterModel) { - return false; - } - if (filterIntegration && this._entitySources && this._entities) { - const deviceIntegrations = this._deviceIntegrations( - this._entitySources, - this._entities - ); - if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { - return false; - } - } - return true; + return filterSelectorDevices( + this.selector.device, + device, + deviceIntegrations + ); }; - - private _deviceIntegrations = memoizeOne( - (entitySources: EntitySources, entities: EntityRegistryEntry[]) => { - const deviceIntegrations: Record = {}; - - for (const entity of entities) { - const source = entitySources[entity.entity_id]; - if (!source?.domain) { - continue; - } - - if (!deviceIntegrations[entity.device_id!]) { - deviceIntegrations[entity.device_id!] = []; - } - deviceIntegrations[entity.device_id!].push(source.domain); - } - return deviceIntegrations; - } - ); } declare global { diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 8cb925f5d1..2c7062171c 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -1,12 +1,12 @@ import { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { EntitySources, fetchEntitySourcesWithCache, } from "../../data/entity_sources"; -import { EntitySelector } from "../../data/selector"; +import type { EntitySelector } from "../../data/selector"; +import { filterSelectorEntities } from "../../data/selector"; import { HomeAssistant } from "../../types"; import "../entity/ha-entities-picker"; import "../entity/ha-entity-picker"; @@ -73,37 +73,8 @@ export class HaEntitySelector extends LitElement { } } - private _filterEntities = (entity: HassEntity): boolean => { - const { - domain: filterDomain, - device_class: filterDeviceClass, - integration: filterIntegration, - } = this.selector.entity; - - if (filterDomain) { - const entityDomain = computeStateDomain(entity); - if ( - Array.isArray(filterDomain) - ? !filterDomain.includes(entityDomain) - : entityDomain !== filterDomain - ) { - return false; - } - } - if ( - filterDeviceClass && - entity.attributes.device_class !== filterDeviceClass - ) { - return false; - } - if ( - filterIntegration && - this._entitySources?.[entity.entity_id]?.domain !== filterIntegration - ) { - return false; - } - return true; - }; + private _filterEntities = (entity: HassEntity): boolean => + filterSelectorEntities(this.selector.entity, entity, this._entitySources); } declare global { diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 4720598a77..70443a097a 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -3,17 +3,33 @@ import { HassServiceTarget, UnsubscribeFunc, } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; -import { DeviceRegistryEntry } from "../../data/device_registry"; import { - EntityRegistryEntry, - subscribeEntityRegistry, -} from "../../data/entity_registry"; -import { TargetSelector } from "../../data/selector"; + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { + DeviceRegistryEntry, + getDeviceIntegrationLookup, +} from "../../data/device_registry"; +import type { EntityRegistryEntry } from "../../data/entity_registry"; +import { subscribeEntityRegistry } from "../../data/entity_registry"; +import { + EntitySources, + fetchEntitySourcesWithCache, +} from "../../data/entity_sources"; +import { + filterSelectorDevices, + filterSelectorEntities, + TargetSelector, +} from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; -import { HomeAssistant } from "../../types"; +import type { HomeAssistant } from "../../types"; import "../ha-target-picker"; @customElement("ha-selector-target") @@ -28,119 +44,82 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { @property() public helper?: string; - @state() private _entityPlaformLookup?: Record; - - @state() private _configEntries?: ConfigEntry[]; - @property({ type: Boolean }) public disabled = false; + @state() private _entitySources?: EntitySources; + + @state() private _entities?: EntityRegistryEntry[]; + + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); + public hassSubscribe(): UnsubscribeFunc[] { return [ subscribeEntityRegistry(this.hass.connection!, (entities) => { - const entityLookup = {}; - for (const confEnt of entities) { - if (!confEnt.platform) { - continue; - } - entityLookup[confEnt.entity_id] = confEnt.platform; - } - this._entityPlaformLookup = entityLookup; + this._entities = entities.filter((entity) => entity.device_id !== null); }), ]; } - protected updated(changedProperties) { - if (changedProperties.has("selector")) { - const oldSelector = changedProperties.get("selector"); - if ( - oldSelector !== this.selector && - (this.selector.target.device?.integration || - this.selector.target.entity?.integration) - ) { - this._loadConfigEntries(); - } + protected updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if ( + changedProperties.has("selector") && + this.selector.target.device?.integration && + !this._entitySources + ) { + fetchEntitySourcesWithCache(this.hass).then((sources) => { + this._entitySources = sources; + }); } } - protected render() { + protected render(): TemplateResult { + if ( + (this.selector.target.device?.integration || + this.selector.target.entity?.integration) && + !this._entitySources + ) { + return html``; + } + return html``; } private _filterEntities = (entity: HassEntity): boolean => { - if ( - this.selector.target.entity?.integration || - this.selector.target.device?.integration - ) { - if ( - !this._entityPlaformLookup || - this._entityPlaformLookup[entity.entity_id] !== - (this.selector.target.entity?.integration || - this.selector.target.device?.integration) - ) { - return false; - } + if (!this.selector.target.entity) { + return true; } - return true; - }; - private _filterRegEntities = (entity: EntityRegistryEntry): boolean => { - if (this.selector.target.entity?.integration) { - if (entity.platform !== this.selector.target.entity.integration) { - return false; - } - } - return true; + return filterSelectorEntities( + this.selector.target.entity, + entity, + this._entitySources + ); }; private _filterDevices = (device: DeviceRegistryEntry): boolean => { - if ( - this.selector.target.device?.manufacturer && - device.manufacturer !== this.selector.target.device.manufacturer - ) { - return false; + if (!this.selector.target.device) { + return true; } - if ( - this.selector.target.device?.model && - device.model !== this.selector.target.device.model - ) { - return false; - } - if ( - this.selector.target.device?.integration || - this.selector.target.entity?.integration - ) { - if ( - !this._configEntries?.some((entry) => - device.config_entries.includes(entry.entry_id) - ) - ) { - return false; - } - } - return true; - }; - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => - entry.domain === this.selector.target.device?.integration || - entry.domain === this.selector.target.entity?.integration + const deviceIntegrations = + this._entitySources && this._entities + ? this._deviceIntegrationLookup(this._entitySources, this._entities) + : undefined; + + return filterSelectorDevices( + this.selector.target.device, + device, + deviceIntegrations ); - } + }; static get styles(): CSSResultGroup { return css` diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 63b050c856..72601269c0 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -1,10 +1,11 @@ import { Connection, createCollection } from "home-assistant-js-websocket"; -import { Store } from "home-assistant-js-websocket/dist/store"; +import type { Store } from "home-assistant-js-websocket/dist/store"; import { computeStateName } from "../common/entity/compute_state_name"; import { caseInsensitiveStringCompare } from "../common/string/compare"; import { debounce } from "../common/util/debounce"; -import { HomeAssistant } from "../types"; -import { EntityRegistryEntry } from "./entity_registry"; +import type { HomeAssistant } from "../types"; +import type { EntityRegistryEntry } from "./entity_registry"; +import type { EntitySources } from "./entity_sources"; export interface DeviceRegistryEntry { id: string; @@ -142,3 +143,23 @@ export const getDeviceEntityLookup = ( } return deviceEntityLookup; }; + +export const getDeviceIntegrationLookup = ( + entitySources: EntitySources, + entities: EntityRegistryEntry[] +): Record => { + const deviceIntegrations: Record = {}; + + for (const entity of entities) { + const source = entitySources[entity.entity_id]; + if (!source?.domain || entity.device_id === null) { + continue; + } + + if (!deviceIntegrations[entity.device_id!]) { + deviceIntegrations[entity.device_id!] = []; + } + deviceIntegrations[entity.device_id!].push(source.domain); + } + return deviceIntegrations; +}; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 00ff94b00d..65f451ab26 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -160,3 +160,16 @@ export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) => entries.sort((entry1, entry2) => caseInsensitiveStringCompare(entry1.name || "", entry2.name || "") ); + +export const getEntityPlatformLookup = ( + entities: EntityRegistryEntry[] +): Record => { + const entityLookup = {}; + for (const confEnt of entities) { + if (!confEnt.platform) { + continue; + } + entityLookup[confEnt.entity_id] = confEnt.platform; + } + return entityLookup; +}; diff --git a/src/data/selector.ts b/src/data/selector.ts index d0f3b0e61c..1b3997b6de 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -1,3 +1,8 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import { computeStateDomain } from "../common/entity/compute_state_domain"; +import type { DeviceRegistryEntry } from "./device_registry"; +import type { EntitySources } from "./entity_sources"; + export type Selector = | ActionSelector | AddonSelector @@ -35,18 +40,22 @@ export interface AddonSelector { }; } +export interface SelectorDevice { + integration?: DeviceSelector["device"]["integration"]; + manufacturer?: DeviceSelector["device"]["manufacturer"]; + model?: DeviceSelector["device"]["model"]; +} + +export interface SelectorEntity { + integration?: EntitySelector["entity"]["integration"]; + domain?: EntitySelector["entity"]["domain"]; + device_class?: EntitySelector["entity"]["device_class"]; +} + export interface AreaSelector { area: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; + entity?: SelectorEntity; + device?: SelectorDevice; multiple?: boolean; }; } @@ -89,10 +98,7 @@ export interface DeviceSelector { integration?: string; manufacturer?: string; model?: string; - entity?: { - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; + entity?: SelectorEntity; multiple?: boolean; }; } @@ -201,16 +207,8 @@ export interface StringSelector { export interface TargetSelector { target: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; + entity?: SelectorEntity; + device?: SelectorDevice; }; } @@ -227,3 +225,69 @@ export interface TimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types time: {}; } + +export const filterSelectorDevices = ( + filterDevice: SelectorDevice, + device: DeviceRegistryEntry, + deviceIntegrationLookup: Record | undefined +): boolean => { + const { + manufacturer: filterManufacturer, + model: filterModel, + integration: filterIntegration, + } = filterDevice; + + if (filterManufacturer && device.manufacturer !== filterManufacturer) { + return false; + } + + if (filterModel && device.model !== filterModel) { + return false; + } + + if (filterIntegration && deviceIntegrationLookup) { + if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) { + return false; + } + } + return true; +}; + +export const filterSelectorEntities = ( + filterEntity: SelectorEntity, + entity: HassEntity, + entitySources?: EntitySources +): boolean => { + const { + domain: filterDomain, + device_class: filterDeviceClass, + integration: filterIntegration, + } = filterEntity; + + if (filterDomain) { + const entityDomain = computeStateDomain(entity); + if ( + Array.isArray(filterDomain) + ? !filterDomain.includes(entityDomain) + : entityDomain !== filterDomain + ) { + return false; + } + } + + if ( + filterDeviceClass && + entity.attributes.device_class !== filterDeviceClass + ) { + return false; + } + + if ( + filterIntegration && + entitySources?.[entity.entity_id]?.domain !== filterIntegration + ) { + return false; + } + + return true; +}; From b582a4d0149baee9eff6e7877c069d29fd744a35 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 11:39:08 +0200 Subject: [PATCH 18/58] Bump node to 16 (#13221) --- .github/workflows/ci.yaml | 2 +- .github/workflows/demo.yaml | 2 +- .github/workflows/nightly.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/translations.yaml | 2 +- .nvmrc | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c0f6369fb2..ba3a011912 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ on: - master env: - NODE_VERSION: 14 + NODE_VERSION: 16 NODE_OPTIONS: --max_old_space_size=6144 jobs: diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml index 05fd6a28e0..68c9d6e0c2 100644 --- a/.github/workflows/demo.yaml +++ b/.github/workflows/demo.yaml @@ -6,7 +6,7 @@ on: - dev env: - NODE_VERSION: 14 + NODE_VERSION: 16 NODE_OPTIONS: --max_old_space_size=6144 jobs: diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index e272963950..4edf96041b 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -7,7 +7,7 @@ on: env: PYTHON_VERSION: 3.8 - NODE_VERSION: 14 + NODE_VERSION: 16 NODE_OPTIONS: --max_old_space_size=6144 permissions: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b3e7cf3d94..450d5fc166 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,7 +7,7 @@ on: env: PYTHON_VERSION: 3.8 - NODE_VERSION: 14 + NODE_VERSION: 16 NODE_OPTIONS: --max_old_space_size=6144 # Set default workflow permissions diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 0a798b1dc4..6cc03e0883 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -8,7 +8,7 @@ on: - src/translations/en.json env: - NODE_VERSION: 14 + NODE_VERSION: 16 jobs: upload: diff --git a/.nvmrc b/.nvmrc index 8351c19397..b6a7d89c68 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14 +16 From 9309a4c7bcbbc1b9bf9073b23e8a6b88008478fe Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 19 Jul 2022 04:44:02 -0500 Subject: [PATCH 19/58] Update language when ZHA or Zwave arent installed (re: supported brands) (#13191) --- .../config-flow/step-flow-pick-handler.ts | 35 +++++++++++++++++-- src/translations/en.json | 5 +-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 6812d118ec..d0afab3efb 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -309,7 +309,21 @@ class StepFlowPickHandler extends LitElement { // If the component isn't loaded, ask them to load the integration first showConfirmationDialog(this, { text: this.hass.localize( - "ui.panel.config.integrations.config_flow.missing_zwave_js" + "ui.panel.config.integrations.config_flow.missing_zwave_zigbee", + { + integration: "Z-Wave", + supported_hardware_link: html`${this.hass.localize( + "ui.panel.config.integrations.config_flow.supported_hardware" + )}`, + } + ), + confirmText: this.hass.localize( + "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { fireEvent(this, "handler-picked", { @@ -328,7 +342,24 @@ class StepFlowPickHandler extends LitElement { if (!isComponentLoaded(this.hass, "zha")) { showConfirmationDialog(this, { text: this.hass.localize( - "ui.panel.config.integrations.config_flow.missing_zha" + "ui.panel.config.integrations.config_flow.missing_zwave_zigbee", + { + integration: "Zigbee", + supported_hardware_link: html`${this.hass.localize( + "ui.panel.config.integrations.config_flow.supported_hardware" + )}`, + } + ), + confirmText: this.hass.localize( + "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { fireEvent(this, "handler-picked", { diff --git a/src/translations/en.json b/src/translations/en.json index d916459869..a807666c00 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2852,8 +2852,9 @@ "not_loaded": "The integration could not be loaded, try to restart Home Assistant.", "missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?", "supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?", - "missing_zwave_js": "To add a Z-Wave device, you first need to set up the Z-Wave integration. Do you want to do that now?", - "missing_zha": "To add a Zigbee device, you first need to set up the Zigbee Home Automation integration. Do you want to do that now?" + "missing_zwave_zigbee": "To add a {integration} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.", + "supported_hardware": "supported hardware", + "proceed": "Proceed" } }, "users": { From 6ac4560b3629d36307e1f33b6e55bed2750033c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 12:07:51 +0200 Subject: [PATCH 20/58] Use YAML in developer tools events (#13222) --- src/components/ha-yaml-editor.ts | 2 +- src/panels/developer-tools/event/event-subscribe-card.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index bfa01b73b9..0367840912 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -41,7 +41,7 @@ export class HaYamlEditor extends LitElement { try { this._yaml = value && !isEmpty(value) - ? dump(value, { schema: this.yamlSchema }) + ? dump(value, { schema: this.yamlSchema, quotingType: '"' }) : ""; } catch (err: any) { // eslint-disable-next-line no-console diff --git a/src/panels/developer-tools/event/event-subscribe-card.ts b/src/panels/developer-tools/event/event-subscribe-card.ts index 6e8d889f6a..2600a23178 100644 --- a/src/panels/developer-tools/event/event-subscribe-card.ts +++ b/src/panels/developer-tools/event/event-subscribe-card.ts @@ -4,6 +4,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { formatTime } from "../../../common/datetime/format_time"; import "../../../components/ha-card"; +import "../../../components/ha-yaml-editor"; import "../../../components/ha-textfield"; import { HomeAssistant } from "../../../types"; @@ -74,7 +75,10 @@ class EventSubscribeCard extends LitElement { ev.id )} ${formatTime(new Date(ev.event.time_fired), this.hass!.locale)}: -
${JSON.stringify(ev.event, null, 4)}
+
` )} From 1c7d3fe61019b29ffeae239afaeb1e8db57c3ecd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Jul 2022 05:09:20 -0500 Subject: [PATCH 21/58] Sync frontend recoverable states with core (#13197) --- src/data/config_entries.ts | 12 +++++++++++- .../config/integrations/ha-integration-card.ts | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 88667cf2ab..5a3757b8e4 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -11,7 +11,8 @@ export interface ConfigEntry { | "migration_error" | "setup_retry" | "not_loaded" - | "failed_unload"; + | "failed_unload" + | "setup_in_progress"; supports_options: boolean; supports_remove_device: boolean; supports_unload: boolean; @@ -28,12 +29,21 @@ export type ConfigEntryMutableParams = Partial< > >; +// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81 export const ERROR_STATES: ConfigEntry["state"][] = [ "migration_error", "setup_error", "setup_retry", ]; +// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81 +export const RECOVERABLE_STATES: ConfigEntry["state"][] = [ + "not_loaded", + "loaded", + "setup_error", + "setup_retry", +]; + export const getConfigEntries = ( hass: HomeAssistant, filters?: { type?: "helper" | "integration"; domain?: string } diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 40484aa881..1bd0992125 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -31,6 +31,7 @@ import { reloadConfigEntry, updateConfigEntry, ERROR_STATES, + RECOVERABLE_STATES, } from "../../../data/config_entries"; import type { DeviceRegistryEntry } from "../../../data/device_registry"; import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics"; @@ -366,7 +367,7 @@ export class HaIntegrationCard extends LitElement { ` : ""} ${!item.disabled_by && - (item.state === "loaded" || item.state === "setup_retry") && + RECOVERABLE_STATES.includes(item.state) && item.supports_unload && item.source !== "system" ? html` From 72443b4f24c19b4ed586d097351137449e173dcd Mon Sep 17 00:00:00 2001 From: Yosi Levy <37745463+yosilevy@users.noreply.github.com> Date: Tue, 19 Jul 2022 13:09:50 +0300 Subject: [PATCH 22/58] RTL card fixes (#13207) --- src/components/ha-attributes.ts | 1 + src/panels/lovelace/cards/hui-media-control-card.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ha-attributes.ts b/src/components/ha-attributes.ts index 0c137a983b..f39f5b9685 100644 --- a/src/components/ha-attributes.ts +++ b/src/components/ha-attributes.ts @@ -76,6 +76,7 @@ class HaAttributes extends LitElement { css` .attribute-container { margin-bottom: 8px; + direction: ltr; } .data-entry { display: flex; diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index 18bf718876..087338f1d2 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -673,7 +673,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { margin-inline-end: initial; padding-inline-start: 0; padding-inline-end: 8px; - direction: var(--direction); + direction: ltr; } .controls > div { From 157b3ba5f2dfcf48582d9c57f9bfc2c012eb3358 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 19 Jul 2022 06:11:05 -0400 Subject: [PATCH 23/58] Clean up zwave_js device actions logic (#13185) --- .../integration-elements/zwave_js/device-actions.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts index 3ff12f5386..2a5de14122 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts @@ -100,19 +100,14 @@ export const getZwaveDeviceActions = async ( action: async () => { if ( isNodeFirmwareUpdateInProgress || - (await fetchZwaveNodeIsFirmwareUpdateInProgress(hass, device.id)) - ) { - showZWaveJUpdateFirmwareNodeDialog(el, { - device, - }); - } else if ( - await showConfirmationDialog(el, { + (await fetchZwaveNodeIsFirmwareUpdateInProgress(hass, device.id)) || + (await showConfirmationDialog(el, { text: hass.localize( "ui.panel.config.zwave_js.update_firmware.warning" ), dismissText: hass.localize("ui.common.no"), confirmText: hass.localize("ui.common.yes"), - }) + })) ) { showZWaveJUpdateFirmwareNodeDialog(el, { device, From 36b49099508a53c9574d1d2088cc3189bac99288 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 12:28:57 +0200 Subject: [PATCH 24/58] Bump Python to 3.10 (#13223) --- .github/workflows/nightly.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 4edf96041b..c130b4ae91 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -6,7 +6,7 @@ on: - cron: "0 1 * * *" env: - PYTHON_VERSION: 3.8 + PYTHON_VERSION: 3.10 NODE_VERSION: 16 NODE_OPTIONS: --max_old_space_size=6144 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 450d5fc166..e9cb8fdf74 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,7 +6,7 @@ on: - published env: - PYTHON_VERSION: 3.8 + PYTHON_VERSION: 3.10 NODE_VERSION: 16 NODE_OPTIONS: --max_old_space_size=6144 From 8b675cdbba952c7149de5a6f734d415c191eac8e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 13:14:20 +0200 Subject: [PATCH 25/58] Remove unused mypy config from pyproject (#13224) --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7eb2cd6854..593f01ee66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,8 +23,3 @@ include-package-data = true [tool.setuptools.packages.find] include = ["hass_frontend*"] - -[tool.mypy] -python_version = 3.4 -show_error_codes = true -strict = true From 05418fc83b08fabd813c4b5944466860441bc506 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Jul 2022 13:17:13 +0200 Subject: [PATCH 26/58] Fix dev tools event (#13225) --- .../event/event-subscribe-card.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/panels/developer-tools/event/event-subscribe-card.ts b/src/panels/developer-tools/event/event-subscribe-card.ts index 2600a23178..0b0ce881b6 100644 --- a/src/panels/developer-tools/event/event-subscribe-card.ts +++ b/src/panels/developer-tools/event/event-subscribe-card.ts @@ -2,10 +2,11 @@ import "@material/mwc-button"; import { HassEvent } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; import { formatTime } from "../../../common/datetime/format_time"; import "../../../components/ha-card"; -import "../../../components/ha-yaml-editor"; import "../../../components/ha-textfield"; +import "../../../components/ha-yaml-editor"; import { HomeAssistant } from "../../../types"; @customElement("event-subscribe-card") @@ -66,21 +67,27 @@ class EventSubscribeCard extends LitElement {
- ${this._events.map( - (ev) => html` -
- ${this.hass!.localize( - "ui.panel.developer-tools.tabs.events.event_fired", - "name", - ev.id - )} - ${formatTime(new Date(ev.event.time_fired), this.hass!.locale)}: - -
- ` + ${repeat( + this._events, + (event) => event.id, + (event) => + html` +
+ ${this.hass!.localize( + "ui.panel.developer-tools.tabs.events.event_fired", + "name", + event.id + )} + ${formatTime( + new Date(event.event.time_fired), + this.hass!.locale + )}: + +
+ ` )}
From bd50d6a6a3975b45479617eb1d297812568a67ec Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 19 Jul 2022 09:25:47 -0500 Subject: [PATCH 27/58] Start the Repairs dashboard (#13192) Co-authored-by: Bram Kragten --- src/common/const.ts | 1 + src/data/repairs.ts | 62 ++++++ src/data/translation.ts | 3 +- src/panels/config/ha-panel-config.ts | 11 + .../config/repairs/dialog-repairs-issue.ts | 107 ++++++++++ .../repairs/ha-config-repairs-dashboard.ts | 100 ++++++++++ .../config/repairs/ha-config-repairs.ts | 129 ++++++++++++ .../config/repairs/show-dialog-repair-flow.ts | 188 ++++++++++++++++++ .../repairs/show-repair-issue-dialog.ts | 19 ++ src/translations/en.json | 23 +++ 10 files changed, 642 insertions(+), 1 deletion(-) create mode 100644 src/data/repairs.ts create mode 100644 src/panels/config/repairs/dialog-repairs-issue.ts create mode 100644 src/panels/config/repairs/ha-config-repairs-dashboard.ts create mode 100644 src/panels/config/repairs/ha-config-repairs.ts create mode 100644 src/panels/config/repairs/show-dialog-repair-flow.ts create mode 100644 src/panels/config/repairs/show-repair-issue-dialog.ts diff --git a/src/common/const.ts b/src/common/const.ts index ee9f94000c..0e16d6db32 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -76,6 +76,7 @@ export const FIXED_DOMAIN_ICONS = { configurator: mdiCog, conversation: mdiTextToSpeech, counter: mdiCounter, + demo: mdiHomeAssistant, fan: mdiFan, google_assistant: mdiGoogleAssistant, group: mdiGoogleCirclesCommunities, diff --git a/src/data/repairs.ts b/src/data/repairs.ts new file mode 100644 index 0000000000..e9459d9b1b --- /dev/null +++ b/src/data/repairs.ts @@ -0,0 +1,62 @@ +import type { HomeAssistant } from "../types"; +import { DataEntryFlowStep } from "./data_entry_flow"; + +export interface RepairsIssue { + domain: string; + issue_id: string; + active: boolean; + is_fixable: boolean; + severity?: "error" | "warning" | "critical"; + breaks_in_ha_version?: string; + ignored: boolean; + created: string; + dismissed_version?: string; + learn_more_url?: string; + translation_key?: string; + translation_placeholders?: Record; +} + +export const fetchRepairsIssues = async (hass: HomeAssistant) => + hass.callWS<{ issues: RepairsIssue[] }>({ + type: "resolution_center/list_issues", + }); + +export const dismissRepairsIssue = async ( + hass: HomeAssistant, + issue: RepairsIssue +) => + hass.callWS({ + type: "resolution_center/dismiss_issue", + issue_id: issue.issue_id, + domain: issue.domain, + }); + +export const createRepairsFlow = ( + hass: HomeAssistant, + handler: string, + issue_id: string +) => + hass.callApi("POST", "resolution_center/issues/fix", { + handler, + issue_id, + }); + +export const fetchRepairsFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi( + "GET", + `resolution_center/issues/fix/${flowId}` + ); + +export const handleRepairsFlowStep = ( + hass: HomeAssistant, + flowId: string, + data: Record +) => + hass.callApi( + "POST", + `resolution_center/issues/fix/${flowId}`, + data + ); + +export const deleteRepairsFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi("DELETE", `resolution_center/issues/fix/${flowId}`); diff --git a/src/data/translation.ts b/src/data/translation.ts index e7c632ea57..cadcfe0ca7 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -39,7 +39,8 @@ export type TranslationCategory = | "mfa_setup" | "system_health" | "device_class" - | "application_credentials"; + | "application_credentials" + | "issues"; export const fetchTranslationPreferences = (hass: HomeAssistant) => fetchFrontendUserData(hass.connection, "language"); diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 7f7b93df45..67cbdcfa62 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -9,6 +9,7 @@ import { mdiHeart, mdiInformation, mdiInformationOutline, + mdiLifebuoy, mdiLightningBolt, mdiMapMarkerRadius, mdiMathLog, @@ -267,6 +268,12 @@ export const configSections: { [name: string]: PageNavigation[] } = { iconPath: mdiUpdate, iconColor: "#3B808E", }, + { + path: "/config/repairs", + translationKey: "repairs", + iconPath: mdiLifebuoy, + iconColor: "#5c995c", + }, { component: "logs", path: "/config/logs", @@ -448,6 +455,10 @@ class HaPanelConfig extends HassRouterPage { tag: "ha-config-section-updates", load: () => import("./core/ha-config-section-updates"), }, + repairs: { + tag: "ha-config-repairs-dashboard", + load: () => import("./repairs/ha-config-repairs-dashboard"), + }, users: { tag: "ha-config-users", load: () => import("./users/ha-config-users"), diff --git a/src/panels/config/repairs/dialog-repairs-issue.ts b/src/panels/config/repairs/dialog-repairs-issue.ts new file mode 100644 index 0000000000..88054fc805 --- /dev/null +++ b/src/panels/config/repairs/dialog-repairs-issue.ts @@ -0,0 +1,107 @@ +import "@material/mwc-button/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-alert"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import type { RepairsIssue } from "../../../data/repairs"; +import { haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import type { RepairsIssueDialogParams } from "./show-repair-issue-dialog"; + +@customElement("dialog-repairs-issue") +class DialogRepairsIssue extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _issue?: RepairsIssue; + + @state() private _params?: RepairsIssueDialogParams; + + @state() private _error?: string; + + public showDialog(params: RepairsIssueDialogParams): void { + this._params = params; + this._issue = this._params.issue; + } + + public closeDialog() { + this._params = undefined; + this._issue = undefined; + this._error = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._issue) { + return html``; + } + + return html` + +
+ ${this._error + ? html`${this._error}` + : ""} + ${this.hass.localize( + `component.${this._issue.domain}.issues.${this._issue.issue_id}.${ + this._issue.translation_key || "description" + }`, + this._issue.translation_placeholders + )} + ${this._issue.breaks_in_ha_version + ? html` + This will no longer work as of the + ${this._issue.breaks_in_ha_version} release of Home Assistant. + ` + : ""} + The issue is ${this._issue.severity} severity + ${this._issue.is_fixable ? "and fixable" : "but not fixable"}. + ${this._issue.dismissed_version + ? html` + This issue has been dismissed in version + ${this._issue.dismissed_version}. + ` + : ""} +
+ ${this._issue.learn_more_url + ? html` + + + + ` + : ""} +
+ `; + } + + static styles: CSSResultGroup = [ + haStyleDialog, + css` + a { + text-decoration: none; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-repairs-issue": DialogRepairsIssue; + } +} diff --git a/src/panels/config/repairs/ha-config-repairs-dashboard.ts b/src/panels/config/repairs/ha-config-repairs-dashboard.ts new file mode 100644 index 0000000000..c36b810683 --- /dev/null +++ b/src/panels/config/repairs/ha-config-repairs-dashboard.ts @@ -0,0 +1,100 @@ +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../components/ha-card"; +import type { RepairsIssue } from "../../../data/repairs"; +import { fetchRepairsIssues } from "../../../data/repairs"; +import "../../../layouts/hass-subpage"; +import type { HomeAssistant } from "../../../types"; +import "./ha-config-repairs"; + +@customElement("ha-config-repairs-dashboard") +class HaConfigRepairsDashboard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow!: boolean; + + @state() private _repairsIssues: RepairsIssue[] = []; + + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + this._fetchIssues(); + } + + protected render(): TemplateResult { + return html` + +
+ +
+ ${this._repairsIssues.length + ? html` + + ` + : html` +
+ ${this.hass.localize( + "ui.panel.config.repairs.no_repairs" + )} +
+ `} +
+
+
+
+ `; + } + + private async _fetchIssues(): Promise { + const [, repairsIssues] = await Promise.all([ + this.hass.loadBackendTranslation("issues"), + fetchRepairsIssues(this.hass), + ]); + + this._repairsIssues = repairsIssues.issues; + } + + static styles = css` + .content { + padding: 28px 20px 0; + max-width: 1040px; + margin: 0 auto; + } + + ha-card { + max-width: 600px; + margin: 0 auto; + height: 100%; + justify-content: space-between; + flex-direction: column; + display: flex; + margin-bottom: max(24px, env(safe-area-inset-bottom)); + } + + .card-content { + display: flex; + justify-content: space-between; + flex-direction: column; + padding: 0; + } + + .no-repairs { + padding: 16px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-repairs-dashboard": HaConfigRepairsDashboard; + } +} diff --git a/src/panels/config/repairs/ha-config-repairs.ts b/src/panels/config/repairs/ha-config-repairs.ts new file mode 100644 index 0000000000..625753ac2c --- /dev/null +++ b/src/panels/config/repairs/ha-config-repairs.ts @@ -0,0 +1,129 @@ +import "@material/mwc-list/mwc-list"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { relativeTime } from "../../../common/datetime/relative_time"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-alert"; +import "../../../components/ha-card"; +import "../../../components/ha-list-item"; +import "../../../components/ha-svg-icon"; +import { domainToName } from "../../../data/integration"; +import type { RepairsIssue } from "../../../data/repairs"; +import "../../../layouts/hass-subpage"; +import type { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { showRepairsFlowDialog } from "./show-dialog-repair-flow"; +import { showRepairsIssueDialog } from "./show-repair-issue-dialog"; + +@customElement("ha-config-repairs") +class HaConfigRepairs extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ attribute: false }) + public repairsIssues?: RepairsIssue[]; + + protected render(): TemplateResult { + if (!this.repairsIssues?.length) { + return html``; + } + + const issues = this.repairsIssues; + + return html` +
+ ${this.hass.localize("ui.panel.config.repairs.title", { + count: this.repairsIssues.length, + })} +
+ + ${issues.map((issue) => + issue.ignored + ? "" + : html` + + + ${this.hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.title` + )} + + ${issue.created + ? relativeTime(new Date(issue.created), this.hass.locale) + : ""} + + + ` + )} + + `; + } + + private _openShowMoreDialog(ev): void { + const issue = ev.currentTarget.issue as RepairsIssue; + if (issue.is_fixable) { + showRepairsFlowDialog(this, issue, () => { + // @ts-ignore + fireEvent(this, "update-issues"); + }); + } else { + showRepairsIssueDialog(this, { issue: (ev.currentTarget as any).issue }); + } + } + + static styles = css` + :host { + --mdc-list-vertical-padding: 0; + } + .title { + font-size: 16px; + padding: 16px; + padding-bottom: 0; + } + button.show-more { + color: var(--primary-color); + text-align: left; + cursor: pointer; + background: none; + border-width: initial; + border-style: none; + border-color: initial; + border-image: initial; + padding: 16px; + font: inherit; + } + button.show-more:focus { + outline: none; + text-decoration: underline; + } + ha-list-item { + cursor: pointer; + font-size: 16px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-repairs": HaConfigRepairs; + } +} diff --git a/src/panels/config/repairs/show-dialog-repair-flow.ts b/src/panels/config/repairs/show-dialog-repair-flow.ts new file mode 100644 index 0000000000..837ad1a9fa --- /dev/null +++ b/src/panels/config/repairs/show-dialog-repair-flow.ts @@ -0,0 +1,188 @@ +import { html } from "lit"; +import { domainToName } from "../../../data/integration"; +import { + createRepairsFlow, + deleteRepairsFlow, + fetchRepairsFlow, + handleRepairsFlowStep, + RepairsIssue, +} from "../../../data/repairs"; +import { + loadDataEntryFlowDialog, + showFlowDialog, +} from "../../../dialogs/config-flow/show-dialog-data-entry-flow"; + +export const loadRepairFlowDialog = loadDataEntryFlowDialog; + +export const showRepairsFlowDialog = ( + element: HTMLElement, + issue: RepairsIssue, + dialogClosedCallback?: (params: { flowFinished: boolean }) => void +): void => + showFlowDialog( + element, + { + startFlowHandler: issue.domain, + domain: issue.domain, + dialogClosedCallback, + }, + { + loadDevicesAndAreas: false, + createFlow: async (hass, handler) => { + const [step] = await Promise.all([ + createRepairsFlow(hass, handler, issue.issue_id), + hass.loadBackendTranslation("issues", issue.domain), + ]); + return step; + }, + fetchFlow: async (hass, flowId) => { + const [step] = await Promise.all([ + fetchRepairsFlow(hass, flowId), + hass.loadBackendTranslation("issues", issue.domain), + ]); + return step; + }, + handleFlowStep: handleRepairsFlowStep, + deleteFlow: deleteRepairsFlow, + + renderAbortDescription(hass, step) { + const description = hass.localize( + `component.${issue.domain}.issues.abort.${step.reason}`, + step.description_placeholders + ); + + return description + ? html` + + ` + : ""; + }, + + renderShowFormStepHeader(hass, step) { + return ( + hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.step.${step.step_id}.title` + ) || hass.localize(`ui.dialogs.issues_flow.form.header`) + ); + }, + + renderShowFormStepDescription(hass, step) { + const description = hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + + renderShowFormStepFieldLabel(hass, step, field) { + return hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.step.${step.step_id}.data.${field.name}` + ); + }, + + renderShowFormStepFieldHelper(hass, step, field) { + return hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.step.${step.step_id}.data_description.${field.name}` + ); + }, + + renderShowFormStepFieldError(hass, step, error) { + return hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.error.${error}`, + step.description_placeholders + ); + }, + + renderExternalStepHeader(_hass, _step) { + return ""; + }, + + renderExternalStepDescription(_hass, _step) { + return ""; + }, + + renderCreateEntryDescription(hass, _step) { + return html` +

${hass.localize(`ui.dialogs.repairs.success.description`)}

+ `; + }, + + renderShowFormProgressHeader(hass, step) { + return ( + hass.localize( + `component.${issue.domain}.issues.step.${issue.issue_id}.fix_flow.${step.step_id}.title` + ) || hass.localize(`component.${issue.domain}.title`) + ); + }, + + renderShowFormProgressDescription(hass, step) { + const description = hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.progress.${step.progress_action}`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + + renderMenuHeader(hass, step) { + return ( + hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.step.${step.step_id}.title` + ) || hass.localize(`component.${issue.domain}.title`) + ); + }, + + renderMenuDescription(hass, step) { + const description = hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + + renderMenuOption(hass, step, option) { + return hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.step.${step.step_id}.menu_issues.${option}`, + step.description_placeholders + ); + }, + + renderLoadingDescription(hass, reason) { + return ( + hass.localize( + `component.${issue.domain}.issues.${issue.issue_id}.fix_flow.loading` + ) || + hass.localize(`ui.dialogs.repairs.loading.${reason}`, { + integration: domainToName(hass.localize, issue.domain), + }) + ); + }, + } + ); diff --git a/src/panels/config/repairs/show-repair-issue-dialog.ts b/src/panels/config/repairs/show-repair-issue-dialog.ts new file mode 100644 index 0000000000..4591591c7c --- /dev/null +++ b/src/panels/config/repairs/show-repair-issue-dialog.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import type { RepairsIssue } from "../../../data/repairs"; + +export interface RepairsIssueDialogParams { + issue: RepairsIssue; +} + +export const loadRepairsIssueDialog = () => import("./dialog-repairs-issue"); + +export const showRepairsIssueDialog = ( + element: HTMLElement, + repairsIssueParams: RepairsIssueDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-repairs-issue", + dialogImport: loadRepairsIssueDialog, + dialogParams: repairsIssueParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index a807666c00..7e91122e20 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -955,6 +955,18 @@ "description": "Options successfully saved." } }, + "repair_flow": { + "form": { + "header": "Repair issue" + }, + "loading": { + "loading_flow": "Please wait while the repair for {integration} is being initialized", + "loading_step": "[%key:ui::panel::config::integrations::config_flow::loading::loading_step%]" + }, + "success": { + "description": "The issue is repaired!" + } + }, "config_entry_system_options": { "title": "System Options for {integration}", "enable_new_entities_label": "Enable newly added entities.", @@ -1216,6 +1228,17 @@ "leave_beta": "[%key:supervisor::system::supervisor::leave_beta_action%]", "skipped": "Skipped" }, + "repairs": { + "caption": "Repairs", + "description": "Find and fix issues with your Home Assistant instance", + "title": "{count} {count, plural,\n one {repair}\n other {repairs}\n}", + "no_repairs": "There are currently no repairs available", + "dialog": { + "title": "Repair", + "fix": "Repair", + "learn": "Learn more" + } + }, "areas": { "caption": "Areas", "description": "Group devices and entities into areas", From cb256bc3864fc96917bce5cca75019b2354f5b71 Mon Sep 17 00:00:00 2001 From: Felix Rudat Date: Tue, 19 Jul 2022 17:45:48 +0200 Subject: [PATCH 28/58] Add auto_fit config option to lovelace map card --- src/panels/lovelace/cards/hui-map-card.ts | 1 + src/panels/lovelace/cards/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 79a96c9861..2c725dfeba 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -134,6 +134,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { )} .zoom=${this._config.default_zoom ?? 14} .paths=${this._getHistoryPaths(this._config, this._history)} + .autoFit=${this._config.auto_fit} .darkMode=${this._config.dark_mode} > ; hours_to_show?: number; From d41159591ca451db77ecb3756ea19630adce299a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Jul 2022 20:56:50 +0200 Subject: [PATCH 29/58] Change map styles to "Voyager" (#13227) --- src/common/dom/setup-leaflet-map.ts | 21 ++++++++------------- src/components/map/ha-map.ts | 26 ++++++++++---------------- src/resources/styles.ts | 1 + 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index 8d0d658f5c..1ce5c20f4e 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -5,8 +5,7 @@ export type LeafletModuleType = typeof import("leaflet"); export type LeafletDrawModuleType = typeof import("leaflet-draw"); export const setupLeafletMap = async ( - mapElement: HTMLElement, - darkMode?: boolean + mapElement: HTMLElement ): Promise<[Map, LeafletModuleType, TileLayer]> => { if (!mapElement.parentNode) { throw new Error("Cannot setup Leaflet map on disconnected element"); @@ -23,7 +22,7 @@ export const setupLeafletMap = async ( mapElement.parentNode.appendChild(style); map.setView([52.3731339, 4.8903147], 13); - const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map); + const tileLayer = createTileLayer(Leaflet).addTo(map); return [map, Leaflet, tileLayer]; }; @@ -31,23 +30,19 @@ export const setupLeafletMap = async ( export const replaceTileLayer = ( leaflet: LeafletModuleType, map: Map, - tileLayer: TileLayer, - darkMode: boolean + tileLayer: TileLayer ): TileLayer => { map.removeLayer(tileLayer); - tileLayer = createTileLayer(leaflet, darkMode); + tileLayer = createTileLayer(leaflet); tileLayer.addTo(map); return tileLayer; }; -const createTileLayer = ( - leaflet: LeafletModuleType, - darkMode: boolean -): TileLayer => +const createTileLayer = (leaflet: LeafletModuleType): TileLayer => leaflet.tileLayer( - `https://{s}.basemaps.cartocdn.com/${ - darkMode ? "dark_all" : "light_all" - }/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`, + `https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${ + leaflet.Browser.retina ? "@2x.png" : ".png" + }`, { attribution: '© OpenStreetMap, © CARTO', diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index f422f053d6..a123414675 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -6,21 +6,19 @@ import { Map, Marker, Polyline, - TileLayer, } from "leaflet"; import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { LeafletModuleType, - replaceTileLayer, setupLeafletMap, } from "../../common/dom/setup-leaflet-map"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; -import "./ha-entity-marker"; +import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; import { HomeAssistant } from "../../types"; import "../ha-icon-button"; -import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; +import "./ha-entity-marker"; const getEntityId = (entity: string | HaMapEntity): string => typeof entity === "string" ? entity : entity.entity_id; @@ -60,8 +58,6 @@ export class HaMap extends ReactiveElement { private Leaflet?: LeafletModuleType; - private _tileLayer?: TileLayer; - private _resizeObserver?: ResizeObserver; private _mapItems: Array = []; @@ -142,12 +138,6 @@ export class HaMap extends ReactiveElement { return; } const darkMode = this.darkMode ?? this.hass.themes.darkMode; - this._tileLayer = replaceTileLayer( - this.Leaflet!, - this.leafletMap!, - this._tileLayer!, - darkMode - ); this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode); } @@ -159,10 +149,7 @@ export class HaMap extends ReactiveElement { this.shadowRoot!.append(map); } const darkMode = this.darkMode ?? this.hass.themes.darkMode; - [this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( - map, - darkMode - ); + [this.leafletMap, this.Leaflet] = await setupLeafletMap(map); this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode); this._loaded = true; } @@ -473,6 +460,13 @@ export class HaMap extends ReactiveElement { .dark { color: #ffffff; } + .leaflet-tile-pane { + filter: var(--map-filter); + } + .dark .leaflet-bar a { + background: var(--card-background-color); + color: #ffffff; + } .leaflet-marker-draggable { cursor: move !important; } diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 1a71387b56..b5635a7d34 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -46,6 +46,7 @@ export const darkStyles = { "codemirror-qualifier": "#DECB6B", "codemirror-type": "#DECB6B", "energy-grid-return-color": "#a280db", + "map-filter": "invert(.9) hue-rotate(170deg) grayscale(.7)", }; export const derivedStyles = { From a4b92fef3a5a07f60f2fb61d5b4fc2dbc4dc5552 Mon Sep 17 00:00:00 2001 From: Maximilian Ertl Date: Tue, 19 Jul 2022 20:59:45 +0200 Subject: [PATCH 30/58] Correctly set "allow-downloads" flag on iframes (#13218) --- src/panels/iframe/ha-panel-iframe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/iframe/ha-panel-iframe.ts b/src/panels/iframe/ha-panel-iframe.ts index 14aec1fdcd..f2d98b0e33 100644 --- a/src/panels/iframe/ha-panel-iframe.ts +++ b/src/panels/iframe/ha-panel-iframe.ts @@ -36,7 +36,7 @@ class HaPanelIframe extends LitElement { >