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..516e8c6704 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -6,8 +6,8 @@ on: - cron: "0 1 * * *" env: - PYTHON_VERSION: 3.8 - NODE_VERSION: 14 + PYTHON_VERSION: "3.10" + 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..ed017d1744 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,8 +6,8 @@ on: - published env: - PYTHON_VERSION: 3.8 - NODE_VERSION: 14 + PYTHON_VERSION: "3.10" + NODE_VERSION: 16 NODE_OPTIONS: --max_old_space_size=6144 # Set default workflow permissions @@ -21,7 +21,7 @@ jobs: name: Release runs-on: ubuntu-latest permissions: - contents: write # Required to upload release assets + contents: write # Required to upload release assets steps: - name: Checkout the repository uses: actions/checkout@v3 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 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 diff --git a/build-scripts/removedIcons.json b/build-scripts/removedIcons.json index fe51488c70..02499346dd 100644 --- a/build-scripts/removedIcons.json +++ b/build-scripts/removedIcons.json @@ -1 +1,30 @@ -[] +[ + { + "path": "M20,20H7A2,2 0 0,1 5,18V8.94L2.23,5.64C2.09,5.47 2,5.24 2,5A1,1 0 0,1 3,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20M8.5,7A0.5,0.5 0 0,0 8,7.5V8.5A0.5,0.5 0 0,0 8.5,9H18.5A0.5,0.5 0 0,0 19,8.5V7.5A0.5,0.5 0 0,0 18.5,7H8.5M8.5,11A0.5,0.5 0 0,0 8,11.5V12.5A0.5,0.5 0 0,0 8.5,13H18.5A0.5,0.5 0 0,0 19,12.5V11.5A0.5,0.5 0 0,0 18.5,11H8.5M8.5,15A0.5,0.5 0 0,0 8,15.5V16.5A0.5,0.5 0 0,0 8.5,17H13.5A0.5,0.5 0 0,0 14,16.5V15.5A0.5,0.5 0 0,0 13.5,15H8.5Z", + "name": "android-messages" + }, + { + "path": "M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,12L17.5,10.5L15,12V4H20V12Z", + "name": "book-variant-multiple" + }, + { + "path": "M21,14H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10L8,21V22H16V21L14,18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z", + "name": "desktop-mac" + }, + { + "path": "M21,14V4H3V14H21M21,2A2,2 0 0,1 23,4V16A2,2 0 0,1 21,18H14L16,21V22H8V21L10,18H3C1.89,18 1,17.1 1,16V4C1,2.89 1.89,2 3,2H21M4,5H15V10H4V5M16,5H20V7H16V5M20,8V13H16V8H20M4,11H9V13H4V11M10,11H15V13H10V11Z", + "name": "desktop-mac-dashboard" + }, + { + "path": "M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z", + "name": "discord" + }, + { + "path": "M8.06,7.78C7.5,7.78 7.17,7.73 7.08,7.64L6.66,13.73C7.19,14.05 7.88,14.3 8.72,14.5C9.56,14.71 10.78,14.77 12.38,14.67C13.97,14.58 15.63,14.23 17.34,13.64L16.55,4.22C15.67,5.09 14.38,5.91 12.66,6.66C11.13,7.31 9.81,7.69 8.72,7.78H8.06M7.97,5.34C7.28,5.94 7,6.34 7.13,6.56C7.22,6.78 7.7,6.84 8.58,6.75C9.67,6.66 10.91,6.31 12.28,5.72C13.22,5.31 14.03,4.88 14.72,4.41C15.41,3.94 15.88,3.55 16.13,3.23C16.38,2.92 16.47,2.7 16.41,2.58C16.34,2.42 16.03,2.34 15.47,2.34C14.34,2.34 12.94,2.7 11.25,3.42C9.81,4.05 8.72,4.69 7.97,5.34M17.34,2.2C17.41,2.33 17.44,2.47 17.44,2.63L18.61,17C18.61,18.73 18,20.09 16.83,21.07C15.64,22.05 14.03,22.55 12,22.55C10,22.55 8.4,22.04 7.2,21C6,20 5.39,18.64 5.39,16.92L6.09,6.47C6.09,6.22 6.2,5.94 6.42,5.63C6.64,5.31 6.84,5.06 7.03,4.88L7.36,4.59C8.33,3.78 9.5,3.08 10.88,2.5C11.81,2.08 12.73,1.77 13.62,1.57C14.5,1.37 15.3,1.3 16,1.38C16.71,1.46 17.16,1.73 17.34,2.2Z", + "name": "google-home" + }, + { + "path": "M19.25,19H4.75V3H19.25M14,22H10V21H14M18,0H6A3,3 0 0,0 3,3V21A3,3 0 0,0 6,24H18A3,3 0 0,0 21,21V3A3,3 0 0,0 18,0Z", + "name": "tablet-android" + } +] diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 958e14e0e3..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,9 +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 { mockConfig } from "./stubs/config"; -import { energyEntities } from "./stubs/entities"; class HaDemo extends HomeAssistantAppEl { protected async _initializeHass() { @@ -51,8 +52,36 @@ 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, + has_entity_name: false, + }, + { + 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, + has_entity_name: false, + }, + ]); 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); +}; diff --git a/gallery/sidebar.js b/gallery/sidebar.js index 14a8d38fe8..156d7c2969 100644 --- a/gallery/sidebar.js +++ b/gallery/sidebar.js @@ -8,7 +8,7 @@ module.exports = [ { category: "lovelace", // Label for in the sidebar - header: "Lovelace", + header: "Dashboards", // Specify order of pages. Any pages in the category folder but not listed here will // automatically be added after the pages listed here. pages: ["introduction"], @@ -34,7 +34,7 @@ module.exports = [ }, { category: "misc", - header: "Miscelaneous", + header: "Miscellaneous", }, { category: "brand", diff --git a/gallery/src/pages/lovelace/area-card.ts b/gallery/src/pages/lovelace/area-card.ts index cd80ca2064..30f98b07de 100644 --- a/gallery/src/pages/lovelace/area-card.ts +++ b/gallery/src/pages/lovelace/area-card.ts @@ -31,7 +31,7 @@ const ENTITIES = [ friendly_name: "Office Light", }), getEntity("fan", "kitchen", "on", { - friendly_name: "Second Office Fan", + friendly_name: "Kitchen Fan", }), getEntity("binary_sensor", "kitchen_door", "on", { friendly_name: "Office Door", @@ -102,7 +102,7 @@ class DemoArea extends LitElement { picture: "/images/office.jpg", }, { - name: "Second Office", + name: "Kitchen", area_id: "kitchen", picture: "/images/kitchen.png", }, diff --git a/gallery/src/pages/lovelace/introduction.markdown b/gallery/src/pages/lovelace/introduction.markdown index a6fa62a9a7..c7bfc791d9 100644 --- a/gallery/src/pages/lovelace/introduction.markdown +++ b/gallery/src/pages/lovelace/introduction.markdown @@ -1,11 +1,11 @@ --- title: Introduction --- -Lovelace has many different cards. Each card allows the user to tell +Dashboards have many different cards. Each card allows the user to tell a different story about what is going on in their house. These cards are very customizable, as no household is the same. This gallery helps our developers and designers to see all the different states that each card can be in. -Check [the Lovelace documentation](https://www.home-assistant.io/lovelace) for instructions on how to get started with Lovelace. +Check [the Dashboards documentation](https://www.home-assistant.io/dashboards/) for instructions on how to get started with Dashboards. 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/gallery/src/pages/more-info/light.ts b/gallery/src/pages/more-info/light.ts index 530b95909e..999a4908ad 100644 --- a/gallery/src/pages/more-info/light.ts +++ b/gallery/src/pages/more-info/light.ts @@ -69,7 +69,7 @@ const ENTITIES = [ effect_list: ["random", "colorloop"], }), getEntity("light", "color_RGB_light", "on", { - friendly_name: "Color Effets Light", + friendly_name: "Color Effects Light", brightness: 255, rgb_color: [30, 100, 255], supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index 56c9ecaadf..c11608d595 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -81,10 +81,10 @@ class HassioAddonRepositoryEl extends LitElement { ? this.supervisor.localize( "common.new_version_available" ) - : this.supervisor.localize("addon.installed") + : this.supervisor.localize("addon.state.installed") : addon.available - ? this.supervisor.localize("addon.not_installed") - : this.supervisor.localize("addon.not_available")} + ? this.supervisor.localize("addon.state.not_installed") + : this.supervisor.localize("addon.state.not_available")} .iconClass=${addon.installed ? addon.update_available ? "update" diff --git a/hassio/src/addon-view/config/hassio-addon-config.ts b/hassio/src/addon-view/config/hassio-addon-config.ts index 196d345bba..bca38c669e 100644 --- a/hassio/src/addon-view/config/hassio-addon-config.ts +++ b/hassio/src/addon-view/config/hassio-addon-config.ts @@ -336,7 +336,7 @@ class HassioAddonConfig extends LitElement { fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { this._error = this.supervisor.localize( - "addon.common.update_available", + "addon.failed_to_reset", "error", extractApiErrorMessage(err) ); diff --git a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts index e16647b1e5..db64cb1622 100644 --- a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts +++ b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts @@ -81,7 +81,7 @@ class HassioAddonDocumentationDashboard extends LitElement { ); } catch (err: any) { this._error = this.supervisor.localize( - "addon.documentation.get_logs", + "addon.documentation.get_documentation", "error", extractApiErrorMessage(err) ); 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` { - 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), }); diff --git a/hassio/src/supervisor-base-element.ts b/hassio/src/supervisor-base-element.ts index a5d3fbb7f6..7d9ee78d07 100644 --- a/hassio/src/supervisor-base-element.ts +++ b/hassio/src/supervisor-base-element.ts @@ -25,7 +25,7 @@ import { } from "../../src/data/supervisor/supervisor"; import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; import { urlSyncMixin } from "../../src/state/url-sync-mixin"; -import { HomeAssistant, Route } from "../../src/types"; +import { HomeAssistant, Route, TranslationDict } from "../../src/types"; import { getTranslation } from "../../src/util/common-translation"; declare global { @@ -124,9 +124,13 @@ export class SupervisorBaseElement extends urlSyncMixin( this.supervisor = { ...this.supervisor, - localize: await computeLocalize(this.constructor.prototype, language, { - [language]: data, - }), + localize: await computeLocalize( + this.constructor.prototype, + language, + { + [language]: data, + } + ), }; } diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index 2623cdcf25..e1acfc74f1 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -26,7 +26,7 @@ import { import { UNHEALTHY_REASON_URL, UNSUPPORTED_REASON_URL, -} from "../../../src/panels/config/system-health/ha-config-system-health"; +} from "../../../src/panels/config/repairs/dialog-system-information"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; import { bytesToString } from "../../../src/util/bytes-to-string"; diff --git a/package.json b/package.json index b0f426ea1c..2b7bbf967a 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,8 @@ "@material/mwc-textfield": "0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/top-app-bar": "14.0.0-canary.261f2db59.0", - "@mdi/js": "6.9.96", - "@mdi/svg": "6.9.96", + "@mdi/js": "7.0.96", + "@mdi/svg": "7.0.96", "@polymer/app-layout": "^3.1.0", "@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-icon": "^3.0.1", diff --git a/pyproject.toml b/pyproject.toml index 9ccb502873..2edefde21d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20220707.1" +version = "20220727.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" @@ -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 diff --git a/src/common/const.ts b/src/common/const.ts index ee9f94000c..68489f95d2 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -47,7 +47,7 @@ import { mdiRobotVacuum, mdiScriptText, mdiSineWave, - mdiTextToSpeech, + mdiMicrophoneMessage, mdiThermometer, mdiThermostat, mdiTimerOutline, @@ -74,8 +74,9 @@ export const FIXED_DOMAIN_ICONS = { camera: mdiVideo, climate: mdiThermostat, configurator: mdiCog, - conversation: mdiTextToSpeech, + conversation: mdiMicrophoneMessage, counter: mdiCounter, + demo: mdiHomeAssistant, fan: mdiFan, google_assistant: mdiGoogleAssistant, group: mdiGoogleCirclesCommunities, 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/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 82c10c6b48..7e39f8bcc0 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -8,6 +8,7 @@ import { mdiCalendar, mdiCast, mdiCastConnected, + mdiCastOff, mdiChartSankey, mdiCheckCircleOutline, mdiClock, @@ -25,7 +26,15 @@ import { mdiPowerPlug, mdiPowerPlugOff, mdiRestart, + mdiSpeaker, + mdiSpeakerOff, + mdiSpeakerPause, + mdiSpeakerPlay, mdiSwapHorizontal, + mdiTelevision, + mdiTelevisionOff, + mdiTelevisionPause, + mdiTelevisionPlay, mdiToggleSwitchVariant, mdiToggleSwitchVariantOff, mdiWeatherNight, @@ -127,7 +136,40 @@ export const domainIconWithoutDefault = ( } case "media_player": - return compareState === "playing" ? mdiCastConnected : mdiCast; + switch (stateObj?.attributes.device_class) { + case "speaker": + switch (compareState) { + case "playing": + return mdiSpeakerPlay; + case "paused": + return mdiSpeakerPause; + case "off": + return mdiSpeakerOff; + default: + return mdiSpeaker; + } + case "tv": + switch (compareState) { + case "playing": + return mdiTelevisionPlay; + case "paused": + return mdiTelevisionPause; + case "off": + return mdiTelevisionOff; + default: + return mdiTelevision; + } + default: + switch (compareState) { + case "playing": + case "paused": + return mdiCastConnected; + case "off": + return mdiCastOff; + default: + return mdiCast; + } + } case "switch": switch (stateObj?.attributes.device_class) { diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts new file mode 100644 index 0000000000..ff62cacbd9 --- /dev/null +++ b/src/common/integrations/protocolIntegrationPicked.ts @@ -0,0 +1,88 @@ +import { html } from "lit"; +import { getConfigEntries } from "../../data/config_entries"; +import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; +import type { HomeAssistant } from "../../types"; +import { documentationUrl } from "../../util/documentation-url"; +import { isComponentLoaded } from "../config/is_component_loaded"; +import { fireEvent } from "../dom/fire_event"; +import { navigate } from "../navigate"; + +export const protocolIntegrationPicked = async ( + element: HTMLElement, + hass: HomeAssistant, + slug: string +) => { + if (slug === "zwave_js") { + const entries = await getConfigEntries(hass, { + domain: "zwave_js", + }); + + if (!entries.length) { + // If the component isn't loaded, ask them to load the integration first + showConfirmationDialog(element, { + text: hass.localize( + "ui.panel.config.integrations.config_flow.missing_zwave_zigbee", + { + integration: "Z-Wave", + supported_hardware_link: html`${hass.localize( + "ui.panel.config.integrations.config_flow.supported_hardware" + )}`, + } + ), + confirmText: hass.localize( + "ui.panel.config.integrations.config_flow.proceed" + ), + confirm: () => { + fireEvent(element, "handler-picked", { + handler: "zwave_js", + }); + }, + }); + return; + } + + showZWaveJSAddNodeDialog(element, { + 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(hass, "zha")) { + showConfirmationDialog(element, { + text: hass.localize( + "ui.panel.config.integrations.config_flow.missing_zwave_zigbee", + { + integration: "Zigbee", + supported_hardware_link: html`${hass.localize( + "ui.panel.config.integrations.config_flow.supported_hardware" + )}`, + } + ), + confirmText: hass.localize( + "ui.panel.config.integrations.config_flow.proceed" + ), + confirm: () => { + fireEvent(element, "handler-picked", { + handler: "zha", + }); + }, + }); + return; + } + + navigate("/config/zha/add"); + } +}; diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts index f82d6f0e38..06a16904f1 100644 --- a/src/common/translations/localize.ts +++ b/src/common/translations/localize.ts @@ -3,10 +3,39 @@ import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-plur import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill"; import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill"; import IntlMessageFormat from "intl-messageformat"; -import { Resources } from "../../types"; +import { Resources, TranslationDict } from "../../types"; import { getLocalLanguage } from "../../util/common-translation"; -export type LocalizeFunc = (key: string, ...args: any[]) => string; +// Exclude some patterns from key type checking for now +// These are intended to be removed as errors are fixed +// Fixing component category will require tighter definition of types from backend and/or web socket +type LocalizeKeyExceptions = + | `${string}` + | `panel.${string}` + | `state.${string}` + | `state_attributes.${string}` + | `state_badge.${string}` + | `ui.${string}` + | `${keyof TranslationDict["supervisor"]}.${string}` + | `component.${string}`; + +// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types +type FlattenObjectKeys< + T extends Record, + Key extends keyof T = keyof T +> = Key extends string + ? T[Key] extends Record + ? `${Key}.${FlattenObjectKeys}` + : `${Key}` + : never; + +export type LocalizeFunc< + Dict extends Record = TranslationDict +> = ( + key: FlattenObjectKeys | LocalizeKeyExceptions, + ...args: any[] +) => string; + interface FormatType { [format: string]: any; } @@ -65,12 +94,14 @@ export const polyfillsLoaded = * } */ -export const computeLocalize = async ( +export const computeLocalize = async < + Dict extends Record = TranslationDict +>( cache: any, language: string, resources: Resources, formats?: FormatsType -): Promise => { +): Promise> => { if (polyfillsLoaded) { await polyfillsLoaded; } diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 84c4132849..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, @@ -376,6 +380,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/components/ha-addon-picker.ts b/src/components/ha-addon-picker.ts index ab605172f2..4c3363a610 100644 --- a/src/components/ha-addon-picker.ts +++ b/src/components/ha-addon-picker.ts @@ -84,20 +84,20 @@ class HaAddonPicker extends LitElement { } else { showAlertDialog(this, { title: this.hass.localize( - "ui.componencts.addon-picker.error.no_supervisor.title" + "ui.components.addon-picker.error.no_supervisor.title" ), text: this.hass.localize( - "ui.componencts.addon-picker.error.no_supervisor.description" + "ui.components.addon-picker.error.no_supervisor.description" ), }); } } catch (err: any) { showAlertDialog(this, { title: this.hass.localize( - "ui.componencts.addon-picker.error.fetch_addons.title" + "ui.components.addon-picker.error.fetch_addons.title" ), text: this.hass.localize( - "ui.componencts.addon-picker.error.fetch_addons.description" + "ui.components.addon-picker.error.fetch_addons.description" ), }); } 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/components/ha-blueprint-picker.ts b/src/components/ha-blueprint-picker.ts index e45e31b6da..9f8a33cd23 100644 --- a/src/components/ha-blueprint-picker.ts +++ b/src/components/ha-blueprint-picker.ts @@ -51,7 +51,7 @@ class HaBluePrintPicker extends LitElement { return html` html` - ${title} +
${title}
@@ -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-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 ? " *" : ""}` + : ""} + + + ` : ""} ; - - @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/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index e55e4d14fd..8be862f4a3 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -21,6 +21,7 @@ import "@polymer/paper-item/paper-icon-item"; import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResult, @@ -44,7 +45,9 @@ import { PersistentNotification, subscribeNotifications, } from "../data/persistent_notification"; +import { subscribeRepairsIssueRegistry } from "../data/repairs"; import { updateCanInstall, UpdateEntity } from "../data/update"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; @@ -177,7 +180,7 @@ const computePanels = memoizeOne( let Sortable; @customElement("ha-sidebar") -class HaSidebar extends LitElement { +class HaSidebar extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, reflect: true }) public narrow!: boolean; @@ -192,6 +195,8 @@ class HaSidebar extends LitElement { @state() private _updatesCount = 0; + @state() private _issuesCount = 0; + @state() private _renderEmptySortable = false; private _mouseLeaveTimeout?: number; @@ -214,6 +219,16 @@ class HaSidebar extends LitElement { private _sortable?; + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { + this._issuesCount = repairs.issues.filter( + (issue) => !issue.ignored + ).length; + }), + ]; + } + protected render() { if (!this.hass) { return html``; @@ -238,6 +253,7 @@ class HaSidebar extends LitElement { changedProps.has("alwaysExpand") || changedProps.has("_externalConfig") || changedProps.has("_updatesCount") || + changedProps.has("_issuesCount") || changedProps.has("_notifications") || changedProps.has("editMode") || changedProps.has("_renderEmptySortable") || @@ -500,7 +516,7 @@ class HaSidebar extends LitElement { } private _renderConfiguration(title: string | null) { - return html` - ${!this.alwaysExpand && this._updatesCount > 0 + ${!this.alwaysExpand && + (this._updatesCount > 0 || this._issuesCount > 0) ? html` - ${this._updatesCount} + ${this._updatesCount + this._issuesCount} ` : ""} ${title} - ${this.alwaysExpand && this._updatesCount > 0 + ${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0) ? html` - ${this._updatesCount} + ${this._updatesCount + this._issuesCount} ` : ""} 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/components/ha-textfield.ts b/src/components/ha-textfield.ts index 32c9bbde6f..15f92ad470 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -83,7 +83,7 @@ export class HaTextField extends TextFieldBase { } input { - text-align: var(--text-field-text-align); + text-align: var(--text-field-text-align, start); } /* Chrome, Safari, Edge, Opera */ diff --git a/src/components/ha-theme-picker.ts b/src/components/ha-theme-picker.ts index af064958b5..95c568f592 100644 --- a/src/components/ha-theme-picker.ts +++ b/src/components/ha-theme-picker.ts @@ -23,7 +23,7 @@ export class HaThemePicker extends LitElement { return html` ${this.hass!.localize( - "ui.components.theme_picker.no_theme" + "ui.components.theme-picker.no_theme" )} ${Object.keys(this.hass!.themes.themes) 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/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/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/data/device_registry.ts b/src/data/device_registry.ts index 63b050c856..a026221819 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; @@ -20,7 +21,7 @@ export interface DeviceRegistryEntry { area_id: string | null; name_by_user: string | null; entry_type: "service" | null; - disabled_by: string | null; + disabled_by: "user" | "integration" | "config_entry" | null; configuration_url: string | null; } @@ -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 4c7a931d0f..99ced8904e 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -13,15 +13,16 @@ export interface EntityRegistryEntry { config_entry_id: string | null; device_id: string | null; area_id: string | null; - disabled_by: string | null; - hidden_by: string | null; + disabled_by: "user" | "device" | "integration" | "config_entry" | null; + hidden_by: Exclude; 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; @@ -37,6 +38,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; @@ -155,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/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 }; diff --git a/src/data/repairs.ts b/src/data/repairs.ts new file mode 100644 index 0000000000..48577448fa --- /dev/null +++ b/src/data/repairs.ts @@ -0,0 +1,95 @@ +import type { Connection } from "home-assistant-js-websocket"; +import { createCollection } from "home-assistant-js-websocket"; +import type { Store } from "home-assistant-js-websocket/dist/store"; +import { debounce } from "../common/util/debounce"; +import type { HomeAssistant } from "../types"; +import type { 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 severitySort = { + critical: 1, + error: 2, + warning: 3, +}; + +export const fetchRepairsIssues = (conn: Connection) => + conn.sendMessagePromise<{ issues: RepairsIssue[] }>({ + type: "repairs/list_issues", + }); + +export const ignoreRepairsIssue = async ( + hass: HomeAssistant, + issue: RepairsIssue, + ignore: boolean +) => + hass.callWS({ + type: "repairs/ignore_issue", + issue_id: issue.issue_id, + domain: issue.domain, + ignore, + }); + +export const createRepairsFlow = ( + hass: HomeAssistant, + handler: string, + issue_id: string +) => + hass.callApi("POST", "repairs/issues/fix", { + handler, + issue_id, + }); + +export const fetchRepairsFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi("GET", `repairs/issues/fix/${flowId}`); + +export const handleRepairsFlowStep = ( + hass: HomeAssistant, + flowId: string, + data: Record +) => + hass.callApi("POST", `repairs/issues/fix/${flowId}`, data); + +export const deleteRepairsFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi("DELETE", `repairs/issues/fix/${flowId}`); + +const subscribeRepairsIssueUpdates = ( + conn: Connection, + store: Store<{ issues: RepairsIssue[] }> +) => + conn.subscribeEvents( + debounce( + () => + fetchRepairsIssues(conn).then((repairs) => + store.setState(repairs, true) + ), + 500, + true + ), + "repairs_issue_registry_updated" + ); + +export const subscribeRepairsIssueRegistry = ( + conn: Connection, + onChange: (repairs: { issues: RepairsIssue[] }) => void +) => + createCollection<{ issues: RepairsIssue[] }>( + "_repairsIssueRegistry", + fetchRepairsIssues, + subscribeRepairsIssueUpdates, + conn, + onChange + ); 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; +}; diff --git a/src/data/supervisor/supervisor.ts b/src/data/supervisor/supervisor.ts index 965d51506b..62812b2c59 100644 --- a/src/data/supervisor/supervisor.ts +++ b/src/data/supervisor/supervisor.ts @@ -1,7 +1,7 @@ import { Connection, getCollection } from "home-assistant-js-websocket"; import { Store } from "home-assistant-js-websocket/dist/store"; import { LocalizeFunc } from "../../common/translations/localize"; -import { HomeAssistant } from "../../types"; +import { HomeAssistant, TranslationDict } from "../../types"; import { HassioAddonsInfo } from "../hassio/addon"; import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host"; import { NetworkInfo } from "../hassio/network"; @@ -67,7 +67,7 @@ export interface Supervisor { os: HassioHassOSInfo; addon: HassioAddonsInfo; store: SupervisorStore; - localize: LocalizeFunc; + localize: LocalizeFunc; } export const supervisorApiWsRequest = ( diff --git a/src/data/supported_brands.ts b/src/data/supported_brands.ts new file mode 100644 index 0000000000..8f0afcf884 --- /dev/null +++ b/src/data/supported_brands.ts @@ -0,0 +1,27 @@ +import { SupportedBrandObj } from "../dialogs/config-flow/step-flow-pick-handler"; +import type { HomeAssistant } from "../types"; + +export type SupportedBrandHandler = Record; + +export const getSupportedBrands = (hass: HomeAssistant) => + hass.callWS>({ + type: "supported_brands", + }); + +export const getSupportedBrandsLookup = ( + supportedBrands: Record +): Record> => { + const supportedBrandsIntegrations: Record< + string, + Partial + > = {}; + for (const [d, domainBrands] of Object.entries(supportedBrands)) { + for (const [slug, name] of Object.entries(domainBrands)) { + supportedBrandsIntegrations[slug] = { + name, + supported_flows: [d], + }; + } + } + return supportedBrandsIntegrations; +}; 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/data/user.ts b/src/data/user.ts index 39493be53e..9ffc23ba2f 100644 --- a/src/data/user.ts +++ b/src/data/user.ts @@ -4,7 +4,7 @@ import { mdiHomeCircleOutline, mdiCancel, } from "@mdi/js"; -import { HomeAssistant } from "../types"; +import { HomeAssistant, TranslationDict } from "../types"; import { Credential } from "./auth"; export const SYSTEM_GROUP_ID_ADMIN = "system-admin"; @@ -21,7 +21,7 @@ export interface User { is_active: boolean; local_only: boolean; system_generated: boolean; - group_ids: string[]; + group_ids: (keyof TranslationDict["groups"])[]; credentials: Credential[]; } @@ -95,7 +95,12 @@ export const computeUserBadges = ( includeSystem: boolean ) => { const labels: [string, string][] = []; - const translate = (key) => hass.localize(`ui.panel.config.users.${key}`); + const translate = ( + key: Extract< + keyof TranslationDict["ui"]["panel"]["config"]["users"], + `is_${string}` + > + ) => hass.localize(`ui.panel.config.users.${key}`); if (user.is_owner) { labels.push([OWNER_ICON, translate("is_owner")]); 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/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..fdd0e12ebc 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -14,19 +14,19 @@ import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked"; 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 { getConfigEntries } from "../../data/config_entries"; +import "../../components/search-input"; 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 +35,10 @@ interface HandlerObj { is_helper?: boolean; } +export interface SupportedBrandObj extends HandlerObj { + supported_flows: string[]; +} + declare global { // for fire event interface HASSDomEvents { @@ -63,11 +67,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 +253,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 +267,43 @@ 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 { + await protocolIntegrationPicked(this, this.hass, slug); + // This closes dialog. + fireEvent(this, "flow-update"); + } + private _maybeSubmit(ev: KeyboardEvent) { if (ev.key !== "Enter") { return; 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/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index eb725e4bde..afd1854fcc 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -206,6 +206,7 @@ class MoreInfoMediaPlayer extends LitElement { flex-wrap: wrap; align-items: center; --mdc-theme-primary: currentColor; + direction: ltr; } .basic-controls { @@ -213,6 +214,15 @@ class MoreInfoMediaPlayer extends LitElement { flex-grow: 1; } + .volume { + direction: ltr; + } + + .source-input, + .sound-input { + direction: var(--direction); + } + .volume, .source-input, .sound-input { @@ -225,6 +235,9 @@ class MoreInfoMediaPlayer extends LitElement { .sound-input ha-select { margin-left: 10px; flex-grow: 1; + margin-inline-start: 10px; + margin-inline-end: initial; + direction: var(--direction); } .tts { diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index d05e441781..64f9d0900c 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -329,6 +329,9 @@ export class HaVoiceCommandDialog extends LitElement { ha-icon-button { color: var(--secondary-text-color); margin-right: -24px; + margin-inline-end: -24px; + margin-inline-start: initial; + direction: var(--direction); } ha-icon-button[active] { @@ -373,19 +376,25 @@ export class HaVoiceCommandDialog extends LitElement { .message.user { margin-left: 24px; - float: right; + margin-inline-start: 24px; + margin-inline-end: initial; + float: var(--float-end); text-align: right; border-bottom-right-radius: 0px; background-color: var(--light-primary-color); color: var(--text-light-primary-color, var(--primary-text-color)); + direction: var(--direction); } .message.hass { margin-right: 24px; - float: left; + margin-inline-end: 24px; + margin-inline-start: initial; + float: var(--float-start); border-bottom-left-radius: 0px; background-color: var(--primary-color); color: var(--text-primary-color); + direction: var(--direction); } .message a { 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/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 1ff795dafc..9c0ea74e89 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item"; import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; -import { HassEntities } from "home-assistant-js-websocket"; +import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, @@ -23,6 +23,11 @@ import "../../../components/ha-menu-button"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tip"; import { CloudStatus } from "../../../data/cloud"; +import { + RepairsIssue, + severitySort, + subscribeRepairsIssueRegistry, +} from "../../../data/repairs"; import { checkForEntityUpdates, filterUpdateEntitiesWithInstall, @@ -31,11 +36,13 @@ import { import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import "../../../layouts/ha-app-layout"; import { PageNavigation } from "../../../layouts/hass-tabs-subpage"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; +import "../repairs/ha-config-repairs"; import "./ha-config-navigation"; import "./ha-config-updates"; @@ -104,7 +111,7 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => { }; @customElement("ha-config-dashboard") -class HaConfigDashboard extends LitElement { +class HaConfigDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, reflect: true }) @@ -118,6 +125,11 @@ class HaConfigDashboard extends LitElement { @state() private _tip?: string; + @state() private _repairsIssues: { issues: RepairsIssue[]; total: number } = { + issues: [], + total: 0, + }; + private _pages = memoizeOne((clouStatus, isLoaded) => { const pages: PageNavigation[] = []; if (clouStatus && isLoaded) { @@ -133,10 +145,34 @@ class HaConfigDashboard extends LitElement { return [...pages, ...configSections.dashboard]; }); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { + const repairsIssues = repairs.issues.filter((issue) => !issue.ignored); + + this._repairsIssues = { + issues: repairsIssues + .sort((a, b) => severitySort[a.severity] - severitySort[b.severity]) + .slice(0, repairsIssues.length === 3 ? repairsIssues.length : 2), + total: repairsIssues.length, + }; + + const integrations: Set = new Set(); + for (const issue of this._repairsIssues.issues) { + integrations.add(issue.domain); + } + this.hass.loadBackendTranslation("issues", [...integrations]); + }), + ]; + } + protected render(): TemplateResult { - const [canInstallUpdates, totalUpdates] = + const { updates: canInstallUpdates, total: totalUpdates } = this._filterUpdateEntitiesWithInstall(this.hass.states); + const { issues: repairsIssues, total: totalRepairIssues } = + this._repairsIssues; + return html` @@ -174,26 +210,60 @@ class HaConfigDashboard extends LitElement { .isWide=${this.isWide} full-width > - ${canInstallUpdates.length + ${repairsIssues.length || canInstallUpdates.length ? html` - - ${totalUpdates > canInstallUpdates.length - ? html` - ${this.hass.localize( - "ui.panel.config.updates.more_updates", - { - count: totalUpdates - canInstallUpdates.length, - } - )} - ` + ${repairsIssues.length + ? html` + + ${totalRepairIssues > repairsIssues.length + ? html` + + ${this.hass.localize( + "ui.panel.config.repairs.more_repairs", + { + count: + totalRepairIssues - repairsIssues.length, + } + )} + + ` + : ""} + ` + : ""} + ${repairsIssues.length && canInstallUpdates.length + ? html`
` + : ""} + ${canInstallUpdates.length + ? html` + + ${totalUpdates > canInstallUpdates.length + ? html` + + ${this.hass.localize( + "ui.panel.config.updates.more_updates", + { + count: + totalUpdates - canInstallUpdates.length, + } + )} + + ` + : ""} + ` : ""} ` : ""} + { + (entities: HassEntities): { updates: UpdateEntity[]; total: number } => { const updates = filterUpdateEntitiesWithInstall(entities); - return [ - updates.slice(0, updates.length === 3 ? updates.length : 2), - updates.length, - ]; + return { + updates: updates.slice(0, updates.length === 3 ? updates.length : 2), + total: updates.length, + }; } ); @@ -268,9 +338,12 @@ class HaConfigDashboard extends LitElement { color: var(--primary-text-color); } a.button { - display: block; - color: var(--primary-color); - padding: 16px; + display: inline-block; + color: var(--primary-text-color); + padding: 6px 16px; + margin: 8px 16px 16px 16px; + border-radius: 32px; + border: 1px solid var(--divider-color); } .title { font-size: 16px; @@ -300,6 +373,16 @@ class HaConfigDashboard extends LitElement { .keep-together { display: inline-block; } + + hr { + height: 1px; + background-color: var( + --ha-card-border-color, + var(--divider-color, #e0e0e0) + ); + border: none; + margin-top: 0; + } `, ]; } 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/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; 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, 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..f67de2be45 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 @@ -6,10 +6,14 @@ import "../../../../components/ha-area-picker"; import "../../../../components/ha-dialog"; import type { HaSwitch } from "../../../../components/ha-switch"; import "../../../../components/ha-textfield"; -import { computeDeviceName } from "../../../../data/device_registry"; +import { + computeDeviceName, + DeviceRegistryEntry, +} from "../../../../data/device_registry"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; +import "../../../../components/ha-alert"; @customElement("dialog-device-registry-detail") class DialogDeviceRegistryDetail extends LitElement { @@ -21,11 +25,11 @@ class DialogDeviceRegistryDetail extends LitElement { @state() private _params?: DeviceRegistryDetailDialogParams; - @property() public _areaId?: string | null; + @state() private _areaId!: string; - @state() private _disabledBy!: string | null; + @state() private _disabledBy!: DeviceRegistryEntry["disabled_by"]; - @state() private _submitting?: boolean; + @state() private _submitting = false; public async showDialog( params: DeviceRegistryDetailDialogParams @@ -33,7 +37,7 @@ class DialogDeviceRegistryDetail extends LitElement { this._params = params; this._error = undefined; this._nameByUser = this._params.device.name_by_user || ""; - this._areaId = this._params.device.area_id; + this._areaId = this._params.device.area_id || ""; this._disabledBy = this._params.device.disabled_by; await this.updateComplete; } @@ -169,9 +173,6 @@ class DialogDeviceRegistryDetail extends LitElement { haStyle, haStyleDialog, css` - .form { - padding-bottom: 24px; - } mwc-button.warning { margin-right: auto; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index a91b6eb9d2..3ccf58df9b 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -344,7 +344,7 @@ export class HaConfigDevicePage extends LitElement { .disabled=${device.disabled_by} .label=${device.disabled_by ? this.hass.localize( - "ui.panel.config.devices.automation.create_disabled", + "ui.panel.config.devices.automation.create_disable", "type", this.hass.localize( `ui.panel.config.devices.type.${ @@ -437,7 +437,7 @@ export class HaConfigDevicePage extends LitElement { .disabled=${device.disabled_by} .label=${device.disabled_by ? this.hass.localize( - "ui.panel.config.devices.scene.create_disabled", + "ui.panel.config.devices.scene.create_disable", "type", this.hass.localize( `ui.panel.config.devices.type.${ @@ -530,7 +530,7 @@ export class HaConfigDevicePage extends LitElement { .disabled=${device.disabled_by} .label=${device.disabled_by ? this.hass.localize( - "ui.panel.config.devices.script.create_disabled", + "ui.panel.config.devices.script.create_disable", "type", this.hass.localize( `ui.panel.config.devices.type.${ @@ -768,26 +768,27 @@ export class HaConfigDevicePage extends LitElement { : "" } - ${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""} + ${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
- ${["control", "sensor", "config", "diagnostic"].map((category) => - // Make sure we render controls if no other cards will be rendered - entitiesByCategory[category].length > 0 || - (entities.length === 0 && category === "control") - ? html` - - - ` - : "" + ${(["control", "sensor", "config", "diagnostic"] as const).map( + (category) => + // Make sure we render controls if no other cards will be rendered + entitiesByCategory[category].length > 0 || + (entities.length === 0 && category === "control") + ? html` + + + ` + : "" )}
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/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index de7d374318..3d915e2db4 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -13,6 +13,7 @@ import { subscribeDeviceRegistry, } from "../../../data/device_registry"; import { + EntityRegistryEntry, EntityRegistryEntryUpdateParams, ExtEntityRegistryEntry, updateEntityRegistryEntry, @@ -25,7 +26,7 @@ import type { HomeAssistant } from "../../../types"; export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public entry!: ExtEntityRegistryEntry; + @property({ attribute: false }) public entry!: ExtEntityRegistryEntry; @state() private _origEntityId!: string; @@ -33,7 +34,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @state() private _areaId?: string | null; - @state() private _disabledBy!: string | null; + @state() private _disabledBy!: EntityRegistryEntry["disabled_by"]; @state() private _hiddenBy!: string | null; @@ -41,7 +42,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @state() private _device?: DeviceRegistryEntry; - @state() private _submitting?: boolean; + @state() private _submitting = false; public async updateEntry(): Promise { this._submitting = true; @@ -145,8 +146,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { > @@ -182,8 +183,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { name="hiddendisabled" value="enabled" .checked=${!this._hiddenBy && !this._disabledBy} - .disabled=${this._device?.disabled_by || - (this._disabledBy && + .disabled=${!!this._device?.disabled_by || + (this._disabledBy !== null && !( this._disabledBy === "user" || this._disabledBy === "integration" @@ -200,8 +201,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { name="hiddendisabled" value="hidden" .checked=${this._hiddenBy !== null} - .disabled=${this._device?.disabled_by || - (this._disabledBy && + .disabled=${!!this._device?.disabled_by || + (this._disabledBy !== null && !( this._disabledBy === "user" || this._disabledBy === "integration" @@ -218,8 +219,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { name="hiddendisabled" value="disabled" .checked=${this._disabledBy !== null} - .disabled=${this._device?.disabled_by || - (this._disabledBy && + .disabled=${!!this._device?.disabled_by || + (this._disabledBy !== null && !( this._disabledBy === "user" || this._disabledBy === "integration" @@ -302,3 +303,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { `; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-registry-basic-editor": HaEntityRegistryBasicEditor; + } +} diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 799d221004..c3ae97d0e3 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -53,6 +53,7 @@ import { updateDeviceRegistryEntry, } from "../../../data/device_registry"; import { + EntityRegistryEntry, EntityRegistryEntryUpdateParams, ExtEntityRegistryEntry, fetchEntityRegistry, @@ -105,6 +106,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"], @@ -124,7 +129,7 @@ const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"]; export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public entry!: ExtEntityRegistryEntry; + @property({ type: Object }) public entry!: ExtEntityRegistryEntry; @state() private _name!: string; @@ -138,9 +143,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _areaId?: string | null; - @state() private _disabledBy!: string | null; + @state() private _disabledBy!: EntityRegistryEntry["disabled_by"]; - @state() private _hiddenBy!: string | null; + @state() private _hiddenBy!: EntityRegistryEntry["hidden_by"]; @state() private _device?: DeviceRegistryEntry; @@ -235,7 +240,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 +366,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 && @@ -601,9 +631,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { name="hiddendisabled" value="enabled" .checked=${!this._hiddenBy && !this._disabledBy} - .disabled=${(this._hiddenBy && this._hiddenBy !== "user") || - this._device?.disabled_by || - (this._disabledBy && + .disabled=${(this._hiddenBy !== null && + this._hiddenBy !== "user") || + !!this._device?.disabled_by || + (this._disabledBy !== null && this._disabledBy !== "user" && this._disabledBy !== "integration")} @change=${this._viewStatusChanged} @@ -861,10 +892,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 ( @@ -1023,12 +1054,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/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 0a92026932..154f2cb424 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -15,6 +15,7 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoize from "memoize-one"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; @@ -85,11 +86,11 @@ export interface EntityRow extends StateEntity { export class HaConfigEntities extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @property() public route!: Route; + @property({ attribute: false }) public route!: Route; @state() private _entities?: EntityRegistryEntry[]; @@ -174,7 +175,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { type: "icon", template: (_, entry: EntityRow) => html` @@ -237,12 +238,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { hidden: narrow || !showDisabled, filterable: true, width: "15%", - template: (disabled_by) => - this.hass.localize( - `ui.panel.config.devices.disabled_by.${disabled_by}` - ) || - disabled_by || - "—", + template: (disabled_by: EntityRegistryEntry["disabled_by"]) => + disabled_by === null + ? "—" + : this.hass.localize(`config_entry.disabled_by.${disabled_by}`), }, status: { title: this.hass.localize( @@ -736,6 +735,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { readonly: true, selectable: false, entity_category: null, + has_entity_name: false, }); } if (changed) { @@ -1011,3 +1011,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ]; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-config-entities": HaConfigEntities; + } +} diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 7f7b93df45..c910dfdbeb 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -6,9 +6,9 @@ import { mdiCog, mdiDatabase, mdiDevices, - mdiHeart, mdiInformation, mdiInformationOutline, + mdiLifebuoy, mdiLightningBolt, mdiMapMarkerRadius, mdiMathLog, @@ -267,6 +267,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", @@ -315,13 +321,6 @@ export const configSections: { [name: string]: PageNavigation[] } = { iconColor: "#301A8E", component: "hassio", }, - { - path: "/config/system_health", - translationKey: "system_health", - iconPath: mdiHeart, - iconColor: "#507FfE", - components: ["system_health", "hassio"], - }, ], about: [ { @@ -440,14 +439,14 @@ class HaPanelConfig extends HassRouterPage { tag: "ha-config-section-storage", load: () => import("./storage/ha-config-section-storage"), }, - system_health: { - tag: "ha-config-system-health", - load: () => import("./system-health/ha-config-system-health"), - }, updates: { 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/hardware/dialog-hardware-available.ts b/src/panels/config/hardware/dialog-hardware-available.ts index 9413c8acb7..aee0f828e7 100644 --- a/src/panels/config/hardware/dialog-hardware-available.ts +++ b/src/panels/config/hardware/dialog-hardware-available.ts @@ -89,7 +89,7 @@ class DialogHardwareAvailable extends LitElement implements HassDialog { )} @@ -97,7 +97,9 @@ class DialogHardwareAvailable extends LitElement implements HassDialog { .hass=${this.hass} .filter=${this._filter} @value-changed=${this._handleSearchChange} - .label=${this.hass.localize("common.search")} + .label=${this.hass.localize( + "ui.panel.config.hardware.available_hardware.search" + )} >
diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 532e344340..ebeea3e350 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -202,7 +202,7 @@ class HaConfigHardware extends LitElement { title: this.hass.localize("ui.panel.config.hardware.reboot_host"), text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"), confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"), - dismissText: this.hass.localize("common.cancel"), + dismissText: this.hass.localize("ui.common.cancel"), }); if (!confirmed) { @@ -236,7 +236,7 @@ class HaConfigHardware extends LitElement { "ui.panel.config.hardware.shutdown_host_confirm" ), confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"), - dismissText: this.hass.localize("common.cancel"), + dismissText: this.hass.localize("ui.common.cancel"), }); if (!confirmed) { diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index beb0bb0cb0..82ce8bf60f 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -14,7 +14,8 @@ import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import type { HASSDomEvent } from "../../../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import type { LocalizeFunc } from "../../../common/translations/localize"; @@ -49,6 +50,10 @@ import { fetchIntegrationManifests, IntegrationManifest, } from "../../../data/integration"; +import { + getSupportedBrands, + getSupportedBrandsLookup, +} from "../../../data/supported_brands"; import { scanUSBDevices } from "../../../data/usb"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { @@ -677,49 +682,84 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { if (!domain) { return; } + const handlers = await getConfigFlowHandlers(this.hass, "integration"); - if (!handlers.includes(domain)) { - if (HELPER_DOMAINS.includes(domain)) { - navigate(`/config/helpers/add?domain=${domain}`, { - replace: true, - }); + // Integration exists, so we can just create a flow + if (handlers.includes(domain)) { + const localize = await localizePromise; + if ( + !(await showConfirmationDialog(this, { + title: localize("ui.panel.config.integrations.confirm_new", { + integration: domainToName(localize, domain), + }), + })) + ) { return; } - const helpers = await getConfigFlowHandlers(this.hass, "helper"); - if (helpers.includes(domain)) { - navigate(`/config/helpers/add?domain=${domain}`, { - replace: true, - }); - return; - } - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.integrations.config_flow.error" - ), + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._handleFlowUpdated(); + }, + startFlowHandler: domain, + manifest: this._manifests[domain], + showAdvanced: this.hass.userData?.showAdvanced, + }); + } + + const supportedBrands = await getSupportedBrands(this.hass); + const supportedBrandsIntegrations = + getSupportedBrandsLookup(supportedBrands); + + // Supported brand exists, so we can just create a flow + if (Object.keys(supportedBrandsIntegrations).includes(domain)) { + const brand = supportedBrandsIntegrations[domain]; + const slug = brand.supported_flows![0]; + + showConfirmationDialog(this, { text: this.hass.localize( - "ui.panel.config.integrations.config_flow.no_config_flow" + "ui.panel.config.integrations.config_flow.supported_brand_flow", + { + supported_brand: brand.name, + flow_domain_name: domainToName(this.hass.localize, slug), + } ), + confirm: () => { + if (["zha", "zwave_js"].includes(slug)) { + protocolIntegrationPicked(this, this.hass, slug); + return; + } + + fireEvent(this, "handler-picked", { + handler: slug, + }); + }, + }); + + return; + } + + // If not an integration or supported brand, try helper else show alert + if (HELPER_DOMAINS.includes(domain)) { + navigate(`/config/helpers/add?domain=${domain}`, { + replace: true, }); return; } - const localize = await localizePromise; - if ( - !(await showConfirmationDialog(this, { - title: localize("ui.panel.config.integrations.confirm_new", { - integration: domainToName(localize, domain), - }), - })) - ) { + const helpers = await getConfigFlowHandlers(this.hass, "helper"); + if (helpers.includes(domain)) { + navigate(`/config/helpers/add?domain=${domain}`, { + replace: true, + }); return; } - showConfigFlowDialog(this, { - dialogClosedCallback: () => { - this._handleFlowUpdated(); - }, - startFlowHandler: domain, - manifest: this._manifests[domain], - showAdvanced: this.hass.userData?.showAdvanced, + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.no_config_flow" + ), }); } diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 40484aa881..348cdc3e6f 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -7,7 +7,7 @@ import { mdiDotsVertical, mdiOpenInNew, } from "@mdi/js"; -import "@polymer/paper-item"; +import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -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"; @@ -63,13 +64,15 @@ export class HaIntegrationCard extends LitElement { @property() public domain!: string; - @property() public items!: ConfigEntryExtended[]; + @property({ attribute: false }) public items!: ConfigEntryExtended[]; - @property() public manifest?: IntegrationManifest; + @property({ attribute: false }) public manifest?: IntegrationManifest; - @property() public entityRegistryEntries!: EntityRegistryEntry[]; + @property({ attribute: false }) + public entityRegistryEntries!: EntityRegistryEntry[]; - @property() public deviceRegistryEntries!: DeviceRegistryEntry[]; + @property({ attribute: false }) + public deviceRegistryEntries!: DeviceRegistryEntry[]; @property() public selectedConfigEntryId?: string; @@ -178,7 +181,7 @@ export class HaIntegrationCard extends LitElement { const services = this._getServices(item, this.deviceRegistryEntries); const entities = this._getEntities(item, this.entityRegistryEntries); - let stateText: [string, ...unknown[]] | undefined; + let stateText: Parameters | undefined; let stateTextExtra: TemplateResult | string | undefined; if (item.disabled_by) { @@ -224,7 +227,7 @@ export class HaIntegrationCard extends LitElement { for (const [items, localizeKey] of [ [devices, "devices"], [services, "services"], - ] as [DeviceRegistryEntry[], string][]) { + ] as const) { if (items.length === 0) { continue; } @@ -366,7 +369,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` diff --git a/src/panels/config/integrations/ha-integration-header.ts b/src/panels/config/integrations/ha-integration-header.ts index 84988ab146..f954b5adf0 100644 --- a/src/panels/config/integrations/ha-integration-header.ts +++ b/src/panels/config/integrations/ha-integration-header.ts @@ -12,13 +12,13 @@ import { brandsUrl } from "../../../util/brands-url"; export class HaIntegrationHeader extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public banner!: string; + @property() public banner?: string; @property() public localizedDomainName?: string; @property() public domain!: string; - @property() public label!: string; + @property() public label?: string; @property({ attribute: false }) public manifest?: IntegrationManifest; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts index c1a75807c7..9f6cb644dc 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts @@ -401,7 +401,7 @@ class DialogZWaveJSAddNode extends LitElement {
- ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : this._status === "finished" @@ -451,7 +451,7 @@ class DialogZWaveJSAddNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : this._status === "provisioned" @@ -469,7 +469,7 @@ class DialogZWaveJSAddNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ""} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts index 8044a6b366..3cf31dc360 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts @@ -120,7 +120,7 @@ class DialogZWaveJSHealNetwork extends LitElement { )} - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} @@ -140,7 +140,7 @@ class DialogZWaveJSHealNetwork extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} @@ -160,7 +160,7 @@ class DialogZWaveJSHealNetwork extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} @@ -180,7 +180,7 @@ class DialogZWaveJSHealNetwork extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts index 5615d78d40..f880f5c20f 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts @@ -166,7 +166,7 @@ class DialogZWaveJSHealNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} @@ -186,7 +186,7 @@ class DialogZWaveJSHealNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts index 5b3bda96b5..0c3eda988d 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts @@ -85,7 +85,7 @@ class DialogZWaveJSReinterviewNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} @@ -105,7 +105,7 @@ class DialogZWaveJSReinterviewNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} @@ -125,7 +125,7 @@ class DialogZWaveJSReinterviewNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-node.ts index 2216b00a3e..bad6ef4597 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-node.ts @@ -110,7 +110,7 @@ class DialogZWaveJSRemoveNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} @@ -132,7 +132,7 @@ class DialogZWaveJSRemoveNode extends LitElement { - ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ${this.hass.localize("ui.common.close")} ` : ``} diff --git a/src/panels/config/logs/system-log-card.ts b/src/panels/config/logs/system-log-card.ts index 8286791f25..617d1bf8a1 100644 --- a/src/panels/config/logs/system-log-card.ts +++ b/src/panels/config/logs/system-log-card.ts @@ -102,7 +102,7 @@ export class SystemLogCard extends LitElement {
${item.message[0]}
-
+
${this._timestamp(item)} – ${html`(${this.hass.localize( @@ -209,6 +209,11 @@ export class SystemLogCard extends LitElement { .empty-content { direction: var(--direction); } + + .row-secondary { + direction: var(--direction); + text-align: left; + } `; } } diff --git a/src/panels/config/network/dialog-ip-detail.ts b/src/panels/config/network/dialog-ip-detail.ts new file mode 100644 index 0000000000..7a8e293f0d --- /dev/null +++ b/src/panels/config/network/dialog-ip-detail.ts @@ -0,0 +1,91 @@ +import "@material/mwc-button/mwc-button"; +import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import type { NetworkInterface } from "../../../data/hassio/network"; +import { haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import type { IPDetailDialogParams } from "./show-ip-detail-dialog"; + +@customElement("dialog-ip-detail") +class DialogIPDetail extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: IPDetailDialogParams; + + @state() private _interface?: NetworkInterface; + + public showDialog(params: IPDetailDialogParams): void { + this._params = params; + this._interface = this._params.interface; + } + + public closeDialog() { + this._params = undefined; + this._interface = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._interface) { + return html``; + } + + const ipv4 = this._interface.ipv4; + const ipv6 = this._interface.ipv6; + + return html` + + ${ipv4 + ? html` +
+

IPv4

+ ${ipv4.address + ? html`
IP Address: ${ipv4.address?.join(", ")}
` + : ""} + ${ipv4.gateway ? html`
Gateway: ${ipv4.gateway}
` : ""} + ${ipv4.method ? html`
Method: ${ipv4.method}
` : ""} + ${ipv4.nameservers?.length + ? html` +
Name Servers: ${ipv4.nameservers?.join(", ")}
+ ` + : ""} +
+ ` + : ""} + ${ipv6 + ? html` +
+

IPv6

+ ${ipv6.address + ? html`
IP Address: ${ipv6.address?.join(", ")}
` + : ""} + ${ipv6.gateway ? html`
Gateway: ${ipv6.gateway}
` : ""} + ${ipv6.method ? html`
Method: ${ipv6.method}
` : ""} + ${ipv6.nameservers?.length + ? html` +
Name Servers: ${ipv6.nameservers?.join(", ")}
+ ` + : ""} +
+ ` + : ""} +
+ `; + } + + static styles: CSSResultGroup = haStyleDialog; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-ip-detail": DialogIPDetail; + } +} diff --git a/src/panels/config/network/show-ip-detail-dialog.ts b/src/panels/config/network/show-ip-detail-dialog.ts new file mode 100644 index 0000000000..cc4a4fedfe --- /dev/null +++ b/src/panels/config/network/show-ip-detail-dialog.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import type { NetworkInterface } from "../../../data/hassio/network"; + +export interface IPDetailDialogParams { + interface?: NetworkInterface; +} + +export const loadIPDetailDialog = () => import("./dialog-ip-detail"); + +export const showIPDetailDialog = ( + element: HTMLElement, + dialogParams: IPDetailDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-ip-detail", + dialogImport: loadIPDetailDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/network/supervisor-network.ts b/src/panels/config/network/supervisor-network.ts index 5621a666e6..664092e1d5 100644 --- a/src/panels/config/network/supervisor-network.ts +++ b/src/panels/config/network/supervisor-network.ts @@ -1,13 +1,16 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-list/mwc-list"; +import { ActionDetail } from "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list-item"; import "@material/mwc-tab"; import "@material/mwc-tab-bar"; +import { mdiDotsVertical } from "@mdi/js"; import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { cache } from "lit/directives/cache"; import "../../../components/ha-alert"; +import "../../../components/ha-button-menu"; +import "../../../components/ha-card"; import "../../../components/ha-circular-progress"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-formfield"; @@ -29,7 +32,7 @@ import { showConfirmationDialog, } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; -import "../../../components/ha-card"; +import { showIPDetailDialog } from "./show-ip-detail-dialog"; const IP_VERSIONS = ["ipv4", "ipv6"]; @@ -236,9 +239,25 @@ export class HassioNetwork extends LitElement { ` : this.hass.localize("ui.common.save")} + + + IP Information +
`; } + private _handleAction(ev: CustomEvent) { + switch (ev.detail.index) { + case 0: + showIPDetailDialog(this, { interface: this._interface }); + break; + } + } + private _selectAP(event) { this._wifiConfiguration = event.currentTarget.ap; this._dirty = true; 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/panels/config/repairs/dialog-integration-startup.ts b/src/panels/config/repairs/dialog-integration-startup.ts new file mode 100644 index 0000000000..971be731f6 --- /dev/null +++ b/src/panels/config/repairs/dialog-integration-startup.ts @@ -0,0 +1,57 @@ +import "@material/mwc-button/mwc-button"; +import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-card"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import { haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import "./integrations-startup-time"; + +@customElement("dialog-integration-startup") +class DialogIntegrationStartup extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _opened = false; + + public showDialog(): void { + this._opened = true; + } + + public closeDialog() { + this._opened = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._opened) { + return html``; + } + + return html` + + + + `; + } + + static styles: CSSResultGroup = haStyleDialog; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-integration-startup": DialogIntegrationStartup; + } +} 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..43c02074aa --- /dev/null +++ b/src/panels/config/repairs/dialog-repairs-issue.ts @@ -0,0 +1,148 @@ +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 { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-markdown"; +import { ignoreRepairsIssue, 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; + + public showDialog(params: RepairsIssueDialogParams): void { + this._params = params; + this._issue = this._params.issue; + } + + public closeDialog() { + if (this._params?.dialogClosedCallback) { + this._params.dialogClosedCallback(); + } + + this._params = undefined; + this._issue = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._issue) { + return html``; + } + + return html` + +
+ ${this.hass.localize( + "ui.panel.config.repairs.dialog.alert_not_fixable" + )} + ${this._issue.breaks_in_ha_version + ? this.hass.localize( + "ui.panel.config.repairs.dialog.breaks_in_version", + { version: this._issue.breaks_in_ha_version } + ) + : ""} + + + ${this._issue.dismissed_version + ? html` +
+ ${this.hass.localize( + "ui.panel.config.repairs.dialog.ignored_in_version", + { version: this._issue.dismissed_version } + )} + ` + : ""} +
+ ${this._issue.learn_more_url + ? html` + + + + ` + : ""} + +
+ `; + } + + private _ignoreIssue() { + ignoreRepairsIssue(this.hass, this._issue!, !this._issue!.ignored); + this.closeDialog(); + } + + static styles: CSSResultGroup = [ + haStyleDialog, + css` + ha-alert { + margin-bottom: 16px; + display: block; + } + a { + text-decoration: none; + } + .dismissed { + font-style: italic; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-repairs-issue": DialogRepairsIssue; + } +} diff --git a/src/panels/config/system-health/ha-config-system-health.ts b/src/panels/config/repairs/dialog-system-information.ts similarity index 61% rename from src/panels/config/system-health/ha-config-system-health.ts rename to src/panels/config/repairs/dialog-system-information.ts index ba3c2862b0..d6e6851734 100644 --- a/src/panels/config/system-health/ha-config-system-health.ts +++ b/src/panels/config/repairs/dialog-system-information.ts @@ -1,17 +1,15 @@ -import { ActionDetail } from "@material/mwc-list"; -import "@material/mwc-list/mwc-list-item"; -import { mdiContentCopy } from "@mdi/js"; -import { UnsubscribeFunc } from "home-assistant-js-websocket/dist/types"; +import "@material/mwc-button/mwc-button"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatDateTime } from "../../../common/datetime/format_date_time"; +import { fireEvent } from "../../../common/dom/fire_event"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; import { subscribePollingCollection } from "../../../common/util/subscribe-polling"; import "../../../components/ha-alert"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; -import "../../../components/ha-circular-progress"; +import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-metric"; import { fetchHassioStats, HassioStats } from "../../../data/hassio/common"; import { @@ -25,12 +23,11 @@ import { SystemHealthInfo, } from "../../../data/system_health"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import "../../../layouts/hass-subpage"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; -import "./integrations-card"; +import "../../../components/ha-circular-progress"; const sortKeys = (a: string, b: string) => { if (a === "homeassistant") { @@ -53,28 +50,40 @@ export const UNHEALTHY_REASON_URL = { privileged: "/more-info/unsupported/privileged", }; -@customElement("ha-config-system-health") -class HaConfigSystemHealth extends SubscribeMixin(LitElement) { +@customElement("dialog-system-information") +class DialogSystemInformation extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow!: boolean; - - @state() private _info?: SystemHealthInfo; - - @state() private _supervisorStats?: HassioStats; + @state() private _systemInfo?: SystemHealthInfo; @state() private _resolutionInfo?: HassioResolution; + @state() private _supervisorStats?: HassioStats; + @state() private _coreStats?: HassioStats; - @state() private _error?: { code: string; message: string }; + @state() private _opened = false; - public hassSubscribe(): Array> { + private _subscriptions?: Array>; + + public showDialog(): void { + this._opened = true; + this.hass!.loadBackendTranslation("system_health"); + this._subscribe(); + } + + public closeDialog() { + this._opened = false; + this._unsubscribe(); + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _subscribe(): void { const subs: Array> = []; if (isComponentLoaded(this.hass, "system_health")) { subs.push( subscribeSystemHealthInfo(this.hass!, (info) => { - this._info = info; + this._systemInfo = info; }) ); } @@ -93,149 +102,51 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) { 10000 ) ); + fetchHassioResolution(this.hass).then((data) => { this._resolutionInfo = data; }); } - return subs; + this._subscriptions = subs; } - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); + private _unsubscribe() { + while (this._subscriptions?.length) { + const unsub = this._subscriptions.pop()!; + if (unsub instanceof Promise) { + unsub.then((unsubFunc) => unsubFunc()); + } else { + unsub(); + } + } + this._subscriptions = undefined; - this.hass!.loadBackendTranslation("system_health"); + this._systemInfo = undefined; + this._resolutionInfo = undefined; + this._coreStats = undefined; + this._supervisorStats = undefined; } protected render(): TemplateResult { - const sections: TemplateResult[] = []; - - if (!this._info) { - sections.push( - html` -
- -
- ` - ); - } else { - const domains = Object.keys(this._info).sort(sortKeys); - for (const domain of domains) { - const domainInfo = this._info[domain]; - const keys: TemplateResult[] = []; - - for (const key of Object.keys(domainInfo.info)) { - let value: unknown; - - if ( - domainInfo.info[key] && - typeof domainInfo.info[key] === "object" - ) { - const info = domainInfo.info[key] as SystemCheckValueObject; - - if (info.type === "pending") { - value = html` - - `; - } else if (info.type === "failed") { - value = html` - ${info.error}${!info.more_info - ? "" - : html` - – - - ${this.hass.localize( - "ui.panel.config.info.system_health.more_info" - )} - - `} - `; - } else if (info.type === "date") { - value = formatDateTime(new Date(info.value), this.hass.locale); - } - } else { - value = domainInfo.info[key]; - } - - keys.push(html` - - - ${this.hass.localize( - `component.${domain}.system_health.info.${key}` - ) || key} - - ${value} - - `); - } - if (domain !== "homeassistant") { - sections.push( - html` -
-

${domainToName(this.hass.localize, domain)}

- ${!domainInfo.manage_url - ? "" - : html` - - - ${this.hass.localize( - "ui.panel.config.info.system_health.manage" - )} - - - `} -
- ` - ); - } - sections.push(html` - - ${keys} -
- `); - } + if (!this._opened) { + return html``; } + const sections = this._getSections(); + return html` - - ${this._error - ? html` - ${this._error.message || this._error.code} - ` - : ""} - ${this._info - ? html` - - - - ${this.hass.localize("ui.panel.config.info.copy_raw")} - - - ${this.hass.localize("ui.panel.config.info.copy_github")} - - - ` - : ""} -
+
${this._resolutionInfo ? html`${this._resolutionInfo.unhealthy.length ? html` @@ -265,66 +176,63 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) { : ""} ` : ""} - -
${sections}
-
+
${sections}
+ ${!this._coreStats && !this._supervisorStats ? "" : html` - -
- ${this._coreStats - ? html` -

- ${this.hass.localize( - "ui.panel.config.system_health.core_stats" - )} -

- - - ` - : ""} - ${this._supervisorStats - ? html` -

- ${this.hass.localize( - "ui.panel.config.system_health.supervisor_stats" - )} -

- - - ` - : ""} -
-
+
+ ${this._coreStats + ? html` +

+ ${this.hass.localize( + "ui.panel.config.system_health.core_stats" + )} +

+ + + ` + : ""} + ${this._supervisorStats + ? html` +

+ ${this.hass.localize( + "ui.panel.config.system_health.supervisor_stats" + )} +

+ + + ` + : ""} +
`} - -
- + + `; } @@ -386,17 +294,111 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) { }); } - private async _copyInfo(ev: CustomEvent): Promise { - const github = ev.detail.index === 1; + private _getSections(): TemplateResult[] { + const sections: TemplateResult[] = []; + + if (!this._systemInfo) { + sections.push( + html` +
+ +
+ ` + ); + } else { + const domains = Object.keys(this._systemInfo).sort(sortKeys); + for (const domain of domains) { + const domainInfo = this._systemInfo[domain]; + const keys: TemplateResult[] = []; + + for (const key of Object.keys(domainInfo.info)) { + let value: unknown; + + if ( + domainInfo.info[key] && + typeof domainInfo.info[key] === "object" + ) { + const info = domainInfo.info[key] as SystemCheckValueObject; + + if (info.type === "pending") { + value = html` + + `; + } else if (info.type === "failed") { + value = html` + ${info.error}${!info.more_info + ? "" + : html` + – + + ${this.hass.localize( + "ui.panel.config.info.system_health.more_systemInfo" + )} + + `} + `; + } else if (info.type === "date") { + value = formatDateTime(new Date(info.value), this.hass.locale); + } + } else { + value = domainInfo.info[key]; + } + + keys.push(html` + + + ${this.hass.localize( + `component.${domain}.system_health.info.${key}` + ) || key} + + ${value} + + `); + } + if (domain !== "homeassistant") { + sections.push( + html` +
+

${domainToName(this.hass.localize, domain)}

+ ${!domainInfo.manage_url + ? "" + : html` + + + ${this.hass.localize( + "ui.panel.config.info.system_health.manage" + )} + + + `} +
+ ` + ); + } + sections.push(html` + + ${keys} +
+ `); + } + } + return sections; + } + + private async _copyInfo(): Promise { let haContent: string | undefined; const domainParts: string[] = []; - for (const domain of Object.keys(this._info!).sort(sortKeys)) { - const domainInfo = this._info![domain]; + for (const domain of Object.keys(this._systemInfo!).sort(sortKeys)) { + const domainInfo = this._systemInfo![domain]; let first = true; const parts = [ `${ - github && domain !== "homeassistant" + domain !== "homeassistant" ? `
${domainToName( this.hass.localize, domain @@ -408,7 +410,7 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) { for (const key of Object.keys(domainInfo.info)) { let value: unknown; - if (typeof domainInfo.info[key] === "object") { + if (domainInfo.info[key] && typeof domainInfo.info[key] === "object") { const info = domainInfo.info[key] as SystemCheckValueObject; if (info.type === "pending") { @@ -421,11 +423,11 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) { } else { value = domainInfo.info[key]; } - if (github && first) { + if (first) { parts.push(`${key} | ${value}\n-- | --`); first = false; } else { - parts.push(`${key}${github ? " | " : ": "}${value}`); + parts.push(`${key} | ${value}`); } } @@ -433,16 +435,14 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) { haContent = parts.join("\n"); } else { domainParts.push(parts.join("\n")); - if (github && domain !== "homeassistant") { + if (domain !== "homeassistant") { domainParts.push("
"); } } } await copyToClipboard( - `${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join( - "\n\n" - )}` + `${"## "}System Information\n${haContent}\n\n${domainParts.join("\n\n")}` ); showToast(this, { @@ -450,73 +450,50 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) { }); } - static styles: CSSResultGroup = css` - .content { - padding: 28px 20px 0; - max-width: 1040px; - margin: 0 auto; - } - integrations-card { - max-width: 600px; - display: block; - max-width: 600px; - margin: 0 auto; - margin-bottom: 24px; - margin-bottom: max(24px, env(safe-area-inset-bottom)); - } - ha-card { - display: block; - max-width: 600px; - margin: 0 auto; - padding-bottom: 16px; - margin-bottom: 24px; - } - ha-alert { - display: block; - max-width: 500px; - margin: 0 auto; - margin-bottom: max(24px, env(safe-area-inset-bottom)); - } - table { - width: 100%; - } + static styles: CSSResultGroup = [ + haStyleDialog, + css` + ha-alert { + margin-bottom: 16px; + display: block; + } + table { + width: 100%; + } - td:first-child { - width: 45%; - } + td:first-child { + width: 45%; + } - td:last-child { - direction: ltr; - } + td:last-child { + direction: ltr; + } - .loading-container { - display: flex; - align-items: center; - justify-content: center; - } + .loading-container { + display: flex; + align-items: center; + justify-content: center; + } - .card-header { - justify-content: space-between; - display: flex; - align-items: center; - } + .card-header { + justify-content: space-between; + display: flex; + align-items: center; + } - .error { - color: var(--error-color); - } + .error { + color: var(--error-color); + } - a { - color: var(--primary-color); - } - - a.manage { - text-decoration: none; - } - `; + a.manage { + text-decoration: none; + } + `, + ]; } declare global { interface HTMLElementTagNameMap { - "ha-config-system-health": HaConfigSystemHealth; + "dialog-system-information": DialogSystemInformation; } } 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..3f60e5f2ca --- /dev/null +++ b/src/panels/config/repairs/ha-config-repairs-dashboard.ts @@ -0,0 +1,187 @@ +import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base"; +import { mdiDotsVertical } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; +import "../../../components/ha-card"; +import { + RepairsIssue, + severitySort, + subscribeRepairsIssueRegistry, +} from "../../../data/repairs"; +import "../../../layouts/hass-subpage"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import type { HomeAssistant } from "../../../types"; +import "./ha-config-repairs"; +import { showIntegrationStartupDialog } from "./show-integration-startup-dialog"; +import { showSystemInformationDialog } from "./show-system-information-dialog"; + +@customElement("ha-config-repairs-dashboard") +class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow!: boolean; + + @state() private _repairsIssues: RepairsIssue[] = []; + + @state() private _showIgnored = false; + + private _getFilteredIssues = memoizeOne( + (showIgnored: boolean, repairsIssues: RepairsIssue[]) => + showIgnored + ? repairsIssues + : repairsIssues.filter((issue) => !issue.ignored) + ); + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { + this._repairsIssues = repairs.issues.sort( + (a, b) => severitySort[a.severity] - severitySort[b.severity] + ); + const integrations: Set = new Set(); + for (const issue of this._repairsIssues) { + integrations.add(issue.domain); + } + this.hass.loadBackendTranslation("issues", [...integrations]); + }), + ]; + } + + protected render(): TemplateResult { + const issues = this._getFilteredIssues( + this._showIgnored, + this._repairsIssues + ); + + return html` + +
+ + + ${isComponentLoaded(this.hass, "system_health") || + isComponentLoaded(this.hass, "hassio") + ? html` + + ${this.hass.localize( + "ui.panel.config.repairs.system_information" + )} + + ` + : ""} + + ${this.hass.localize( + "ui.panel.config.repairs.integration_startup_time" + )} + + + ${this._showIgnored + ? this.hass.localize("ui.panel.config.repairs.hide_ignored") + : this.hass.localize("ui.panel.config.repairs.show_ignored")} + + +
+
+ +
+ ${this._repairsIssues.length + ? html` + + ` + : html` +
+ ${this.hass.localize( + "ui.panel.config.repairs.no_repairs" + )} +
+ `} +
+
+
+
+ `; + } + + private _showSystemInformationDialog( + ev: CustomEvent + ): void { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + + showSystemInformationDialog(this); + } + + private _showIntegrationStartupDialog( + ev: CustomEvent + ): void { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + + showIntegrationStartupDialog(this); + } + + private _toggleIgnored(ev: CustomEvent): void { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + + this._showIgnored = !this._showIgnored; + } + + 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..e516822113 --- /dev/null +++ b/src/panels/config/repairs/ha-config-repairs.ts @@ -0,0 +1,143 @@ +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 "../../../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[]; + + @property({ type: Number }) + public total?: number; + + 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.total || this.repairsIssues.length, + })} +
+ + ${issues.map( + (issue) => html` + + + ${this.hass.localize( + `component.${issue.domain}.issues.${ + issue.translation_key || issue.issue_id + }.title` + )} + + ${issue.created + ? relativeTime(new Date(issue.created), this.hass.locale) + : ""} + ${issue.ignored + ? ` - ${this.hass.localize( + "ui.panel.config.repairs.dialog.ignored_in_version_short", + { version: issue.dismissed_version } + )}` + : ""} + + ${!this.narrow + ? html`` + : ""} + + ` + )} + + `; + } + + private _openShowMoreDialog(ev): void { + const issue = ev.currentTarget.issue as RepairsIssue; + if (issue.is_fixable) { + showRepairsFlowDialog(this, issue); + } else { + showRepairsIssueDialog(this, { + issue, + }); + } + } + + static styles = css` + :host { + --mdc-list-vertical-padding: 0; + } + .title { + font-size: 16px; + padding: 16px; + padding-bottom: 0; + } + .ignored { + opacity: var(--light-secondary-opacity); + } + 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/system-health/integrations-card.ts b/src/panels/config/repairs/integrations-startup-time.ts similarity index 55% rename from src/panels/config/system-health/integrations-card.ts rename to src/panels/config/repairs/integrations-startup-time.ts index 2188160eb7..fb73329905 100644 --- a/src/panels/config/system-health/integrations-card.ts +++ b/src/panels/config/repairs/integrations-startup-time.ts @@ -21,8 +21,8 @@ import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { documentationUrl } from "../../../util/documentation-url"; -@customElement("integrations-card") -class IntegrationsCard extends LitElement { +@customElement("integrations-startup-time") +class IntegrationsStartupTime extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean }) public narrow = false; @@ -45,57 +45,47 @@ class IntegrationsCard extends LitElement { } return html` - - - ${this._setups?.map((setup) => { - const manifest = this._manifests && this._manifests[setup.domain]; - const docLink = manifest - ? manifest.is_built_in - ? documentationUrl( - this.hass, - `/integrations/${manifest.domain}` - ) - : manifest.documentation - : ""; + + ${this._setups?.map((setup) => { + const manifest = this._manifests && this._manifests[setup.domain]; + const docLink = manifest + ? manifest.is_built_in + ? documentationUrl(this.hass, `/integrations/${manifest.domain}`) + : manifest.documentation + : ""; - const setupSeconds = setup.seconds?.toFixed(2); - return html` - - - - ${domainToName(this.hass.localize, setup.domain, manifest)} - - ${setup.domain} -
- ${setupSeconds ? html`${setupSeconds} s` : ""} -
-
- `; - })} -
-
+ const setupSeconds = setup.seconds?.toFixed(2); + return html` + + + + ${domainToName(this.hass.localize, setup.domain, manifest)} + + ${setup.domain} +
+ ${setupSeconds ? html`${setupSeconds} s` : ""} +
+
+ `; + })} + `; } @@ -149,6 +139,6 @@ class IntegrationsCard extends LitElement { declare global { interface HTMLElementTagNameMap { - "integrations-card": IntegrationsCard; + "integrations-startup-time": IntegrationsStartupTime; } } 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..e16fa8af32 --- /dev/null +++ b/src/panels/config/repairs/show-dialog-repair-flow.ts @@ -0,0 +1,210 @@ +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.translation_key || 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.translation_key || 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.translation_key || issue.issue_id + }.fix_flow.step.${step.step_id}.data.${field.name}` + ); + }, + + renderShowFormStepFieldHelper(hass, step, field) { + return hass.localize( + `component.${issue.domain}.issues.${ + issue.translation_key || 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.translation_key || 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.translation_key || 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.translation_key || 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.translation_key || 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.translation_key || 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.translation_key || 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.translation_key || 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-integration-startup-dialog.ts b/src/panels/config/repairs/show-integration-startup-dialog.ts new file mode 100644 index 0000000000..5eb9124645 --- /dev/null +++ b/src/panels/config/repairs/show-integration-startup-dialog.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const loadIntegrationStartupDialog = () => + import("./dialog-integration-startup"); + +export const showIntegrationStartupDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-integration-startup", + dialogImport: loadIntegrationStartupDialog, + dialogParams: {}, + }); +}; 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..c88c28ac77 --- /dev/null +++ b/src/panels/config/repairs/show-repair-issue-dialog.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import type { RepairsIssue } from "../../../data/repairs"; + +export interface RepairsIssueDialogParams { + issue: RepairsIssue; + dialogClosedCallback?: () => void; +} + +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/panels/config/repairs/show-system-information-dialog.ts b/src/panels/config/repairs/show-system-information-dialog.ts new file mode 100644 index 0000000000..1cbf5917f3 --- /dev/null +++ b/src/panels/config/repairs/show-system-information-dialog.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const loadSystemInformationDialog = () => + import("./dialog-system-information"); + +export const showSystemInformationDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-system-information", + dialogImport: loadSystemInformationDialog, + dialogParams: undefined, + }); +}; diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index d93a01748b..9001fa82f1 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -30,13 +30,13 @@ import { showUserDetailDialog } from "./show-dialog-user-detail"; export class HaConfigUsers extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public _users: User[] = []; + @property({ attribute: false }) public _users: User[] = []; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @property() public route!: Route; + @property({ attribute: false }) public route!: Route; private _columns = memoizeOne( (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { @@ -76,7 +76,8 @@ export class HaConfigUsers extends LitElement { width: "20%", direction: "asc", hidden: narrow, - template: (groupIds) => html` ${localize(`groups.${groupIds[0]}`)} `, + template: (groupIds: User["group_ids"]) => + html` ${localize(`groups.${groupIds[0]}`)} `, }, is_active: { title: this.hass.localize( @@ -238,3 +239,9 @@ export class HaConfigUsers extends LitElement { }); } } + +declare global { + interface HTMLElementTagNameMap { + "ha-config-users": HaConfigUsers; + } +} diff --git a/src/panels/developer-tools/event/event-subscribe-card.ts b/src/panels/developer-tools/event/event-subscribe-card.ts index 6e8d889f6a..0b0ce881b6 100644 --- a/src/panels/developer-tools/event/event-subscribe-card.ts +++ b/src/panels/developer-tools/event/event-subscribe-card.ts @@ -2,9 +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-textfield"; +import "../../../components/ha-yaml-editor"; import { HomeAssistant } from "../../../types"; @customElement("event-subscribe-card") @@ -65,18 +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)}: -
${JSON.stringify(ev.event, null, 4)}
-
- ` + ${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 + )}: + +
+ ` )}
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 { >